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
|
@@ -16,13 +16,16 @@ module Hecks
|
|
|
16
16
|
# The attribute kept its name, so `rename`/`move` cannot express it.
|
|
17
17
|
TranslationRetype = Struct.new(:from, :to)
|
|
18
18
|
|
|
19
|
-
# A computed
|
|
20
|
-
#
|
|
21
|
-
|
|
19
|
+
# A new field computed from the record's own text by an expression, read before any other
|
|
20
|
+
# rule runs. The fields the expression reads stay where they are; `drop` removes them.
|
|
21
|
+
# `expression` is the source text and `tree` its parsed plan form.
|
|
22
|
+
TranslationDerive = Struct.new(:to, :expression) do
|
|
23
|
+
def tree = TranslationExpression.parse(expression)
|
|
24
|
+
end
|
|
22
25
|
|
|
23
|
-
# The aggregate's
|
|
24
|
-
#
|
|
25
|
-
TranslationRekey = Struct.new(:
|
|
26
|
+
# The aggregate's id is the value of the field a `derive` writes, for every record where that
|
|
27
|
+
# derive has a value; a record where it has none keeps its id.
|
|
28
|
+
TranslationRekey = Struct.new(:to)
|
|
26
29
|
|
|
27
30
|
# A newly added, required attribute with no source in old data.
|
|
28
31
|
# `default` is what an existing record reads until a command writes a real value;
|
|
@@ -32,12 +35,12 @@ module Hecks
|
|
|
32
35
|
# One aggregate's part of a translation between eras.
|
|
33
36
|
# `drops` makes data loss a declared decision.
|
|
34
37
|
class TranslationAggregate
|
|
35
|
-
attr_reader :name, :was, :renames, :moves, :converts, :drops, :retypes, :
|
|
38
|
+
attr_reader :name, :was, :renames, :moves, :converts, :drops, :retypes, :derives, :rekeys, :backfills
|
|
36
39
|
|
|
37
40
|
# Every optional field and what it holds when the declaration omits it.
|
|
38
41
|
FIELD_DEFAULTS = {
|
|
39
42
|
was: nil, renames: {}, moves: [], converts: [], drops: [], retypes: [],
|
|
40
|
-
|
|
43
|
+
derives: [], rekeys: [], backfills: []
|
|
41
44
|
}.freeze
|
|
42
45
|
|
|
43
46
|
# @param name [String, Symbol] the aggregate's name in the destination era
|
|
@@ -47,8 +50,8 @@ module Hecks
|
|
|
47
50
|
# @param converts [Array<Bluebook::TranslationConvert>] fields remapped via a lookup table
|
|
48
51
|
# @param drops [Array<Symbol>] fields deliberately not carried forward
|
|
49
52
|
# @param retypes [Array<Bluebook::TranslationRetype>] value object or entity type changes
|
|
50
|
-
# @param
|
|
51
|
-
# @param rekeys [Array<Bluebook::TranslationRekey>]
|
|
53
|
+
# @param derives [Array<Bluebook::TranslationDerive>] fields computed from the record's text
|
|
54
|
+
# @param rekeys [Array<Bluebook::TranslationRekey>] the aggregate's identity, recomputed
|
|
52
55
|
# @param backfills [Array<Bluebook::TranslationBackfill>] new required fields, no old source
|
|
53
56
|
def initialize(name:, **given)
|
|
54
57
|
KeywordFields.assign(self, KeywordFields.fill(given, FIELD_DEFAULTS))
|
|
@@ -83,6 +86,11 @@ module Hecks
|
|
|
83
86
|
# @return [Bluebook::TranslationAggregate, nil] the aggregate's translation, or
|
|
84
87
|
# `nil` if `name` carries no translation rules
|
|
85
88
|
def for_aggregate(name) = @aggregates.find { |aggregate| aggregate.name == name.to_s }
|
|
89
|
+
|
|
90
|
+
# This edge as plain data: each aggregate's rules as an ordered list of ops.
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash] see `Bluebook::TranslationPlan`
|
|
93
|
+
def to_plan = TranslationPlan.build(self)
|
|
86
94
|
end
|
|
87
95
|
end
|
|
88
96
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Bluebook
|
|
3
|
+
module TranslationExpression
|
|
4
|
+
# Computes a parsed expression for one record's state. The reference semantics the
|
|
5
|
+
# compiled Postgres form is held to.
|
|
6
|
+
module Evaluator
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# @param tree [Array] a tree `TranslationExpression.parse` returned
|
|
10
|
+
# @param state [Hash] the record's stored state
|
|
11
|
+
# @return [String, nil] the computed text; nil when the expression has no value
|
|
12
|
+
def call(tree, state)
|
|
13
|
+
case tree.first
|
|
14
|
+
when "lit" then tree[1]
|
|
15
|
+
when "null" then nil
|
|
16
|
+
when "ref" then reference(tree[1], state)
|
|
17
|
+
else apply(tree.first, tree.drop(1), state)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Reads a dotted path out of a record; only text counts as a value.
|
|
22
|
+
def reference(path, state)
|
|
23
|
+
value = path.split(".").inject(state) { |held, key| held.is_a?(Hash) ? member(held, key) : nil }
|
|
24
|
+
value.is_a?(String) ? value : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A record's top-level keys are symbols, a value object's are strings.
|
|
28
|
+
def member(hash, key) = hash.key?(key.to_sym) ? hash[key.to_sym] : hash[key]
|
|
29
|
+
|
|
30
|
+
def apply(name, arguments, state)
|
|
31
|
+
return lookup(arguments, state) if name == "lookup"
|
|
32
|
+
return replace(arguments, state) if name == "replace"
|
|
33
|
+
|
|
34
|
+
values = arguments.map { |argument| call(argument, state) }
|
|
35
|
+
return nil if values.any?(&:nil?)
|
|
36
|
+
|
|
37
|
+
fold(name, values)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def fold(name, values)
|
|
41
|
+
case name
|
|
42
|
+
when "lower" then values.first.downcase(:ascii)
|
|
43
|
+
when "upper" then values.first.upcase(:ascii)
|
|
44
|
+
else values.join
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def replace(arguments, state)
|
|
49
|
+
target, from, to = arguments
|
|
50
|
+
call(target, state)&.gsub(from.last) { to.last }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def lookup(arguments, state)
|
|
54
|
+
key, table, fallback = arguments
|
|
55
|
+
value = call(key, state)
|
|
56
|
+
return nil if value.nil?
|
|
57
|
+
|
|
58
|
+
table.fetch(value) { call(fallback, state) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "strscan"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Bluebook
|
|
6
|
+
module TranslationExpression
|
|
7
|
+
# Reads the text of an expression into its tree, refusing anything outside the language.
|
|
8
|
+
class Parser
|
|
9
|
+
NAME = /[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*/
|
|
10
|
+
STRING = /"(?:[^"\\]|\\.)*"/
|
|
11
|
+
|
|
12
|
+
# @param text [String] the expression's source text
|
|
13
|
+
def initialize(text)
|
|
14
|
+
@text = text.to_s
|
|
15
|
+
@scan = StringScanner.new(@text)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Array] the expression tree
|
|
19
|
+
# @raise [TranslationExpression::Refused] if the text is not a well-formed expression
|
|
20
|
+
def call
|
|
21
|
+
tree = expression
|
|
22
|
+
skip
|
|
23
|
+
refuse("unexpected #{@scan.rest[0, 12].inspect} after the expression") unless @scan.eos?
|
|
24
|
+
tree
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def expression
|
|
30
|
+
skip
|
|
31
|
+
return ["lit", string] if @scan.check(/"/)
|
|
32
|
+
|
|
33
|
+
name = @scan.scan(NAME) || refuse("expected a field, a string or a function")
|
|
34
|
+
return function(name) if @scan.check(/\s*\(/)
|
|
35
|
+
|
|
36
|
+
name == "null" ? ["null"] : ["ref", name]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def function(name)
|
|
40
|
+
refuse("#{name} is not a function (#{FUNCTIONS.keys.join(", ")})") unless FUNCTIONS.key?(name)
|
|
41
|
+
punct("(")
|
|
42
|
+
arguments = list { argument(name, _1) }
|
|
43
|
+
punct(")")
|
|
44
|
+
check_arguments(name, arguments)
|
|
45
|
+
[name, *arguments]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def argument(name, index)
|
|
49
|
+
skip
|
|
50
|
+
return table if name == "lookup" && index == 1
|
|
51
|
+
|
|
52
|
+
expression
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def list
|
|
56
|
+
items = []
|
|
57
|
+
loop do
|
|
58
|
+
items << yield(items.size)
|
|
59
|
+
skip
|
|
60
|
+
break unless @scan.skip(/,/)
|
|
61
|
+
end
|
|
62
|
+
items
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def table
|
|
66
|
+
punct("{")
|
|
67
|
+
pairs = list { pair }
|
|
68
|
+
punct("}")
|
|
69
|
+
refuse("a lookup table repeats a key") unless pairs.map(&:first).uniq.size == pairs.size
|
|
70
|
+
pairs.to_h
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def pair
|
|
74
|
+
skip
|
|
75
|
+
key = string
|
|
76
|
+
punct(":")
|
|
77
|
+
skip
|
|
78
|
+
[key, string]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def string
|
|
82
|
+
token = @scan.scan(STRING) || refuse("expected a string")
|
|
83
|
+
JSON.parse(token, quirks_mode: true)
|
|
84
|
+
rescue JSON::ParserError
|
|
85
|
+
refuse("#{token} is not a well-formed string")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def punct(mark)
|
|
89
|
+
skip
|
|
90
|
+
@scan.skip(Regexp.new(Regexp.escape(mark))) || refuse("expected #{mark.inspect}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def skip = @scan.skip(/\s+/)
|
|
94
|
+
|
|
95
|
+
def check_arguments(name, arguments)
|
|
96
|
+
arity = FUNCTIONS.fetch(name)
|
|
97
|
+
refuse("#{name} takes #{arity} expressions, not #{arguments.size}") if arity && arguments.size != arity
|
|
98
|
+
refuse("concat takes two or more expressions") if arity.nil? && arguments.size < 2
|
|
99
|
+
check_literals(name, arguments)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def check_literals(name, arguments)
|
|
103
|
+
case name
|
|
104
|
+
when "replace" then check_replace(arguments)
|
|
105
|
+
when "lookup" then check_lookup(arguments)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def check_replace(arguments)
|
|
110
|
+
_target, from, to = arguments
|
|
111
|
+
refuse("replace needs string literals for what to find and what to put") unless [from, to].all? { _1.first == "lit" }
|
|
112
|
+
refuse("replace cannot look for an empty string") if from.last.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def check_lookup(arguments)
|
|
116
|
+
_key, table, fallback = arguments
|
|
117
|
+
refuse("lookup needs a table of at least one pair") unless table.is_a?(Hash) && !table.empty?
|
|
118
|
+
refuse("lookup needs a string literal or null as its fallback") unless %w[lit null].include?(fallback.first)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def refuse(why)
|
|
122
|
+
raise Refused, "#{why} (in #{@text.inspect} at offset #{@scan.pos})"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require_relative "translation_expression/parser"
|
|
2
|
+
require_relative "translation_expression/evaluator"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Bluebook
|
|
6
|
+
# The expression a `derive` rule computes a new field from: text functions over the record's
|
|
7
|
+
# stored text, with no storage engine in sight. It parses to a tree of plain arrays, which
|
|
8
|
+
# is also its plan form:
|
|
9
|
+
#
|
|
10
|
+
# ["ref", "name.value"] ["lit", "text"] ["null"]
|
|
11
|
+
# ["lower", x] ["upper", x] ["concat", x, y, ...]
|
|
12
|
+
# ["replace", x, ["lit", from], ["lit", to]]
|
|
13
|
+
# ["lookup", x, {"key" => "value"}, ["lit", default] | ["null"]]
|
|
14
|
+
#
|
|
15
|
+
# A reference that is missing or not text has no value, and a function with a valueless
|
|
16
|
+
# argument has none either.
|
|
17
|
+
module TranslationExpression
|
|
18
|
+
# Raised when the text of an expression is not one the language accepts.
|
|
19
|
+
class Refused < StandardError; end
|
|
20
|
+
|
|
21
|
+
# The functions and how many expressions each takes; `nil` means "two or more".
|
|
22
|
+
FUNCTIONS = { "lower" => 1, "upper" => 1, "concat" => nil, "replace" => 3, "lookup" => 3 }.freeze
|
|
23
|
+
|
|
24
|
+
# Parses an expression's text into its tree.
|
|
25
|
+
#
|
|
26
|
+
# @param text [String] the expression, e.g. `lower(name.value)`
|
|
27
|
+
# @return [Array] the expression tree
|
|
28
|
+
# @raise [Refused] if the text is not a well-formed expression
|
|
29
|
+
def self.parse(text) = Parser.new(text).call
|
|
30
|
+
|
|
31
|
+
# Computes an expression's value for one record's state.
|
|
32
|
+
#
|
|
33
|
+
# @param tree [Array] a tree `parse` returned
|
|
34
|
+
# @param state [Hash] the record's stored state
|
|
35
|
+
# @return [String, nil] the text it computes; nil when it has no value
|
|
36
|
+
def self.evaluate(tree, state) = Evaluator.call(tree, state)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Bluebook
|
|
3
|
+
# A translation as data: each aggregate's rules as an ordered list of ops any runtime can
|
|
4
|
+
# interpret, with nothing in them that names a storage engine. In the order they apply to a
|
|
5
|
+
# record, each op has its name under "op" plus:
|
|
6
|
+
#
|
|
7
|
+
# derive "to" "expression" (a tree) rename "from" "to" move "from" "to" (dotted paths)
|
|
8
|
+
# convert "from" "to" "values" ([old, new] pairs) drop "path" retype "from" "to"
|
|
9
|
+
# backfill "path" "default" rekey_to "to"
|
|
10
|
+
module TranslationPlan
|
|
11
|
+
VERSION = 1
|
|
12
|
+
|
|
13
|
+
# The op names, in the order they apply; one per rule list of a `TranslationAggregate`.
|
|
14
|
+
OPS = %w[derive rename move convert drop retype backfill rekey_to].freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# @param translation [Bluebook::Translation] a declared translation edge
|
|
19
|
+
# @return [Hash] the edge as plain data, ready for `JSON.generate`
|
|
20
|
+
def build(translation)
|
|
21
|
+
{
|
|
22
|
+
"version" => VERSION, "domain" => translation.domain, "from" => translation.from.to_s,
|
|
23
|
+
"to" => translation.to.to_s, "retired" => translation.retired.map(&:to_s),
|
|
24
|
+
"aggregates" => translation.aggregates.map { |aggregate| aggregate_plan(aggregate) }
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def aggregate_plan(aggregate)
|
|
29
|
+
{ "name" => aggregate.name, "was" => aggregate.was, "ops" => ops(aggregate) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ops(aggregate)
|
|
33
|
+
[*derives(aggregate), *renames(aggregate), *moves(aggregate), *converts(aggregate), *drops(aggregate),
|
|
34
|
+
*retypes(aggregate), *backfills(aggregate), *rekeys(aggregate)]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def derives(aggregate)
|
|
38
|
+
aggregate.derives.map { |d| { "op" => "derive", "to" => d.to, "expression" => d.tree } }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def renames(aggregate)
|
|
42
|
+
aggregate.renames.map { |from, to| { "op" => "rename", "from" => from.to_s, "to" => to.to_s } }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def moves(aggregate)
|
|
46
|
+
aggregate.moves.map { |m| { "op" => "move", "from" => m.from, "to" => m.to } }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def converts(aggregate)
|
|
50
|
+
aggregate.converts.map do |c|
|
|
51
|
+
{ "op" => "convert", "from" => c.from, "to" => c.to, "values" => c.values.map { |old, new| [old, new] } }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def drops(aggregate) = aggregate.drops.map { |path| { "op" => "drop", "path" => path.to_s } }
|
|
56
|
+
|
|
57
|
+
def retypes(aggregate)
|
|
58
|
+
aggregate.retypes.map { |r| { "op" => "retype", "from" => r.from, "to" => r.to } }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def backfills(aggregate)
|
|
62
|
+
aggregate.backfills.map { |b| { "op" => "backfill", "path" => b.name.to_s, "default" => b.default } }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def rekeys(aggregate)
|
|
66
|
+
aggregate.rekeys.map { |rekey| { "op" => "rekey_to", "to" => rekey.to } }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/hecks/bluebook.rb
CHANGED
|
@@ -29,7 +29,9 @@ require_relative "bluebook/ask_resolution"
|
|
|
29
29
|
require_relative "bluebook/process_manager"
|
|
30
30
|
require_relative "bluebook/aggregate"
|
|
31
31
|
require_relative "bluebook/hecksagon"
|
|
32
|
+
require_relative "bluebook/translation_expression"
|
|
32
33
|
require_relative "bluebook/translation"
|
|
34
|
+
require_relative "bluebook/translation_plan"
|
|
33
35
|
|
|
34
36
|
require_relative "bluebook/assembly/contract"
|
|
35
37
|
require_relative "bluebook/assembly/contracts"
|
data/lib/hecks/chapters.rb
CHANGED
|
@@ -5,8 +5,8 @@ require_relative "bluebook/meta_validator"
|
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
# The chapters the gem carries that a hecksagon can attach by name (ADR 0080): the language
|
|
8
|
-
# declared in itself, Expression, Tenancy, Deploy, Site, Tickets and QualityControl.
|
|
9
|
-
# members stay in `Framework`; `table` and `attach!` find a name across both.
|
|
8
|
+
# declared in itself, Expression, Tenancy, Deploy, Site, Tickets, Composer and QualityControl.
|
|
9
|
+
# Framework members stay in `Framework`; `table` and `attach!` find a name across both.
|
|
10
10
|
#
|
|
11
11
|
# A chapter is named by the `Hecks.bluebook "Name"` header of its files, and may span several.
|
|
12
12
|
# Beside its bluebook a chapter may carry what every hecksagon attaching it needs, whatever
|
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
# Where attachable chapters live, relative to `lib/hecks/`.
|
|
17
17
|
GLOBS = %w[language/**/*.bluebook grammar/expression.bluebook tenancy/bluebook/*.bluebook
|
|
18
18
|
deploy/bluebook/*.bluebook site/bluebook/*.bluebook tickets/bluebook/*.bluebook
|
|
19
|
-
quality_control/*.bluebook].freeze
|
|
19
|
+
composer/bluebook/*.bluebook quality_control/*.bluebook].freeze
|
|
20
20
|
|
|
21
21
|
# Every attachable chapter, by name, with the files that declare it.
|
|
22
22
|
#
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
require_relative "../agent"
|
|
6
|
+
require_relative "jev"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Adapters
|
|
10
|
+
# Real `composer_agent` fulfillment for `draft` and `wire`: the developer's own `claude` run
|
|
11
|
+
# as the `hecks-composer` agent, which edits the workspace's files in place. Transport only;
|
|
12
|
+
# whether the edit is any good is the Composer's own check to judge.
|
|
13
|
+
#
|
|
14
|
+
# The agent's write guard confines it to domain files inside the git checkout, so the
|
|
15
|
+
# workspace must sit inside one.
|
|
16
|
+
module ClaudeComposer
|
|
17
|
+
TIMEOUT_SECONDS = 300
|
|
18
|
+
|
|
19
|
+
AGENT = "hecks-composer".freeze
|
|
20
|
+
|
|
21
|
+
DRAFT = "Edit the domain files in %<dir>s as the request below says. Change only what it asks " \
|
|
22
|
+
"for, write the bluebook first, then any hecksagon wiring it needs. Reply with one line " \
|
|
23
|
+
"saying what you changed.".freeze
|
|
24
|
+
|
|
25
|
+
WIRE = "Change how the chapter in %<dir>s is wired, as the request below says: edit only its " \
|
|
26
|
+
".hecksagon and .world files, and bind adapters that already exist. Reply with one line " \
|
|
27
|
+
"saying what you changed.".freeze
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# @param prompt [String] what the author asked for
|
|
32
|
+
# @param dir [String] the workspace directory
|
|
33
|
+
# @return [Hash] `"note"` the agent's account of what it changed
|
|
34
|
+
# @raise [Ports::Agent::Unavailable] see {Composer::Agent::Unavailable}
|
|
35
|
+
def draft(prompt:, dir:) = call(DRAFT, prompt, dir)
|
|
36
|
+
|
|
37
|
+
# @param prompt [String] what the author asked for
|
|
38
|
+
# @param dir [String] the workspace directory
|
|
39
|
+
# @return [Hash] `"note"` the agent's account of what it changed
|
|
40
|
+
def wire(prompt:, dir:) = call(WIRE, prompt, dir)
|
|
41
|
+
|
|
42
|
+
# Naming what a prompt asks for is the `jev` categorizer's work, which this adapter lends.
|
|
43
|
+
#
|
|
44
|
+
# @param prompt [String] what the author typed
|
|
45
|
+
# @return [Hash] `"intent"` one of {Composer::Agent::INTENTS}
|
|
46
|
+
# @raise [Composer::Agent::Unavailable] if `jev` cannot answer
|
|
47
|
+
def classify(prompt:) = Jev.classify(prompt: prompt)
|
|
48
|
+
|
|
49
|
+
# @api private
|
|
50
|
+
def call(task, prompt, dir)
|
|
51
|
+
root = checkout_of(dir)
|
|
52
|
+
message = "#{format(task, dir: dir)}\n\nRequest: #{prompt}"
|
|
53
|
+
stdout, status = run(root, message)
|
|
54
|
+
raise Composer::Agent::Unavailable, "claude exited #{status.exitstatus}: #{stdout}" unless status.success?
|
|
55
|
+
|
|
56
|
+
{ "note" => JSON.parse(stdout).fetch("result").to_s.lines.first.to_s.strip }
|
|
57
|
+
rescue JSON::ParserError, KeyError => e
|
|
58
|
+
raise Composer::Agent::ValidationError, "claude's reply was not the expected envelope: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Argv spawn, never a shell, so a `claude` shell alias cannot apply.
|
|
62
|
+
# @api private
|
|
63
|
+
def run(root, message)
|
|
64
|
+
Timeout.timeout(TIMEOUT_SECONDS) do
|
|
65
|
+
Open3.capture2("claude", "-p", "--agent", AGENT, "--output-format", "json",
|
|
66
|
+
"--permission-mode", "acceptEdits", stdin_data: message, chdir: root)
|
|
67
|
+
end
|
|
68
|
+
rescue Timeout::Error
|
|
69
|
+
raise Composer::Agent::Unavailable, "claude did not answer within #{TIMEOUT_SECONDS}s"
|
|
70
|
+
rescue Errno::ENOENT => e
|
|
71
|
+
raise Composer::Agent::Unavailable, "claude is not on PATH: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @api private
|
|
75
|
+
def checkout_of(dir)
|
|
76
|
+
top, status = Open3.capture2("git", "-C", dir, "rev-parse", "--show-toplevel")
|
|
77
|
+
raise Composer::Agent::Unavailable, "#{dir} is not inside a git checkout" unless status.success?
|
|
78
|
+
|
|
79
|
+
top.strip
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require_relative "../agent"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
# A scripted `composer_agent`, for specs and for trying the editor with no model behind it.
|
|
6
|
+
# Each ask is recorded; `draft` and `wire` run a script that may rewrite the workspace's files,
|
|
7
|
+
# so a test sees the same thing the real agent leaves behind.
|
|
8
|
+
module FakeAgent
|
|
9
|
+
class << self
|
|
10
|
+
# @return [Array<Hash>] every ask, as `{ask:, prompt:, dir:}` in the order made
|
|
11
|
+
def calls = @calls ||= []
|
|
12
|
+
|
|
13
|
+
# @return [#call, nil] takes `(prompt, dir)` and edits files under `dir`; may return a note
|
|
14
|
+
attr_accessor :edit
|
|
15
|
+
|
|
16
|
+
# @return [#call, nil] takes `prompt` and answers an intent
|
|
17
|
+
attr_accessor :intent
|
|
18
|
+
|
|
19
|
+
# Forgets the calls and both scripts.
|
|
20
|
+
# @return [void]
|
|
21
|
+
def reset!
|
|
22
|
+
@calls = []
|
|
23
|
+
@edit = nil
|
|
24
|
+
@intent = nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# @param prompt [String] what the author asked for
|
|
31
|
+
# @param dir [String] the workspace directory
|
|
32
|
+
# @return [Hash] `"note"` what the script reported
|
|
33
|
+
def draft(prompt:, dir:) = edited(:draft, prompt, dir)
|
|
34
|
+
|
|
35
|
+
# @param prompt [String] what the author asked for
|
|
36
|
+
# @param dir [String] the workspace directory
|
|
37
|
+
# @return [Hash] `"note"` what the script reported
|
|
38
|
+
def wire(prompt:, dir:) = edited(:wire, prompt, dir)
|
|
39
|
+
|
|
40
|
+
# @param prompt [String] what the author typed
|
|
41
|
+
# @return [Hash] `"intent"` what the script answered, `"edit"` without one
|
|
42
|
+
def classify(prompt:)
|
|
43
|
+
FakeAgent.calls << { ask: :classify, prompt: prompt, dir: nil }
|
|
44
|
+
{ "intent" => FakeAgent.intent ? FakeAgent.intent.call(prompt) : "edit" }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @api private
|
|
48
|
+
def edited(ask, prompt, dir)
|
|
49
|
+
FakeAgent.calls << { ask: ask, prompt: prompt, dir: dir }
|
|
50
|
+
note = FakeAgent.edit&.call(prompt, dir)
|
|
51
|
+
{ "note" => note.is_a?(String) ? note : "changed nothing" }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
require_relative "../agent"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# `composer_agent` fulfillment for `classify` alone: the `jev` categorizer, which answers a
|
|
10
|
+
# multiple-choice question about a piece of text. It carries no key of its own; the script
|
|
11
|
+
# reads one from the environment or from 1Password, so nothing secret is written here.
|
|
12
|
+
#
|
|
13
|
+
# `draft` and `wire` are not this adapter's work.
|
|
14
|
+
module Jev
|
|
15
|
+
QUESTIONS = {
|
|
16
|
+
"intent" => {
|
|
17
|
+
"type" => "choice",
|
|
18
|
+
"instructions" => "What is the author of a domain bluebook asking an editor to do?",
|
|
19
|
+
"criteria" => {
|
|
20
|
+
"edit" => "change what the domain declares: an aggregate, command, attribute, rule or lifecycle",
|
|
21
|
+
"wire" => "change how the domain is wired to the outside: persistence, adapters, ports, deployment",
|
|
22
|
+
"question" => "explain or answer something about the domain, changing nothing",
|
|
23
|
+
"new_domain" => "start a new domain from a description of a business"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# @param prompt [String] what the author typed
|
|
31
|
+
# @return [Hash] `"intent"` one of {Composer::Agent::INTENTS}
|
|
32
|
+
# @raise [Composer::Agent::Unavailable] if `jev` is missing, fails or answers nothing
|
|
33
|
+
def classify(prompt:)
|
|
34
|
+
answers = ask(prompt)
|
|
35
|
+
{ "intent" => choice(answers["intent"]) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @api private
|
|
39
|
+
def ask(prompt)
|
|
40
|
+
Dir.mktmpdir("hecks-jev") { |dir| JSON.parse(run(prompt, File.join(dir, "questions.json"))) }
|
|
41
|
+
rescue Errno::ENOENT => e
|
|
42
|
+
raise Composer::Agent::Unavailable, "jev is not on PATH: #{e.message}"
|
|
43
|
+
rescue JSON::ParserError => e
|
|
44
|
+
raise Composer::Agent::Unavailable, "jev's answer was not JSON: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Runs `jev` on the prompt, with the questions written to `path` beside it.
|
|
48
|
+
# @api private
|
|
49
|
+
def run(prompt, path)
|
|
50
|
+
File.write(path, JSON.generate(QUESTIONS))
|
|
51
|
+
out, err, status = Open3.capture3(ENV.fetch("HECKS_JEV", "jev"), path, "-", stdin_data: prompt)
|
|
52
|
+
return out if status.success?
|
|
53
|
+
|
|
54
|
+
raise Composer::Agent::Unavailable, "jev exited #{status.exitstatus}: #{err.lines.first}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# `jev` names a choice by its key; accept the bare key or a row that carries it.
|
|
58
|
+
# @api private
|
|
59
|
+
def choice(answer)
|
|
60
|
+
return answer unless answer.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
answer.values_at("choice", "answer", "value", "label").compact.first
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|