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
|
@@ -53,6 +53,14 @@ module Hecks
|
|
|
53
53
|
# argument became indistinguishable from one carrying a literal string.
|
|
54
54
|
def with_spec_rows(node) = pair_rows(node.to_h[:with_spec])
|
|
55
55
|
|
|
56
|
+
# THE SAME READ, ONE LEVEL IN — `compensates` folds `DispatchSpec`
|
|
57
|
+
# into the language's own `compensates_command_name`/
|
|
58
|
+
# `compensates_with_spec` (`Assembly::Contracts`' own comment on
|
|
59
|
+
# "Dispatch"), so its OWN with_spec pairs live nested one hash
|
|
60
|
+
# down from where `with_spec_rows` looks. `&.dig(...)` — no
|
|
61
|
+
# compensation at all is not an error, it is `pair_rows(nil)`, empty.
|
|
62
|
+
def compensates_with_spec_rows(node) = pair_rows(node.to_h[:compensates]&.dig(:with_spec))
|
|
63
|
+
|
|
56
64
|
# A read model carries the same options an ask does, plus its filters — see
|
|
57
65
|
# option_rows.
|
|
58
66
|
def read_model_option_rows(node) = option_rows(node, filters: true)
|
|
@@ -155,10 +163,12 @@ module Hecks
|
|
|
155
163
|
# was being handed a blank and could never refuse.
|
|
156
164
|
def mutation_rows(node)
|
|
157
165
|
Array(node.mutations).flat_map do |mutation|
|
|
158
|
-
# `:delegate` (CommandBuilder#delegates_to's
|
|
159
|
-
# the SAME multi-binding
|
|
160
|
-
#
|
|
161
|
-
|
|
166
|
+
# `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
|
|
167
|
+
# #corrects_impl's own comments) ride the SAME multi-binding
|
|
168
|
+
# shape `:append` does — `with: {...}`/the assembled
|
|
169
|
+
# `as:`/`reason:`/`reverses:` hash is a field map, same as
|
|
170
|
+
# append's own `fields:`.
|
|
171
|
+
next set_row(mutation) unless [:append, :delegate, :corrects].include?(mutation.op)
|
|
162
172
|
|
|
163
173
|
mutation.source.map do |field, argument|
|
|
164
174
|
# Spelled the way Mutation#appended_fields spells it, because
|
|
@@ -207,9 +207,22 @@ module Hecks
|
|
|
207
207
|
# at a time.
|
|
208
208
|
def members_row(row) = members_of(row)
|
|
209
209
|
|
|
210
|
+
# THE KEY IS STRINGIFIED, NEVER THE VALUE — the same split
|
|
211
|
+
# `Bluebook::ValueObject#to_h`'s own `members:` emission makes
|
|
212
|
+
# (lib/hecks/bluebook/value_object.rb, L7 docs/audits/
|
|
213
|
+
# 2026-08-11-bug-triage.md). `Pair.value` is declared `String`
|
|
214
|
+
# (shape.bluebook), but a real pair can hold whatever native Ruby
|
|
215
|
+
# type the source member line actually wrote (`StatementFrequency`'s
|
|
216
|
+
# own `retention_months: 84`, examples/banking/bluebook/
|
|
217
|
+
# statements.bluebook) — `text` already unwraps the stored Value to
|
|
218
|
+
# that real type, so calling `.to_s` here erased it a second time,
|
|
219
|
+
# on the side spec/round_trip_spec.rb compares straight against a
|
|
220
|
+
# fresh raw load (`Reconstruction.of` directly, no Assembly in
|
|
221
|
+
# between): `declared 84, read back "84"` the moment `to_h` stopped
|
|
222
|
+
# erasing it on the OTHER side and this one kept erasing it alone.
|
|
210
223
|
def members_of(value_object_row)
|
|
211
224
|
Array(value_object_row[:members]).map do |member|
|
|
212
|
-
Array(member[:pairs]).map { |pair| [text(pair[:key]).to_s, text(pair[:value])
|
|
225
|
+
Array(member[:pairs]).map { |pair| [text(pair[:key]).to_s, text(pair[:value])] }
|
|
213
226
|
end
|
|
214
227
|
end
|
|
215
228
|
|
|
@@ -238,7 +251,12 @@ module Hecks
|
|
|
238
251
|
# Symbol there — the IR was not uniform about it, and only a round trip
|
|
239
252
|
# ever said so.
|
|
240
253
|
identified_by: identity_paths(row),
|
|
241
|
-
|
|
254
|
+
# `text(row[:aggregate])` — the OWNING aggregate, the same one
|
|
255
|
+
# `Judge#owning_aggregate_ref` resolved this piece's own
|
|
256
|
+
# attribute types against on the way in, so reconstruction reads
|
|
257
|
+
# a value-object-typed attribute back the identical way an
|
|
258
|
+
# aggregate's own is (`Shapes#shape_field`'s own comment).
|
|
259
|
+
attributes: Array(row[:attributes]).map { |field| shape_field(field, text(row[:aggregate])) },
|
|
242
260
|
# ADR 0028 — the SAME shape `aggregate(row)`'s own
|
|
243
261
|
# `preconditions:` reads two hand-typed methods up, read by
|
|
244
262
|
# hand for the identical reason: `entity(row)` is hand-typed
|
|
@@ -301,7 +319,28 @@ module Hecks
|
|
|
301
319
|
dispatches: Array(row[:dispatches]).map { |leg| dispatch(leg) })
|
|
302
320
|
end
|
|
303
321
|
|
|
304
|
-
|
|
322
|
+
# `compensates` — TWO FLAT FIELDS on this same row
|
|
323
|
+
# (`process_manager.bluebook`'s own comment on `Dispatch` for
|
|
324
|
+
# why), assembled BY HAND into the shape its own field actually
|
|
325
|
+
# is — `declaration()`'s generic per-field hash-build has no
|
|
326
|
+
# way to turn two cells into a second object, so this reads
|
|
327
|
+
# them directly and passes the result through `extra:`, the
|
|
328
|
+
# same seam `handler`/`process_manager` already use for a
|
|
329
|
+
# `:children` shape a flat Contract cannot describe.
|
|
330
|
+
# `compensates_command_name` absent means no compensation at
|
|
331
|
+
# all — a plain dispatch with nothing to undo. A PLAIN HASH, the
|
|
332
|
+
# SAME declaration shape `to_h` spells for everything else in
|
|
333
|
+
# this file (this file's own top comment) — never a real
|
|
334
|
+
# `DispatchSpec` here; `Assembly#dispatch` is the one place a
|
|
335
|
+
# declaration hash becomes the real object, and building it
|
|
336
|
+
# twice, in two different shapes, is exactly the kind of drift
|
|
337
|
+
# this whole arc exists to remove.
|
|
338
|
+
def dispatch(row)
|
|
339
|
+
name = text(row[:compensates_command_name])
|
|
340
|
+
compensates = name && { command_name: name, with_spec: pairs(row[:compensates_with_spec]) }
|
|
341
|
+
|
|
342
|
+
declaration("Dispatch", row, compensates: compensates)
|
|
343
|
+
end
|
|
305
344
|
|
|
306
345
|
def read_model(row)
|
|
307
346
|
declaration("ReadModel", row).merge(query_name: text(row[:query_name])).merge(options_of(row))
|
|
@@ -52,14 +52,33 @@ module Hecks
|
|
|
52
52
|
def identity_path(part) = text(part[:value]).to_s
|
|
53
53
|
|
|
54
54
|
# An argument, a parameter, a piece's attribute — the three places an
|
|
55
|
-
# attribute is written that carry no owner id to strip
|
|
55
|
+
# attribute is written that carry no owner id to strip... except a
|
|
56
|
+
# piece's own attribute now does : an entity is its own root (repeating
|
|
57
|
+
# the aggregate's whole shape one level down), so ITS attributes name a
|
|
58
|
+
# value object exactly the way an aggregate's own do (`Judge#cell`'s own
|
|
59
|
+
# comment), and reconstruction has to undo the same owned-vs-reference
|
|
60
|
+
# split `attribute`, below, already undoes for an aggregate — not the
|
|
61
|
+
# cruder "any qualified name is a Reference" guess this used before,
|
|
62
|
+
# which mistook a piece's own value-object type for a cross-aggregate
|
|
63
|
+
# head every time.
|
|
56
64
|
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
65
|
+
# `aggregate_id`, passed only for a piece's own attribute (an
|
|
66
|
+
# argument or a parameter carries no value-object type at all — see
|
|
67
|
+
# entity.bluebook's/command.bluebook's own comments on why — so the
|
|
68
|
+
# generic reader path calls this with one argument and gets the
|
|
69
|
+
# OLD reading unchanged), is the piece's OWNING aggregate — the same
|
|
70
|
+
# one its own attribute types were resolved against on the way in
|
|
71
|
+
# (`Judge#owning_aggregate_ref`).
|
|
72
|
+
#
|
|
73
|
+
# A REFERENCE among the other two came in as the head's ID, because
|
|
74
|
+
# that is what Command/Query.Reference offer, so it goes back out as
|
|
75
|
+
# the encoding the IR spells. A qualified name is the tell : an
|
|
76
|
+
# ordinary type names something declared beside it (`Money`,
|
|
77
|
+
# `AccountNumber`) and never carries a chapter, while a head's id
|
|
78
|
+
# always does.
|
|
79
|
+
def shape_field(field, aggregate_id = nil)
|
|
80
|
+
return attribute(field, aggregate_id) if aggregate_id
|
|
81
|
+
|
|
63
82
|
type = text(field[:type]).to_s
|
|
64
83
|
|
|
65
84
|
{
|
|
@@ -221,9 +240,10 @@ module Hecks
|
|
|
221
240
|
# language does not hold is a named gap, not a byte-for-byte one).
|
|
222
241
|
def mutation(target, op, bindings)
|
|
223
242
|
base = { target: target.to_sym, op: op.to_sym, sign: Hecks::Bluebook::Mutation.sign_for(op) }
|
|
224
|
-
# `:delegate` (CommandBuilder#delegates_to's
|
|
225
|
-
# the SAME multi-binding
|
|
226
|
-
|
|
243
|
+
# `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
|
|
244
|
+
# #corrects_impl's own comments) ride the SAME multi-binding
|
|
245
|
+
# shape `:append` does.
|
|
246
|
+
return base.merge(fields: appended(bindings)) if ["append", "delegate", "corrects"].include?(op)
|
|
227
247
|
|
|
228
248
|
base.merge(source: classified(bindings.first))
|
|
229
249
|
end
|
|
@@ -8,16 +8,39 @@ module Hecks
|
|
|
8
8
|
# `lib/hecks/language/bluebook/` declares what a bluebook IS —
|
|
9
9
|
# Chapter, Root, Verb, Shape, Ask, Piece, and the rest, split across files
|
|
10
10
|
# by domain concept and merged into one chapter at load time (see
|
|
11
|
-
# GRAMMAR_FILES below)
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# GRAMMAR_FILES below). This replays a built IR into that domain and turns
|
|
12
|
+
# any refusal into a Malformed, so the meta-domain is what actually judges
|
|
13
|
+
# rather than a description sitting beside the code — for whatever rules
|
|
14
|
+
# it carries. `spec/meta_rules_spec.rb`'s own header names the plan: port
|
|
15
|
+
# the language's rules OUT of builder `raise Malformed` calls and INTO
|
|
16
|
+
# `given`/`invariant` here, where they are declarations any reader of the
|
|
17
|
+
# meta-domain can consume instead of behavior buried in a builder.
|
|
16
18
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
19
|
+
# THIS MIGRATION IS PARTIAL, NOT DONE. As of this writing the meta-domain
|
|
20
|
+
# declares 62 given/invariant/ensures rules (`Hecks::QueryIR.collect_rules`
|
|
21
|
+
# against `grammar_registry.bluebook("Bluebook")` enumerates them) —
|
|
22
|
+
# `spec/meta_rule_reachability_spec.rb` is what proves, per declaration,
|
|
23
|
+
# not per verb, that most of them still lack a spec exercising the
|
|
24
|
+
# refusal at all (see that file's own KNOWN_GAPS for the current count).
|
|
25
|
+
# Meanwhile `lib/hecks/bluebook/dsl/` still carries well over a hundred
|
|
26
|
+
# `raise Malformed` calls of its own — some are genuinely pre-IR
|
|
27
|
+
# construction errors (arity, argument shape) that cannot become
|
|
28
|
+
# meta-domain rules, and some are exactly the semantic kind this file
|
|
29
|
+
# claims to have moved (see `aggregate_builder.rb`'s `seal_*` passes:
|
|
30
|
+
# a mutation into a field the aggregate never declares, a lifecycle guard
|
|
31
|
+
# on an aggregate with no lifecycle). No doc currently inventories which
|
|
32
|
+
# is which, or tracks migrating the latter — that inventory is the actual
|
|
33
|
+
# next step, not a "delete the folder" thought experiment.
|
|
34
|
+
#
|
|
35
|
+
# So: "delete language/bluebook/ and validation stops" is true for the 62
|
|
36
|
+
# rules actually declared here, and false for whatever a builder's own
|
|
37
|
+
# `raise Malformed` still checks — the language does not yet own its own
|
|
38
|
+
# enforcement end to end, and this comment used to claim it already did.
|
|
39
|
+
# The self-hosting mechanism itself is real and is the point worth
|
|
40
|
+
# keeping : a self-description that only describes is indistinguishable
|
|
41
|
+
# from enforcement, and the first version of this file was deleted for
|
|
42
|
+
# exactly that reason. What is not yet real is that self-hosting being
|
|
43
|
+
# the WHOLE of validation.
|
|
21
44
|
#
|
|
22
45
|
# The meta-domain is loaded ONCE and its registry reused ; each bluebook is
|
|
23
46
|
# judged in a fresh in-memory store so no domain can see another's records.
|
|
@@ -131,6 +154,12 @@ module Hecks
|
|
|
131
154
|
# dead code, never actually invoked.
|
|
132
155
|
builder = registry.bluebook_builder(name) { raise "internal: no open builder for #{name}" }
|
|
133
156
|
builder.resolve_pending_chapter_givens!
|
|
157
|
+
# THE ENTITY-SCOPED ANALOGUE, one level down — same reason,
|
|
158
|
+
# same timing: a bare entity-level given left PENDING by any
|
|
159
|
+
# file of this chapter (`EntityBuilder#pending_chapter_entity_
|
|
160
|
+
# given`) must resolve before anything below reads a piece's
|
|
161
|
+
# own `Given` fields too.
|
|
162
|
+
builder.resolve_pending_chapter_entity_givens!
|
|
134
163
|
|
|
135
164
|
# `BluebookBuilder#build` skipped its own whole-chapter battery
|
|
136
165
|
# (hops, projected fields, correlation keys, event shapes,
|
|
@@ -147,7 +176,48 @@ module Hecks
|
|
|
147
176
|
end
|
|
148
177
|
end
|
|
149
178
|
|
|
150
|
-
|
|
179
|
+
# `&& !@forcing_fixpoint` — see `while_forcing_fixpoint` below, whose own
|
|
180
|
+
# window must win even while a growth spec's `while_disabled` is open,
|
|
181
|
+
# for the reason recorded there. Otherwise the SAME stack-restore shape
|
|
182
|
+
# `while_shadow_parsing`/`while_forcing_fixpoint` use, not a bare env
|
|
183
|
+
# toggle any more — it used to be exactly that (`ENV["HECKS_META_
|
|
184
|
+
# VALIDATION"] == "off"`, read directly, with no `previous`/`ensure` of
|
|
185
|
+
# its own), and the gap between "bare toggle" and "stack-restore" was
|
|
186
|
+
# not cosmetic: a test's temporary window could reach code it was never
|
|
187
|
+
# meant to touch. If `grammar_registry`'s ONE-TIME lazy build (below)
|
|
188
|
+
# happened to land inside that window, EVERY language chapter got
|
|
189
|
+
# cached in its raw, never-judged form for the rest of the process —
|
|
190
|
+
# `unmark_scalar`'s String->Integer/Boolean fix (assembly/marks.rb)
|
|
191
|
+
# never ran, so a `Command`'s own `required: true` stayed
|
|
192
|
+
# `required: "true"` forever after, permanently memoized. Found live:
|
|
193
|
+
# an intermittent, parallel_rspec-only ir_golden_spec.rb failure,
|
|
194
|
+
# order-dependent on whether identifier_numeric_coercion_growth_spec.rb's
|
|
195
|
+
# disabled-validation window raced the ONE lazy build in its own worker
|
|
196
|
+
# process — reproduced in isolation by disabling validation before the
|
|
197
|
+
# first `grammar_registry` call. `&& !@forcing_fixpoint` was the first
|
|
198
|
+
# fix and is kept ; converting `@disabled` itself to this shape closes
|
|
199
|
+
# the gap for every OTHER caller of `while_disabled`, not just the one
|
|
200
|
+
# race that was actually observed — nothing outside this file reads
|
|
201
|
+
# `ENV["HECKS_META_VALIDATION"]` any more (confirmed: every one of the
|
|
202
|
+
# dozen growth specs that used to hand-roll `previous = ENV[...] ;
|
|
203
|
+
# ENV[...] = "off" ; ... ; ensure ENV[...] = previous` now calls
|
|
204
|
+
# `while_disabled` instead), so there is no bare global left to race.
|
|
205
|
+
def self.disabled? = @disabled && !@forcing_fixpoint
|
|
206
|
+
|
|
207
|
+
# THE SAME STACK-RESTORE SHAPE `while_shadow_parsing`/`while_forcing_
|
|
208
|
+
# fixpoint` USE. This toggle's real, intended use is a growth spec
|
|
209
|
+
# that boots a scratch bluebook from a tempfile and wants the runtime
|
|
210
|
+
# behaviour without the validation overhead ; that is always a single
|
|
211
|
+
# bounded window around one boot, never a flag meant to survive past
|
|
212
|
+
# it, so the flag itself is scoped in the same `previous`/`ensure`
|
|
213
|
+
# shape rather than a plain assignment a caller could forget to undo.
|
|
214
|
+
def self.while_disabled
|
|
215
|
+
previous = @disabled
|
|
216
|
+
@disabled = true
|
|
217
|
+
yield
|
|
218
|
+
ensure
|
|
219
|
+
@disabled = previous
|
|
220
|
+
end
|
|
151
221
|
|
|
152
222
|
# ADR 0025's own prerequisite (docs/dsl-work-slices.md, S0a): a word
|
|
153
223
|
# a later slice removes from the LIVE grammar must still parse
|
|
@@ -175,6 +245,21 @@ module Hecks
|
|
|
175
245
|
@shadow_parsing = previous
|
|
176
246
|
end
|
|
177
247
|
|
|
248
|
+
# THE SAME STACK-RESTORE SHAPE `while_shadow_parsing` USES, for the
|
|
249
|
+
# same reason: whatever this wraps must never see `disabled?` answer
|
|
250
|
+
# true, however a test elsewhere has the env toggle set at that
|
|
251
|
+
# exact moment. Only `grammar_registry`'s own one-time build (below)
|
|
252
|
+
# wraps itself in this — nothing else needs it, and nothing else
|
|
253
|
+
# should reach for it just to dodge `disabled?` for a domain
|
|
254
|
+
# bluebook, which is precisely the toggle's real, intended use.
|
|
255
|
+
def self.while_forcing_fixpoint
|
|
256
|
+
previous = @forcing_fixpoint
|
|
257
|
+
@forcing_fixpoint = true
|
|
258
|
+
yield
|
|
259
|
+
ensure
|
|
260
|
+
@forcing_fixpoint = previous
|
|
261
|
+
end
|
|
262
|
+
|
|
178
263
|
# The same bluebook judged twice gets the same verdict, and a suite reloads
|
|
179
264
|
# its fixtures constantly — banking alone is ~200 dispatches per build.
|
|
180
265
|
# Keyed on the IR itself, so a CHANGED bluebook is always re-judged.
|
|
@@ -348,9 +433,14 @@ module Hecks
|
|
|
348
433
|
# judged from here on is judged by the language the language itself
|
|
349
434
|
# produced. Outside load_grammar_into on purpose : its ensure clears
|
|
350
435
|
# @bootstrapping, and call() must see bootstrapping? == false to do
|
|
351
|
-
# anything at all.
|
|
352
|
-
|
|
353
|
-
|
|
436
|
+
# anything at all. `while_forcing_fixpoint`-wrapped so a growth
|
|
437
|
+
# spec's own `while_disabled` window can never leave this
|
|
438
|
+
# ONE-TIME build cached in its raw, never-judged form — see
|
|
439
|
+
# `disabled?`'s own comment.
|
|
440
|
+
while_forcing_fixpoint do
|
|
441
|
+
LANGUAGE_CHAPTERS.each { |name| registry.add_bluebook(call(registry.bluebook(name))) }
|
|
442
|
+
load_attached_grammar_into(registry)
|
|
443
|
+
end
|
|
354
444
|
# Stamped LAST, keyed by this registry's own identity rather than
|
|
355
445
|
# a bare boolean — a manual reset (fixpoint_spec.rb's own
|
|
356
446
|
# `@grammar_registry = nil`) makes @grammar_registry not equal
|
|
@@ -46,18 +46,51 @@ module Hecks
|
|
|
46
46
|
# states), so a state nothing ever transitions into or out of no
|
|
47
47
|
# longer exists to be unreachable — the finding this allowlisted
|
|
48
48
|
# cannot occur any more, by construction.
|
|
49
|
-
|
|
49
|
+
#
|
|
50
|
+
# "banking"/NotifyOnClosure, FlagKeyReturn — real, confirmed
|
|
51
|
+
# findings, not bugs to fix. `across "Notifications"` names a
|
|
52
|
+
# domain that does not exist anywhere in this repo — no
|
|
53
|
+
# Notifications bluebook, no hecksagon, nothing to `uses_framework`
|
|
54
|
+
# or `subscribe` to. This is deliberate: `spec/runtime/policy_spec.
|
|
55
|
+
# rb` (a test literally named "records a reaction it cannot
|
|
56
|
+
# deliver rather than swallowing it") and `lib/hecks/runtime/
|
|
57
|
+
# errors.rb`'s own `UnknownVerb` comment both treat "target domain
|
|
58
|
+
# not loaded" as the EXPECTED outcome for it — Notifications is
|
|
59
|
+
# used on purpose to exercise the undelivered-reaction runtime
|
|
60
|
+
# path, not left half-built. There is no real `subscribe` line to
|
|
61
|
+
# add (no event of Notifications' own to name) and no real domain
|
|
62
|
+
# to point `uses_framework` at.
|
|
63
|
+
ALLOWED_FINDINGS = {
|
|
64
|
+
"banking" => [
|
|
65
|
+
[:unacknowledged_relationship, "NotifyOnClosure"],
|
|
66
|
+
[:unknown_target_domain, "NotifyOnClosure"],
|
|
67
|
+
[:unacknowledged_relationship, "FlagKeyReturn"],
|
|
68
|
+
[:unknown_target_domain, "FlagKeyReturn"]
|
|
69
|
+
]
|
|
70
|
+
}.freeze
|
|
50
71
|
|
|
51
72
|
module_function
|
|
52
73
|
|
|
53
|
-
|
|
74
|
+
# `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
|
|
75
|
+
# (every existing caller with no sibling hecksagon, or checking one
|
|
76
|
+
# domain in isolation, behaves exactly as before). `hecksagon` is
|
|
77
|
+
# THIS bluebook's own sibling wiring file, if the caller loaded one
|
|
78
|
+
# (see `emitted_events`'s own comment on why a caller that didn't
|
|
79
|
+
# simply finds none, correctly). `known_domains` is the caller's
|
|
80
|
+
# OWN corpus-wide view — every bluebook/hecksagon name it has
|
|
81
|
+
# booted anywhere, across every domain it has looked at, not just
|
|
82
|
+
# this one — used only to catch a typo'd `across`/`uses_framework`
|
|
83
|
+
# target; see `cross_domain_policy_findings`'s own comment for why
|
|
84
|
+
# this can only ever be a corpus-scoped heuristic, never a general
|
|
85
|
+
# correctness guarantee.
|
|
86
|
+
def call(bluebook, hecksagon: nil, known_domains: nil)
|
|
54
87
|
findings = []
|
|
55
88
|
bluebook.aggregates.each do |aggregate|
|
|
56
89
|
findings.concat(lifecycle_findings(aggregate, aggregate))
|
|
57
90
|
aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
|
|
58
91
|
end
|
|
59
92
|
bluebook.process_managers.each { |pm| findings.concat(saga_findings(bluebook, pm)) }
|
|
60
|
-
bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy)) }
|
|
93
|
+
bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
|
|
61
94
|
findings
|
|
62
95
|
end
|
|
63
96
|
|
|
@@ -151,7 +184,21 @@ module Hecks
|
|
|
151
184
|
# forever, since nothing about *entering* it via default implies
|
|
152
185
|
# anything must eventually move it, unlike a state a transition
|
|
153
186
|
# explicitly delivered somewhere.
|
|
154
|
-
|
|
187
|
+
#
|
|
188
|
+
# Scoped to `default` alone, and only when the lifecycle actually
|
|
189
|
+
# declares real transitions elsewhere: an EMPTY lifecycle (no
|
|
190
|
+
# transitions at all) doesn't get this exemption — that's not "a
|
|
191
|
+
# machine whose entry point deliberately awaits external action,"
|
|
192
|
+
# it's much more likely a lifecycle nobody finished wiring, and
|
|
193
|
+
# should still warn (see spec/fixtures/model_check/lifecycle_
|
|
194
|
+
# findings.bluebook's own Widget::Part, which stays warned on
|
|
195
|
+
# purpose).
|
|
196
|
+
def terminal_exempt(lifecycle)
|
|
197
|
+
return [] if lifecycle.transitions.empty?
|
|
198
|
+
|
|
199
|
+
outgoing_sources = lifecycle.transitions.flat_map { |_, t| Array(t.from) }.to_set
|
|
200
|
+
outgoing_sources.include?(lifecycle.default) ? [] : [lifecycle.default]
|
|
201
|
+
end
|
|
155
202
|
|
|
156
203
|
# ── process managers / sagas ──────────────────────────────────────
|
|
157
204
|
|
|
@@ -200,6 +247,23 @@ module Hecks
|
|
|
200
247
|
"declares no command at — cross-domain dispatch is out of this " \
|
|
201
248
|
"checker's scope, same as CommandRules#resolve_references")
|
|
202
249
|
end
|
|
250
|
+
|
|
251
|
+
# A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
|
|
252
|
+
# shape of the real bug this whole feature closes ("the
|
|
253
|
+
# reversal was written and never armed"), caught at build/
|
|
254
|
+
# model-check time instead of discovered in production. No
|
|
255
|
+
# handler anywhere answers REFUSED (`pm.saga?` false) means
|
|
256
|
+
# `SagaInterpreter#unwind` never runs for this process
|
|
257
|
+
# manager at all, so a declared `compensates` is structurally
|
|
258
|
+
# unreachable — not a warning about style, a dead declaration.
|
|
259
|
+
if !pm.saga? && handler.dispatches.any?(&:compensates)
|
|
260
|
+
handler.dispatches.select(&:compensates).each do |dispatch|
|
|
261
|
+
findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: pm.name,
|
|
262
|
+
message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
|
|
263
|
+
"but no handler anywhere in this saga answers a refusal — the " \
|
|
264
|
+
"compensation is declared and can never fire")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
203
267
|
end
|
|
204
268
|
|
|
205
269
|
if pm.saga? && !reached.include?(pm.saga.from_state)
|
|
@@ -240,8 +304,8 @@ module Hecks
|
|
|
240
304
|
|
|
241
305
|
# ── policies ───────────────────────────────────────────────────────
|
|
242
306
|
|
|
243
|
-
def policy_findings(bluebook, policy)
|
|
244
|
-
return
|
|
307
|
+
def policy_findings(bluebook, policy, hecksagon, known_domains)
|
|
308
|
+
return cross_domain_policy_findings(policy, hecksagon, known_domains) if policy.target_domain
|
|
245
309
|
|
|
246
310
|
emitted = emitted_events(bluebook)
|
|
247
311
|
findings = []
|
|
@@ -276,6 +340,83 @@ module Hecks
|
|
|
276
340
|
findings
|
|
277
341
|
end
|
|
278
342
|
|
|
343
|
+
# ── cross-domain policies (Context Mapping) ───────────────────────
|
|
344
|
+
#
|
|
345
|
+
# `uses_framework "X"` already IS a Shared Kernel relationship — it
|
|
346
|
+
# merges X's own bluebook into THIS registry, no boundary. A cross-
|
|
347
|
+
# domain `policy ... across: "X"` already IS a Customer/Supplier
|
|
348
|
+
# relationship — it dispatches into X over real cross-Lambda RPC in
|
|
349
|
+
# the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
|
|
350
|
+
# word; this makes the CHOICE between them checked instead of a
|
|
351
|
+
# prose comment nobody enforces (`examples/banking/bluebook/
|
|
352
|
+
# banking.hecksagon`'s own hand-written note explaining why
|
|
353
|
+
# Compliance is reached via `across`, never `uses_framework`).
|
|
354
|
+
#
|
|
355
|
+
# NO NEW KEYWORD ANYWHERE — ADR 0025 principle 1 ("one idea, one
|
|
356
|
+
# spelling") refuses a `relationship:`/`as:` argument that would
|
|
357
|
+
# just restate, as a string, the fact the chosen keyword (
|
|
358
|
+
# `uses_framework` vs `across`) already states completely. The
|
|
359
|
+
# DDD vocabulary (Shared Kernel, Customer/Supplier) lives here, in
|
|
360
|
+
# the finding's own name and this comment, and in prose docs — not
|
|
361
|
+
# in the grammar.
|
|
362
|
+
def cross_domain_policy_findings(policy, hecksagon, known_domains)
|
|
363
|
+
return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
|
|
364
|
+
|
|
365
|
+
target = policy.target_domain
|
|
366
|
+
findings = []
|
|
367
|
+
|
|
368
|
+
if hecksagon.framework_members.include?(target)
|
|
369
|
+
# SHARED KERNEL AND CUSTOMER/SUPPLIER ARE MUTUALLY EXCLUSIVE
|
|
370
|
+
# CLAIMS about the SAME target — `uses_framework` means "X is
|
|
371
|
+
# loaded in-process, right here"; `across` means "X is a
|
|
372
|
+
# separate deployment, reached only by RPC." Declaring both is
|
|
373
|
+
# either a pointless RPC to a domain already local, or a
|
|
374
|
+
# `uses_framework` that isn't really doing what its name says.
|
|
375
|
+
findings << Finding.new(kind: :contradictory_relationship, severity: :error, subject: policy.name,
|
|
376
|
+
message: "across #{target.inspect} dispatches over RPC (Customer/Supplier), " \
|
|
377
|
+
"but this hecksagon also uses_framework #{target.inspect} (Shared " \
|
|
378
|
+
"Kernel) — #{target} is already loaded in-process here, so the two " \
|
|
379
|
+
"relationship declarations contradict each other for the same " \
|
|
380
|
+
"target domain")
|
|
381
|
+
elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
|
|
382
|
+
# THIS IS WHAT FINALLY GIVES `subscribe` REAL TEETH — checked
|
|
383
|
+
# here, at model-check time, still never routed at runtime
|
|
384
|
+
# (nothing dispatches off a `subscribe` line; see hecksagon.md's
|
|
385
|
+
# own "checked, not routed" section). ADR 0025 names `subscribe`
|
|
386
|
+
# by number as failing the corpus-use bar; this is the real use.
|
|
387
|
+
findings << Finding.new(kind: :unacknowledged_relationship, severity: :error, subject: policy.name,
|
|
388
|
+
message: "across #{target.inspect} declares a Customer/Supplier " \
|
|
389
|
+
"relationship, but nothing in this hecksagon records the " \
|
|
390
|
+
"expectation — add subscribe \"#{target}.SomeEvent\" for what " \
|
|
391
|
+
"you expect back from it, or uses_framework #{target.inspect} to " \
|
|
392
|
+
"attach it in-process instead")
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# TYPO DETECTION, DELIBERATELY WEAKER — `known_domains` can only
|
|
396
|
+
# ever be a MONOREPO-SCOPED heuristic: a real external hecks
|
|
397
|
+
# consumer's own domain (this repo's own embryonaut/lifeadelics-
|
|
398
|
+
# shaped case) lives in a genuinely separate repository this
|
|
399
|
+
# corpus scan can never see, so a target this check cannot find
|
|
400
|
+
# is "unknown to THIS corpus," never proof of a typo. Two real,
|
|
401
|
+
# legitimate reasons a target is unresolvable — genuinely
|
|
402
|
+
# undefined by design (the corpus's own "Notifications," used
|
|
403
|
+
# deliberately to exercise the undelivered-reaction runtime path)
|
|
404
|
+
# and real-but-external (a separate repository) — both go in
|
|
405
|
+
# `ALLOWED_FINDINGS`, the same judged-exception mechanism this
|
|
406
|
+
# file already uses for ExternalSettlement, rather than a new
|
|
407
|
+
# keyword invented to declare "this one's fine."
|
|
408
|
+
if known_domains && !known_domains.include?(target)
|
|
409
|
+
findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
|
|
410
|
+
message: "across #{target.inspect} names a domain nowhere in the corpus " \
|
|
411
|
+
"this check has booted — a typo, or a real domain intentionally " \
|
|
412
|
+
"outside this corpus (undefined by design, or living in a " \
|
|
413
|
+
"separate repository) belongs in ALLOWED_FINDINGS, named and " \
|
|
414
|
+
"explained, not silently assumed correct")
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
findings
|
|
418
|
+
end
|
|
419
|
+
|
|
279
420
|
# ── shared enumeration ────────────────────────────────────────────
|
|
280
421
|
|
|
281
422
|
# A PORT OPERATION EMITS TOO — the primary/driving port an adapter
|
|
@@ -66,6 +66,14 @@ module Hecks
|
|
|
66
66
|
def validate(pattern)
|
|
67
67
|
chars = pattern.to_s.chars
|
|
68
68
|
index = 0
|
|
69
|
+
# A CHARACTER-CLASS INTERIOR IS A DIFFERENT ALPHABET : inside `[...]`,
|
|
70
|
+
# `*`, `+`, `?`, `(`, `?` are literal characters, not quantifiers or
|
|
71
|
+
# group syntax — `[*+]` means "a literal asterisk or plus". `]` is
|
|
72
|
+
# only the class's close when it isn't the first character after `[`
|
|
73
|
+
# or `[^` (where it is itself a literal, per POSIX bracket-expression
|
|
74
|
+
# rules).
|
|
75
|
+
in_class = false
|
|
76
|
+
class_start = nil
|
|
69
77
|
|
|
70
78
|
while index < chars.length
|
|
71
79
|
if chars[index] == "\\"
|
|
@@ -78,14 +86,36 @@ module Hecks
|
|
|
78
86
|
next
|
|
79
87
|
end
|
|
80
88
|
|
|
89
|
+
if in_class
|
|
90
|
+
if chars[index] == "]" && index != class_start
|
|
91
|
+
in_class = false
|
|
92
|
+
index += 1
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return refuse(:posix_class) if posix_class_at?(chars, index)
|
|
97
|
+
|
|
98
|
+
index += 1
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if chars[index] == "["
|
|
103
|
+
return refuse(:posix_class) if posix_class_at?(chars, index)
|
|
104
|
+
|
|
105
|
+
in_class = true
|
|
106
|
+
class_start = index + 1
|
|
107
|
+
class_start += 1 if chars[class_start] == "^"
|
|
108
|
+
index += 1
|
|
109
|
+
next
|
|
110
|
+
end
|
|
111
|
+
|
|
81
112
|
if chars[index] == "(" && chars[index + 1] == "?"
|
|
82
113
|
return refuse(:lookahead) if %w[= !].include?(chars[index + 2])
|
|
83
114
|
return refuse(:lookbehind) if chars[index + 2] == "<" && %w[= !].include?(chars[index + 3])
|
|
84
115
|
return refuse(:atomic_group) if chars[index + 2] == ">"
|
|
85
116
|
end
|
|
86
117
|
|
|
87
|
-
return refuse(:
|
|
88
|
-
return refuse(:possessive) if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
|
|
118
|
+
return refuse(:possessive) if possessive_at?(chars, index)
|
|
89
119
|
|
|
90
120
|
index += 1
|
|
91
121
|
end
|
|
@@ -115,6 +145,40 @@ module Hecks
|
|
|
115
145
|
cursor += 1 while chars[cursor]&.match?(/[a-zA-Z]/)
|
|
116
146
|
chars[cursor] == ":" && chars[cursor + 1] == "]"
|
|
117
147
|
end
|
|
148
|
+
|
|
149
|
+
# A possessive quantifier is `*+`, `++`, `?+`, or a bounded `{n}`/{n,m}`
|
|
150
|
+
# immediately followed by `+` — only checked OUTSIDE a character class,
|
|
151
|
+
# where `*`, `+`, `?`, `{`, `}` are quantifier syntax rather than
|
|
152
|
+
# literal characters.
|
|
153
|
+
def possessive_at?(chars, index)
|
|
154
|
+
return true if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
|
|
155
|
+
return false unless chars[index] == "{"
|
|
156
|
+
|
|
157
|
+
len = bounded_quantifier_length(chars, index)
|
|
158
|
+
!len.nil? && chars[index + len] == "+"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Length of a `{n}` / `{n,}` / `{n,m}` bound starting at `index`, or nil
|
|
162
|
+
# if what's there isn't one.
|
|
163
|
+
def bounded_quantifier_length(chars, index)
|
|
164
|
+
cursor = index + 1
|
|
165
|
+
digit_seen = false
|
|
166
|
+
|
|
167
|
+
while chars[cursor]&.match?(/[0-9]/)
|
|
168
|
+
digit_seen = true
|
|
169
|
+
cursor += 1
|
|
170
|
+
end
|
|
171
|
+
return nil unless digit_seen
|
|
172
|
+
|
|
173
|
+
if chars[cursor] == ","
|
|
174
|
+
cursor += 1
|
|
175
|
+
cursor += 1 while chars[cursor]&.match?(/[0-9]/)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return nil unless chars[cursor] == "}"
|
|
179
|
+
|
|
180
|
+
cursor - index + 1
|
|
181
|
+
end
|
|
118
182
|
end
|
|
119
183
|
end
|
|
120
184
|
end
|