hecks 3.14.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/exe/hecks +1 -1
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +9 -16
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +2 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/app/responses.rb +3 -1
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +3 -80
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +7 -13
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value/unwrapping.rb +60 -0
- data/lib/hecks/runtime/value.rb +10 -21
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
- data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +306 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +151 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/presenters.rs +335 -0
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +574 -229
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +1037 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +39 -7
- data/rust/host/src/payments/tests.rs +1427 -282
- data/rust/host/src/payments.rs +391 -112
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +555 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -1
- data/rust/parser/src/build/named_pattern.rs +4 -1
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -516
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +22 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +244 -114
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +244 -148
- data/rust/src/kernel/mod.rs +47 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/present.rs +261 -0
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +85 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/rust/host/src/approval.rs +0 -652
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a6fed3e5b894585b2fa7253696873a4218980f6982b4afeeda5effdd5a4aba4
|
|
4
|
+
data.tar.gz: b29a6ce60d5ae9142f0b53ee062979397f3c1384ce2f6da8c70b14396425c989
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77b6974a39536f06fd5bffeb121899ca35b1563e5db172bac48dfeef426c445ed459bea60bc004ea29fdb14b14149a72467b61dddce02aa1cc61b2bb9c11b944
|
|
7
|
+
data.tar.gz: 56fd3680ca94d0060d0ff5e1324c3ccbefb03f5dfc4ac03760cdfb8ee6ad2081c70cef85e4161a66098acb396bdb17c2a485185ff473bee49546a9f0a7336d65
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require_relative "payment_signature"
|
|
4
|
+
|
|
5
|
+
module PizzaShop
|
|
6
|
+
# A stand-in for the payment processor, for tests and a local run: it writes the delivery a
|
|
7
|
+
# Stripe-style processor would send when a payment succeeds, and signs it with the shared
|
|
8
|
+
# secret. It talks to no network and holds no real credentials.
|
|
9
|
+
#
|
|
10
|
+
# Only the processor is fake. `PaymentWebhook`, which receives the delivery, is the adapter a
|
|
11
|
+
# real deployment runs.
|
|
12
|
+
class FakeProcessor
|
|
13
|
+
# @param secret [String] the webhook signing secret this processor shares with the shop
|
|
14
|
+
def initialize(secret:)
|
|
15
|
+
@secret = secret
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Writes and signs the delivery for a payment that succeeded.
|
|
19
|
+
#
|
|
20
|
+
# @param pizza [String] the pizza's name, carried in `metadata` as the shop attached it
|
|
21
|
+
# @param customer_name [String] who paid, also carried in `metadata`
|
|
22
|
+
# @param cents [Integer] the amount the processor received, in cents
|
|
23
|
+
# @param at [Time] the moment the delivery is signed
|
|
24
|
+
# @return [Array(String, String)] the JSON body and its signature header value
|
|
25
|
+
def payment_succeeded(pizza:, customer_name:, cents:, at: Time.now)
|
|
26
|
+
body = JSON.generate(
|
|
27
|
+
id: "evt_#{SecureRandom.hex(8)}",
|
|
28
|
+
type: PaymentWebhook::SUCCEEDED,
|
|
29
|
+
data: { object: { amount_received: cents, metadata: { pizza: pizza, customer_name: customer_name } } }
|
|
30
|
+
)
|
|
31
|
+
[body, PaymentSignature.sign(@secret, body, at: at)]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "openssl"
|
|
2
|
+
require "rack/utils"
|
|
3
|
+
|
|
4
|
+
module PizzaShop
|
|
5
|
+
# The signature a payment processor puts on each webhook delivery, in the timestamped scheme
|
|
6
|
+
# (`t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">`) that Stripe-style processors use.
|
|
7
|
+
#
|
|
8
|
+
# Kept apart from `PaymentWebhook` so the same scheme signs a delivery (`FakeProcessor`) and
|
|
9
|
+
# checks one, and so the check can be read without the HTTP around it.
|
|
10
|
+
module PaymentSignature
|
|
11
|
+
# Raised for a delivery that cannot prove it came from the processor.
|
|
12
|
+
class Invalid < StandardError; end
|
|
13
|
+
|
|
14
|
+
# How far either side of now a delivery's timestamp may sit before it is refused as a
|
|
15
|
+
# replay, in seconds.
|
|
16
|
+
TOLERANCE = 300
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Signs a body the way the processor does.
|
|
21
|
+
#
|
|
22
|
+
# @param secret [String] the shared webhook signing secret
|
|
23
|
+
# @param body [String] the exact bytes of the delivery
|
|
24
|
+
# @param at [Time] the moment the delivery is signed
|
|
25
|
+
# @return [String] the header value, `t=<unix seconds>,v1=<hex digest>`
|
|
26
|
+
def sign(secret, body, at: Time.now)
|
|
27
|
+
stamp = at.to_i
|
|
28
|
+
"t=#{stamp},v1=#{digest(secret, stamp, body)}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Checks a delivery's signature header against its body.
|
|
32
|
+
#
|
|
33
|
+
# Compared in constant time: `==` leaks how many leading bytes of a forged digest matched.
|
|
34
|
+
#
|
|
35
|
+
# @param secret [String] the shared webhook signing secret
|
|
36
|
+
# @param header [String, nil] the delivery's signature header value
|
|
37
|
+
# @param body [String] the exact bytes of the delivery
|
|
38
|
+
# @param now [Time] the moment the delivery arrived
|
|
39
|
+
# @return [void]
|
|
40
|
+
# @raise [Invalid] if the header is missing or malformed, the timestamp is outside
|
|
41
|
+
# `TOLERANCE`, or the digest does not match
|
|
42
|
+
def verify!(secret, header, body, now: Time.now)
|
|
43
|
+
stamp, signed = parts(header)
|
|
44
|
+
raise Invalid, "the signature is #{(now.to_i - stamp).abs}s old, past #{TOLERANCE}s" if stale?(stamp, now)
|
|
45
|
+
return if Rack::Utils.secure_compare(digest(secret, stamp, body), signed)
|
|
46
|
+
|
|
47
|
+
raise Invalid, "the signature does not match the body"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @api private
|
|
51
|
+
def parts(header)
|
|
52
|
+
fields = header.to_s.split(",").to_h { |pair| pair.split("=", 2) }
|
|
53
|
+
raise Invalid, "no signature, or not in the t=…,v1=… form" unless fields["t"].to_s.match?(/\A\d+\z/) && fields["v1"]
|
|
54
|
+
|
|
55
|
+
[fields["t"].to_i, fields["v1"]]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @api private
|
|
59
|
+
def stale?(stamp, now) = (now.to_i - stamp).abs > TOLERANCE
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
def digest(secret, stamp, body)
|
|
63
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, "#{stamp}.#{body}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "rack"
|
|
3
|
+
require_relative "payment_signature"
|
|
4
|
+
|
|
5
|
+
module PizzaShop
|
|
6
|
+
# The driving adapter between a payment processor and Pizzas: a Rack app that verifies the
|
|
7
|
+
# processor's signed "payment succeeded" delivery and restates it in the domain's words through
|
|
8
|
+
# `Order.PaymentGateway.Receive`, the port `pizzas.hecksagon` declares.
|
|
9
|
+
#
|
|
10
|
+
# It does not decide whether the pizza sells. The port only emits `PizzaPaymentReceived`;
|
|
11
|
+
# `OnPizzaPaymentReceived` reaches `Purchase`, whose `given` rules decide. A payment the domain
|
|
12
|
+
# declines is still answered 200, saying why: the fact is true, and a non-2xx would only make
|
|
13
|
+
# the processor retry it.
|
|
14
|
+
class PaymentWebhook
|
|
15
|
+
# The header the processor signs each delivery in.
|
|
16
|
+
SIGNATURE_HEADER = "HTTP_STRIPE_SIGNATURE".freeze
|
|
17
|
+
|
|
18
|
+
# The processor's event type for money that arrived; every other type is acknowledged.
|
|
19
|
+
SUCCEEDED = "payment_intent.succeeded".freeze
|
|
20
|
+
|
|
21
|
+
# `secret:` has no default: a missing secret would silently accept anyone's delivery.
|
|
22
|
+
#
|
|
23
|
+
# @param runtime [Hecks::Runtime::Dispatcher] the booted Pizzas runtime the port is told on
|
|
24
|
+
# @param secret [String] the processor's webhook signing secret
|
|
25
|
+
# @param clock [#call] answers the current `Time`, so a test can sign in the past
|
|
26
|
+
# @raise [ArgumentError] if `secret` is nil or empty
|
|
27
|
+
def initialize(runtime:, secret:, clock: -> { Time.now })
|
|
28
|
+
raise ArgumentError, "no payment webhook secret configured" if secret.to_s.empty?
|
|
29
|
+
|
|
30
|
+
@runtime = runtime
|
|
31
|
+
@secret = secret
|
|
32
|
+
@clock = clock
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Answers one delivery from the processor.
|
|
36
|
+
#
|
|
37
|
+
# @param env [Hash{String => Object}] the Rack environment
|
|
38
|
+
# @return [Array(Integer, Hash{String => String}, Array<String>)] the Rack response: 405 for
|
|
39
|
+
# anything but POST, 401 for a bad signature, 400 for a body that is not JSON, 422 for a
|
|
40
|
+
# payment the domain cannot read, otherwise 200 with what happened as JSON
|
|
41
|
+
def call(env)
|
|
42
|
+
request = Rack::Request.new(env)
|
|
43
|
+
return respond(405, error: "MethodNotAllowed", message: "POST only") unless request.post?
|
|
44
|
+
|
|
45
|
+
body = request.body.read
|
|
46
|
+
PaymentSignature.verify!(@secret, request.get_header(SIGNATURE_HEADER), body, now: @clock.call)
|
|
47
|
+
route(JSON.parse(body))
|
|
48
|
+
rescue PaymentSignature::Invalid => e
|
|
49
|
+
respond(401, error: "InvalidSignature", message: e.message)
|
|
50
|
+
rescue JSON::ParserError => e
|
|
51
|
+
respond(400, error: "MalformedPayload", message: e.message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def route(event)
|
|
57
|
+
return respond(200, ok: true, ignored: event["type"].to_s) unless event["type"] == SUCCEEDED
|
|
58
|
+
|
|
59
|
+
receive(event.dig("data", "object") || {})
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def receive(payment)
|
|
63
|
+
pizza, customer = payment.fetch("metadata", {}).values_at("pizza", "customer_name")
|
|
64
|
+
return unreadable("metadata.pizza and metadata.customer_name name the order") unless pizza && customer
|
|
65
|
+
|
|
66
|
+
respond(200, ok: true, pizza: pizza, **tell(pizza, customer, payment["amount_received"]))
|
|
67
|
+
rescue *Hecks::Runtime::DOMAIN_REFUSALS => e
|
|
68
|
+
unreadable(e.message)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Reads this delivery's reactions off the log by position, which assumes deliveries are
|
|
72
|
+
# handled one at a time, as the single-threaded development server does.
|
|
73
|
+
def tell(pizza, customer, cents)
|
|
74
|
+
before = @runtime.registry.reaction_log.size
|
|
75
|
+
result = @runtime.dispatch("Pizzas::Order.PaymentGateway.Receive",
|
|
76
|
+
to: pizza, with: { customer_name: { value: customer }, amount: { cents: cents } })
|
|
77
|
+
declined = @runtime.registry.reaction_log[before..].find { |reaction| !reaction[:delivered] }
|
|
78
|
+
{ events: result.events.map(&:name), sold: declined.nil?, refused: declined&.dig(:reason) }.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def unreadable(message) = respond(422, error: "UnreadablePayment", message: message)
|
|
82
|
+
|
|
83
|
+
def respond(status, body)
|
|
84
|
+
[status, { "content-type" => "application/json" }, [JSON.generate(body)]]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -45,6 +45,39 @@ Hecks.behaviors "Pizzas" do
|
|
|
45
45
|
expect emits: ["PizzaPurchased"]
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
test "Purchase refuses a payment that does not cover the price" do
|
|
49
|
+
tests "Purchase", on: "Order"
|
|
50
|
+
setup "CreatePizza", name: "Underpaid", pizza: { price_cents: { cents: 1200 }, size: "large" }
|
|
51
|
+
setup "AddTopping", name: "Underpaid", topping: "Basil", amount: 1
|
|
52
|
+
input name: "Underpaid", customer_name: "Chris", amount: { cents: 500 }
|
|
53
|
+
expect refused: "the payment covers the price"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The processor's way in: the port only announces the payment, and OnPizzaPaymentReceived
|
|
57
|
+
# carries it to Purchase, whose rules decide whether the pizza sells.
|
|
58
|
+
test "a payment received through the gateway sells the pizza" do
|
|
59
|
+
tests "Pizzas::Order.PaymentGateway.Receive"
|
|
60
|
+
setup "CreatePizza", name: "Paid For", pizza: { price_cents: { cents: 1200 }, size: "large" }
|
|
61
|
+
setup "AddTopping", name: "Paid For", topping: "Basil", amount: 1
|
|
62
|
+
input to: "Paid For", with: { customer_name: { value: "Chris" }, amount: { cents: 1200 } }
|
|
63
|
+
expect emits: ["PizzaPaymentReceived", "PizzaPurchased"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test "an underpayment through the gateway is recorded, but sells nothing" do
|
|
67
|
+
tests "Pizzas::Order.PaymentGateway.Receive"
|
|
68
|
+
setup "CreatePizza", name: "Short", pizza: { price_cents: { cents: 1200 }, size: "large" }
|
|
69
|
+
setup "AddTopping", name: "Short", topping: "Basil", amount: 1
|
|
70
|
+
input to: "Short", with: { customer_name: { value: "Chris" }, amount: { cents: 500 } }
|
|
71
|
+
expect emits: ["PizzaPaymentReceived"]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
test "a payment through the gateway for a pizza with no toppings sells nothing" do
|
|
75
|
+
tests "Pizzas::Order.PaymentGateway.Receive"
|
|
76
|
+
setup "CreatePizza", name: "Plain", pizza: { price_cents: { cents: 900 }, size: "small" }
|
|
77
|
+
input to: "Plain", with: { customer_name: { value: "Chris" }, amount: { cents: 900 } }
|
|
78
|
+
expect emits: ["PizzaPaymentReceived"]
|
|
79
|
+
end
|
|
80
|
+
|
|
48
81
|
test "a sold pizza is no longer Available" do
|
|
49
82
|
tests "Available", on: "Order", kind: :query
|
|
50
83
|
setup "CreatePizza", name: "Fresh", pizza: { price_cents: { cents: 1000 }, size: "small" }
|
|
@@ -5,14 +5,16 @@ Hecks.hecksagon "Pizzas" do
|
|
|
5
5
|
Pizzas::Order.persisted_by("Memory")
|
|
6
6
|
|
|
7
7
|
# Every way in the example is named: the console (Ruby), `hecks run` (Cli), the forms and
|
|
8
|
-
# storehouse reader (Json
|
|
8
|
+
# storehouse reader (Json, served with the payment webhook by `examples/pizzas/config.ru`) and
|
|
9
|
+
# `hecks mcp` (Mcp). An adapter left out would refuse the domain.
|
|
9
10
|
driven_by "Ruby"
|
|
10
11
|
driven_by "Cli"
|
|
11
12
|
driven_by "Json"
|
|
12
13
|
driven_by "Mcp"
|
|
13
14
|
|
|
14
|
-
# The primary port: an outside adapter (
|
|
15
|
-
#
|
|
15
|
+
# The primary port: an outside adapter (the payment webhook in
|
|
16
|
+
# `examples/pizzas/adapters/payment_webhook.rb`) translates an external fact into our
|
|
17
|
+
# vocabulary; business rules stay on Purchase, reached via OnPizzaPaymentReceived.
|
|
16
18
|
Pizzas::Order.port "PaymentGateway" do
|
|
17
19
|
operation "Receive" do
|
|
18
20
|
attribute :customer_name, CustomerName
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# The pizza shop on the web: both driving adapters in front of one booted Pizzas.
|
|
2
|
+
#
|
|
3
|
+
# PAYMENT_WEBHOOK_SECRET=whsec_local bundle exec rackup examples/pizzas/config.ru -p 9292
|
|
4
|
+
#
|
|
5
|
+
# `/` is the forms app (the Json driving adapter `pizzas.hecksagon` admits): a page per command,
|
|
6
|
+
# so a chef creates a pizza and adds toppings and a customer buys one. `/webhooks/payments` is
|
|
7
|
+
# where the payment processor reports a payment, told to the domain through
|
|
8
|
+
# `Order.PaymentGateway.Receive`. A development server: no sign-in and no CSRF protection.
|
|
9
|
+
require "hecks"
|
|
10
|
+
require "hecks/forms"
|
|
11
|
+
require_relative "adapters/payment_webhook"
|
|
12
|
+
|
|
13
|
+
runtime = Hecks.boot(__dir__)
|
|
14
|
+
Hecks::Forms.configure("PizzaShop") { expose "Pizzas" }
|
|
15
|
+
|
|
16
|
+
map "/webhooks/payments" do
|
|
17
|
+
run PizzaShop::PaymentWebhook.new(runtime: runtime, secret: ENV.fetch("PAYMENT_WEBHOOK_SECRET"))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
map "/" do
|
|
21
|
+
run Hecks::Forms::App.for(registry: runtime.registry, app_name: "PizzaShop")
|
|
22
|
+
end
|
data/exe/hecks
CHANGED
|
@@ -4,8 +4,6 @@ require_relative "../../bluebook/expression/canonical_form"
|
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
7
|
-
class NotExtractable < StandardError; end
|
|
8
|
-
|
|
9
7
|
# Extracts a `given`/`ensures`/invariant block's own source text via
|
|
10
8
|
# `prism`, canonicalised into readable, comparable text (era diffing, docs).
|
|
11
9
|
module Prism
|
|
@@ -16,7 +16,6 @@ require_relative "driven/local_storage"
|
|
|
16
16
|
require_relative "driven/prism"
|
|
17
17
|
require_relative "driven/folder"
|
|
18
18
|
require_relative "driven/d1"
|
|
19
|
-
require_relative "driven/mock_stripe_adapter"
|
|
20
19
|
require_relative "driven/tenant_provisioner"
|
|
21
20
|
require_relative "driven/secure_random_identity"
|
|
22
21
|
require_relative "driven/in_process_key_vault"
|
|
@@ -25,6 +24,5 @@ require_relative "driven/system_clock"
|
|
|
25
24
|
# `identity_generation` adapter would make that port ambiguous for every boot.
|
|
26
25
|
require_relative "driven/governance_authorization"
|
|
27
26
|
require_relative "driven/identity_registry"
|
|
28
|
-
require_relative "driven/google_authentication"
|
|
29
27
|
require_relative "driven/claude_code"
|
|
30
28
|
require_relative "driven/script_runner"
|
|
@@ -81,28 +81,21 @@ module Hecks
|
|
|
81
81
|
# other value is a leaf
|
|
82
82
|
# @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape
|
|
83
83
|
# with Symbol keys; a leaf is returned as it came
|
|
84
|
-
def deep_symbolize(value)
|
|
85
|
-
case value
|
|
86
|
-
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
87
|
-
when Array then value.map { |item| deep_symbolize(item) }
|
|
88
|
-
else value
|
|
89
|
-
end
|
|
90
|
-
end
|
|
84
|
+
def deep_symbolize(value) = Runtime::Value.deep_symbolize(value)
|
|
91
85
|
|
|
92
|
-
# Unwraps a record, a query row, or any value holding `Runtime::Value`s into plain
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# `Handle`, which `materialize` does not recognize.
|
|
86
|
+
# Unwraps a record, a query row, or any value holding `Runtime::Value`s into the plain
|
|
87
|
+
# data a caller is answered with. A single-field value object answers as its bare
|
|
88
|
+
# field (`name: "Margherita"`), the shape a command takes it in; stored state stays
|
|
89
|
+
# wrapped.
|
|
97
90
|
#
|
|
98
91
|
# @param value [Adapters::Driving::Handle, Runtime::Value, Hash, Array, Object] what to
|
|
99
|
-
# unwrap;
|
|
100
|
-
# a `Handle` is read through its `to_h`, so its `:id` comes along
|
|
92
|
+
# unwrap; a `Handle` is read through its `to_h`, so its `:id` comes along
|
|
101
93
|
# @return [Hash, Array, Object] the same data with every `Runtime::Value` replaced
|
|
102
|
-
# by a Hash of its fields
|
|
94
|
+
# by its sole field's value, or by a Hash of its fields when it has more than one;
|
|
95
|
+
# any other value is returned as it came
|
|
103
96
|
def materialize(value)
|
|
104
97
|
value = value.to_h if value.is_a?(Handle)
|
|
105
|
-
Runtime::Value.
|
|
98
|
+
Runtime::Value.materialize_unwrapped(value)
|
|
106
99
|
end
|
|
107
100
|
|
|
108
101
|
# Turns a command's JSON body, raw or already parsed, into the `to:`/`with:`
|
|
@@ -101,8 +101,8 @@ module Hecks
|
|
|
101
101
|
["TranslationAggregate", "move"] => :move_impl,
|
|
102
102
|
["TranslationAggregate", "convert"] => :convert_impl,
|
|
103
103
|
["TranslationAggregate", "retype"] => :retype_impl,
|
|
104
|
-
["TranslationAggregate", "
|
|
105
|
-
["TranslationAggregate", "
|
|
104
|
+
["TranslationAggregate", "derive"] => :derive_impl,
|
|
105
|
+
["TranslationAggregate", "rekey_to"] => :rekey_to_impl,
|
|
106
106
|
["TranslationAggregate", "backfill"] => :backfill_impl,
|
|
107
107
|
["TranslationAggregate", "unresolved"] => :unresolved_impl,
|
|
108
108
|
["Query", "limit"] => :limit_impl,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Bluebook
|
|
3
|
+
module DSL
|
|
4
|
+
# The `derive` and `rekey_to` words of a `.translation` aggregate block: the two rules that
|
|
5
|
+
# compute from the record's own text and need no SQL.
|
|
6
|
+
module DerivationWords
|
|
7
|
+
# Declares a new field computed from the record's own text.
|
|
8
|
+
#
|
|
9
|
+
# The expression reads the held era's data, so it runs before any other rule and sees the
|
|
10
|
+
# fields exactly as they were stored. It changes nothing it reads: remove a source with
|
|
11
|
+
# `drop`. A record where the expression has no value (a field missing or not text) gets
|
|
12
|
+
# no new field.
|
|
13
|
+
#
|
|
14
|
+
# @param to [Symbol, String] the field's path in the destination era
|
|
15
|
+
# @param as [String] the expression, in `Bluebook::TranslationExpression`'s language
|
|
16
|
+
# @return [Array<Bluebook::TranslationDerive>] every derive declared so far, this one last
|
|
17
|
+
# @raise [Bluebook::DSL::Malformed] if `to` or `as` is empty, `to` is derived twice, or `as`
|
|
18
|
+
# is not a well-formed expression
|
|
19
|
+
def derive_impl(to, as:)
|
|
20
|
+
raise Malformed, "a derive needs a destination path" if to.to_s.empty?
|
|
21
|
+
raise Malformed, "a derive needs its as: expression" if as.to_s.empty?
|
|
22
|
+
raise Malformed, "#{to} is derived twice" if derived?(to)
|
|
23
|
+
|
|
24
|
+
@derives << TranslationDerive.new(to.to_s, checked_expression(as))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Declares that the aggregate's identity is the value of a field a `derive` computes.
|
|
28
|
+
#
|
|
29
|
+
# The id is that value for every record where the derive has a value, and stays what it was
|
|
30
|
+
# for the rest.
|
|
31
|
+
#
|
|
32
|
+
# @param to [Symbol, String] the destination field of a `derive` declared earlier
|
|
33
|
+
# @return [Array<Bluebook::TranslationRekey>] every rekey declared so far, this one last
|
|
34
|
+
# @raise [Bluebook::DSL::Malformed] if `to` is empty, no earlier `derive` writes it, or the
|
|
35
|
+
# aggregate is already rekeyed
|
|
36
|
+
def rekey_to_impl(to)
|
|
37
|
+
raise Malformed, "a rekey_to needs the field to take the id from" if to.to_s.empty?
|
|
38
|
+
raise Malformed, "rekey_to #{to} needs a derive of #{to} declared before it" unless derived?(to)
|
|
39
|
+
raise Malformed, "an aggregate is rekeyed once" unless @rekeys.empty?
|
|
40
|
+
|
|
41
|
+
@rekeys << TranslationRekey.new(to.to_s)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def derived?(to) = @derives.any? { |derive| derive.to == to.to_s }
|
|
47
|
+
|
|
48
|
+
# The expression's text, once the language has accepted it.
|
|
49
|
+
def checked_expression(text)
|
|
50
|
+
TranslationExpression.parse(text)
|
|
51
|
+
text.to_s
|
|
52
|
+
rescue TranslationExpression::Refused => e
|
|
53
|
+
raise Malformed, "a derive's as: expression is refused: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require_relative "derivation_words"
|
|
1
2
|
require_relative "word_gate"
|
|
2
3
|
module Hecks
|
|
3
4
|
module Bluebook
|
|
@@ -7,6 +8,7 @@ module Hecks
|
|
|
7
8
|
class TranslationAggregateBuilder
|
|
8
9
|
GRAMMAR_CONTEXT = "TranslationAggregate".freeze
|
|
9
10
|
|
|
11
|
+
include DerivationWords
|
|
10
12
|
include WordGate
|
|
11
13
|
|
|
12
14
|
# @param name [String, Symbol] the aggregate's name in the destination era
|
|
@@ -86,40 +88,6 @@ module Hecks
|
|
|
86
88
|
@retypes << TranslationRetype.new(old_type.to_s, to.to_s)
|
|
87
89
|
end
|
|
88
90
|
|
|
89
|
-
# Declares a field computed by a hand-written Postgres SQL expression.
|
|
90
|
-
#
|
|
91
|
-
# The scaffold never proposes one; a human writes it, and human-sampled review is
|
|
92
|
-
# its only verification.
|
|
93
|
-
#
|
|
94
|
-
# @param old_path [String, Symbol] the source field's path in the held era
|
|
95
|
-
# @param to [String, Symbol] the field's path in the destination era
|
|
96
|
-
# @param sql [String] the Postgres SQL expression computing the destination value
|
|
97
|
-
# @return [Array<Bluebook::TranslationCompute>] every compute declared so far, this one
|
|
98
|
-
# last
|
|
99
|
-
# @raise [Bluebook::DSL::Malformed] if `old_path`, `to`, or `sql` is empty
|
|
100
|
-
def compute_impl(old_path, to:, sql:)
|
|
101
|
-
raise Malformed, "a compute needs a destination path (to:)" if to.to_s.empty?
|
|
102
|
-
raise Malformed, "a compute needs a source path" if old_path.to_s.empty?
|
|
103
|
-
raise Malformed, "a compute needs its sql: expression" if sql.to_s.empty?
|
|
104
|
-
|
|
105
|
-
@computes << TranslationCompute.new(old_path.to_s, to.to_s, sql.to_s)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Declares a hand-written Postgres SQL expression that recomputes the aggregate's own
|
|
109
|
-
# identity.
|
|
110
|
-
#
|
|
111
|
-
# No path arguments, unlike the rules above: only the record's key is recomputed, not
|
|
112
|
-
# state moved into it. Verified the same human-reviewed-only way as `compute`.
|
|
113
|
-
#
|
|
114
|
-
# @param sql [String] the Postgres SQL expression computing the destination identity
|
|
115
|
-
# @return [Array<Bluebook::TranslationRekey>] every rekey declared so far, this one last
|
|
116
|
-
# @raise [Bluebook::DSL::Malformed] if `sql` is empty
|
|
117
|
-
def rekey_impl(sql:)
|
|
118
|
-
raise Malformed, "a rekey needs its sql: expression" if sql.to_s.empty?
|
|
119
|
-
|
|
120
|
-
@rekeys << TranslationRekey.new(sql.to_s)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
91
|
# Declares a newly added, required attribute and the default existing records read
|
|
124
92
|
# until a real value is written.
|
|
125
93
|
#
|
|
@@ -159,7 +127,8 @@ module Hecks
|
|
|
159
127
|
def build
|
|
160
128
|
TranslationAggregate.new(
|
|
161
129
|
name: @name, was: @was, renames: @renames, moves: @moves, converts: @converts,
|
|
162
|
-
drops: @drops, retypes: @retypes,
|
|
130
|
+
drops: @drops, retypes: @retypes, derives: @derives, rekeys: @rekeys,
|
|
131
|
+
backfills: @backfills
|
|
163
132
|
)
|
|
164
133
|
end
|
|
165
134
|
|
|
@@ -171,7 +140,7 @@ module Hecks
|
|
|
171
140
|
@converts = []
|
|
172
141
|
@drops = []
|
|
173
142
|
@retypes = []
|
|
174
|
-
@
|
|
143
|
+
@derives = []
|
|
175
144
|
@rekeys = []
|
|
176
145
|
@backfills = []
|
|
177
146
|
end
|
|
@@ -182,7 +151,7 @@ module Hecks
|
|
|
182
151
|
rendered = Array(candidates).map { |candidate| render_path(candidate) }
|
|
183
152
|
if rendered.empty?
|
|
184
153
|
"#{@name}'s translation leaves #{render_path(name)} unresolved (no candidate matched — " \
|
|
185
|
-
"consider drop
|
|
154
|
+
"consider drop) — replace unresolved with a real rule before booting."
|
|
186
155
|
else
|
|
187
156
|
"#{@name}'s translation leaves #{render_path(name)} unresolved (candidates: #{rendered.join(", ")}) — " \
|
|
188
157
|
"replace unresolved with a rename, move, convert, or drop before booting."
|
|
@@ -97,15 +97,6 @@ module Hecks
|
|
|
97
97
|
"`value >= 100 && value <= 599` in place of `value.between?(100, 599)`"
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
# Every name a rule resolves at its root: the first segment of each `lookup` path, unique.
|
|
101
|
-
def lookup_heads(ast)
|
|
102
|
-
heads = []
|
|
103
|
-
each_node(ast) do |node|
|
|
104
|
-
heads << node["path"].first.to_s if node["op"] == "lookup" && node["path"].is_a?(::Array)
|
|
105
|
-
end
|
|
106
|
-
heads.uniq
|
|
107
|
-
end
|
|
108
|
-
|
|
109
100
|
# Visits `node` and every Hash nested inside it, depth-first.
|
|
110
101
|
def each_node(node, &block)
|
|
111
102
|
case node
|
|
@@ -64,7 +64,7 @@ module Hecks
|
|
|
64
64
|
attr_reader :domain, :binds, :subscriptions, :attachments, :translates, :driving
|
|
65
65
|
|
|
66
66
|
# The driving adapters `driven_by` can name.
|
|
67
|
-
DRIVING_ADAPTERS = %w[Ruby Cli Json Mcp].freeze
|
|
67
|
+
DRIVING_ADAPTERS = %w[Ruby Cli Json Mcp Web].freeze
|
|
68
68
|
|
|
69
69
|
# Every optional keyword and what it holds when the declaration omits it.
|
|
70
70
|
FIELD_DEFAULTS = {
|
|
@@ -3,7 +3,7 @@ module Hecks
|
|
|
3
3
|
module MetaValidator
|
|
4
4
|
class TranslationJudge
|
|
5
5
|
# Offers each rule collection of one translated aggregate: renames, moves, converts,
|
|
6
|
-
# drops, retypes,
|
|
6
|
+
# drops, retypes, derives, rekeys and backfills.
|
|
7
7
|
module Rules
|
|
8
8
|
private
|
|
9
9
|
|
|
@@ -44,19 +44,16 @@ module Hecks
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
Array(aggregate.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
to: v(compute.to),
|
|
52
|
-
sql: v(compute.sql) })
|
|
47
|
+
def judge_derives(aggregate, name)
|
|
48
|
+
Array(aggregate.derives).each do |derive|
|
|
49
|
+
fields = { to: v(derive.to), expression: v(derive.expression) }
|
|
50
|
+
send_to("Translation::TranslationAggregate.AddDerive", name, to: name, with: fields)
|
|
53
51
|
end
|
|
54
52
|
end
|
|
55
53
|
|
|
56
54
|
def judge_rekeys(aggregate, name)
|
|
57
|
-
|
|
58
|
-
send_to("Translation::TranslationAggregate.
|
|
59
|
-
with: { sql: v(rekey.sql) })
|
|
55
|
+
aggregate.rekeys.each do |rekey|
|
|
56
|
+
send_to("Translation::TranslationAggregate.AddRekeyTo", name, to: name, with: { to: v(rekey.to) })
|
|
60
57
|
end
|
|
61
58
|
end
|
|
62
59
|
|
|
@@ -8,6 +8,9 @@ module Hecks
|
|
|
8
8
|
class TranslationJudge
|
|
9
9
|
include Rules
|
|
10
10
|
|
|
11
|
+
# The rule lists an aggregate carries, in the order they are offered to the language.
|
|
12
|
+
RULE_KINDS = %i[renames moves converts drops retypes derives rekeys backfills].freeze
|
|
13
|
+
|
|
11
14
|
attr_reader :refusals
|
|
12
15
|
|
|
13
16
|
# @param translation [Bluebook::Translation] the built translation to judge
|
|
@@ -67,14 +70,7 @@ module Hecks
|
|
|
67
70
|
name = aggregate.name
|
|
68
71
|
declare_aggregate(translation, aggregate, name)
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
judge_moves(aggregate, name)
|
|
72
|
-
judge_converts(aggregate, name)
|
|
73
|
-
judge_drops(aggregate, name)
|
|
74
|
-
judge_retypes(aggregate, name)
|
|
75
|
-
judge_computes(aggregate, name)
|
|
76
|
-
judge_rekeys(aggregate, name)
|
|
77
|
-
judge_backfills(aggregate, name)
|
|
73
|
+
RULE_KINDS.each { |kind| send("judge_#{kind}", aggregate, name) }
|
|
78
74
|
end
|
|
79
75
|
|
|
80
76
|
def declare_aggregate(translation, aggregate, name)
|
|
@@ -203,17 +203,6 @@ module Hecks
|
|
|
203
203
|
registry
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
-
# Whether `grammar_registry`'s memoized singleton has finished its
|
|
207
|
-
# fixpoint judge and attached-chapter load.
|
|
208
|
-
#
|
|
209
|
-
# A cache keyed on "finished" instead of its own inputs already missed
|
|
210
|
-
# data for a whole build window once.
|
|
211
|
-
#
|
|
212
|
-
# @return [Boolean]
|
|
213
|
-
def self.grammar_registry_ready?
|
|
214
|
-
@grammar_registry && @grammar_ready_for == @grammar_registry.object_id
|
|
215
|
-
end
|
|
216
|
-
|
|
217
206
|
# Loads every `ATTACHED_GRAMMAR_DIR` chapter file into `registry`,
|
|
218
207
|
# judged the ordinary way since the fixpoint has already run.
|
|
219
208
|
#
|