hecks 3.14.0 → 4.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/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- 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/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/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/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/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.rb +13 -0
- 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/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.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 +300 -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 +145 -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/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- 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 +990 -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 +483 -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 +229 -112
- 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 +230 -148
- data/rust/src/kernel/mod.rs +46 -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/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 +82 -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
|
@@ -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
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require_relative "../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Composer
|
|
5
|
+
# The agent the Composer asks, on the hecksagon side: one port, `composer_agent`, resolved to
|
|
6
|
+
# the single adapter a registry holds for it. The caller never sees the adapter, only answers
|
|
7
|
+
# this module has checked, so a spec can stand a scripted adapter in for the real one.
|
|
8
|
+
#
|
|
9
|
+
# Two of the asks change the workspace on disk and answer with a note about what changed:
|
|
10
|
+
# `draft` edits a bluebook, `wire` edits the `.hecksagon` and `.world` files that bind it. The
|
|
11
|
+
# third, `classify`, only names what a prompt is asking for.
|
|
12
|
+
module Agent
|
|
13
|
+
NAME = "composer_agent".freeze
|
|
14
|
+
PORT = File.expand_path("ports/composer_agent.port", __dir__).freeze
|
|
15
|
+
ADAPTERS = File.expand_path("adapters", __dir__).freeze
|
|
16
|
+
|
|
17
|
+
# What a prompt can be asking for.
|
|
18
|
+
INTENTS = %w[edit wire question new_domain].freeze
|
|
19
|
+
|
|
20
|
+
# The adapter's answer arrived but was unusable; asking again will not help.
|
|
21
|
+
class ValidationError < StandardError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Nothing answered (missing binary, dead subprocess, timeout); retry or fall back.
|
|
25
|
+
class Unavailable < StandardError
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Asks the agent to change a workspace's bluebooks as the prompt says.
|
|
31
|
+
#
|
|
32
|
+
# @param registry [Runtime::Registry] a registry holding the port and one adapter for it
|
|
33
|
+
# @param prompt [String] what the author asked for
|
|
34
|
+
# @param dir [String] the workspace directory, whose files the agent edits in place
|
|
35
|
+
# @return [String] the agent's one-line account of what it changed
|
|
36
|
+
# @raise [ValidationError] unless the answer is a Hash with a non-blank `"note"`
|
|
37
|
+
# @raise [Unavailable] if the adapter cannot answer
|
|
38
|
+
# @raise [Runtime::WiringError] unless exactly one adapter implements the port
|
|
39
|
+
def draft(registry, prompt:, dir:)
|
|
40
|
+
note(adapter(registry).draft(prompt: prompt, dir: dir))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Asks the agent to change how a workspace's chapter is wired, never what it declares.
|
|
44
|
+
#
|
|
45
|
+
# @param registry [Runtime::Registry] a registry holding the port and one adapter for it
|
|
46
|
+
# @param prompt [String] what the author asked for
|
|
47
|
+
# @param dir [String] the workspace directory, whose `.hecksagon` and `.world` files the
|
|
48
|
+
# agent edits in place
|
|
49
|
+
# @return [String] the agent's one-line account of what it changed
|
|
50
|
+
# @raise [ValidationError] unless the answer is a Hash with a non-blank `"note"`
|
|
51
|
+
# @raise [Unavailable] if the adapter cannot answer
|
|
52
|
+
def wire(registry, prompt:, dir:)
|
|
53
|
+
note(adapter(registry).wire(prompt: prompt, dir: dir))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Names what a prompt is asking for.
|
|
57
|
+
#
|
|
58
|
+
# @param registry [Runtime::Registry] a registry holding the port and one adapter for it
|
|
59
|
+
# @param prompt [String] what the author typed
|
|
60
|
+
# @return [String] one of `Composer::Agent::INTENTS`
|
|
61
|
+
# @raise [ValidationError] unless the answer is a Hash whose `"intent"` is one of those
|
|
62
|
+
# @raise [Unavailable] if the adapter cannot answer
|
|
63
|
+
def classify(registry, prompt:)
|
|
64
|
+
answer = adapter(registry).classify(prompt: prompt)
|
|
65
|
+
intent = answer["intent"] if answer.is_a?(Hash)
|
|
66
|
+
return intent if INTENTS.include?(intent)
|
|
67
|
+
|
|
68
|
+
raise ValidationError, "an intent is one of #{INTENTS.join(", ")}, got #{intent.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Builds a registry in which exactly one adapter implements the port.
|
|
72
|
+
#
|
|
73
|
+
# @param adapter_file [String] the adapter's `.adapter` declaration
|
|
74
|
+
# @return [Runtime::Registry] a registry holding the port and that adapter
|
|
75
|
+
def registry_with(adapter_file)
|
|
76
|
+
registry = Runtime::Registry.new
|
|
77
|
+
Hecks.with_registry(registry) do
|
|
78
|
+
Kernel.load(PORT)
|
|
79
|
+
Kernel.load(adapter_file)
|
|
80
|
+
end
|
|
81
|
+
registry
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Finds the single adapter bound to the port.
|
|
85
|
+
#
|
|
86
|
+
# @param registry [Runtime::Registry] the registry to search
|
|
87
|
+
# @return [Module] the adapter implementing the port
|
|
88
|
+
# @raise [Runtime::WiringError] if none, or more than one, implements the port
|
|
89
|
+
def adapter(registry)
|
|
90
|
+
implementations = registry.adapters.values.select { |found| found.port == NAME }
|
|
91
|
+
return registry.adapter_class(implementations.first.name) if implementations.size == 1
|
|
92
|
+
|
|
93
|
+
raise Runtime::WiringError, wiring_refusal(implementations)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @api private
|
|
97
|
+
def note(answer)
|
|
98
|
+
text = answer["note"].to_s.strip if answer.is_a?(Hash)
|
|
99
|
+
return text unless text.nil? || text.empty?
|
|
100
|
+
|
|
101
|
+
raise ValidationError, "an agent's answer carries a non-blank \"note\", got #{answer.inspect}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @api private
|
|
105
|
+
def wiring_refusal(implementations)
|
|
106
|
+
return "no adapter implements the #{NAME} port — nothing can edit the draft" if implementations.empty?
|
|
107
|
+
|
|
108
|
+
names = implementations.map(&:name).sort.join(", ")
|
|
109
|
+
"#{implementations.size} adapters implement the #{NAME} port (#{names}) — the runtime will not choose for you"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|