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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require_relative "board_edit/command_text"
|
|
2
|
+
require_relative "board_edit/source"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Composer
|
|
6
|
+
# What the board does to the text, on the hecksagon side: each action on a note is one exact
|
|
7
|
+
# edit of the bluebook, with no model in the loop, so a board change is instant and the text
|
|
8
|
+
# stays the single source of truth. It answers the patched file; the editor applies it as an
|
|
9
|
+
# ordinary edit, so Undo takes it back.
|
|
10
|
+
#
|
|
11
|
+
# Fields, rules and givens are not on the board: they stay in the text, where the agent can
|
|
12
|
+
# fill them in.
|
|
13
|
+
module BoardEdit
|
|
14
|
+
# Raised when an action cannot be carried out, in the board's own words.
|
|
15
|
+
class Refused < StandardError; end
|
|
16
|
+
|
|
17
|
+
NAME = /\A[A-Z][A-Za-z0-9]*\z/
|
|
18
|
+
DOTTED = /\A([A-Z][A-Za-z0-9]*)\.([A-Z][A-Za-z0-9]*)\z/
|
|
19
|
+
|
|
20
|
+
# Each action and the arguments it takes.
|
|
21
|
+
OPERATIONS = {
|
|
22
|
+
"add_aggregate" => %i[name], "add_command" => %i[aggregate name event],
|
|
23
|
+
"add_event" => %i[aggregate command name], "add_policy" => %i[name on trigger],
|
|
24
|
+
"delete" => %i[kind name aggregate], "delete_event" => %i[aggregate command name]
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# @param stage [String] a staged directory holding the workspace as it is on screen
|
|
30
|
+
# @param action [String] one of the keys of `OPERATIONS`
|
|
31
|
+
# @param args [Hash{String => String}] the action's arguments
|
|
32
|
+
# @return [Hash] the patched file: `path`, its whole new `text`, and the `line` to show
|
|
33
|
+
# @raise [Refused] if the action is unknown or cannot be carried out
|
|
34
|
+
def call(stage:, action:, args:)
|
|
35
|
+
wanted = OPERATIONS.fetch(action) { raise Refused, "the board has no action called #{action.inspect}" }
|
|
36
|
+
values = wanted.to_h { |key| [key, args[key.to_s].to_s.strip] }
|
|
37
|
+
public_send(action, Source.new(stage), **values)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @api private
|
|
41
|
+
def add_aggregate(source, name:)
|
|
42
|
+
named!(name, "an aggregate")
|
|
43
|
+
refuse!("#{name} is already an aggregate") if source.places.of("aggregate", name)
|
|
44
|
+
header = source.chapter
|
|
45
|
+
source.insert(header[:file], source.block_end(header), aggregate_lines(name))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A new aggregate is known by a key of its own, so its first command can create it.
|
|
49
|
+
# @api private
|
|
50
|
+
def aggregate_lines(name)
|
|
51
|
+
["", " aggregate \"#{name}\" do", " description \"Describe #{name}.\"", "",
|
|
52
|
+
" attribute :key, #{name}Key", " identified_by :key", "",
|
|
53
|
+
" value_object \"#{name}Key\" do", " attribute :value, String", " end", " end"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @api private
|
|
57
|
+
def add_command(source, aggregate:, name:, event:)
|
|
58
|
+
named!(name, "a command")
|
|
59
|
+
named!(event, "an event") unless event.empty?
|
|
60
|
+
where = aggregate_at(source, aggregate)
|
|
61
|
+
refuse!("#{aggregate} already has a command #{name}") if source.places.of("command", name, owner: aggregate)
|
|
62
|
+
close = source.block_end(where)
|
|
63
|
+
block = source.slice(where[:file], where[:line], close)
|
|
64
|
+
lines = CommandText.lines(block: block, indent: source.indent(where), aggregate: aggregate, name: name, event: event)
|
|
65
|
+
source.insert(where[:file], close, lines)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @api private
|
|
69
|
+
def add_event(source, aggregate:, command:, name:)
|
|
70
|
+
named!(name, "an event")
|
|
71
|
+
where = command_at(source, aggregate, command)
|
|
72
|
+
refuse!("#{command} already emits #{name}") if emit_line(source, where, name)
|
|
73
|
+
pad = " " * (source.indent(where) + 2)
|
|
74
|
+
source.insert(where[:file], source.block_end(where), ["#{pad}emits #{name}"])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @api private
|
|
78
|
+
def add_policy(source, name:, on:, trigger:)
|
|
79
|
+
named!(name, "a policy")
|
|
80
|
+
refuse!("there is already a policy #{name}") if source.places.of("policy", name)
|
|
81
|
+
event = dotted!(on, "the event")
|
|
82
|
+
target = dotted!(trigger, "the command")
|
|
83
|
+
command_at(source, target[1], target[2])
|
|
84
|
+
header = source.chapter
|
|
85
|
+
source.insert(header[:file], source.block_end(header), policy_lines(name, event, target))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @api private
|
|
89
|
+
def policy_lines(name, event, target)
|
|
90
|
+
["", " policy \"#{name}\" do", " on #{event[1]}::#{event[2]}", " trigger #{target[1]}::#{target[2]}", " end"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @api private
|
|
94
|
+
def delete(source, kind:, name:, aggregate:)
|
|
95
|
+
where = case kind
|
|
96
|
+
when "aggregate" then aggregate_at(source, name)
|
|
97
|
+
when "command" then command_at(source, aggregate, name)
|
|
98
|
+
when "policy" then source.places.of("policy", name) || refuse!("there is no policy #{name}")
|
|
99
|
+
else refuse!("the board cannot delete a #{kind.inspect}")
|
|
100
|
+
end
|
|
101
|
+
source.remove(where[:file], where[:line] - 1, source.block_end(where))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @api private
|
|
105
|
+
def delete_event(source, aggregate:, command:, name:)
|
|
106
|
+
where = command_at(source, aggregate, command)
|
|
107
|
+
line = emit_line(source, where, name) or refuse!("#{command} does not emit #{name}")
|
|
108
|
+
source.remove(where[:file], line, line)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @api private
|
|
112
|
+
def aggregate_at(source, name)
|
|
113
|
+
source.places.of("aggregate", name) || refuse!("there is no aggregate #{name}")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @api private
|
|
117
|
+
def command_at(source, aggregate, name)
|
|
118
|
+
source.places.of("command", name, owner: aggregate) || refuse!("#{aggregate} has no command #{name}")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The 0-based index of the line in a command's block that emits the event, or nil.
|
|
122
|
+
# @api private
|
|
123
|
+
def emit_line(source, where, name)
|
|
124
|
+
from = where[:line]
|
|
125
|
+
block = source.slice(where[:file], from, source.block_end(where))
|
|
126
|
+
found = block.index { |text| text.match?(/\A\s*emits\s+("?)#{Regexp.escape(name)}\1\s*\z/) }
|
|
127
|
+
found && (from + found)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @api private
|
|
131
|
+
def dotted!(text, what)
|
|
132
|
+
DOTTED.match(text) || refuse!("#{what} is written Aggregate.Name, not #{text.inspect}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @api private
|
|
136
|
+
def named!(name, what)
|
|
137
|
+
refuse!("#{what} needs a name that starts with a capital letter, with letters and digits only") unless NAME.match?(name)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @api private
|
|
141
|
+
def refuse!(message) = raise(Refused, message)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require "hecks"
|
|
2
|
+
require "hecks/bluebook/model_check"
|
|
3
|
+
require "hecks/runtime/capability_graph"
|
|
4
|
+
require_relative "board"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Composer
|
|
8
|
+
# Judges a workspace as it stands on screen: parse it, boot it, then draw and read back what
|
|
9
|
+
# it declares. It changes nothing on disk and holds nothing between calls, so a caller may
|
|
10
|
+
# run it on every pause in typing.
|
|
11
|
+
module Check
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param dir [String] the workspace directory
|
|
15
|
+
# @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
|
|
16
|
+
# @param chapter_name [String, nil] the chapter to draw; the first bluebook's when nil
|
|
17
|
+
# @return [Hash] `diagnostic` (nil, or `{file:, line:, message:, expected:}` for the first
|
|
18
|
+
# refusal), `files` (the workspace's declaration files), `outline` (see {Outline}),
|
|
19
|
+
# `chapter` (the name drawn), `diagrams` (Mermaid text by file name), `statements` (plain
|
|
20
|
+
# sentences), `findings` (`{kind:, severity:, subject:, message:}` per model-check
|
|
21
|
+
# finding), `wiring` (see {wiring}, nil until the workspace boots), `board` (the
|
|
22
|
+
# event-storming notes, see {Board}, nil until it boots) and `elapsed_ms`
|
|
23
|
+
def call(dir:, buffers: {}, chapter_name: nil)
|
|
24
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
25
|
+
result = Workspace.stage(dir: dir, buffers: buffers) { |stage| judge(stage, chapter_name) }
|
|
26
|
+
result.merge(elapsed_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @api private
|
|
30
|
+
def judge(stage, chapter_name)
|
|
31
|
+
files = Workspace.files(stage)
|
|
32
|
+
verdict(stage, empty(files, Outline.call(stage, files)), chapter_name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# What the parser and the boot say of the staged text, laid over `base`.
|
|
36
|
+
# @api private
|
|
37
|
+
def verdict(stage, base, chapter_name)
|
|
38
|
+
refusal = Parse.call(stage)
|
|
39
|
+
return base.merge(diagnostic: refusal) if refusal
|
|
40
|
+
|
|
41
|
+
project(stage, base, chapter_name || Parse.bluebooks(stage).keys.first)
|
|
42
|
+
rescue Parse::ParserMissing
|
|
43
|
+
raise
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
base.merge(diagnostic: { file: nil, line: nil, message: e.message.lines.first.to_s.chomp, expected: [] })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private
|
|
49
|
+
def project(stage, base, name)
|
|
50
|
+
registry = Hecks.boot(stage, install_driving: false, environment: "memory").registry
|
|
51
|
+
chapter = registry.bluebook(name)
|
|
52
|
+
base.merge(chapter: name, wiring: wiring(registry, name), board: Board.call(chapter, outline: base[:outline]),
|
|
53
|
+
**read(chapter, registry.hecksagon(name)))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The diagrams, statements and model-check findings of a booted chapter.
|
|
57
|
+
# @api private
|
|
58
|
+
def read(chapter, hecksagon)
|
|
59
|
+
{ diagrams: Projections::Diagrams.call(bluebook: chapter, options: { hecksagon: hecksagon }),
|
|
60
|
+
statements: Projections::Statements.call(bluebook: chapter),
|
|
61
|
+
findings: Bluebook::ModelCheck.call(chapter, hecksagon: hecksagon).map(&:to_h) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# What the chapter is wired to: the binds its hecksagon declares, the chapters it attaches,
|
|
65
|
+
# the driving adapters it admits, the world's default adapter, and each declared port with
|
|
66
|
+
# the adapters bound to it.
|
|
67
|
+
#
|
|
68
|
+
# @param registry [Runtime::Registry] a booted registry
|
|
69
|
+
# @param name [String] the chapter's name
|
|
70
|
+
# @return [Hash] `binds`, `attaches`, `driving`, `default_adapter` and `ports`
|
|
71
|
+
def wiring(registry, name)
|
|
72
|
+
hecksagon = registry.hecksagon(name)
|
|
73
|
+
{
|
|
74
|
+
binds: (hecksagon&.binds || []).map { |bind| bind.to_h.slice(:aggregate, :verb, :adapter, :role) },
|
|
75
|
+
attaches: hecksagon&.member_chapters || [],
|
|
76
|
+
driving: hecksagon&.driving || [],
|
|
77
|
+
default_adapter: registry.world(name)&.default_adapter,
|
|
78
|
+
ports: Runtime::CapabilityGraph.new(registry).fulfillments
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @api private
|
|
83
|
+
def empty(files, outline)
|
|
84
|
+
{ diagnostic: nil, files: files, outline: outline, chapter: nil, diagrams: {}, statements: [],
|
|
85
|
+
findings: [], wiring: nil, board: nil }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "workspace"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Composer
|
|
6
|
+
# The domains the editor can open, on the hecksagon side: every directory under a checkout
|
|
7
|
+
# that holds a bluebook, in groups, and the copy of one that the editor works on.
|
|
8
|
+
#
|
|
9
|
+
# A domain is opened as a scratch copy under `tmp/composer/`, so saving, the agent and Undo
|
|
10
|
+
# never touch the real files. Only an id from {known} can be opened, so a request cannot name
|
|
11
|
+
# a path of its own.
|
|
12
|
+
module Domains
|
|
13
|
+
# Where scratch copies live, relative to the checkout (it is not committed).
|
|
14
|
+
SCRATCH = "tmp/composer".freeze
|
|
15
|
+
|
|
16
|
+
# The groups the picker shows, in order, each with the ids it takes.
|
|
17
|
+
GROUPS = [["Examples", %r{\Aexamples/[^/]+\z}],
|
|
18
|
+
["Stress domains", %r{\Aqa/stress_domains/[^/]+\z}],
|
|
19
|
+
["Hecks chapters", %r{\Alib/hecks/}],
|
|
20
|
+
["Quality control", /\Aqa\z/],
|
|
21
|
+
["Composer fixtures", %r{\Aspec/fixtures/composer/[^/]+\z}]].freeze
|
|
22
|
+
|
|
23
|
+
# Raised when an id is not one of the known domains.
|
|
24
|
+
class Unknown < ArgumentError; end
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# @param root [String] the checkout to search
|
|
29
|
+
# @return [Array<Hash>] `{id:, group:}` for each known domain, in group then id order, where
|
|
30
|
+
# `id` is the domain's directory relative to `root`
|
|
31
|
+
def known(root:)
|
|
32
|
+
entries = owners(root).filter_map { |id| entry_for(id) }
|
|
33
|
+
entries.sort_by { |entry| [rank(entry[:group]), entry[:id]] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Copies a known domain's declaration files into a fresh scratch directory.
|
|
37
|
+
#
|
|
38
|
+
# @param root [String] the checkout
|
|
39
|
+
# @param id [String] one of the ids {known} lists
|
|
40
|
+
# @return [String] the scratch directory the editor should open
|
|
41
|
+
# @raise [Unknown] if `id` is not a known domain
|
|
42
|
+
def open(root:, id:)
|
|
43
|
+
raise Unknown, "#{id.inspect} is not a domain this checkout knows" unless known?(root, id)
|
|
44
|
+
|
|
45
|
+
scratch = File.join(root, SCRATCH, id.tr("/", "_"))
|
|
46
|
+
FileUtils.rm_rf(scratch)
|
|
47
|
+
FileUtils.mkdir_p(scratch)
|
|
48
|
+
Workspace.copy_declarations(source(root, id), scratch)
|
|
49
|
+
scratch
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @api private
|
|
53
|
+
def known?(root, id) = known(root: root).any? { |entry| entry[:id] == id }
|
|
54
|
+
|
|
55
|
+
# @api private
|
|
56
|
+
def entry_for(id)
|
|
57
|
+
group = GROUPS.find { |_, pattern| pattern.match?(id) }
|
|
58
|
+
{ id: id, group: group.first } if group
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
def rank(group) = GROUPS.index { |name, _| name == group }
|
|
63
|
+
|
|
64
|
+
# @api private
|
|
65
|
+
def owners(root)
|
|
66
|
+
Dir.glob("**/*.bluebook", base: root).map { |path| owner(File.dirname(path)) }.uniq
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A `bluebook/` folder stands for the domain that holds it.
|
|
70
|
+
# @api private
|
|
71
|
+
def owner(dir) = File.basename(dir) == "bluebook" ? File.dirname(dir) : dir
|
|
72
|
+
|
|
73
|
+
# Where a domain's bluebooks are: its `bluebook/` folder if it has one, else itself.
|
|
74
|
+
# @api private
|
|
75
|
+
def source(root, id)
|
|
76
|
+
folder = File.join(root, id, "bluebook")
|
|
77
|
+
File.directory?(folder) ? folder : File.join(root, id)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Composer
|
|
5
|
+
# Runs one long-running command at a time in the background, on the hecksagon side like the
|
|
6
|
+
# other composer files. Fuzzing and conformance runs take seconds to minutes, so the caller
|
|
7
|
+
# starts a job and the page polls {#state}.
|
|
8
|
+
#
|
|
9
|
+
# A job is a call to the injected runner on a worker thread. A watchdog thread ends it when
|
|
10
|
+
# it outlives the timeout. Cancelling or timing out kills the worker thread only: a child
|
|
11
|
+
# process the command spawned is not killed and keeps running until it exits on its own.
|
|
12
|
+
# All state sits behind one mutex, so {#state} is safe to call from any thread.
|
|
13
|
+
class Jobs
|
|
14
|
+
# Raised by {#start} while another job is still running.
|
|
15
|
+
class Busy < StandardError; end
|
|
16
|
+
|
|
17
|
+
# How much of a job's output {#state} keeps, counted from the end.
|
|
18
|
+
OUTPUT_LIMIT = 20_000
|
|
19
|
+
|
|
20
|
+
# @param runner [#call] takes `kind:, verb:, args:, confirm:` and returns
|
|
21
|
+
# `{ok:, status:, output:}`
|
|
22
|
+
# @param timeout [Numeric] seconds a job may run before it is marked "timed_out"
|
|
23
|
+
def initialize(runner:, timeout: 600)
|
|
24
|
+
@runner = runner
|
|
25
|
+
@timeout = timeout
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
@settled = ConditionVariable.new
|
|
28
|
+
@generation = 0
|
|
29
|
+
@state = blank
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param kind [String] the kind of command the runner dispatches on
|
|
33
|
+
# @param verb [String] the command's verb
|
|
34
|
+
# @param args [Hash] the command's arguments
|
|
35
|
+
# @param confirm [String, nil] the confirmation the runner requires, if any
|
|
36
|
+
# @return [true]
|
|
37
|
+
# @raise [Busy] when a job is running
|
|
38
|
+
def start(kind:, verb:, args: {}, confirm: nil) # rubocop:disable Naming/PredicateMethod
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
raise Busy, "a job is already running" if running?
|
|
41
|
+
|
|
42
|
+
@state = blank.merge(status: "running", verb: verb, started_at: stamp)
|
|
43
|
+
launch(@generation += 1, { kind: kind, verb: verb, args: args, confirm: confirm })
|
|
44
|
+
end
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Hash] `status` ("idle", "running", "done", "failed", "cancelled" or
|
|
49
|
+
# "timed_out"), `verb`, `started_at` and `finished_at` (ISO 8601 or nil), `ok` (nil until
|
|
50
|
+
# the job settles) and `output` (the last {OUTPUT_LIMIT} characters)
|
|
51
|
+
def state
|
|
52
|
+
@mutex.synchronize { @state.dup }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Stops the running job's worker thread. A child process it spawned is not killed.
|
|
56
|
+
# @return [Boolean] true when a job was running
|
|
57
|
+
def cancel
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
return false unless running?
|
|
60
|
+
|
|
61
|
+
@worker.kill
|
|
62
|
+
settle("cancelled", "")
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
def blank
|
|
71
|
+
{ status: "idle", verb: nil, started_at: nil, finished_at: nil, ok: nil, output: "" }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @api private
|
|
75
|
+
def running? = @state[:status] == "running"
|
|
76
|
+
|
|
77
|
+
# @api private
|
|
78
|
+
def current?(generation) = generation == @generation && running?
|
|
79
|
+
|
|
80
|
+
# @api private
|
|
81
|
+
def stamp = Time.now.utc.iso8601(3)
|
|
82
|
+
|
|
83
|
+
# @api private
|
|
84
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
85
|
+
|
|
86
|
+
# Starts the worker and its watchdog; the caller holds the mutex.
|
|
87
|
+
# @api private
|
|
88
|
+
def launch(generation, call)
|
|
89
|
+
@worker = Thread.new { work(generation, call) }
|
|
90
|
+
Thread.new { watch(generation) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @api private
|
|
94
|
+
def work(generation, call)
|
|
95
|
+
result = @runner.call(**call)
|
|
96
|
+
conclude(generation, result[:ok] ? "done" : "failed", result[:output])
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
conclude(generation, "failed", e.message)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @api private
|
|
102
|
+
def conclude(generation, status, output)
|
|
103
|
+
@mutex.synchronize { settle(status, output) if current?(generation) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Ends the job when it outlives the timeout; wakes early when the job settles first.
|
|
107
|
+
# @api private
|
|
108
|
+
def watch(generation)
|
|
109
|
+
deadline = now + @timeout
|
|
110
|
+
@mutex.synchronize do
|
|
111
|
+
@settled.wait(@mutex, deadline - now) while current?(generation) && now < deadline
|
|
112
|
+
expire(generation)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @api private
|
|
117
|
+
def expire(generation)
|
|
118
|
+
return unless current?(generation)
|
|
119
|
+
|
|
120
|
+
@worker.kill
|
|
121
|
+
settle("timed_out", "timed out after #{@timeout} seconds")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Records how the job ended; the caller holds the mutex.
|
|
125
|
+
# @api private
|
|
126
|
+
def settle(status, output)
|
|
127
|
+
text = output.to_s
|
|
128
|
+
text = text[-OUTPUT_LIMIT..] if text.length > OUTPUT_LIMIT
|
|
129
|
+
@state = @state.merge(status: status, finished_at: stamp, ok: status == "done", output: text)
|
|
130
|
+
@settled.broadcast
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Composer
|
|
3
|
+
# Proposes which panels the editor shows beside the text, on the hecksagon side like the
|
|
4
|
+
# other composer files. It is a small rule set, not a model call: the same inputs always give
|
|
5
|
+
# the same answer, and it reads only the author's intent and the check's verdict.
|
|
6
|
+
module Layout
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# @param intent [String] "edit", "wire", "question" or "new_domain"
|
|
10
|
+
# @param check [Hash] the result of {Check.call}
|
|
11
|
+
# @param changed [Hash] the files the last request changed, by path
|
|
12
|
+
# @return [Hash] `right` (one of the panel names) and `bottom` (always "agent")
|
|
13
|
+
def propose(intent:, check:, changed:)
|
|
14
|
+
{ right: right(intent, check, changed), bottom: "agent" }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# A refusal or an error finding comes first; then the author's intent decides.
|
|
18
|
+
# @api private
|
|
19
|
+
def right(intent, check, _changed)
|
|
20
|
+
return "findings" if refused?(check)
|
|
21
|
+
|
|
22
|
+
case intent
|
|
23
|
+
when "wire" then "wiring"
|
|
24
|
+
when "question" then "statements"
|
|
25
|
+
else "diagram"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @api private
|
|
30
|
+
def refused?(check)
|
|
31
|
+
return true if check[:diagnostic]
|
|
32
|
+
|
|
33
|
+
Array(check[:findings]).any? { |finding| finding[:severity].to_s == "error" }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "hecks"
|
|
3
|
+
require_relative "parse"
|
|
4
|
+
require_relative "plain"
|
|
5
|
+
require_relative "workspace"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Composer
|
|
9
|
+
# A workspace domain booted once and kept, so the Console and Records panels can dispatch
|
|
10
|
+
# commands against it and read back what it holds. It sits on the driving side of the
|
|
11
|
+
# hecksagon: the editor drives the domain, the domain keeps its records in memory, and the
|
|
12
|
+
# staged copy of the files lives until `close`.
|
|
13
|
+
class Live
|
|
14
|
+
# @return [String] the chapter the workspace booted
|
|
15
|
+
attr_reader :chapter_name
|
|
16
|
+
|
|
17
|
+
# Stages the workspace and boots it in memory.
|
|
18
|
+
#
|
|
19
|
+
# @param dir [String] the workspace directory
|
|
20
|
+
# @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
|
|
21
|
+
# @return [Live] a booted workspace
|
|
22
|
+
# @raise [StandardError, ScriptError] whatever the parse or the boot refused with (a
|
|
23
|
+
# bluebook that is not valid Ruby raises a SyntaxError); the stage is removed
|
|
24
|
+
def self.open(dir:, buffers: {})
|
|
25
|
+
stage = Workspace.materialize(dir: dir, buffers: buffers)
|
|
26
|
+
begin
|
|
27
|
+
runtime = Hecks.boot(stage, install_driving: false, environment: "memory")
|
|
28
|
+
new(stage: stage, runtime: runtime, chapter_name: Parse.bluebooks(stage).keys.first)
|
|
29
|
+
rescue StandardError, ScriptError
|
|
30
|
+
FileUtils.rm_rf(stage)
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param stage [String] the staged directory this domain booted from
|
|
36
|
+
# @param runtime [Runtime::Dispatcher] the booted domain
|
|
37
|
+
# @param chapter_name [String] the chapter's name
|
|
38
|
+
def initialize(stage:, runtime:, chapter_name:)
|
|
39
|
+
@stage = stage
|
|
40
|
+
@runtime = runtime
|
|
41
|
+
@chapter_name = chapter_name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The aggregate commands the chapter declares, each with a starter value for every argument.
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Hash>] per command: `verb`, `aggregate`, `command`, `goal`, `creates`,
|
|
47
|
+
# `args` (`{name:, type:, optional:}` each) and `template` (argument name to starter value)
|
|
48
|
+
def commands
|
|
49
|
+
aggregates.flat_map do |aggregate|
|
|
50
|
+
aggregate.commands.map { |command| describe(aggregate, command) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Runs one command against the booted domain.
|
|
55
|
+
#
|
|
56
|
+
# @param verb [String] the dispatcher's form, such as "Ledger::Account.Credit"
|
|
57
|
+
# @param to [String, nil] the identity of the record the command addresses
|
|
58
|
+
# @param with [Hash] the command's facts by argument name; keys may be strings
|
|
59
|
+
# @return [Hash] `{ok: true, events:, instance:}` with the event names and the plain row, or
|
|
60
|
+
# `{ok: false, kind:, error:}` with the refusal's class name and first message line
|
|
61
|
+
def dispatch(verb:, to: nil, with: {})
|
|
62
|
+
accepted(@runtime.dispatch(verb, to: to, with: symbolize(with)))
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
refused(e)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [Hash{String => Array<Hash>}] each aggregate's name to its records as plain rows
|
|
68
|
+
def records
|
|
69
|
+
aggregates.to_h do |aggregate|
|
|
70
|
+
rows = @runtime.registry.repository(@chapter_name, aggregate).all
|
|
71
|
+
[aggregate.hecks_name, Plain.call(rows.to_a)]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param limit [Integer] how many of the latest events to return
|
|
76
|
+
# @return [Array<Hash>] the last `limit` events, oldest first, as plain Hashes with `name`,
|
|
77
|
+
# `aggregate`, `id`, `payload` and `occurred_at`
|
|
78
|
+
def history(limit = 200)
|
|
79
|
+
@runtime.events.to_a.last(limit).map { |event| Plain.call(event.to_h) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Removes the staged copy of the workspace. Calling it again does nothing.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
def close
|
|
86
|
+
FileUtils.rm_rf(@stage)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @api private
|
|
90
|
+
def aggregates = @runtime.registry.bluebook(@chapter_name).aggregates
|
|
91
|
+
|
|
92
|
+
# @api private
|
|
93
|
+
def accepted(result)
|
|
94
|
+
{ ok: true, events: result.events.map { |event| event.to_h[:name] },
|
|
95
|
+
instance: Plain.call(result.instance) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @api private
|
|
99
|
+
def refused(error)
|
|
100
|
+
{ ok: false, kind: error.class.name.to_s.split("::").last,
|
|
101
|
+
error: error.message.lines.first.to_s.chomp }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @api private
|
|
105
|
+
def describe(aggregate, command)
|
|
106
|
+
args = arguments(command)
|
|
107
|
+
{ verb: "#{@chapter_name}::#{aggregate.hecks_name}.#{command.hecks_name}",
|
|
108
|
+
aggregate: aggregate.hecks_name, command: command.hecks_name, goal: command.goal.to_s,
|
|
109
|
+
creates: command.references.nil?, args: args, template: template(aggregate, args) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @api private
|
|
113
|
+
def template(aggregate, args)
|
|
114
|
+
args.to_h { |arg| [arg[:name], starter(aggregate, arg[:type])] }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @api private
|
|
118
|
+
def arguments(command)
|
|
119
|
+
command.attributes.map do |attr|
|
|
120
|
+
{ name: attr.name.to_s, type: attr.type.to_s, optional: attr.optional? }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A value object of the aggregate gets a Hash of its attributes' defaults; any other type, "".
|
|
125
|
+
# @api private
|
|
126
|
+
def starter(aggregate, type)
|
|
127
|
+
shape = aggregate.value_object(type)
|
|
128
|
+
return "" unless shape
|
|
129
|
+
|
|
130
|
+
shape.attributes.to_h { |attr| [attr.name.to_s, default_for(attr)] }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @api private
|
|
134
|
+
def default_for(attr)
|
|
135
|
+
return attr.default unless attr.default.nil?
|
|
136
|
+
|
|
137
|
+
attr.type.to_s == "Integer" ? 0 : ""
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @api private
|
|
141
|
+
def symbolize(item)
|
|
142
|
+
case item
|
|
143
|
+
when Hash then item.to_h { |key, inner| [key.to_sym, symbolize(inner)] }
|
|
144
|
+
when Array then item.map { |inner| symbolize(inner) }
|
|
145
|
+
else item
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Composer
|
|
3
|
+
# Where each named thing in a workspace is declared, read from the text alone so it works on a
|
|
4
|
+
# draft that does not parse: the editor jumps to a definition and shows a line's `goal` or
|
|
5
|
+
# `description` on hover from it.
|
|
6
|
+
module Outline
|
|
7
|
+
# The constructs that name something a reader may want to jump to.
|
|
8
|
+
KINDS = %w[aggregate entity value_object command query policy process_manager].freeze
|
|
9
|
+
|
|
10
|
+
DECLARATION = /\A\s*(#{KINDS.join("|")})\s+"([^"]+)"/
|
|
11
|
+
SUMMARY = /\A\s*(?:goal|description)\s+"((?:[^"\\]|\\.)*)"/
|
|
12
|
+
|
|
13
|
+
# How many lines below a declaration its `goal` or `description` may sit.
|
|
14
|
+
REACH = 6
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# @param stage [String] a staged directory (see {Workspace.stage})
|
|
19
|
+
# @param files [Array<String>] the staged files, relative to `stage`
|
|
20
|
+
# @return [Array<Hash>] `{file:, line:, kind:, name:, summary:}` for each declaration, in file
|
|
21
|
+
# then line order; `line` counts from 1 and `summary` is nil when none follows
|
|
22
|
+
def call(stage, files)
|
|
23
|
+
files.grep(/\.(bluebook|hecksagon|behaviors|world)\z/).flat_map do |file|
|
|
24
|
+
entries(file, File.readlines(File.join(stage, file), chomp: true))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @api private
|
|
29
|
+
def entries(file, lines)
|
|
30
|
+
lines.each_with_index.filter_map do |text, index|
|
|
31
|
+
found = text.match(DECLARATION) or next
|
|
32
|
+
{ file: file, line: index + 1, kind: found[1], name: found[2], summary: summary(lines, index) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @api private
|
|
37
|
+
def summary(lines, index)
|
|
38
|
+
lines[(index + 1)..(index + REACH)].to_a.each do |text|
|
|
39
|
+
break if text.match?(DECLARATION)
|
|
40
|
+
|
|
41
|
+
found = text.match(SUMMARY)
|
|
42
|
+
return found[1].gsub('\"', '"') if found
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|