Module: Hecks::Runtime::EraGuard
- Extended by:
- ShapeDiff
- Defined in:
- lib/hecks/ports/persistence/plugins/era/era_guard.rb,
lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb
Overview
The shape-drift coverage primitives — ADR 0032. Not a driver: nothing
here walks a registry or reads/writes a held snapshot on its own.
PostgresEra::LineageManager::CoverageCheck calls uncovered_ attributes/unsafe_additions/refuse_uncovered!/refuse_unsafe_ addition!/check_vanished_aggregates! directly, per translation
edge, over its own DB-held shapes; Translation::Reattest and
PostgresEra::LineageManager call shadow_parse directly, to read
historical bluebook text under old grammar defaults. Both are real,
independent, currently-shipped consumers.
A prior version of this module ALSO drove its own top-level check —
check!/check_bluebook!, walking a registry and reading/writing a
held snapshot under data/eras/*.bluebook — duplicating, on its own,
the same per-aggregate walk CoverageCheck already performs against
PostgresEra's own DB-held shapes. Nothing in production ever called
it (only a direct unit spec did); deleted rather than kept unwired,
per ADR 0032. Wanted again, it's rebuilt informed by CoverageCheck's
real orchestration, not resurrected from here.
Defined Under Namespace
Modules: ShapeDiff
Class Method Summary collapse
-
.check_vanished_aggregates!(registry, bluebook, held_bluebook) ⇒ Object
An aggregate that existed in the held text and answers to no current name — renamed silently, with nothing declaring
was:to explain where its data went — is exactly the disease this guards against, and a plain per-aggregate diff would never see it: the current aggregate simply has no held counterpart to compare to. - .parse_bluebook(source, path, shadow:) ⇒ Object
-
.refuse_uncovered!(bluebook, aggregate, uncovered) ⇒ Object
The Layer-1 coverage refusal — one wording, shared with whoever calls it (today,
PostgresEra::LineageManager::CoverageCheck's own mint-time coverage check). -
.refuse_unsafe_addition!(bluebook, aggregate, unsafe) ⇒ Object
The addition-side sibling of refuse_uncovered! above — same wording shape, different cause: nothing vanished or changed type, something new arrived that an existing record has no way to hold.
- .render_path(path) ⇒ Object
-
.shadow_parse(source, path) ⇒ Object
Parses held source into its own IR, in a scratch registry so a past era's text never touches the one actually booting.
- .suggestion(path) ⇒ Object
Methods included from ShapeDiff
attribute_signature, diff_type, nested_type, possibly_absent?, shape, uncovered_attributes, unsafe_additions
Class Method Details
.check_vanished_aggregates!(registry, bluebook, held_bluebook) ⇒ Object
An aggregate that existed in the held text and answers to no
current name — renamed silently, with nothing declaring was: to
explain where its data went — is exactly the disease this guards
against, and a plain per-aggregate diff would never see it: the
current aggregate simply has no held counterpart to compare to.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/hecks/ports/persistence/plugins/era/era_guard.rb', line 37 def check_vanished_aggregates!(registry, bluebook, held_bluebook) held_bluebook.aggregates.each do |held_aggregate| claimed = bluebook.aggregates.any? do |aggregate| aggregate.name == held_aggregate.name || registry.translations.any? do |translation| translation.domain == bluebook.name && translation.for_aggregate(aggregate.name)&.was == held_aggregate.name end end claimed ||= registry.translations.any? do |translation| translation.domain == bluebook.name && translation.retired.include?(held_aggregate.name) end next if claimed raise WiringError, "cannot boot #{bluebook.name}: #{held_aggregate.name} existed and now doesn't, " \ "and nothing declares was: #{held_aggregate.name.inspect} to explain where its data went." end end |
.parse_bluebook(source, path, shadow:) ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/hecks/ports/persistence/plugins/era/era_guard.rb', line 147 def parse_bluebook(source, path, shadow:) scratch = Registry.new loading = Ports::Loading.bootstrap run = lambda do Hecks.with_registry(scratch) do loading.load_library Kernel.eval(source, TOPLEVEL_BINDING, path, 1) end end shadow ? Hecks::Bluebook::MetaValidator.while_shadow_parsing(&run) : run.call scratch.bluebooks.values.first end |
.refuse_uncovered!(bluebook, aggregate, uncovered) ⇒ Object
The Layer-1 coverage refusal — one wording, shared with whoever
calls it (today, PostgresEra::LineageManager::CoverageCheck's
own mint-time coverage check).
60 61 62 63 64 65 66 |
# File 'lib/hecks/ports/persistence/plugins/era/era_guard.rb', line 60 def refuse_uncovered!(bluebook, aggregate, uncovered) raise WiringError, "cannot boot #{bluebook.name}::#{aggregate.name}: its shape changed and " \ "#{uncovered.map { |path| render_path(path) }.join(', ')} #{uncovered.size == 1 ? 'is' : 'are'} not " \ "explained by any rename, move, convert, retype, or drop. Update bluebook/translations/*.bluebook, e.g. " \ "#{suggestion(uncovered.first)}." end |
.refuse_unsafe_addition!(bluebook, aggregate, unsafe) ⇒ Object
The addition-side sibling of refuse_uncovered! above — same wording shape, different cause: nothing vanished or changed type, something new arrived that an existing record has no way to hold.
71 72 73 74 75 76 77 78 |
# File 'lib/hecks/ports/persistence/plugins/era/era_guard.rb', line 71 def refuse_unsafe_addition!(bluebook, aggregate, unsafe) raise WiringError, "cannot boot #{bluebook.name}::#{aggregate.name}: #{unsafe.map { |name| ":#{name}" }.join(', ')} " \ "#{unsafe.size == 1 ? 'is new and required' : 'are new and required'}, with no default: to fill " \ "an existing record and no translation explaining what one should read there. Give it a " \ "default:, make it optional: true or list_of, or declare bluebook/translations/*.bluebook, e.g. " \ "`backfill :#{unsafe.first}, default: ...`." end |
.render_path(path) ⇒ Object
80 |
# File 'lib/hecks/ports/persistence/plugins/era/era_guard.rb', line 80 def render_path(path) = path.include?(".") ? path.inspect : ":#{path}" |
.shadow_parse(source, path) ⇒ Object
Parses held source into its own IR, in a scratch registry so a past era's text never touches the one actually booting.
NORMAL PARSE FIRST, shadow only as a FALLBACK — not shadow-parsing
unconditionally, which is what this used to do. A handful of DSL
defaults fork on MetaValidator.shadow_parsing? for a reason
that has NOTHING to do with syntax the live grammar can no longer
read at all (identified_by { }, belongs_to, has_one,
has_many — genuinely removed spellings, exactly what shadow-
parsing exists to keep readable): reference_to's own default
mint name (default_reference_name, attribute_collector.rb)
changed from _id-suffixed to bare under ADR 0025, and THAT fork
applies even to text using nothing but current, live syntax.
Held text minted under the CURRENT grammar — every real era in
this corpus today, since nothing has ever minted a second one —
parses fine normally; only the reference-naming DEFAULT differed
once shadow mode engaged unconditionally, so it silently
reconstructed a DIFFERENT shape (and hash) than a fresh parse of
the identical text — the same text hashing two different ways
depending on which code path read it, breaking ensure_named!'s
own from/to edge lookup with a spurious "no translation edge
covers it" refusal that has nothing to do with any real
translation gap.
A normal parse can only ever SUCCEED on text the live grammar
fully understands — there is no way for it to silently produce a
wrong-but-plausible answer for genuinely legacy text, since every
removed spelling refuses loudly (Malformed) rather than
degrading. So: try normal first — if the ordinary grammar reads
this text without complaint, that IS the canonical, unambiguous
interpretation, the same one label_of/mint_hash on the same
source text always computes, whoever's asking. Only on a
Malformed refusal — the one signal that actually means "this
spelling doesn't exist anymore" — fall back to the legacy
grammar, exactly as before this change. Any OTHER exception (a
genuine syntax error, an unrelated validation refusal) propagates
unchanged; swallowing it here to retry under shadow mode would
risk masking a real defect in the held text behind a confusing
second failure instead of the original, more specific one.
MetaValidator.while_shadow_parsing (ADR 0025, docs/dsl-work-
slices.md's S0a) is what makes the fallback a LEGACY grammar
rather than just a second copy of today's: it stops
BluebookBuilder.build from judging this text against the
grammar as it stands NOW, which is the one thing that would make
a removed spelling refuse HISTORY the day it is removed from
live source. The scratch registry is throwaway either way —
nothing here is dispatched against or exposed to the real one —
so skipping the judge/assemble round-trip changes nothing this
method reads: shape, uncovered_attributes, and friends only
ever ask the built IR for its own structure.
141 142 143 144 145 |
# File 'lib/hecks/ports/persistence/plugins/era/era_guard.rb', line 141 def shadow_parse(source, path) parse_bluebook(source, path, shadow: false) rescue Hecks::Bluebook::DSL::Malformed parse_bluebook(source, path, shadow: true) end |
.suggestion(path) ⇒ Object
82 83 84 85 86 87 88 |
# File 'lib/hecks/ports/persistence/plugins/era/era_guard.rb', line 82 def suggestion(path) if path.include?(".") "`move #{path.inspect}, to: #{path.inspect}` or `drop #{path.inspect}`" else "`rename :#{path}, to: :new_name` or `drop :#{path}`" end end |