hecks 3.13.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.bluebook +4 -4
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
- 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/named_pattern.rb +33 -0
- 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 +3 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/cli/domain_stub.rb +1 -1
- data/lib/hecks/cli/interview_aggregate.rb +4 -4
- 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/deploy/bluebook/deploy.bluebook +34 -34
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/framework/bluebook/change.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +58 -37
- 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 +15 -92
- data/lib/hecks/hecks/hecks.bluebook +2 -2
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +10 -16
- data/lib/hecks/hecks/tooling.bluebook +22 -22
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
- data/lib/hecks/language/bluebook/command.bluebook +1 -0
- data/lib/hecks/language/bluebook/entity.bluebook +1 -0
- data/lib/hecks/language/bluebook/query.bluebook +1 -0
- data/lib/hecks/language/bluebook/shape.bluebook +8 -7
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +51 -51
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value.rb +13 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/site/bluebook/site.bluebook +5 -5
- data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
- data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
- 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/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +300 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +145 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +990 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +191 -0
- data/rust/host/src/payments/tests.rs +1445 -275
- data/rust/host/src/payments.rs +409 -121
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +483 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -0
- data/rust/parser/src/build/named_pattern.rs +26 -0
- 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 -510
- 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 +43 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +229 -112
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +230 -148
- data/rust/src/kernel/mod.rs +46 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +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/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
- data/rust/host/src/approval.rs +0 -652
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require_relative "../board"
|
|
2
|
+
require_relative "../outline"
|
|
3
|
+
require_relative "../workspace"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Composer
|
|
7
|
+
module BoardEdit
|
|
8
|
+
# The bluebook text of a staged workspace as lines, with what a board edit needs to change
|
|
9
|
+
# it: where a declaration is, where its block ends (found by indentation, as the bluebooks
|
|
10
|
+
# are laid out), and a way to put a patched file back as one result.
|
|
11
|
+
class Source
|
|
12
|
+
# @return [Board::Places] where each aggregate, command and policy is declared
|
|
13
|
+
attr_reader :places
|
|
14
|
+
|
|
15
|
+
# @param stage [String] a staged directory (see {Workspace.stage})
|
|
16
|
+
def initialize(stage)
|
|
17
|
+
files = Workspace.files(stage)
|
|
18
|
+
@places = Board::Places.new(Outline.call(stage, files))
|
|
19
|
+
@lines = files.grep(/\.bluebook\z/).to_h { |file| [file, File.read(File.join(stage, file)).lines.map(&:chomp)] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param at [Hash] `{file:, line:}` of a declaration
|
|
23
|
+
# @return [Integer] the 0-based index of the `end` that closes it
|
|
24
|
+
# @raise [Refused] if the block has no closing `end` at its own indentation
|
|
25
|
+
def block_end(at)
|
|
26
|
+
lines = @lines.fetch(at[:file])
|
|
27
|
+
start = at[:line] - 1
|
|
28
|
+
pattern = /\A {#{indent_of(lines[start])}}end\b/
|
|
29
|
+
((start + 1)...lines.size).find { |index| lines[index].match?(pattern) } or
|
|
30
|
+
raise Refused, "cannot find where the block at #{at[:file]}:#{at[:line]} ends"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param at [Hash] `{file:, line:}` of a declaration
|
|
34
|
+
# @return [Integer] how many spaces its line is indented
|
|
35
|
+
def indent(at) = indent_of(@lines.fetch(at[:file])[at[:line] - 1])
|
|
36
|
+
|
|
37
|
+
# The file and `end` of the chapter's own block, where new aggregates and policies go.
|
|
38
|
+
# @return [Hash] `{file:, line:}` of the chapter header
|
|
39
|
+
# @raise [Refused] if no file declares the chapter
|
|
40
|
+
def chapter
|
|
41
|
+
file = @lines.keys.sort.find { |path| @lines[path].any? { |line| line.match?(/\A\s*Hecks\.bluebook\s+"/) } }
|
|
42
|
+
raise Refused, "no file declares the chapter" unless file
|
|
43
|
+
|
|
44
|
+
{ file: file, line: @lines[file].index { |line| line.match?(/\A\s*Hecks\.bluebook\s+"/) } + 1 }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param file [String] a bluebook path
|
|
48
|
+
# @param from [Integer] first 0-based line to read
|
|
49
|
+
# @param upto [Integer] last 0-based line to read
|
|
50
|
+
# @return [Array<String>] those lines
|
|
51
|
+
def slice(file, from, upto) = @lines.fetch(file)[from..upto]
|
|
52
|
+
|
|
53
|
+
# Puts new lines before a line of a file, with a blank line above them unless one is there.
|
|
54
|
+
# @return [Hash] the patched file as `{path:, text:, line:}`
|
|
55
|
+
def insert(file, before, new_lines)
|
|
56
|
+
lines = @lines.fetch(file)
|
|
57
|
+
added = spaced(lines, before, new_lines)
|
|
58
|
+
lines.insert(before, *added)
|
|
59
|
+
patched(file, before + 1 + added.index { |line| !line.strip.empty? }.to_i)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Takes lines out of a file, and the blank line above them when there is one.
|
|
63
|
+
# @return [Hash] the patched file as `{path:, text:, line:}`
|
|
64
|
+
def remove(file, from, upto)
|
|
65
|
+
lines = @lines.fetch(file)
|
|
66
|
+
from -= 1 if from.positive? && lines[from - 1].strip.empty?
|
|
67
|
+
lines.slice!(from..upto)
|
|
68
|
+
patched(file, nil)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def indent_of(line) = line[/\A */].size
|
|
74
|
+
|
|
75
|
+
# The new lines, without their leading blank line when one is already above the spot.
|
|
76
|
+
def spaced(lines, before, added)
|
|
77
|
+
added.first.to_s.empty? && lines[before - 1].to_s.strip.empty? ? added.drop(1) : added
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def patched(file, line) = { path: file, text: "#{@lines.fetch(file).join("\n")}\n", line: line }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -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
|