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,67 +1,34 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
1
|
require_relative "era_guard/shape_diff"
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "../ports/persistence/lineage"
|
|
8
|
-
require_relative "era_check"
|
|
9
|
-
require_relative "registry"
|
|
2
|
+
require_relative "../../../../bluebook/dsl/malformed"
|
|
3
|
+
require_relative "../../../../bluebook/meta_validator"
|
|
4
|
+
require_relative "../../../../ports/loading"
|
|
5
|
+
require_relative "../../../../runtime/registry"
|
|
10
6
|
|
|
11
7
|
module Hecks
|
|
12
8
|
module Runtime
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
9
|
+
# The shape-drift coverage primitives — ADR 0032. Not a driver: nothing
|
|
10
|
+
# here walks a registry or reads/writes a held snapshot on its own.
|
|
11
|
+
# `PostgresEra::LineageManager::CoverageCheck` calls `uncovered_
|
|
12
|
+
# attributes`/`unsafe_additions`/`refuse_uncovered!`/`refuse_unsafe_
|
|
13
|
+
# addition!`/`check_vanished_aggregates!` directly, per translation
|
|
14
|
+
# edge, over its own DB-held shapes; `Translation::Reattest` and
|
|
15
|
+
# `PostgresEra::LineageManager` call `shadow_parse` directly, to read
|
|
16
|
+
# historical bluebook text under old grammar defaults. Both are real,
|
|
17
|
+
# independent, currently-shipped consumers.
|
|
17
18
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
19
|
+
# A prior version of this module ALSO drove its own top-level check —
|
|
20
|
+
# `check!`/`check_bluebook!`, walking a registry and reading/writing a
|
|
21
|
+
# held snapshot under `data/eras/*.bluebook` — duplicating, on its own,
|
|
22
|
+
# the same per-aggregate walk `CoverageCheck` already performs against
|
|
23
|
+
# `PostgresEra`'s own DB-held shapes. Nothing in production ever called
|
|
24
|
+
# it (only a direct unit spec did); deleted rather than kept unwired,
|
|
25
|
+
# per ADR 0032. Wanted again, it's rebuilt informed by `CoverageCheck`'s
|
|
26
|
+
# real orchestration, not resurrected from here.
|
|
22
27
|
module EraGuard
|
|
23
28
|
extend ShapeDiff
|
|
24
29
|
|
|
25
30
|
module_function
|
|
26
31
|
|
|
27
|
-
def check!(registry, directory)
|
|
28
|
-
registry.bluebooks.each_value { |bluebook| check_bluebook!(registry, bluebook, directory) }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def check_bluebook!(registry, bluebook, directory)
|
|
32
|
-
current_text = EraCheck.source_text_for(bluebook, directory)
|
|
33
|
-
return unless current_text
|
|
34
|
-
|
|
35
|
-
era_dir = File.join(registry.root, "data", "eras")
|
|
36
|
-
held_path = File.join(era_dir, "#{Naming.snake(bluebook.name)}.bluebook")
|
|
37
|
-
|
|
38
|
-
FileUtils.mkdir_p(era_dir)
|
|
39
|
-
unless File.exist?(held_path)
|
|
40
|
-
File.write(held_path, current_text)
|
|
41
|
-
return
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
held_bluebook = shadow_parse(File.read(held_path, encoding: "UTF-8"), held_path)
|
|
45
|
-
drifted = false
|
|
46
|
-
|
|
47
|
-
bluebook.aggregates.each do |aggregate|
|
|
48
|
-
lineage = Ports::Persistence::Lineage.for(registry, bluebook.name, aggregate)
|
|
49
|
-
held_aggregate = held_bluebook.aggregate(lineage&.ancestor_name || aggregate.name)
|
|
50
|
-
next unless held_aggregate
|
|
51
|
-
next if shape(aggregate) == shape(held_aggregate)
|
|
52
|
-
|
|
53
|
-
drifted = true
|
|
54
|
-
uncovered = uncovered_attributes(aggregate, held_aggregate, lineage)
|
|
55
|
-
refuse_uncovered!(bluebook, aggregate, uncovered) unless uncovered.empty?
|
|
56
|
-
|
|
57
|
-
unsafe = unsafe_additions(aggregate, held_aggregate, lineage)
|
|
58
|
-
refuse_unsafe_addition!(bluebook, aggregate, unsafe) unless unsafe.empty?
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
check_vanished_aggregates!(registry, bluebook, held_bluebook)
|
|
62
|
-
File.write(held_path, current_text) if drifted
|
|
63
|
-
end
|
|
64
|
-
|
|
65
32
|
# An aggregate that existed in the held text and answers to no
|
|
66
33
|
# current name — renamed silently, with nothing declaring `was:` to
|
|
67
34
|
# explain where its data went — is exactly the disease this guards
|
|
@@ -87,8 +54,9 @@ module Hecks
|
|
|
87
54
|
end
|
|
88
55
|
end
|
|
89
56
|
|
|
90
|
-
# The Layer-1 coverage refusal — one wording,
|
|
91
|
-
#
|
|
57
|
+
# The Layer-1 coverage refusal — one wording, shared with whoever
|
|
58
|
+
# calls it (today, `PostgresEra::LineageManager::CoverageCheck`'s
|
|
59
|
+
# own mint-time coverage check).
|
|
92
60
|
def refuse_uncovered!(bluebook, aggregate, uncovered)
|
|
93
61
|
raise WiringError,
|
|
94
62
|
"cannot boot #{bluebook.name}::#{aggregate.name}: its shape changed and " \
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "append_only"
|
|
3
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../naming"
|
|
2
|
+
require_relative "../../append_only"
|
|
3
|
+
require_relative "../../../../runtime/registry"
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Ports
|
|
@@ -91,7 +91,7 @@ module Hecks
|
|
|
91
91
|
# value-object hash, and a shallow dup would quietly mutate
|
|
92
92
|
# the caller's copy of the original entry.
|
|
93
93
|
state = deep_dup(entry.state)
|
|
94
|
-
@renames
|
|
94
|
+
apply_renames(state, @renames)
|
|
95
95
|
@moves.each { |move| apply_move(state, move) }
|
|
96
96
|
@converts.each { |convert| apply_convert(state, convert) }
|
|
97
97
|
@drops.each { |name| apply_drop(state, name) }
|
|
@@ -183,6 +183,33 @@ module Hecks
|
|
|
183
183
|
end
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
# M27 (docs/audits/2026-08-10-main-bug-audit.md,
|
|
187
|
+
# docs/audits/2026-08-11-bug-triage.md) — SIMULTANEOUS, not
|
|
188
|
+
# sequential: `state[new] = state.delete(old)` per rename, run
|
|
189
|
+
# one rule at a time against the SAME hash it was reading from,
|
|
190
|
+
# loses data the instant one rule's destination is another
|
|
191
|
+
# rule's source. A swap (`rename :a, to: :b` alongside
|
|
192
|
+
# `rename :b, to: :a`) on `{a: 1, b: 2}` used to produce
|
|
193
|
+
# `{a: 1}` — the first rule wrote `b: 1` over the real `b: 2`
|
|
194
|
+
# before the second rule ever got a chance to read it, and the
|
|
195
|
+
# value the whole edge was supposed to preserve (2, moved to
|
|
196
|
+
# `:a`) was gone. The standard fix: snapshot every rule's OLD
|
|
197
|
+
# key and value from `state` FIRST, then remove every old key
|
|
198
|
+
# and only THEN write every new key — a rename never reads a
|
|
199
|
+
# key this same pass has already written to, so a swap or a
|
|
200
|
+
# longer chain applies as one permutation, not a sequence of
|
|
201
|
+
# edits each stepping on the last.
|
|
202
|
+
def apply_renames(state, renames)
|
|
203
|
+
snapshot = renames.filter_map { |old_name, new_name| [old_name, new_name, state[old_name]] if state.key?(old_name) }
|
|
204
|
+
snapshot.each { |old_name, _new_name, _value| state.delete(old_name) }
|
|
205
|
+
# rubocop:disable Style/CombinableLoops -- NOT combinable: a swap
|
|
206
|
+
# (:a<->:b) needs every delete done before any write, or the first
|
|
207
|
+
# rename's write becomes the second rename's delete target — see
|
|
208
|
+
# this method's own comment above.
|
|
209
|
+
snapshot.each { |_old_name, new_name, value| state[new_name] = value }
|
|
210
|
+
# rubocop:enable Style/CombinableLoops
|
|
211
|
+
end
|
|
212
|
+
|
|
186
213
|
def apply_drop(state, name)
|
|
187
214
|
name = name.to_s
|
|
188
215
|
top, member = name.split(".", 2)
|
data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
1
|
+
require_relative "../../era_tamper"
|
|
2
|
+
require_relative "../../../../../../runtime/registry"
|
|
3
|
+
require_relative "../../storage_shape"
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../naming"
|
|
2
|
+
require_relative "../../translation/rule_compiler"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Adapters
|
|
@@ -67,15 +67,41 @@ module Hecks
|
|
|
67
67
|
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))", [name])
|
|
68
68
|
next if table_exists?(name)
|
|
69
69
|
|
|
70
|
+
# `operation` + a NULLABLE `state` — H3, docs/audits/2026-08-
|
|
71
|
+
# 10-main-bug-audit.md: a delete used to just `DELETE FROM`
|
|
72
|
+
# this table, leaving NO row at all for an id carried in
|
|
73
|
+
# from an ancestor era. `compile_head!`'s union below then
|
|
74
|
+
# had nothing current-era to outrank the ancestor
|
|
75
|
+
# matview's own (still-live-looking) `save` row with, so a
|
|
76
|
+
# deleted ancestor-carried record kept winning `DISTINCT
|
|
77
|
+
# ON` forever. A delete now upserts a TOMBSTONE row here
|
|
78
|
+
# instead (`operation = 'delete'`, `state` NULL) — the
|
|
79
|
+
# exact same ordinal-guarded upsert every write already
|
|
80
|
+
# uses, so it participates in the union/`DISTINCT ON`
|
|
81
|
+
# exactly like a save does, and out-ranks the ancestor row
|
|
82
|
+
# by ordinal the same way a real re-save already did
|
|
83
|
+
# ("re-saves are masked correctly" — the audit's own
|
|
84
|
+
# phrasing for why this half of the read path was never
|
|
85
|
+
# broken).
|
|
70
86
|
@db.exec(<<~SQL)
|
|
71
87
|
CREATE TABLE #{quote(name)} (
|
|
72
|
-
id
|
|
73
|
-
ordinal
|
|
74
|
-
|
|
88
|
+
id text PRIMARY KEY,
|
|
89
|
+
ordinal bigint NOT NULL,
|
|
90
|
+
operation text NOT NULL DEFAULT 'save',
|
|
91
|
+
state jsonb
|
|
75
92
|
)
|
|
76
93
|
SQL
|
|
77
94
|
end
|
|
78
95
|
end
|
|
96
|
+
# SELF-HEALING for a table this ADR predates — same idiom as
|
|
97
|
+
# `postgres/schema_builder.rb`'s own `hecks_version` backfill:
|
|
98
|
+
# unconditional, runs on every boot, a no-op once the column
|
|
99
|
+
# is there. A table created before H3's fix has no
|
|
100
|
+
# `operation` column and a `state NOT NULL` constraint a
|
|
101
|
+
# tombstone's NULL state would violate; both are corrected
|
|
102
|
+
# here regardless of which branch above just ran.
|
|
103
|
+
@db.exec("ALTER TABLE #{quote(name)} ADD COLUMN IF NOT EXISTS operation text NOT NULL DEFAULT 'save'")
|
|
104
|
+
@db.exec("ALTER TABLE #{quote(name)} ALTER COLUMN state DROP NOT NULL")
|
|
79
105
|
backfill_head_snapshot!(name, storage_name, era)
|
|
80
106
|
end
|
|
81
107
|
|
|
@@ -105,10 +131,13 @@ module Hecks
|
|
|
105
131
|
end,
|
|
106
132
|
upsert: lambda do |rows|
|
|
107
133
|
rows.each do |row|
|
|
134
|
+
# Always `'save'` — `source_sql` above already filtered
|
|
135
|
+
# to `operation = 'save'`, so nothing this backfill ever
|
|
136
|
+
# sees is a delete tombstone.
|
|
108
137
|
@db.exec_params(
|
|
109
|
-
"INSERT INTO #{quote(name)} (id, ordinal, state) VALUES ($1, $2, $3) " \
|
|
110
|
-
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal,
|
|
111
|
-
"WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
|
|
138
|
+
"INSERT INTO #{quote(name)} (id, ordinal, operation, state) VALUES ($1, $2, 'save', $3) " \
|
|
139
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
140
|
+
"state = EXCLUDED.state WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
|
|
112
141
|
[row["id"], row["ordinal"], row["state"]]
|
|
113
142
|
)
|
|
114
143
|
end
|
|
@@ -149,9 +178,15 @@ module Hecks
|
|
|
149
178
|
# already keeps the snapshot current as of every write.
|
|
150
179
|
def ensure_first_head!(storage_name)
|
|
151
180
|
ensure_head_snapshot!(storage_name, 1)
|
|
181
|
+
# `WHERE operation = 'save'` — era 1 has no ancestor tail to
|
|
182
|
+
# worry about, but the snapshot table can now hold delete
|
|
183
|
+
# tombstones too (H3, see `ensure_head_snapshot!`'s own
|
|
184
|
+
# comment), and a tombstone's `state` is NULL: without this
|
|
185
|
+
# filter a deleted id would still resolve here, just to a nil
|
|
186
|
+
# state, instead of correctly falling out of the head at all.
|
|
152
187
|
@db.exec(<<~SQL)
|
|
153
188
|
CREATE OR REPLACE VIEW #{quote(head_view(storage_name))} AS
|
|
154
|
-
SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))}
|
|
189
|
+
SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))} WHERE operation = 'save'
|
|
155
190
|
SQL
|
|
156
191
|
end
|
|
157
192
|
|
|
@@ -359,13 +394,27 @@ module Hecks
|
|
|
359
394
|
# under this era specifically.
|
|
360
395
|
ensure_head_snapshot!(storage_name, era)
|
|
361
396
|
@db.exec("DROP VIEW IF EXISTS #{quote(head_view(storage_name))}")
|
|
397
|
+
# THE CURRENT-ERA SIDE READS ITS OWN `operation` COLUMN NOW —
|
|
398
|
+
# H3 (docs/audits/2026-08-10-main-bug-audit.md). This used to
|
|
399
|
+
# hardcode `'save' AS operation` here, on the reasoning that a
|
|
400
|
+
# delete simply removed its row from the snapshot table. But
|
|
401
|
+
# for an id carried in from an ancestor era, that left the
|
|
402
|
+
# ancestor matview's own `save` row as the ONLY row on either
|
|
403
|
+
# side of this union — which then won `DISTINCT ON` and
|
|
404
|
+
# `WHERE operation = 'save'` let it straight through, so a
|
|
405
|
+
# deleted ancestor-carried record kept resurrecting forever.
|
|
406
|
+
# The snapshot side now upserts a real tombstone row on
|
|
407
|
+
# delete (`ensure_head_snapshot!`'s own comment) instead of
|
|
408
|
+
# deleting the row, so reading its actual `operation` here
|
|
409
|
+
# lets that tombstone outrank the stale ancestor `save` row
|
|
410
|
+
# by ordinal, exactly the way a genuine re-save already did.
|
|
362
411
|
@db.exec(<<~SQL)
|
|
363
412
|
CREATE VIEW #{quote(head_view(storage_name))} AS
|
|
364
413
|
SELECT id, state FROM (
|
|
365
414
|
SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, operation, state FROM (
|
|
366
415
|
SELECT ordinal, aggregate_id, operation, state FROM #{quote(view)}
|
|
367
416
|
UNION ALL
|
|
368
|
-
SELECT ordinal, id AS aggregate_id,
|
|
417
|
+
SELECT ordinal, id AS aggregate_id, operation, state
|
|
369
418
|
FROM #{quote(head_snapshot(storage_name, era))}
|
|
370
419
|
) merged ORDER BY aggregate_id, ordinal DESC
|
|
371
420
|
) latest WHERE operation = 'save'
|
|
@@ -78,7 +78,35 @@ module Hecks
|
|
|
78
78
|
# to revoke them from the owner itself); a deployment's app
|
|
79
79
|
# role connects as a NON-owner and gets exactly INSERT, per
|
|
80
80
|
# era, at mint time.
|
|
81
|
-
|
|
81
|
+
#
|
|
82
|
+
# GUARDED, same reasoning as the RLS ALTER TABLE calls just
|
|
83
|
+
# below: REVOKE still writes pg_class.relacl (and takes the
|
|
84
|
+
# matching lock) even when the resulting privileges are
|
|
85
|
+
# unchanged, so an unconditional reissue on every ordinary
|
|
86
|
+
# reboot raced two concurrent boots into
|
|
87
|
+
# `PG::InternalError: tuple concurrently updated` — read
|
|
88
|
+
# first, touch the catalog only on the boot that actually
|
|
89
|
+
# needs to.
|
|
90
|
+
#
|
|
91
|
+
# `relacl IS NULL`, not `has_table_privilege('public', ...,
|
|
92
|
+
# 'UPDATE')` — found live, not assumed: a brand-new table's
|
|
93
|
+
# PUBLIC privilege is already "no UPDATE" by Postgres's own
|
|
94
|
+
# default (nothing has ever been explicitly granted to
|
|
95
|
+
# PUBLIC), so `has_table_privilege` answers false BOTH before
|
|
96
|
+
# the REVOKE has ever run AND after it has — indistinguishable
|
|
97
|
+
# by that check alone, which meant the very first boot's own
|
|
98
|
+
# REVOKE never actually ran, `relacl` stayed NULL forever, and
|
|
99
|
+
# "journal rows accept no UPDATE/DELETE from PUBLIC" was true
|
|
100
|
+
# only by accident of Postgres's default, not by the explicit
|
|
101
|
+
# privilege revocation this method exists to record.
|
|
102
|
+
# `relacl IS NULL` means "default ACL, nothing explicit yet" —
|
|
103
|
+
# exactly the one-time signal needed, and (like the RLS flags
|
|
104
|
+
# below) `pg_table_is_visible(oid)`, not a bare relname match,
|
|
105
|
+
# for the same shared-instance/storehouse reason.
|
|
106
|
+
relacl_null = @db.exec_params(
|
|
107
|
+
"SELECT relacl IS NULL FROM pg_class WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
|
|
108
|
+
).getvalue(0, 0)
|
|
109
|
+
@db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC") if relacl_null == "t"
|
|
82
110
|
# RLS goes on AT PROVISIONING, never mid-life — enabling it
|
|
83
111
|
# later would deny every role that has no policy yet, on
|
|
84
112
|
# whatever the shape of the schema happened to be at that
|
data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../runtime/registry"
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Adapters
|
|
@@ -74,6 +74,7 @@ module Hecks
|
|
|
74
74
|
compile_head!(aggregate, era, label, edges, full: true)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
77
78
|
winners.each do |id, side|
|
|
78
79
|
aggregates.each do |aggregate|
|
|
79
80
|
state =
|
|
@@ -101,10 +102,16 @@ module Hecks
|
|
|
101
102
|
"VALUES ($1, $2, $3, 'save', $4) RETURNING ordinal",
|
|
102
103
|
[era, aggregate.storage_name, id, state]
|
|
103
104
|
)[0]["ordinal"]
|
|
105
|
+
# Always `'save'` — a merge winner is read from either
|
|
106
|
+
# the matview's own `operation = 'save'` rows or the new
|
|
107
|
+
# world's live head (`new_states`, captured from
|
|
108
|
+
# `head_view`, which is save-only by construction), so
|
|
109
|
+
# nothing reaching this INSERT is ever a delete.
|
|
104
110
|
@db.exec_params(
|
|
105
|
-
"INSERT INTO #{quote(head_snapshot(aggregate.storage_name, era))} (id, ordinal,
|
|
106
|
-
"
|
|
107
|
-
"
|
|
111
|
+
"INSERT INTO #{quote(head_snapshot(aggregate.storage_name, era))} (id, ordinal, operation, state) " \
|
|
112
|
+
"VALUES ($1, $2, 'save', $3) " \
|
|
113
|
+
"ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
|
|
114
|
+
"state = EXCLUDED.state WHERE #{quote(head_snapshot(aggregate.storage_name, era))}.ordinal < EXCLUDED.ordinal",
|
|
108
115
|
[id, ordinal, state]
|
|
109
116
|
)
|
|
110
117
|
end
|
|
@@ -7,7 +7,27 @@ module Hecks
|
|
|
7
7
|
# equal to the port's reference entry-JSON transform by the
|
|
8
8
|
# cross-execution equivalence spec; the SQL here is a compilation
|
|
9
9
|
# target, not a second source of truth.
|
|
10
|
+
#
|
|
11
|
+
# LOCKED, unlike every other statement `ensure_base!` runs — those
|
|
12
|
+
# are all `CREATE ... IF NOT EXISTS`/`ADD COLUMN IF NOT EXISTS`,
|
|
13
|
+
# which Postgres itself resolves safely under concurrent boots.
|
|
14
|
+
# `CREATE OR REPLACE FUNCTION` is not: it always rewrites the
|
|
15
|
+
# `pg_proc` row, so two sessions racing to (re)install the SAME
|
|
16
|
+
# function — these six are shared/global, not per-domain, so any
|
|
17
|
+
# two domains' concurrent first-boots can collide here — hit a
|
|
18
|
+
# real `PG::InternalError: tuple concurrently updated`, not a
|
|
19
|
+
# graceful no-op. `nested_transaction` is the same
|
|
20
|
+
# already-open-transaction-safe wrapper `ensure_field_cache!`
|
|
21
|
+
# uses for its own advisory lock; a fixed, domain-independent key
|
|
22
|
+
# is correct since these functions have no domain of their own.
|
|
10
23
|
def install_transforms!
|
|
24
|
+
nested_transaction("hecks_tr_functions") do
|
|
25
|
+
@db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_tr_functions'))", [])
|
|
26
|
+
install_transform_functions!
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install_transform_functions!
|
|
11
31
|
@db.exec(<<~SQL)
|
|
12
32
|
CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
|
|
13
33
|
LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
@@ -9,7 +9,7 @@ require_relative "lineage/resumable_backfill"
|
|
|
9
9
|
require_relative "lineage/head_compiler"
|
|
10
10
|
require_relative "lineage/field_cache"
|
|
11
11
|
require_relative "lineage/transform_installer"
|
|
12
|
-
require_relative "
|
|
12
|
+
require_relative "../../../../../naming"
|
|
13
13
|
|
|
14
14
|
module Hecks
|
|
15
15
|
module Adapters
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
5
|
-
require_relative "
|
|
1
|
+
require_relative "../../lineage"
|
|
2
|
+
require_relative "../../era_guard"
|
|
3
|
+
require_relative "../../../../../../runtime/identity"
|
|
4
|
+
require_relative "../../../../../../runtime/registry"
|
|
5
|
+
require_relative "../../translation/audit"
|
|
6
6
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Adapters
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative "../lineage"
|
|
2
|
-
require_relative "
|
|
2
|
+
require_relative "../../storage_shape"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Adapters
|
|
@@ -14,7 +14,10 @@ module Hecks
|
|
|
14
14
|
db = PostgresEra.connect_for(bluebook.name, settings)
|
|
15
15
|
lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
|
|
16
16
|
lineage.ensure_base!
|
|
17
|
-
|
|
17
|
+
# Both spellings honored, key? first — never `||`, which cannot
|
|
18
|
+
# tell a genuinely stored `false` apart from an absent key (see
|
|
19
|
+
# PostgresEra.setting's own comment for the full reasoning).
|
|
20
|
+
role = settings.key?(:role) ? settings[:role] : settings["role"]
|
|
18
21
|
|
|
19
22
|
held = lineage.eras
|
|
20
23
|
if held.empty?
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require_relative "../lineage"
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
2
|
+
require_relative "../../../../../../runtime/registry"
|
|
3
|
+
require_relative "../../translation/audit"
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
require_relative "
|
|
2
|
-
require_relative "
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
1
|
+
require_relative "../../../../../../runtime/registry"
|
|
2
|
+
require_relative "../../storage_shape"
|
|
3
|
+
require_relative "../../translation/audit"
|
|
4
|
+
require_relative "../../translation/scaffold"
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Adapters
|
data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb
RENAMED
|
@@ -4,8 +4,8 @@ require_relative "lineage_manager/era_resolver"
|
|
|
4
4
|
require_relative "lineage_manager/minter"
|
|
5
5
|
require_relative "lineage_manager/merge_coordinator"
|
|
6
6
|
require_relative "lineage_manager/coverage_check"
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
7
|
+
require_relative "../era_guard"
|
|
8
|
+
require_relative "../../../../../runtime/registry"
|
|
9
9
|
|
|
10
10
|
module Hecks
|
|
11
11
|
module Adapters
|