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
|
@@ -62,7 +62,7 @@ module Hecks
|
|
|
62
62
|
|
|
63
63
|
# Permission rules are a feature of `claude`: any other command would run unconfined.
|
|
64
64
|
def refuse_unconfinable!(words, profile)
|
|
65
|
-
return if profile.nil? ||
|
|
65
|
+
return if profile.nil? || words == default_command(profile)
|
|
66
66
|
|
|
67
67
|
raise Failed, "permission confinement applies only to the default claude command"
|
|
68
68
|
end
|
|
@@ -79,15 +79,14 @@ module Hecks
|
|
|
79
79
|
%w[claude -p --permission-mode] + [profile.permission_mode] + profile.tool_flags
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# Runs `words`
|
|
83
|
-
#
|
|
82
|
+
# Runs `words` with only the environment the profile names, in its own process group so a
|
|
83
|
+
# timeout takes the agent's children with it. The reader starts first so an agent that
|
|
84
|
+
# writes before it has read its whole prompt cannot block both sides on a full pipe.
|
|
84
85
|
def run_confined(words, prompt, chdir, profile)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Open3.popen2e(profile.environment, *profile.confine(words),
|
|
86
|
+
Open3.popen2e(profile.environment, *words,
|
|
88
87
|
chdir: chdir, unsetenv_others: true, pgroup: true) do |stdin, out, waiter|
|
|
89
|
-
feed(stdin, prompt)
|
|
90
88
|
reader = Thread.new { out.read }
|
|
89
|
+
feed(stdin, prompt)
|
|
91
90
|
finish(waiter, reader, profile.timeout)
|
|
92
91
|
end
|
|
93
92
|
end
|
|
@@ -1,93 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "tmpdir"
|
|
4
|
-
|
|
5
3
|
module Hecks
|
|
6
4
|
module Adapters
|
|
7
|
-
# What one `Agent` run may do: which tools it holds, where it may write,
|
|
8
|
-
#
|
|
5
|
+
# What one `Agent` run may do: which tools it holds, where it may write, which environment
|
|
6
|
+
# variables it sees and how long it may run.
|
|
9
7
|
#
|
|
10
|
-
# Confinement
|
|
11
|
-
#
|
|
8
|
+
# Confinement is the permission rules of the `claude` command itself, so a run keeps the
|
|
9
|
+
# user's own login. Only the default `claude` command understands them; `Agent` refuses to
|
|
10
|
+
# run any other command under a profile.
|
|
12
11
|
class AgentProfile
|
|
13
|
-
# What the network rule lets through: nothing, outbound HTTPS and name lookups, or anything.
|
|
14
|
-
NETWORKS = %i[none https any].freeze
|
|
15
|
-
|
|
16
|
-
# How a run is confined: under the operating system's sandbox, or by the permission rules
|
|
17
|
-
# of the `claude` command itself. Only the second leaves the user's own login usable, since
|
|
18
|
-
# the sandbox refuses the keychain and `~/.claude`.
|
|
19
|
-
CONFINEMENTS = %i[sandbox permissions].freeze
|
|
20
|
-
|
|
21
12
|
# The variables every confined agent sees besides the ones its profile names.
|
|
22
13
|
BASE_ENV = %w[PATH HOME USER LOGNAME SHELL LANG LC_ALL TERM TMPDIR].freeze
|
|
23
14
|
|
|
24
|
-
# The tools that write files; a
|
|
15
|
+
# The tools that write files; a confined run holds them only through path rules.
|
|
25
16
|
WRITING_TOOLS = %w[Write Edit MultiEdit NotebookEdit].freeze
|
|
26
17
|
|
|
27
|
-
# Home-relative places an agent may not read: credentials and the harness's own state.
|
|
28
|
-
CREDENTIAL_PATHS = %w[
|
|
29
|
-
.ssh .aws .gnupg .claude .config/gh .config/op .gem/credentials .netrc Library/Keychains
|
|
30
|
-
].freeze
|
|
31
|
-
|
|
32
|
-
SANDBOX = "/usr/bin/sandbox-exec"
|
|
33
|
-
|
|
34
|
-
# The keywords `new` takes besides the four lists, with what each defaults to.
|
|
35
|
-
LIMIT_DEFAULTS = { timeout: nil, budget: nil, confinement: :sandbox }.freeze
|
|
36
|
-
|
|
37
18
|
# @return [Array<String>] the tools the agent is given
|
|
38
19
|
attr_reader :tools
|
|
39
|
-
# @return [Array<String>] the directories the agent may write
|
|
20
|
+
# @return [Array<String>] the directories the agent may write
|
|
40
21
|
attr_reader :writable
|
|
41
|
-
# @return [Symbol] one of `NETWORKS`
|
|
42
|
-
attr_reader :network
|
|
43
22
|
# @return [Array<String>] the environment variable names the agent may see besides `BASE_ENV`
|
|
44
23
|
attr_reader :env
|
|
45
24
|
# @return [Integer, nil] seconds the run may take; nil for no limit
|
|
46
25
|
attr_reader :timeout
|
|
47
26
|
# @return [Float, nil] the most the run may spend, in dollars, for an agent that takes one
|
|
48
27
|
attr_reader :budget
|
|
49
|
-
# @return [Symbol] one of `CONFINEMENTS`
|
|
50
|
-
attr_reader :confinement
|
|
51
28
|
|
|
52
29
|
# @param tools [Array<String>] the tools the agent is given
|
|
53
30
|
# @param writable [Array<String>] directories the agent may write
|
|
54
|
-
# @param network [Symbol] one of `NETWORKS`
|
|
55
31
|
# @param env [Array<String>] environment variable names to pass through
|
|
56
|
-
# @param
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
# @raise [ArgumentError] when `network` or `confinement` is not one it knows
|
|
60
|
-
def initialize(tools: [], writable: [], network: :none, env: [], **limits)
|
|
61
|
-
limits = LIMIT_DEFAULTS.merge(reject_unknown(limits))
|
|
62
|
-
ensure_known(:network, NETWORKS, network)
|
|
63
|
-
ensure_known(:confinement, CONFINEMENTS, limits[:confinement])
|
|
32
|
+
# @param timeout [Integer, nil] seconds the run may take
|
|
33
|
+
# @param budget [Float, nil] dollars it may spend
|
|
34
|
+
def initialize(tools: [], writable: [], env: [], timeout: nil, budget: nil)
|
|
64
35
|
@tools = tools.dup.freeze
|
|
65
36
|
@writable = writable.dup.freeze
|
|
66
|
-
@network = network
|
|
67
37
|
@env = env.dup.freeze
|
|
68
|
-
@timeout
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# @return [Boolean] whether this profile is enforced by the operating system's sandbox
|
|
72
|
-
def sandboxed?
|
|
73
|
-
confinement == :sandbox
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# @return [Boolean] whether this machine can apply this profile's confinement
|
|
77
|
-
def available?
|
|
78
|
-
!sandboxed? || File.executable?(SANDBOX)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# @param words [Array<String>] the agent's command
|
|
82
|
-
# @return [Array<String>] `words`, run under the sandbox with this profile's policy; the
|
|
83
|
-
# words unchanged when the confinement is the command's own permission rules
|
|
84
|
-
def confine(words)
|
|
85
|
-
sandboxed? ? [SANDBOX, "-p", policy, *words] : words
|
|
38
|
+
@timeout = timeout
|
|
39
|
+
@budget = budget
|
|
86
40
|
end
|
|
87
41
|
|
|
88
42
|
# @return [String] the `claude` permission mode this profile runs under
|
|
89
43
|
def permission_mode
|
|
90
|
-
|
|
44
|
+
"dontAsk"
|
|
91
45
|
end
|
|
92
46
|
|
|
93
47
|
# @param source [Hash{String => String}] the environment to take values from
|
|
@@ -96,79 +50,27 @@ module Hecks
|
|
|
96
50
|
source.to_h.slice(*BASE_ENV, *env)
|
|
97
51
|
end
|
|
98
52
|
|
|
99
|
-
# The `claude` flags this profile's tools and budget become.
|
|
53
|
+
# The `claude` flags this profile's tools and budget become. The writing tools are held only
|
|
54
|
+
# through an `Edit` rule per writable directory, which every file-writing tool answers to.
|
|
100
55
|
#
|
|
101
56
|
# @return [Array<String>]
|
|
102
57
|
def tool_flags
|
|
103
58
|
flags = tools.empty? ? [] : ["--tools", tools.join(","), "--allowedTools", *allowed_tools]
|
|
104
|
-
flags += ["--strict-mcp-config"]
|
|
59
|
+
flags += ["--strict-mcp-config"]
|
|
105
60
|
flags + (budget ? ["--max-budget-usd", budget.to_s] : [])
|
|
106
61
|
end
|
|
107
62
|
|
|
108
|
-
# @return [String] the sandbox policy: reads open but for credentials, writes where allowed
|
|
109
|
-
def policy
|
|
110
|
-
[
|
|
111
|
-
"(version 1)", "(allow default)",
|
|
112
|
-
"(deny file-write*)", write_rule, "(deny file-read*#{paths_clause(credential_paths)})",
|
|
113
|
-
*network_rules
|
|
114
|
-
].join("\n")
|
|
115
|
-
end
|
|
116
|
-
|
|
117
63
|
private
|
|
118
64
|
|
|
119
|
-
# The tools allowed without asking. A permissions-confined run holds the writing tools only
|
|
120
|
-
# through an `Edit` rule per writable directory, which every file-writing tool answers to.
|
|
121
65
|
def allowed_tools
|
|
122
|
-
return [tools.join(",")] if sandboxed?
|
|
123
|
-
|
|
124
66
|
tools - WRITING_TOOLS + writable.map { |path| "Edit(/#{real(path)}/**)" }
|
|
125
67
|
end
|
|
126
68
|
|
|
127
|
-
def write_rule
|
|
128
|
-
allowed = [real(Dir.tmpdir), *writable.map { |path| real(path) }]
|
|
129
|
-
devices = '(literal "/dev/null") (literal "/dev/tty") (regex #"^/dev/fd/")'
|
|
130
|
-
"(allow file-write*#{paths_clause(allowed)} #{devices})"
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def credential_paths
|
|
134
|
-
CREDENTIAL_PATHS.map { |path| File.join(Dir.home, path) }
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def paths_clause(paths)
|
|
138
|
-
paths.map { |path| " (subpath #{quote(path)})" }.join
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def network_rules
|
|
142
|
-
case network
|
|
143
|
-
when :any then []
|
|
144
|
-
when :https then ["(deny network*)", '(allow network-outbound (remote tcp "*:443") (remote udp "*:53"))',
|
|
145
|
-
'(allow network-outbound (remote unix-socket (path-literal "/private/var/run/mDNSResponder")))']
|
|
146
|
-
else ["(deny network*)"]
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def reject_unknown(limits)
|
|
151
|
-
unknown = limits.keys - LIMIT_DEFAULTS.keys
|
|
152
|
-
return limits if unknown.empty?
|
|
153
|
-
|
|
154
|
-
raise ArgumentError, "unknown keyword#{"s" if unknown.size > 1}: #{unknown.map(&:inspect).join(", ")}"
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def ensure_known(name, allowed, value)
|
|
158
|
-
return if allowed.include?(value)
|
|
159
|
-
|
|
160
|
-
raise ArgumentError, "#{name} must be one of #{allowed.inspect}, not #{value.inspect}"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
69
|
def real(path)
|
|
164
70
|
File.realpath(path)
|
|
165
71
|
rescue SystemCallError
|
|
166
72
|
File.expand_path(path)
|
|
167
73
|
end
|
|
168
|
-
|
|
169
|
-
def quote(path)
|
|
170
|
-
%("#{path.gsub("\\", "\\\\\\\\").gsub('"', '\\"')}")
|
|
171
|
-
end
|
|
172
74
|
end
|
|
173
75
|
end
|
|
174
76
|
end
|
|
@@ -116,7 +116,7 @@ module Hecks
|
|
|
116
116
|
# The profile `--confine` runs the agent under: it writes only the candidates directory.
|
|
117
117
|
def confinement_profile
|
|
118
118
|
FileUtils.mkdir_p(@out_dir)
|
|
119
|
-
Hecks::Adapters::AgentProfile.new(
|
|
119
|
+
Hecks::Adapters::AgentProfile.new(tools: CONFINED_TOOLS, writable: [@out_dir],
|
|
120
120
|
timeout: CONFINED_TIMEOUT, budget: CONFINED_BUDGET)
|
|
121
121
|
end
|
|
122
122
|
|
|
@@ -43,7 +43,6 @@ module Hecks
|
|
|
43
43
|
gates = BootGates.new
|
|
44
44
|
load_bound_adapters!(registry)
|
|
45
45
|
Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
|
|
46
|
-
check_compute_rules_backstop!(registry)
|
|
47
46
|
|
|
48
47
|
gates.run!(:pre_verify, registry, directory)
|
|
49
48
|
registry.verify!
|
|
@@ -67,25 +66,6 @@ module Hecks
|
|
|
67
66
|
(bound + default).each { |name| load_adapter(registry, name) }
|
|
68
67
|
end
|
|
69
68
|
|
|
70
|
-
# A backstop only: fires when a translation declares a `computes`/
|
|
71
|
-
# `rekeys` rule but no persistence plugin is loaded to interpret it.
|
|
72
|
-
# Any loaded plugin's own compute-rules gate refuses earlier, by name,
|
|
73
|
-
# whenever one is loaded — this only runs when nothing was.
|
|
74
|
-
def check_compute_rules_backstop!(registry)
|
|
75
|
-
return if Ports::Persistence.plugins_loaded?
|
|
76
|
-
|
|
77
|
-
registry.translations.each do |translation|
|
|
78
|
-
translation.aggregates.each do |aggregate|
|
|
79
|
-
next if aggregate.computes.empty? && aggregate.rekeys.empty?
|
|
80
|
-
|
|
81
|
-
raise WiringError,
|
|
82
|
-
"cannot boot #{translation.domain}::#{aggregate.name}: a compute/rekey rule is declared, but no " \
|
|
83
|
-
"persistence plugin that can interpret it is loaded (e.g. require " \
|
|
84
|
-
"\"hecks/ports/persistence/plugins/era\")"
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
69
|
private
|
|
90
70
|
|
|
91
71
|
def load_adapter(registry, name)
|
data/lib/hecks/runtime/outbox.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "json"
|
|
|
2
2
|
require "securerandom"
|
|
3
3
|
require "time"
|
|
4
4
|
require_relative "event"
|
|
5
|
+
require_relative "value"
|
|
5
6
|
require_relative "../naming"
|
|
6
7
|
|
|
7
8
|
module Hecks
|
|
@@ -48,19 +49,11 @@ module Hecks
|
|
|
48
49
|
name: hash[:name],
|
|
49
50
|
aggregate: hash[:aggregate],
|
|
50
51
|
id: hash[:id],
|
|
51
|
-
payload: deep_symbolize(hash[:payload] || {}),
|
|
52
|
+
payload: Value.deep_symbolize(hash[:payload] || {}),
|
|
52
53
|
occurred_at: hash[:occurred_at],
|
|
53
54
|
correlation: hash[:correlation]
|
|
54
55
|
).emit!
|
|
55
56
|
end
|
|
56
|
-
|
|
57
|
-
def deep_symbolize(value)
|
|
58
|
-
case value
|
|
59
|
-
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
60
|
-
when Array then value.map { |element| deep_symbolize(element) }
|
|
61
|
-
else value
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
57
|
end
|
|
65
58
|
end
|
|
66
59
|
end
|
|
@@ -101,15 +101,7 @@ module Hecks
|
|
|
101
101
|
def spread(answer)
|
|
102
102
|
return { answered: answer } unless answer.is_a?(Hash)
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def deep_symbolize(value)
|
|
108
|
-
case value
|
|
109
|
-
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
110
|
-
when Array then value.map { |element| deep_symbolize(element) }
|
|
111
|
-
else value
|
|
112
|
-
end
|
|
104
|
+
Value.deep_symbolize(answer)
|
|
113
105
|
end
|
|
114
106
|
|
|
115
107
|
# The record, if there is one. A missing record is not an error here: the ask still
|
|
@@ -117,8 +117,8 @@ module Hecks
|
|
|
117
117
|
[domain, aggregate]
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
# Not every aggregate in a routed domain is Lambda-bound (a
|
|
121
|
-
#
|
|
120
|
+
# Not every aggregate in a routed domain is Lambda-bound (a persistence binding
|
|
121
|
+
# may name another adapter). Test the adapter's capability, not its name.
|
|
122
122
|
def remote_bound?(domain, aggregate)
|
|
123
123
|
adapter_name = Ports::Persistence::BindingPolicy.resolve(@registry, domain, aggregate).adapter
|
|
124
124
|
@registry.adapter_class(adapter_name) <= Ports::Persistence::RemoteRuntime
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Runtime
|
|
3
|
+
class Value
|
|
4
|
+
# The shape a caller is answered with: a single-attribute value object as its bare field,
|
|
5
|
+
# following the declared types down through nested value objects and lists. Extended into
|
|
6
|
+
# `Value` beside `Coercion`; `materialize` keeps the wrapped shape stored state depends on.
|
|
7
|
+
module Unwrapping
|
|
8
|
+
# `materialize`, but a single-attribute value object unwraps to its bare field. What
|
|
9
|
+
# `group_by` rows and `Driving::Json.materialize` answer with.
|
|
10
|
+
#
|
|
11
|
+
# @param value [Object] the value to materialize; anything that is not a `Runtime::Value`,
|
|
12
|
+
# Array or Hash passes through unchanged
|
|
13
|
+
# @return [Object] `value` with every nested `Runtime::Value` replaced by its own sole
|
|
14
|
+
# field's value (recursively unwrapped), or by its own `Hash` of fields when it has
|
|
15
|
+
# more than one
|
|
16
|
+
def materialize_unwrapped(value)
|
|
17
|
+
case value
|
|
18
|
+
when self then unwrap_typed(value.value_object, value.raw_fields)
|
|
19
|
+
when Array then value.map { |item| materialize_unwrapped(item) }
|
|
20
|
+
when Hash then value.transform_values { |item| materialize_unwrapped(item) }
|
|
21
|
+
else value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# A value object's fields by its declared attributes. A nested value object is often held
|
|
28
|
+
# as a plain Hash, so its own type is looked up rather than read off the field.
|
|
29
|
+
def unwrap_typed(value_object, fields)
|
|
30
|
+
sole = value_object.sole_attribute
|
|
31
|
+
return unwrap_field(value_object, sole, fields[sole.name]) if sole
|
|
32
|
+
|
|
33
|
+
value_object.attributes.to_h { |attr| [attr.name, unwrap_field(value_object, attr, fields[attr.name])] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def unwrap_field(value_object, attribute, held)
|
|
37
|
+
case held
|
|
38
|
+
when Array then held.map { |item| unwrap_field(value_object, attribute, item) }
|
|
39
|
+
when Hash then unwrap_held_hash(value_object, attribute, held)
|
|
40
|
+
else materialize_unwrapped(held)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def unwrap_held_hash(value_object, attribute, held)
|
|
45
|
+
nested = nested_value_object(value_object, attribute.type)
|
|
46
|
+
return materialize_unwrapped(held) unless nested
|
|
47
|
+
|
|
48
|
+
unwrap_typed(nested, held.transform_keys(&:to_sym))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The value object a field's type names, resolved against the aggregate that declares
|
|
52
|
+
# `value_object`; nil for a primitive, or when the owner is not known.
|
|
53
|
+
def nested_value_object(value_object, type)
|
|
54
|
+
owner = value_object.respond_to?(:hecks_owner) ? value_object.hecks_owner : nil
|
|
55
|
+
owner.respond_to?(:value_object) ? value_object_for(owner, type) : nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/hecks/runtime/value.rb
CHANGED
|
@@ -9,12 +9,13 @@ require_relative "value/flags"
|
|
|
9
9
|
require_relative "value/identifiers"
|
|
10
10
|
require_relative "value/entity_list_coercion"
|
|
11
11
|
require_relative "value/admission"
|
|
12
|
+
require_relative "value/unwrapping"
|
|
12
13
|
|
|
13
14
|
module Hecks
|
|
14
15
|
module Runtime
|
|
15
16
|
# A typed value object: frozen fields, read by name.
|
|
16
17
|
# Construction lives in value/coercion.rb, field_checks.rb, entity_list_coercion.rb and
|
|
17
|
-
# admission.rb.
|
|
18
|
+
# admission.rb; the shape a caller is answered with in unwrapping.rb.
|
|
18
19
|
class Value
|
|
19
20
|
extend FieldChecks
|
|
20
21
|
extend Coercion
|
|
@@ -24,6 +25,7 @@ module Hecks
|
|
|
24
25
|
extend Identifiers
|
|
25
26
|
extend EntityListCoercion
|
|
26
27
|
extend Admission
|
|
28
|
+
extend Unwrapping
|
|
27
29
|
|
|
28
30
|
attr_reader :value_object
|
|
29
31
|
|
|
@@ -102,32 +104,19 @@ module Hecks
|
|
|
102
104
|
end
|
|
103
105
|
end
|
|
104
106
|
|
|
105
|
-
#
|
|
106
|
-
# Opt-in for `group_by` rows; `materialize` keeps the wrapped shape callers depend on.
|
|
107
|
+
# Converts every Hash key to a Symbol, at every depth of parsed JSON.
|
|
107
108
|
#
|
|
108
|
-
# @param value [Object]
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
# more than one
|
|
113
|
-
def self.materialize_unwrapped(value)
|
|
109
|
+
# @param value [Hash, Array, Object] a Hash or Array is walked; any other value is a leaf
|
|
110
|
+
# @return [Hash{Symbol => Object}, Array, Object] a new structure of the same shape with
|
|
111
|
+
# Symbol keys; a leaf is returned as it came
|
|
112
|
+
def self.deep_symbolize(value)
|
|
114
113
|
case value
|
|
115
|
-
when
|
|
116
|
-
when Array then value.map { |item|
|
|
117
|
-
when Hash then value.transform_values { |item| materialize_unwrapped(item) }
|
|
114
|
+
when Hash then value.to_h { |key, item| [key.to_sym, deep_symbolize(item)] }
|
|
115
|
+
when Array then value.map { |item| deep_symbolize(item) }
|
|
118
116
|
else value
|
|
119
117
|
end
|
|
120
118
|
end
|
|
121
119
|
|
|
122
|
-
def self.unwrap_value_object(value)
|
|
123
|
-
sole = value.value_object.sole_attribute
|
|
124
|
-
return materialize_unwrapped(value[sole.name]) if sole
|
|
125
|
-
|
|
126
|
-
# Not `value.to_h`: it materializes nested values to Hashes, ending the recursion.
|
|
127
|
-
value.value_object.attributes.to_h { |attr| [attr.name, materialize_unwrapped(value[attr.name])] }
|
|
128
|
-
end
|
|
129
|
-
private_class_method :unwrap_value_object
|
|
130
|
-
|
|
131
120
|
# Reduces an append-only `list_of` sub-log to its current state: the latest row per `key`.
|
|
132
121
|
# Grouping only; what counts as removed, and the ordering, belong to the caller.
|
|
133
122
|
#
|
|
@@ -36,6 +36,7 @@ module Hecks
|
|
|
36
36
|
ir = shaped(exporter.call(@registry).fetch(@domain_name))
|
|
37
37
|
ir[:lineage] = shaped(exporter.lineage(@registry, @domain_name))
|
|
38
38
|
ir[:persistence] = shaped(exporter.persistence(@registry, @domain_name))
|
|
39
|
+
add_persistence_wiring(ir, exporter)
|
|
39
40
|
add_seams(ir, exporter)
|
|
40
41
|
add_edges_and_source(ir)
|
|
41
42
|
ir
|
|
@@ -43,8 +44,28 @@ module Hecks
|
|
|
43
44
|
|
|
44
45
|
private
|
|
45
46
|
|
|
47
|
+
# The adapters and the world that the host checks the persistence against.
|
|
48
|
+
def add_persistence_wiring(document, exporter)
|
|
49
|
+
document[:adapters] = adapters(exporter, document[:persistence])
|
|
50
|
+
document[:world] = world(exporter)
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
def shaped(payload) = self.class.json_shaped(payload)
|
|
47
54
|
|
|
55
|
+
# The bound persistence adapters' declarations, from the `.adapter` files under the
|
|
56
|
+
# source root's `lib/` and the domain's own directory, so the host can look them up.
|
|
57
|
+
def adapters(exporter, persistence)
|
|
58
|
+
files = exporter.adapter_files_under([File.join(RustBuild::ROOT, "lib"), File.dirname(@bluebook_dir)])
|
|
59
|
+
shaped(exporter.persistence_adapters(persistence, files))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The target's world persistence, from `<domain directory name>.world` beside the bluebooks
|
|
63
|
+
# (names only), so the host can check what the world binds against the adapters.
|
|
64
|
+
def world(exporter)
|
|
65
|
+
path = File.join(@bluebook_dir, "#{File.basename(File.dirname(@bluebook_dir))}.world")
|
|
66
|
+
shaped(exporter.persistence_world(path, @domain_name))
|
|
67
|
+
end
|
|
68
|
+
|
|
48
69
|
def add_seams(document, exporter)
|
|
49
70
|
seams = SEAMS.to_h { |seam| [seam, exporter.public_send(seam, @registry, @domain_name)] }
|
|
50
71
|
seams.each { |seam, value| document[seam] = shaped(value) unless value.empty? }
|
|
@@ -56,14 +77,12 @@ module Hecks
|
|
|
56
77
|
"hecksagon, not a deploy-time guess, names the identity verbs."
|
|
57
78
|
end
|
|
58
79
|
|
|
59
|
-
# Edges carry their own precompiled SQL, so a boot-time mint only executes it.
|
|
60
|
-
#
|
|
80
|
+
# Edges carry their own precompiled SQL, so a boot-time mint only executes it. The source
|
|
81
|
+
# text is verbatim: the era's integrity digest is
|
|
61
82
|
# the SHA256 of it, not of anything re-derived from the parsed IR.
|
|
62
83
|
def add_edges_and_source(document)
|
|
63
84
|
edges = Hecks::Projector::Exporter.translations(@registry).select { |edge| edge[:domain] == @domain_name }
|
|
64
85
|
document[:translations] = shaped(edges)
|
|
65
|
-
approvals = Hecks::Translation::ApprovalFile.read_all(@bluebook_dir)
|
|
66
|
-
document[:approvals] = shaped(approvals) unless approvals.empty?
|
|
67
86
|
document[:source_text] = Hecks::Runtime::EraCheck.source_text_for(
|
|
68
87
|
@registry.bluebooks.fetch(@domain_name), @bluebook_dir, registry: @registry
|
|
69
88
|
)
|
|
@@ -31,7 +31,6 @@ module Hecks
|
|
|
31
31
|
# @raise [Failure] when the target is not installed or a step fails
|
|
32
32
|
def call(argv)
|
|
33
33
|
domain = argv.first or raise Failure, "usage: hecks build_wasm <domain>"
|
|
34
|
-
return build_with_rust(domain) if ENV["HECKS_PARSER"] == "rust" && ENV["HECKS_CODEGEN"] == "rust"
|
|
35
34
|
|
|
36
35
|
require_target!
|
|
37
36
|
rust_dir = RustBuild.rust_dir
|
|
@@ -48,17 +47,6 @@ module Hecks
|
|
|
48
47
|
raise Failure, "project_rust failed for #{domain}" unless status.zero?
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
# With the Rust parser and codegen, `hecks-build` runs the same steps and writes the same
|
|
52
|
-
# outputs.
|
|
53
|
-
def build_with_rust(domain)
|
|
54
|
-
build_dir = File.join(ROOT, "rust", "build")
|
|
55
|
-
puts "== cargo build (hecks-build) =="
|
|
56
|
-
RustBuild.command!("cargo", "build", chdir: build_dir)
|
|
57
|
-
puts "== hecks-build #{domain} --wasm =="
|
|
58
|
-
RustBuild.command!(File.join(build_dir, "target", "debug", "hecks-build"), domain, "--wasm")
|
|
59
|
-
0
|
|
60
|
-
end
|
|
61
|
-
|
|
62
50
|
def require_target!
|
|
63
51
|
return if RustBuild.target_installed?(TARGET)
|
|
64
52
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require_relative "../rust_format"
|
|
4
5
|
|
|
5
6
|
module Hecks
|
|
6
7
|
module RustBuild
|
|
@@ -9,17 +10,8 @@ module Hecks
|
|
|
9
10
|
module WriteIfChanged
|
|
10
11
|
module_function
|
|
11
12
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# Every write is recorded against every open ancestor directory.
|
|
15
|
-
# `push_directory`/`pop_and_prune` are the same operation split for non-adjacent writes.
|
|
16
|
-
def track_directory(dir)
|
|
17
|
-
push_directory(dir)
|
|
18
|
-
yield
|
|
19
|
-
ensure
|
|
20
|
-
pop_and_prune(dir)
|
|
21
|
-
end
|
|
22
|
-
|
|
13
|
+
# Starts recording every path written or confirmed unchanged inside `dir`, against every open
|
|
14
|
+
# ancestor directory too; `pop_and_prune` closes it and deletes the files never touched.
|
|
23
15
|
def push_directory(dir)
|
|
24
16
|
FileUtils.mkdir_p(dir)
|
|
25
17
|
stack.push([File.expand_path(dir), Set.new])
|
|
@@ -56,6 +48,7 @@ module Hecks
|
|
|
56
48
|
# Returns whether it wrote.
|
|
57
49
|
def call(path, content)
|
|
58
50
|
record_touch(path)
|
|
51
|
+
content = RustFormat.call(content) if RustFormat.rust?(path)
|
|
59
52
|
# `.b` on both sides: String#== is encoding-aware, so UTF-8 vs ASCII-8BIT
|
|
60
53
|
# content can compare unequal even when the bytes are identical.
|
|
61
54
|
return false if File.exist?(path) && File.binread(path) == content.b
|
|
@@ -64,13 +57,20 @@ module Hecks
|
|
|
64
57
|
true
|
|
65
58
|
end
|
|
66
59
|
|
|
60
|
+
# Fills `tmp` from the block, then lays it out with rustfmt when `path` is Rust source.
|
|
61
|
+
def write_formatted(tmp, path, &)
|
|
62
|
+
File.open(tmp, "w", &)
|
|
63
|
+
File.write(tmp, RustFormat.call(File.read(tmp))) if RustFormat.rust?(path)
|
|
64
|
+
end
|
|
65
|
+
private_class_method :write_formatted
|
|
66
|
+
|
|
67
67
|
# Like `call`, for callers that build content by writing to an IO. Yields a sibling
|
|
68
68
|
# temp file, then renames it into place only if the bytes differ. Returns whether it wrote.
|
|
69
69
|
def block(path, &)
|
|
70
70
|
record_touch(path)
|
|
71
71
|
tmp = "#{path}.tmp-#{Process.pid}-#{rand(1_000_000)}"
|
|
72
72
|
begin
|
|
73
|
-
|
|
73
|
+
write_formatted(tmp, path, &)
|
|
74
74
|
changed = !(File.exist?(path) && FileUtils.compare_file(tmp, path))
|
|
75
75
|
FileUtils.mv(tmp, path) if changed
|
|
76
76
|
changed
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
# Runs generated Rust through rustfmt, so a generator writes what `cargo fmt --check` accepts.
|
|
7
|
+
#
|
|
8
|
+
# Every generator that writes a `.rs` file formats its text here before comparing it with the tree
|
|
9
|
+
# or writing it, so a drift check and a write see the same bytes. The edition is pinned in the
|
|
10
|
+
# call, not read from a `rustfmt.toml`, because a generator may run from any directory.
|
|
11
|
+
module RustFormat
|
|
12
|
+
# Raised when the rustfmt program cannot be found.
|
|
13
|
+
class Missing < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Raised when rustfmt fails for a reason that is not the text: a toolchain that will not
|
|
16
|
+
# install, say. Falling back to the unformatted text would write the wrong bytes without a word.
|
|
17
|
+
class Failed < StandardError; end
|
|
18
|
+
|
|
19
|
+
EDITION = "2021"
|
|
20
|
+
|
|
21
|
+
# The most passes `call` makes; a text still changing after them keeps the last result.
|
|
22
|
+
PASSES = 3
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# @param path [String] where the text will be written
|
|
27
|
+
# @return [Boolean] whether the path is Rust source
|
|
28
|
+
def rust?(path)
|
|
29
|
+
path.to_s.end_with?(".rs")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Formats one Rust source text.
|
|
33
|
+
#
|
|
34
|
+
# Text rustfmt cannot parse is returned as it came, with a warning on stderr: the compiler will
|
|
35
|
+
# name the real fault, and a generator should not be the thing that hides it. Any other failure
|
|
36
|
+
# of rustfmt raises, since the text is not the problem.
|
|
37
|
+
#
|
|
38
|
+
# @param text [String] Rust source
|
|
39
|
+
# @return [String] the same source as rustfmt lays it out
|
|
40
|
+
# @raise [Missing] when rustfmt is not installed
|
|
41
|
+
# @raise [Failed] when rustfmt fails for a reason other than the text
|
|
42
|
+
def call(text)
|
|
43
|
+
PASSES.times do
|
|
44
|
+
formatted = pass(text)
|
|
45
|
+
return formatted if formatted == text
|
|
46
|
+
|
|
47
|
+
text = formatted
|
|
48
|
+
end
|
|
49
|
+
text
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# One rustfmt run. rustfmt does not always reach a fixed point in one pass, so `call` repeats it
|
|
53
|
+
# until the text stops changing, as a later `cargo fmt --check` would have it.
|
|
54
|
+
#
|
|
55
|
+
# @param text [String] Rust source
|
|
56
|
+
# @return [String] the source after one pass, or unchanged when rustfmt cannot parse it
|
|
57
|
+
# @raise [Failed] when rustfmt fails for a reason other than the text
|
|
58
|
+
def pass(text)
|
|
59
|
+
out, err, status = Open3.capture3("rustfmt", "--edition", EDITION, "--emit", "stdout", stdin_data: text)
|
|
60
|
+
return out if status.success?
|
|
61
|
+
raise Failed, "rustfmt failed: #{err.lines.first&.strip}" unless err.include?("<stdin>")
|
|
62
|
+
|
|
63
|
+
warn "hecks: rustfmt could not format generated Rust, leaving it as generated: #{err.lines.first&.strip}"
|
|
64
|
+
text
|
|
65
|
+
rescue Errno::ENOENT
|
|
66
|
+
raise Missing, "rustfmt is not installed: generated Rust is formatted with it; run `rustup component add rustfmt`"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Formats the Rust texts of a path-to-text map and leaves the others alone.
|
|
70
|
+
#
|
|
71
|
+
# @param content [Hash{String => String}] absolute path to text
|
|
72
|
+
# @return [Hash{String => String}] the same map with each `.rs` text formatted
|
|
73
|
+
def files(content)
|
|
74
|
+
content.to_h { |path, text| [path, rust?(path) ? call(text) : text] }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|