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,113 @@
|
|
|
1
|
+
require_relative "../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Composer
|
|
5
|
+
# The agent the Composer asks, on the hecksagon side: one port, `composer_agent`, resolved to
|
|
6
|
+
# the single adapter a registry holds for it. The caller never sees the adapter, only answers
|
|
7
|
+
# this module has checked, so a spec can stand a scripted adapter in for the real one.
|
|
8
|
+
#
|
|
9
|
+
# Two of the asks change the workspace on disk and answer with a note about what changed:
|
|
10
|
+
# `draft` edits a bluebook, `wire` edits the `.hecksagon` and `.world` files that bind it. The
|
|
11
|
+
# third, `classify`, only names what a prompt is asking for.
|
|
12
|
+
module Agent
|
|
13
|
+
NAME = "composer_agent".freeze
|
|
14
|
+
PORT = File.expand_path("ports/composer_agent.port", __dir__).freeze
|
|
15
|
+
ADAPTERS = File.expand_path("adapters", __dir__).freeze
|
|
16
|
+
|
|
17
|
+
# What a prompt can be asking for.
|
|
18
|
+
INTENTS = %w[edit wire question new_domain].freeze
|
|
19
|
+
|
|
20
|
+
# The adapter's answer arrived but was unusable; asking again will not help.
|
|
21
|
+
class ValidationError < StandardError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Nothing answered (missing binary, dead subprocess, timeout); retry or fall back.
|
|
25
|
+
class Unavailable < StandardError
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Asks the agent to change a workspace's bluebooks as the prompt says.
|
|
31
|
+
#
|
|
32
|
+
# @param registry [Runtime::Registry] a registry holding the port and one adapter for it
|
|
33
|
+
# @param prompt [String] what the author asked for
|
|
34
|
+
# @param dir [String] the workspace directory, whose files the agent edits in place
|
|
35
|
+
# @return [String] the agent's one-line account of what it changed
|
|
36
|
+
# @raise [ValidationError] unless the answer is a Hash with a non-blank `"note"`
|
|
37
|
+
# @raise [Unavailable] if the adapter cannot answer
|
|
38
|
+
# @raise [Runtime::WiringError] unless exactly one adapter implements the port
|
|
39
|
+
def draft(registry, prompt:, dir:)
|
|
40
|
+
note(adapter(registry).draft(prompt: prompt, dir: dir))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Asks the agent to change how a workspace's chapter is wired, never what it declares.
|
|
44
|
+
#
|
|
45
|
+
# @param registry [Runtime::Registry] a registry holding the port and one adapter for it
|
|
46
|
+
# @param prompt [String] what the author asked for
|
|
47
|
+
# @param dir [String] the workspace directory, whose `.hecksagon` and `.world` files the
|
|
48
|
+
# agent edits in place
|
|
49
|
+
# @return [String] the agent's one-line account of what it changed
|
|
50
|
+
# @raise [ValidationError] unless the answer is a Hash with a non-blank `"note"`
|
|
51
|
+
# @raise [Unavailable] if the adapter cannot answer
|
|
52
|
+
def wire(registry, prompt:, dir:)
|
|
53
|
+
note(adapter(registry).wire(prompt: prompt, dir: dir))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Names what a prompt is asking for.
|
|
57
|
+
#
|
|
58
|
+
# @param registry [Runtime::Registry] a registry holding the port and one adapter for it
|
|
59
|
+
# @param prompt [String] what the author typed
|
|
60
|
+
# @return [String] one of `Composer::Agent::INTENTS`
|
|
61
|
+
# @raise [ValidationError] unless the answer is a Hash whose `"intent"` is one of those
|
|
62
|
+
# @raise [Unavailable] if the adapter cannot answer
|
|
63
|
+
def classify(registry, prompt:)
|
|
64
|
+
answer = adapter(registry).classify(prompt: prompt)
|
|
65
|
+
intent = answer["intent"] if answer.is_a?(Hash)
|
|
66
|
+
return intent if INTENTS.include?(intent)
|
|
67
|
+
|
|
68
|
+
raise ValidationError, "an intent is one of #{INTENTS.join(", ")}, got #{intent.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Builds a registry in which exactly one adapter implements the port.
|
|
72
|
+
#
|
|
73
|
+
# @param adapter_file [String] the adapter's `.adapter` declaration
|
|
74
|
+
# @return [Runtime::Registry] a registry holding the port and that adapter
|
|
75
|
+
def registry_with(adapter_file)
|
|
76
|
+
registry = Runtime::Registry.new
|
|
77
|
+
Hecks.with_registry(registry) do
|
|
78
|
+
Kernel.load(PORT)
|
|
79
|
+
Kernel.load(adapter_file)
|
|
80
|
+
end
|
|
81
|
+
registry
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Finds the single adapter bound to the port.
|
|
85
|
+
#
|
|
86
|
+
# @param registry [Runtime::Registry] the registry to search
|
|
87
|
+
# @return [Module] the adapter implementing the port
|
|
88
|
+
# @raise [Runtime::WiringError] if none, or more than one, implements the port
|
|
89
|
+
def adapter(registry)
|
|
90
|
+
implementations = registry.adapters.values.select { |found| found.port == NAME }
|
|
91
|
+
return registry.adapter_class(implementations.first.name) if implementations.size == 1
|
|
92
|
+
|
|
93
|
+
raise Runtime::WiringError, wiring_refusal(implementations)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @api private
|
|
97
|
+
def note(answer)
|
|
98
|
+
text = answer["note"].to_s.strip if answer.is_a?(Hash)
|
|
99
|
+
return text unless text.nil? || text.empty?
|
|
100
|
+
|
|
101
|
+
raise ValidationError, "an agent's answer carries a non-blank \"note\", got #{answer.inspect}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @api private
|
|
105
|
+
def wiring_refusal(implementations)
|
|
106
|
+
return "no adapter implements the #{NAME} port — nothing can edit the draft" if implementations.empty?
|
|
107
|
+
|
|
108
|
+
names = implementations.map(&:name).sort.join(", ")
|
|
109
|
+
"#{implementations.size} adapters implement the #{NAME} port (#{names}) — the runtime will not choose for you"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require_relative "agent"
|
|
2
|
+
require_relative "check"
|
|
3
|
+
require_relative "layout"
|
|
4
|
+
require_relative "session"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Composer
|
|
8
|
+
# Carries one request from the author to the agent and back, on the hecksagon side: records it
|
|
9
|
+
# in the draft's conversation, has the prompt classified, asks the agent to change the
|
|
10
|
+
# workspace's files, judges what it left, asks once more when the check refused the result, and
|
|
11
|
+
# records the agent's reply and the verdict.
|
|
12
|
+
#
|
|
13
|
+
# The agent edits the workspace on disk, so the files it changes are read before and after to
|
|
14
|
+
# show the author what moved.
|
|
15
|
+
class Assistant
|
|
16
|
+
# How many times the agent may be asked about one request: the request, then a repair.
|
|
17
|
+
ROUNDS = 2
|
|
18
|
+
|
|
19
|
+
QUESTION_NOTE = "That reads as a question, so nothing was edited.".freeze
|
|
20
|
+
|
|
21
|
+
# @param workspace [String] the domain directory the agent edits
|
|
22
|
+
# @param agents [Runtime::Registry] a registry holding the `composer_agent` port and one
|
|
23
|
+
# adapter
|
|
24
|
+
# @param session [Session] the editor's in-memory draft bookkeeping
|
|
25
|
+
def initialize(workspace:, agents:, session:)
|
|
26
|
+
@workspace = workspace
|
|
27
|
+
@agents = agents
|
|
28
|
+
@session = session
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param prompt [String] what the author asked for
|
|
32
|
+
# @return [Hash] `intent`, `note` (the agent's one-line account), `rounds` (how many times it
|
|
33
|
+
# was asked), `changed` (`{path => {before:, after:}}` for each file that differs), `check`
|
|
34
|
+
# (the whole check on what is now on disk) and `layout` (the panels to open, see {Layout})
|
|
35
|
+
def ask(prompt)
|
|
36
|
+
slug = File.basename(@workspace)
|
|
37
|
+
before = snapshot
|
|
38
|
+
open_draft(slug, before)
|
|
39
|
+
@session.ask(slug: slug, said: prompt)
|
|
40
|
+
intent = classify(prompt)
|
|
41
|
+
note, rounds = edit(intent, prompt)
|
|
42
|
+
outcome(slug, intent, note, rounds, before)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def open_draft(slug, files)
|
|
48
|
+
chapter = Parse.bluebooks(@workspace).keys.first || "Workspace"
|
|
49
|
+
@session.open(slug: slug, chapter_name: chapter, text: files.values.first.to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The prompt's intent; a classifier that cannot answer leaves it an edit.
|
|
53
|
+
def classify(prompt)
|
|
54
|
+
Agent.classify(@agents, prompt: prompt)
|
|
55
|
+
rescue Agent::Unavailable
|
|
56
|
+
"edit"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def edit(intent, prompt)
|
|
60
|
+
return [QUESTION_NOTE, 0] if intent == "question"
|
|
61
|
+
|
|
62
|
+
note = ask_agent(intent, prompt)
|
|
63
|
+
refusal = Check.call(dir: @workspace)[:diagnostic]
|
|
64
|
+
return [note, 1] unless refusal
|
|
65
|
+
|
|
66
|
+
[ask_agent(intent, repair(prompt, refusal)), ROUNDS]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def ask_agent(intent, prompt)
|
|
70
|
+
return Agent.wire(@agents, prompt: prompt, dir: @workspace) if intent == "wire"
|
|
71
|
+
|
|
72
|
+
Agent.draft(@agents, prompt: prompt, dir: @workspace)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def repair(prompt, refusal)
|
|
76
|
+
where = refusal[:line] ? "#{refusal[:file]} line #{refusal[:line]}: " : ""
|
|
77
|
+
expected = refusal[:expected].to_a.empty? ? "" : " (expected one of: #{refusal[:expected].join(", ")})"
|
|
78
|
+
"#{prompt}\n\nYour last edit was refused by the check, so fix it: #{where}#{refusal[:message]}#{expected}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def outcome(slug, intent, note, rounds, before)
|
|
82
|
+
after = snapshot
|
|
83
|
+
check = Check.call(dir: @workspace)
|
|
84
|
+
@session.answer(slug: slug, said: note, text: after.values.first.to_s)
|
|
85
|
+
@session.judge(slug: slug, diagnostic: check[:diagnostic])
|
|
86
|
+
moved = changed(before, after)
|
|
87
|
+
record(moved)
|
|
88
|
+
{ intent: intent, note: note, rounds: rounds, changed: moved, check: check,
|
|
89
|
+
layout: Layout.propose(intent: intent, check: check, changed: moved) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Keeps each file the agent changed in the timeline, as a save would.
|
|
93
|
+
def record(moved)
|
|
94
|
+
moved.each { |path, text| @session.record(path: path, text: text[:after]) if text[:after] }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def changed(before, after)
|
|
98
|
+
(before.keys | after.keys).sort.each_with_object({}) do |path, moved|
|
|
99
|
+
next if before[path] == after[path]
|
|
100
|
+
|
|
101
|
+
moved[path] = { before: before[path], after: after[path] }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def snapshot
|
|
106
|
+
Workspace.files(@workspace).to_h { |path| [path, File.read(File.join(@workspace, path))] }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "hecks"
|
|
2
|
+
require "hecks/behaviors"
|
|
3
|
+
require_relative "workspace"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Composer
|
|
7
|
+
# Runs a workspace's `.behaviors` files as they stand on screen and reports each test as plain
|
|
8
|
+
# data. Like the other composer files it sits on the hecksagon side: it stages the text, calls
|
|
9
|
+
# the behaviors runner and changes nothing on disk.
|
|
10
|
+
module BehaviorRun
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# @param dir [String] the workspace directory
|
|
14
|
+
# @param buffers [Hash{String => String}] unsaved text by path relative to `dir`
|
|
15
|
+
# @return [Hash] `files` (`{path:, parse_error:, runs: [{status:, description:, message:}]}`
|
|
16
|
+
# per behaviors file, paths relative to the workspace, status "pass", "fail" or "error")
|
|
17
|
+
# and `summary` (`total`, `passed`, `failed`, `errored` and `parse_errors` counts)
|
|
18
|
+
# @raise [Workspace::OutsideWorkspace] if a buffer's path leaves the workspace
|
|
19
|
+
def call(dir:, buffers: {})
|
|
20
|
+
Workspace.stage(dir: dir, buffers: buffers) do |stage|
|
|
21
|
+
files = Hecks::Behaviors.run_all(stage).files.map { |file| report(stage, file) }
|
|
22
|
+
{ files: files, summary: tally(files) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @api private
|
|
27
|
+
def report(stage, file)
|
|
28
|
+
{
|
|
29
|
+
path: file.path.to_s.delete_prefix("#{File.realpath(stage)}/").delete_prefix("#{stage}/"),
|
|
30
|
+
parse_error: file.parse_error,
|
|
31
|
+
runs: file.runs.map { |run| run_hash(run) }
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @api private
|
|
36
|
+
def run_hash(run)
|
|
37
|
+
{ status: run.status.to_s, description: run.description, message: run.message }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @api private
|
|
41
|
+
def tally(files)
|
|
42
|
+
runs = files.flat_map { |file| file[:runs] }
|
|
43
|
+
{
|
|
44
|
+
total: runs.size,
|
|
45
|
+
passed: runs.count { |run| run[:status] == "pass" },
|
|
46
|
+
failed: runs.count { |run| run[:status] == "fail" },
|
|
47
|
+
errored: runs.count { |run| run[:status] == "error" },
|
|
48
|
+
parse_errors: files.count { |file| file[:parse_error] }
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# The Composer chapter: a bluebook being written in an editor, by hand or by asking an agent.
|
|
2
|
+
#
|
|
3
|
+
# A Draft is the editor's working copy of one chapter: the text on screen, the conversation that
|
|
4
|
+
# shaped it, and whether the last check of it passed. The files on disk stay the truth; a Draft
|
|
5
|
+
# only holds what is being changed and what was said about it. The check itself (parse, load,
|
|
6
|
+
# project, model-check) and the agent that rewrites the text are the outside, reached through the
|
|
7
|
+
# Composer's hecksagon, so nothing here knows how either is done.
|
|
8
|
+
Hecks.bluebook "Composer" do
|
|
9
|
+
vision "Write a bluebook in an editor and watch the domain draw itself, with an agent that can change the draft when asked."
|
|
10
|
+
|
|
11
|
+
aggregate "Draft" do
|
|
12
|
+
description "One chapter being edited: its text, the conversation about it, and whether its last check passed."
|
|
13
|
+
|
|
14
|
+
attribute :slug, DraftSlug
|
|
15
|
+
attribute :chapter_name, ChapterName
|
|
16
|
+
attribute :text, SourceText
|
|
17
|
+
attribute :note, Note, optional: true
|
|
18
|
+
attribute :turns, list_of(Turn)
|
|
19
|
+
|
|
20
|
+
identified_by :slug
|
|
21
|
+
|
|
22
|
+
value_object "DraftSlug" do
|
|
23
|
+
attribute :value, String, pattern: '[^ \t\n\r]'
|
|
24
|
+
|
|
25
|
+
invariant("a draft is named") { !value.to_s.empty? }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The name a chapter's `Hecks.bluebook "Name"` header declares, which is also the name the
|
|
29
|
+
# check loads it under.
|
|
30
|
+
value_object "ChapterName" do
|
|
31
|
+
attribute :value, String, pattern: '[A-Z][A-Za-z0-9]*'
|
|
32
|
+
|
|
33
|
+
invariant("a chapter is named") { !value.to_s.empty? }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# What is on screen. A draft may be empty or half-written; the check judges it, not this.
|
|
37
|
+
value_object "SourceText" do
|
|
38
|
+
attribute :value, String
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The check's last word on the draft, or the agent's one-line account of what it changed.
|
|
42
|
+
value_object "Note" do
|
|
43
|
+
attribute :value, String
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
value_object "TurnSequence" do
|
|
47
|
+
attribute :value, Integer
|
|
48
|
+
|
|
49
|
+
invariant("a turn sequence is positive") { value.positive? }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
value_object "Speaker" do
|
|
53
|
+
attribute :value, String, one_of: ["author", "agent"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
value_object "Utterance" do
|
|
57
|
+
attribute :value, String
|
|
58
|
+
|
|
59
|
+
invariant("a turn says something") { !value.to_s.empty? }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The sequence is supplied by the caller, who already knows how many turns are on screen, so
|
|
63
|
+
# the order of a conversation is not a fact this chapter invents on write.
|
|
64
|
+
entity "Turn" do
|
|
65
|
+
description "One thing said in the conversation about the draft, by the author or by the agent."
|
|
66
|
+
|
|
67
|
+
attribute :sequence, TurnSequence
|
|
68
|
+
|
|
69
|
+
identified_by :sequence
|
|
70
|
+
|
|
71
|
+
attribute :speaker, Speaker
|
|
72
|
+
attribute :said, Utterance
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
lifecycle :status, default: "drafting" do
|
|
76
|
+
transition "Accept" => "valid", from: %w[drafting valid invalid]
|
|
77
|
+
transition "Reject" => "invalid", from: %w[drafting valid invalid]
|
|
78
|
+
transition "Revise" => "drafting", from: %w[drafting valid invalid]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
command "Begin" do
|
|
82
|
+
role "Author"
|
|
83
|
+
goal "Open a chapter for editing, with the text it starts from"
|
|
84
|
+
|
|
85
|
+
attribute :slug, DraftSlug
|
|
86
|
+
attribute :chapter_name, ChapterName
|
|
87
|
+
attribute :text, SourceText
|
|
88
|
+
|
|
89
|
+
sets :slug
|
|
90
|
+
sets :chapter_name
|
|
91
|
+
sets :text
|
|
92
|
+
|
|
93
|
+
emits "DraftBegun"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
command "Revise" do
|
|
97
|
+
role "Author"
|
|
98
|
+
goal "Replace the draft's text with what is now on screen"
|
|
99
|
+
|
|
100
|
+
reference_to Draft
|
|
101
|
+
attribute :text, SourceText
|
|
102
|
+
|
|
103
|
+
sets :text
|
|
104
|
+
|
|
105
|
+
emits "DraftRevised"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
command "Accept" do
|
|
109
|
+
role "System"
|
|
110
|
+
goal "Record that the draft's text passed its check"
|
|
111
|
+
|
|
112
|
+
reference_to Draft
|
|
113
|
+
attribute :note, Note
|
|
114
|
+
|
|
115
|
+
sets :note
|
|
116
|
+
|
|
117
|
+
emits "DraftAccepted"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
command "Reject" do
|
|
121
|
+
role "System"
|
|
122
|
+
goal "Record that the draft's text failed its check, and why"
|
|
123
|
+
|
|
124
|
+
reference_to Draft
|
|
125
|
+
attribute :note, Note
|
|
126
|
+
|
|
127
|
+
sets :note
|
|
128
|
+
|
|
129
|
+
emits "DraftRejected"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
command "Ask" do
|
|
133
|
+
role "Author"
|
|
134
|
+
goal "Put a request for the agent to the draft's conversation"
|
|
135
|
+
|
|
136
|
+
reference_to Draft
|
|
137
|
+
attribute :sequence, TurnSequence
|
|
138
|
+
attribute :said, Utterance
|
|
139
|
+
|
|
140
|
+
sets :turns, append: { sequence: :sequence, speaker: { value: "author" }, said: :said }
|
|
141
|
+
|
|
142
|
+
emits "TurnAsked"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
command "Answer" do
|
|
146
|
+
role "System"
|
|
147
|
+
goal "Record the agent's reply and the text it left the draft with"
|
|
148
|
+
|
|
149
|
+
reference_to Draft
|
|
150
|
+
attribute :sequence, TurnSequence
|
|
151
|
+
attribute :said, Utterance
|
|
152
|
+
attribute :text, SourceText
|
|
153
|
+
|
|
154
|
+
sets :text
|
|
155
|
+
sets :turns, append: { sequence: :sequence, speaker: { value: "agent" }, said: :said }
|
|
156
|
+
|
|
157
|
+
emits "TurnAnswered"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
query "Drafting" do
|
|
161
|
+
description "Every draft still being edited."
|
|
162
|
+
where(status: "drafting")
|
|
163
|
+
order_by :slug
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
query "Valid" do
|
|
167
|
+
description "Every draft whose last check passed."
|
|
168
|
+
where(status: "valid")
|
|
169
|
+
order_by :slug
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
query "Invalid" do
|
|
173
|
+
description "Every draft whose last check failed."
|
|
174
|
+
where(status: "invalid")
|
|
175
|
+
order_by :slug
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# The Composer domain, bound so it boots standing alone (a fuzz or model_check boot of this
|
|
2
|
+
# directory). Drafts are working state; the files on disk are the truth, so nothing here outlives
|
|
3
|
+
# the process.
|
|
4
|
+
Hecks.hecksagon "Composer" do
|
|
5
|
+
attaches "Governance"
|
|
6
|
+
persisted_by "Memory"
|
|
7
|
+
# The editor reaches this domain through Web, and every tool that boots a corpus domain (fuzz,
|
|
8
|
+
# model_check, the Composer's own Session) through Ruby; the launcher, the forms and MCP do not.
|
|
9
|
+
driven_by "Web"
|
|
10
|
+
driven_by "Ruby"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The anti-corruption layer for Governance, which the chapter's roles are checked against.
|
|
14
|
+
Hecks.hecksagon "Governance" do
|
|
15
|
+
persisted_by "Memory"
|
|
16
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Composer
|
|
3
|
+
# The event-storming board of a chapter, on the hecksagon side: its aggregates with their
|
|
4
|
+
# commands, the events each command emits, and the policies that make one event trigger another
|
|
5
|
+
# command. It is read off the booted chapter, so it always shows what the text declares; the
|
|
6
|
+
# board's own edits go through {BoardEdit}, which rewrites the text.
|
|
7
|
+
module Board
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# @param chapter [Bluebook::Chapter] the booted chapter
|
|
11
|
+
# @param outline [Array<Hash>] the text outline (see {Outline}), which says where each note
|
|
12
|
+
# is declared
|
|
13
|
+
# @return [Hash] `aggregates` (each with `name`, `at` and its `commands`, each command with
|
|
14
|
+
# `name`, `role`, `goal`, `emits` and `at`) and `policies` (`name`, `on`, `trigger`, `at`),
|
|
15
|
+
# where `at` is `{file:, line:}` or nil, and `on` and `trigger` read `Aggregate.Name`
|
|
16
|
+
def call(chapter, outline:)
|
|
17
|
+
places = Places.new(outline)
|
|
18
|
+
{ aggregates: chapter.aggregates.map { |aggregate| aggregate_note(aggregate, places) },
|
|
19
|
+
policies: chapter.policies.map { |policy| policy_note(policy, places) } }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
def aggregate_note(aggregate, places)
|
|
24
|
+
name = aggregate.hecks_name
|
|
25
|
+
{ name: name, at: places.of("aggregate", name),
|
|
26
|
+
commands: aggregate.commands.map { |command| command_note(name, command, places) } }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @api private
|
|
30
|
+
def command_note(owner, command, places)
|
|
31
|
+
name = command.hecks_name
|
|
32
|
+
{ name: name, role: command.role, goal: command.goal, emits: Array(command.emits).map(&:to_s),
|
|
33
|
+
at: places.of("command", name, owner: owner) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @api private
|
|
37
|
+
def policy_note(policy, places)
|
|
38
|
+
{ name: policy.name, on: policy.on_event, trigger: policy.trigger_command,
|
|
39
|
+
at: places.of("policy", policy.name) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Where each declaration sits in the text, found by kind and name, and for a command by the
|
|
43
|
+
# aggregate that holds it.
|
|
44
|
+
class Places
|
|
45
|
+
# @param outline [Array<Hash>] see {Outline}
|
|
46
|
+
def initialize(outline)
|
|
47
|
+
owner = nil
|
|
48
|
+
@entries = outline.map do |entry|
|
|
49
|
+
owner = entry[:name] if entry[:kind] == "aggregate"
|
|
50
|
+
entry.merge(owner: entry[:kind] == "command" ? owner : nil)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param kind [String] `aggregate`, `command` or `policy`
|
|
55
|
+
# @param name [String] the declared name
|
|
56
|
+
# @param owner [String, nil] the aggregate, for a command
|
|
57
|
+
# @return [Hash, nil] `{file:, line:}` of the first match, or nil when the text has none
|
|
58
|
+
def of(kind, name, owner: nil)
|
|
59
|
+
found = @entries.find { |e| e[:kind] == kind && e[:name] == name && e[:owner] == owner }
|
|
60
|
+
found && { file: found[:file], line: found[:line] }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Composer
|
|
3
|
+
module BoardEdit
|
|
4
|
+
# The text of a command the board adds. The first command of an aggregate creates it, setting
|
|
5
|
+
# the key the aggregate is known by; every later one acts on a record of it. A command with an
|
|
6
|
+
# event says so on a line of its own.
|
|
7
|
+
module CommandText
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# @param block [Array<String>] the lines of the aggregate's block, after its declaration
|
|
11
|
+
# @param indent [Integer] how far the aggregate's declaration is indented
|
|
12
|
+
# @param aggregate [String] the aggregate's name
|
|
13
|
+
# @param name [String] the new command's name
|
|
14
|
+
# @param event [String] the event it emits, or an empty string for none
|
|
15
|
+
# @return [Array<String>] the command's lines, indented one step inside the aggregate
|
|
16
|
+
# @raise [Refused] if the aggregate's key cannot be read, for a command that must set it
|
|
17
|
+
def lines(block:, indent:, aggregate:, name:, event:)
|
|
18
|
+
pad = " " * (indent + 2)
|
|
19
|
+
body = acts_on_a_record?(block, indent) ? ["reference_to #{aggregate}"] : creates(block, aggregate)
|
|
20
|
+
inner = [%(goal "Describe #{name}."), *body, (event.empty? ? nil : "emits #{event}")].compact
|
|
21
|
+
["", %(#{pad}command "#{name}" do), *inner.map { |line| "#{pad} #{line}" }, "#{pad}end"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @api private
|
|
25
|
+
def acts_on_a_record?(block, indent) = block.any? { |line| line.match?(/\A {#{indent + 2}}command\s+"/) }
|
|
26
|
+
|
|
27
|
+
# The attribute and `sets` that give a new record its key.
|
|
28
|
+
# @api private
|
|
29
|
+
def creates(block, aggregate)
|
|
30
|
+
field = block.filter_map { |line| line[/\A\s*identified_by\s+:(\w+)\s*\z/, 1] }.first
|
|
31
|
+
type = field && block.filter_map { |line| line[/\A\s*attribute\s+:#{field},\s*([A-Za-z:]+)/, 1] }.first
|
|
32
|
+
raise Refused, "#{aggregate} has no key the board can read; write its first command in the text" unless type
|
|
33
|
+
|
|
34
|
+
["attribute :#{field}, #{type}", "sets :#{field}"]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -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
|