hecks 0.2.0 → 1.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 +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +427 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +145 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +78 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +66 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +166 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +341 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +333 -0
- data/lib/hecks/language/port.bluebook +120 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +174 -0
- data/lib/hecks/ports/access_control.port +9 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +96 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +642 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +44 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +411 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +92 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
require_relative "../runtime/value"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Fuzzing
|
|
5
|
+
# One attribute, one value — in the exact JSON shape the hand-written
|
|
6
|
+
# corpus already uses (a value-object-typed attribute is a nested hash keyed
|
|
7
|
+
# by its own field names ; a reference is the BARE ID of the head it points
|
|
8
|
+
# at, because that is what a reference is). Everything
|
|
9
|
+
# returned is a plain, JSON-safe Ruby value : String keys throughout, never
|
|
10
|
+
# symbols, so a generated step can be dumped straight to JSON.
|
|
11
|
+
#
|
|
12
|
+
# Biased toward the edge cases a hand-written corpus tends to under-sample —
|
|
13
|
+
# empty strings, quotes, commas, unicode, zero, negative numbers — because
|
|
14
|
+
# `marks.rb` names literal-encoding loss "the largest family of bug in this
|
|
15
|
+
# codebase," and a fixed corpus of a few dozen examples rarely happens to
|
|
16
|
+
# exercise the boundary that actually breaks. But mostly not : an attribute
|
|
17
|
+
# named `email`/`currency` almost always needs a specific shape just to get
|
|
18
|
+
# PAST its own invariant (`address.include?("@")`, `currency.size == 3`) —
|
|
19
|
+
# banking's very first fuzz run never got past Customer.Register, because
|
|
20
|
+
# no plain random word ever contains "@". Reaching deep state matters more
|
|
21
|
+
# than edge-casing every single field every single time, so the "normal"
|
|
22
|
+
# path is now name-aware for the couple of shapes that are genuinely
|
|
23
|
+
# common across domains, and edge cases stay real but less frequent.
|
|
24
|
+
module ValueGenerator
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
EDGE_CASE_PROBABILITY = 0.2
|
|
28
|
+
INVALID_MEMBER_PROBABILITY = 0.1
|
|
29
|
+
INVALID_REFERENCE_PROBABILITY = 0.2
|
|
30
|
+
# `Value.for_attribute` → `fields_for`'s own bare-scalar branch
|
|
31
|
+
# (lib/hecks/runtime/value/coercion.rb) has accepted a bare
|
|
32
|
+
# `"large"` in place of `{"value" => "large"}` for any single-field
|
|
33
|
+
# value object since 86727afd — but until this generator actually
|
|
34
|
+
# PRODUCES that shape, nothing exercises it: neither the adapter-
|
|
35
|
+
# agreement gate nor the Rust/WASM `from_json` codegen (rust/project/
|
|
36
|
+
# json_codec.rb) can ever be caught drifting on a shape they're
|
|
37
|
+
# never handed.
|
|
38
|
+
BARE_SCALAR_PROBABILITY = 0.2
|
|
39
|
+
|
|
40
|
+
STRING_EDGE_CASES = [
|
|
41
|
+
"", "x", "with \"quotes\"", "with, a comma", "with a \\backslash",
|
|
42
|
+
"unicode héllo wörld 🎉", "x" * 200, " leading and trailing "
|
|
43
|
+
].freeze
|
|
44
|
+
# PRD 05 (numeric-boundary-coverage) — Bignum (`2**100`, past i64's
|
|
45
|
+
# own ceiling, which Ruby's own `Integer` has no such ceiling for —
|
|
46
|
+
# `rust/src/kernel/json.rs`'s own `integral_i64` doc comment names
|
|
47
|
+
# exactly this: a Rust kernel value CANNOT represent it, so this
|
|
48
|
+
# exercises a real cross-runtime capability gap, not a Ruby-only
|
|
49
|
+
# edge) and its negative twin. Both round-trip through Ruby's own
|
|
50
|
+
# arithmetic/JSON cleanly (confirmed directly: `(2**100).clamp(...)`
|
|
51
|
+
# and `JSON.generate(2**100)` both just work — Integer has no
|
|
52
|
+
# ceiling here), so nothing in THIS runtime needed a fix for these;
|
|
53
|
+
# they're included so a real generated sequence occasionally
|
|
54
|
+
# produces the value at all, since nothing had, repo-wide, before.
|
|
55
|
+
INTEGER_EDGE_CASES = [0, -1, 2_147_483_647, -2_147_483_648, 2**100, -(2**100)].freeze
|
|
56
|
+
# NaN and +/-Infinity — the real find (see `spec/runtime/
|
|
57
|
+
# numeric_boundary_spec.rb`): `Value::Coercion#check_numeric_fields`
|
|
58
|
+
# used to let all three sail through untyped-checked (each really
|
|
59
|
+
# is a Float), reaching either `CommandRules::Arithmetic#clamp`
|
|
60
|
+
# (raw `ArgumentError`, not a domain refusal) or `JSON.generate`
|
|
61
|
+
# (`JSON::GeneratorError`, also not a domain refusal) — both fixed
|
|
62
|
+
# at the source now, so these are safe to generate. -0.0 is
|
|
63
|
+
# deliberately included too even though it was ALREADY safe
|
|
64
|
+
# (finite, round-trips through JSON as `-0.0` cleanly) — a signed
|
|
65
|
+
# zero is exactly the kind of boundary a hand-written corpus never
|
|
66
|
+
# happens to type, and the fuzzer existing to cover it is the
|
|
67
|
+
# point.
|
|
68
|
+
FLOAT_EDGE_CASES = [0.0, -0.0, -0.5, -100.25, Float::NAN, Float::INFINITY, -Float::INFINITY].freeze
|
|
69
|
+
WORDS = %w[alpha bravo charlie delta echo foxtrot golf hotel india juliet].freeze
|
|
70
|
+
CURRENCY_CODES = %w[USD EUR GBP JPY].freeze
|
|
71
|
+
|
|
72
|
+
# The value for one command/query/value-object attribute. `known_ids` is
|
|
73
|
+
# `{aggregate_or_entity_name => [scalar id, ...]}`, supplied by the
|
|
74
|
+
# sequence generator — a reference draws a real id from it most of the
|
|
75
|
+
# time and a fabricated one sometimes, to exercise NotFound as often as
|
|
76
|
+
# the happy path.
|
|
77
|
+
# `context` carries the enclosing value object's own declared name down
|
|
78
|
+
# into a nested primitive field — `EmailAddress`'s `address` field is
|
|
79
|
+
# what actually needs to look email-shaped, but the field itself is
|
|
80
|
+
# just named "address"; the VO's own name is where "email" lives. A
|
|
81
|
+
# combined hint catches both spellings without needing to guess which
|
|
82
|
+
# level a domain happened to name the thing on.
|
|
83
|
+
def value_for(attribute, aggregate, random:, known_ids: {}, context: nil)
|
|
84
|
+
return reference_value(attribute, random: random, known_ids: known_ids) if attribute.reference?
|
|
85
|
+
|
|
86
|
+
# Local first, falling back to a same-chapter identity value object
|
|
87
|
+
# declared on a sibling aggregate — the exact resolution
|
|
88
|
+
# `Value.value_object_for` already does for a real dispatch
|
|
89
|
+
# (runtime/value/coercion.rb). `safe_deposit_boxes.bluebook`'s own
|
|
90
|
+
# `attribute :customer, CustomerNumber` is declared on SafeDepositBox
|
|
91
|
+
# but CustomerNumber itself lives on Customer; without this fallback
|
|
92
|
+
# the generator raised "does not know primitive type" for any such
|
|
93
|
+
# cross-aggregate identity attribute rather than reusing the same
|
|
94
|
+
# lookup the runtime relies on.
|
|
95
|
+
value_object = Runtime::Value.value_object_for(aggregate, attribute.type.to_s)
|
|
96
|
+
return object_for(value_object, aggregate, random: random, known_ids: known_ids) if value_object
|
|
97
|
+
|
|
98
|
+
primitive(attribute.type.to_s, random: random, name: "#{context} #{attribute.name}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def object_for(value_object, aggregate, random:, known_ids:)
|
|
102
|
+
fields =
|
|
103
|
+
if value_object.closed_set? && !value_object.members.empty?
|
|
104
|
+
return invalid_member(value_object, random: random) if random.rand < INVALID_MEMBER_PROBABILITY
|
|
105
|
+
|
|
106
|
+
member = value_object.members.sample(random: random)
|
|
107
|
+
member.to_h { |field, value| [field.to_s, value] }
|
|
108
|
+
else
|
|
109
|
+
value_object.attributes.to_h do |field|
|
|
110
|
+
[field.name.to_s,
|
|
111
|
+
value_for(field, aggregate, random: random, known_ids: known_ids, context: value_object.hecks_name)]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `fields.size == 1` — the SAME test `Behaviour::ValueObject#
|
|
116
|
+
# sole_attribute` names: a genuinely single-field value object,
|
|
117
|
+
# not merely "this particular closed-set member happened to pick
|
|
118
|
+
# one field." Unwrapping ONLY here, not in `invalid_member`
|
|
119
|
+
# above — a deliberately-wrong combination stays a Hash so its
|
|
120
|
+
# own wrongness is what gets exercised, not a second, unrelated
|
|
121
|
+
# shape question.
|
|
122
|
+
return fields.values.first if fields.size == 1 && random.rand < BARE_SCALAR_PROBABILITY
|
|
123
|
+
|
|
124
|
+
fields
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A combination that (almost certainly) isn't one of the closed set's
|
|
128
|
+
# admitted rows — deliberately, to exercise the refusal a `one_of`
|
|
129
|
+
# exists to enforce, not just its happy path.
|
|
130
|
+
def invalid_member(value_object, random:)
|
|
131
|
+
value_object.attributes.to_h { |field|
|
|
132
|
+
[field.name.to_s, primitive(field.type.to_s, random: random, name: field.name.to_s)]
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# THE ID ITSELF. This minted `{"value" => id}` back when a reference was
|
|
137
|
+
# stored wrapped ; the payload gate refuses that shape now, so a fuzzer
|
|
138
|
+
# still emitting it would have every generated reference refused and
|
|
139
|
+
# the SILENT guard would report the fuzzer broken rather than
|
|
140
|
+
# the runtime.
|
|
141
|
+
def reference_value(attribute, random:, known_ids:)
|
|
142
|
+
pool = known_ids[attribute.type.target_name.to_s] || []
|
|
143
|
+
return "missing-#{random.bytes(4).unpack1('H*')}" if pool.empty? || random.rand < INVALID_REFERENCE_PROBABILITY
|
|
144
|
+
|
|
145
|
+
pool.sample(random: random)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def primitive(type_name, random:, name: nil)
|
|
149
|
+
case type_name
|
|
150
|
+
when "String" then string_value(random, name: name)
|
|
151
|
+
when "Integer" then integer_value(random)
|
|
152
|
+
when "Float" then float_value(random)
|
|
153
|
+
when "TrueClass", "FalseClass" then random.rand(2).zero?
|
|
154
|
+
else raise ArgumentError, "ValueGenerator does not know primitive type #{type_name.inspect}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def string_value(random, name: nil)
|
|
159
|
+
return STRING_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
|
|
160
|
+
return email_value(random) if name&.match?(/email/i)
|
|
161
|
+
return CURRENCY_CODES.sample(random: random) if name&.match?(/currency/i)
|
|
162
|
+
|
|
163
|
+
Array.new(random.rand(1..3)) { WORDS.sample(random: random) }.join(" ")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def email_value(random)
|
|
167
|
+
"#{WORDS.sample(random: random)}@#{WORDS.sample(random: random)}.example"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Skewed positive : `cents.positive?`/`!cents.negative?`-style
|
|
171
|
+
# invariants are common across this codebase's example domains, and a
|
|
172
|
+
# sequence that can never get past one never reaches the state a deeper
|
|
173
|
+
# bug would need. Zero and negative are still real, reachable outcomes —
|
|
174
|
+
# via the edge-case pool, deliberately, not by starving them entirely.
|
|
175
|
+
def integer_value(random)
|
|
176
|
+
return INTEGER_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
|
|
177
|
+
|
|
178
|
+
random.rand(1..1000)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def float_value(random)
|
|
182
|
+
return FLOAT_EDGE_CASES.sample(random: random) if random.rand < EDGE_CASE_PROBABILITY
|
|
183
|
+
|
|
184
|
+
random.rand(0.01..1000.0).round(2)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The bare scalar a generated IDENTITY value stands for, for recording into
|
|
188
|
+
# `known_ids`. An identity is declared as a value object, so this opens one ;
|
|
189
|
+
# a reference pointing at this record is already that scalar and needs no
|
|
190
|
+
# opening at all. The two used to be the same reading and are not any more.
|
|
191
|
+
def scalar_of(identity_value)
|
|
192
|
+
identity_value.is_a?(Hash) ? identity_value.values.first.to_s : identity_value.to_s
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def random_id(random)
|
|
196
|
+
"gen-#{random.bytes(4).unpack1('H*')}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
# The fuzzing toolkit bin/fuzz and bin/generate drive. Not required by
|
|
3
|
+
# lib/hecks.rb on purpose — a booted domain never needs it.
|
|
4
|
+
module Fuzzing
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative "fuzzing/value_generator"
|
|
9
|
+
require_relative "fuzzing/invalid_value_generator"
|
|
10
|
+
require_relative "fuzzing/sequence_generator"
|
|
11
|
+
require_relative "fuzzing/replay"
|
|
12
|
+
require_relative "fuzzing/properties"
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Grammar
|
|
3
|
+
# The file surgery under bin/evolve: reading and rewriting the
|
|
4
|
+
# aggregate-local KeywordSeed/ArgumentSeed rows as TEXT, so a
|
|
5
|
+
# proposed word enters the table exactly as a hand would write it
|
|
6
|
+
# and an admitted one loses its ceremony (an absent status reads as
|
|
7
|
+
# admitted — the grown-column convention).
|
|
8
|
+
#
|
|
9
|
+
# Text, not IR, on purpose: the syntax table is source, its comments
|
|
10
|
+
# and grouping are part of the declaration, and a rewrite that
|
|
11
|
+
# round-tripped it through the IR would flatten both. Everything
|
|
12
|
+
# here touches only bare `member ` lines inside Keyword's own body
|
|
13
|
+
# (S3, ADR 0025 — no `one_of do ... end` wrapper anymore) and leaves
|
|
14
|
+
# every other byte alone.
|
|
15
|
+
module Evolve
|
|
16
|
+
class Refusal < StandardError; end
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# bin/evolve's own `--name value` flag reader. Only consumes the
|
|
21
|
+
# NEXT argv element as the value when that element doesn't itself
|
|
22
|
+
# look like a flag — otherwise `--foo --bar` would swallow `--bar`
|
|
23
|
+
# as `--foo`'s value (and `--bar` would then never be seen at
|
|
24
|
+
# all), and a value-less `--foo` at the end of argv would bypass
|
|
25
|
+
# whatever default `foo` promised instead of falling back to it.
|
|
26
|
+
def option(argv, name, default = nil)
|
|
27
|
+
index = argv.index("--#{name}")
|
|
28
|
+
return default unless index
|
|
29
|
+
|
|
30
|
+
value = argv[index + 1]
|
|
31
|
+
value.nil? || value.start_with?("-") ? default : value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The snapshot/restore ceremony bin/evolve wraps every mutating
|
|
35
|
+
# command in: read every `paths` file, run the block, and put
|
|
36
|
+
# every file back exactly as it was if the block raises partway
|
|
37
|
+
# through — `rename`'s keyword-row write followed by its
|
|
38
|
+
# argument cascade across several files (`cascade_argument_rename`
|
|
39
|
+
# above) included, not only a declared gate failing after the
|
|
40
|
+
# block has already returned cleanly (bin/evolve's own `guarded`
|
|
41
|
+
# still handles that half on its own, since it depends on running
|
|
42
|
+
# the gate specs, not on anything this method knows about). A
|
|
43
|
+
# clean return leaves the snapshots unused; the caller decides
|
|
44
|
+
# from there whether the tree stands.
|
|
45
|
+
def restore_on_raise(paths)
|
|
46
|
+
snapshots = paths.to_h { |path| [path, File.read(path)] }
|
|
47
|
+
yield
|
|
48
|
+
rescue StandardError
|
|
49
|
+
snapshots.each { |path, content| File.write(path, content) }
|
|
50
|
+
raise
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def syntax_paths
|
|
54
|
+
Dir.glob(File.expand_path("../language/**/*.bluebook", __dir__)).select do |path|
|
|
55
|
+
source = File.read(path)
|
|
56
|
+
source.include?('value_object "KeywordSeed"') || source.include?('value_object "ArgumentSeed"')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Kept as a narrow compatibility door for callers deliberately doing
|
|
61
|
+
# single-file surgery. Normal operation uses `syntax_paths` and discovers
|
|
62
|
+
# the owning concept from the row itself.
|
|
63
|
+
def syntax_path = syntax_paths.first
|
|
64
|
+
|
|
65
|
+
def paths_for(path) = path ? Array(path) : syntax_paths
|
|
66
|
+
|
|
67
|
+
# The Keyword one_of's member rows, parsed leniently off the text —
|
|
68
|
+
# enough to know each row's (word, context, status), which is all
|
|
69
|
+
# the tool ever asks.
|
|
70
|
+
def keyword_rows(path = nil)
|
|
71
|
+
paths_for(path).flat_map do |candidate|
|
|
72
|
+
blocks = seed_blocks(File.read(candidate), "KeywordSeed")
|
|
73
|
+
raise Refusal, "source declares no KeywordSeed value object" if path && blocks.empty?
|
|
74
|
+
|
|
75
|
+
blocks.flat_map do |block|
|
|
76
|
+
block.scan(/^\s*member (.+)$/).map do |(cells)|
|
|
77
|
+
row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
|
|
78
|
+
{ word: row["word"], context: row["context"],
|
|
79
|
+
status: row.fetch("status", "admitted"), was: row["was"] }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def propose(word:, context:, body: "none", inner: "", opens: "", fills: "", path: nil)
|
|
86
|
+
raise Refusal, "#{context}.#{word} is already declared — one row per (word, context, form)" if keyword_rows(path).any? { |row| row[:word] == word && row[:context] == context }
|
|
87
|
+
|
|
88
|
+
path = owner_path(context: context, word: word, opens: opens, paths: paths_for(path))
|
|
89
|
+
source = File.read(path)
|
|
90
|
+
block = keyword_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || keyword_block(source)
|
|
91
|
+
indent = block[/^(\s*)member /, 1] || " "
|
|
92
|
+
row = %(#{indent}member word: "#{word}", context: "#{context}", body: "#{body}", ) +
|
|
93
|
+
%(inner: "#{inner}", opens: "#{opens}", fills: "#{fills}", status: "proposed"\n)
|
|
94
|
+
|
|
95
|
+
# At the END of the one_of — grouping by context is a courtesy of
|
|
96
|
+
# the hand; a proposed row sits at the bottom until admission,
|
|
97
|
+
# when whoever admits it may move it home.
|
|
98
|
+
closing = block.rindex(/^\s*end\s*$/)
|
|
99
|
+
updated = block[0...closing] + row + block[closing..]
|
|
100
|
+
File.write(path, source.sub(block, updated))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def set_status(word:, context:, to:, path: nil)
|
|
104
|
+
raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated retired].include?(to)
|
|
105
|
+
|
|
106
|
+
path = path_holding_keyword(word, context, paths_for(path))
|
|
107
|
+
source = File.read(path)
|
|
108
|
+
block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
|
|
109
|
+
rows = block.lines.select { |line| member_row?(line, word, context) }
|
|
110
|
+
raise Refusal, "#{context}.#{word} is not declared" if rows.empty?
|
|
111
|
+
|
|
112
|
+
updated = block.lines.map do |line|
|
|
113
|
+
next line unless member_row?(line, word, context)
|
|
114
|
+
|
|
115
|
+
stripped = line.sub(/,\s*status: "[^"]*"/, "")
|
|
116
|
+
# Admitted is the default and stays UNSPELLED — only a word
|
|
117
|
+
# entering or leaving the language carries its status.
|
|
118
|
+
to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
|
|
119
|
+
end.join
|
|
120
|
+
|
|
121
|
+
File.write(path, source.sub(block, updated))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A rename respells the row's word and holds the old spelling in
|
|
125
|
+
# `was:` — one hop only. Renaming an already-renamed word refuses
|
|
126
|
+
# until the language grows real eras for its own words; renaming
|
|
127
|
+
# onto a spelling the context already declares refuses too. The
|
|
128
|
+
# word's Argument rows follow it — row-aware now, not the blind
|
|
129
|
+
# substitution this used to be (see `cascade_argument_rename`).
|
|
130
|
+
def rename(word:, context:, to:, path: nil)
|
|
131
|
+
row = keyword_rows(path).find { |r| r[:word] == word && r[:context] == context }
|
|
132
|
+
raise Refusal, "#{context}.#{word} is not declared" unless row
|
|
133
|
+
raise Refusal, "#{context}.#{word} was already #{row[:was]} — one rename hop, then eras" if row[:was]
|
|
134
|
+
raise Refusal, "#{context}.#{to} is already declared — a rename cannot land on a living word" if keyword_rows(path).any? { |r| r[:word] == to && r[:context] == context }
|
|
135
|
+
|
|
136
|
+
paths = paths_for(path)
|
|
137
|
+
path = path_holding_keyword(word, context, paths)
|
|
138
|
+
source = File.read(path)
|
|
139
|
+
block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
|
|
140
|
+
updated = block.lines.map do |line|
|
|
141
|
+
next line unless member_row?(line, word, context)
|
|
142
|
+
|
|
143
|
+
line.sub(%(word: "#{word}"), %(word: "#{to}"))
|
|
144
|
+
.sub(/\n\z/, %(, was: "#{word}"\n))
|
|
145
|
+
end.join
|
|
146
|
+
source = source.sub(block, updated)
|
|
147
|
+
File.write(path, source)
|
|
148
|
+
|
|
149
|
+
cascade_argument_rename(keyword: word, context: context, to: to, path: paths)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def member_row?(line, word, context)
|
|
153
|
+
line =~ /^\s*member / && line.include?(%(word: "#{word}")) && line.include?(%(context: "#{context}"))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def path_holding_keyword(word, context, paths = syntax_paths)
|
|
157
|
+
paths.find do |candidate|
|
|
158
|
+
keyword_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| member_row?(line, word, context) } }
|
|
159
|
+
end || raise(Refusal, "#{context}.#{word} is not declared")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def path_holding_argument(keyword, context, at, named, paths = syntax_paths)
|
|
163
|
+
paths.find do |candidate|
|
|
164
|
+
argument_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
|
|
165
|
+
end || raise(Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not declared")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# A new row belongs wherever that context's existing rows live. File is
|
|
169
|
+
# intentionally wider than one aggregate; for a new entry point, `opens`
|
|
170
|
+
# identifies the aggregate concept whose file should own it.
|
|
171
|
+
def owner_path(context:, word:, opens: "", paths: syntax_paths)
|
|
172
|
+
if context == "File" && !opens.to_s.empty?
|
|
173
|
+
aggregate_path = paths.find { |candidate| File.read(candidate).match?(/^\s*aggregate "#{Regexp.escape(opens)}" do$/) }
|
|
174
|
+
return aggregate_path if aggregate_path
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
paths.find do |candidate|
|
|
178
|
+
source = File.read(candidate)
|
|
179
|
+
%w[KeywordSeed ArgumentSeed].any? do |seed|
|
|
180
|
+
seed_blocks(source, seed).any? { |block| block.include?(%(context: "#{context}")) }
|
|
181
|
+
end
|
|
182
|
+
end || raise(Refusal, "no aggregate-local syntax table owns context #{context.inspect} for #{word}")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def seed_blocks(source, name)
|
|
186
|
+
opener = /^([ \t]*)value_object "#{Regexp.escape(name)}" do$/
|
|
187
|
+
source.to_enum(:scan, opener).map do
|
|
188
|
+
match = Regexp.last_match
|
|
189
|
+
start = match.begin(0)
|
|
190
|
+
indent = match[1]
|
|
191
|
+
closing = source.index(/^#{Regexp.escape(indent)}end\s*$/, match.end(0))
|
|
192
|
+
closing = source.index(/\n/, closing) + 1
|
|
193
|
+
source[start...closing]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def seed_block(source, name, required: false)
|
|
198
|
+
block = seed_blocks(source, name).first
|
|
199
|
+
unless block
|
|
200
|
+
raise Refusal, "source declares no #{name} value object" if required
|
|
201
|
+
|
|
202
|
+
return
|
|
203
|
+
end
|
|
204
|
+
block
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# From `value_object "KeywordSeed"` to ITS OWN closing `end` — `member`
|
|
208
|
+
# rows sit bare now (S3, ADR 0025 — the `one_of do ... end` wrapper
|
|
209
|
+
# is gone), so the first bare `end` line after the opener already
|
|
210
|
+
# IS the value object's own, the same fact the original one_of-
|
|
211
|
+
# nested version of this method leaned on (nothing else nested
|
|
212
|
+
# inside it either, before or after).
|
|
213
|
+
def keyword_blocks(source) = seed_blocks(source, "KeywordSeed")
|
|
214
|
+
def keyword_block(source) = seed_block(source, "KeywordSeed", required: true)
|
|
215
|
+
|
|
216
|
+
# ── the Argument rows — a word's own arguments, at last with tooling
|
|
217
|
+
# of their own rather than the rename-only cascade above. A word may
|
|
218
|
+
# carry SEVERAL argument rows (one per position, one per named
|
|
219
|
+
# kwarg), so identity here is the full (keyword, context, at, named)
|
|
220
|
+
# tuple, not the two-field key a Keyword row answers to.
|
|
221
|
+
|
|
222
|
+
def argument_rows(path = nil)
|
|
223
|
+
paths_for(path).flat_map do |candidate|
|
|
224
|
+
blocks = seed_blocks(File.read(candidate), "ArgumentSeed")
|
|
225
|
+
raise Refusal, "source declares no ArgumentSeed value object" if path && blocks.empty?
|
|
226
|
+
|
|
227
|
+
blocks.flat_map do |block|
|
|
228
|
+
block.scan(/^\s*member (.+)$/).map do |(cells)|
|
|
229
|
+
row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
|
|
230
|
+
{ keyword: row["keyword"], context: row["context"], at: row["at"].to_s,
|
|
231
|
+
named: row["named"].to_s, kind: row["kind"], required: row["required"],
|
|
232
|
+
fills: row["fills"].to_s, status: row.fetch("status", "admitted") }
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# `pairs_shape` — for a `pairs` argument that fills ONE field with a
|
|
239
|
+
# whole key/value list rather than naming a field per pair (the
|
|
240
|
+
# shape `Handler.dispatch`'s own `with:` already carries). Without
|
|
241
|
+
# it, `spec/syntax_conformance_spec.rb` reads a pairs argument
|
|
242
|
+
# naming a single field as a row that "names a single field, which
|
|
243
|
+
# it cannot fill" — correctly, since the two shapes are genuinely
|
|
244
|
+
# different and only one of them can be checked the same way.
|
|
245
|
+
def propose_argument(keyword:, context:, kind:, required: "false", at: "", named: "", fills: "",
|
|
246
|
+
pairs_shape: nil, path: nil)
|
|
247
|
+
if argument_rows(path).any? { |r| argument_identity(r) == [keyword, context, at, named] }
|
|
248
|
+
raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is " \
|
|
249
|
+
"already declared — one row per (keyword, context, at, named)"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
path = owner_path(context: context, word: keyword, paths: paths_for(path))
|
|
253
|
+
source = File.read(path)
|
|
254
|
+
block = argument_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || argument_block(source)
|
|
255
|
+
indent = block[/^(\s*)member /, 1] || " "
|
|
256
|
+
shape = pairs_shape.to_s.empty? ? "" : %(pairs_shape: "#{pairs_shape}", )
|
|
257
|
+
row = %(#{indent}member keyword: "#{keyword}", context: "#{context}", at: "#{at}", ) +
|
|
258
|
+
%(named: "#{named}", kind: "#{kind}", required: "#{required}", fills: "#{fills}", ) +
|
|
259
|
+
shape + %(status: "proposed"\n)
|
|
260
|
+
|
|
261
|
+
closing = block.rindex(/^\s*end\s*$/)
|
|
262
|
+
updated = block[0...closing] + row + block[closing..]
|
|
263
|
+
File.write(path, source.sub(block, updated))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def set_argument_status(keyword:, context:, to:, at: "", named: "", path: nil)
|
|
267
|
+
raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated retired].include?(to)
|
|
268
|
+
|
|
269
|
+
path = path_holding_argument(keyword, context, at, named, paths_for(path))
|
|
270
|
+
source = File.read(path)
|
|
271
|
+
block = argument_blocks(source).find { |candidate| candidate.lines.any? { |line| argument_row?(line, keyword, context, at, named) } }
|
|
272
|
+
rows = block.lines.select { |line| argument_row?(line, keyword, context, at, named) }
|
|
273
|
+
raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not " \
|
|
274
|
+
"declared" if rows.empty?
|
|
275
|
+
|
|
276
|
+
updated = block.lines.map do |line|
|
|
277
|
+
next line unless argument_row?(line, keyword, context, at, named)
|
|
278
|
+
|
|
279
|
+
stripped = line.sub(/,\s*status: "[^"]*"/, "")
|
|
280
|
+
to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
|
|
281
|
+
end.join
|
|
282
|
+
|
|
283
|
+
File.write(path, source.sub(block, updated))
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def argument_row?(line, keyword, context, at, named)
|
|
287
|
+
line =~ /^\s*member / &&
|
|
288
|
+
line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}")) &&
|
|
289
|
+
line.include?(%(at: "#{at}")) && line.include?(%(named: "#{named}"))
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def argument_identity(row) = [row[:keyword], row[:context], row[:at], row[:named]]
|
|
293
|
+
|
|
294
|
+
# The rename cascade, ROW-AWARE — only the rows that actually belong
|
|
295
|
+
# to the renamed word, spelling updated in place, rather than a
|
|
296
|
+
# blind `gsub` on every `keyword: "word",` substring in the file
|
|
297
|
+
# (which a coincidentally-matching row elsewhere could have
|
|
298
|
+
# corrupted, and which read nothing before writing).
|
|
299
|
+
def cascade_argument_rename(keyword:, context:, to:, path: nil)
|
|
300
|
+
paths_for(path).each do |candidate|
|
|
301
|
+
source = File.read(candidate)
|
|
302
|
+
original = source
|
|
303
|
+
argument_blocks(source).each do |block|
|
|
304
|
+
updated = block.lines.map do |line|
|
|
305
|
+
next line unless line =~ /^\s*member / &&
|
|
306
|
+
line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}"))
|
|
307
|
+
|
|
308
|
+
line.sub(%(keyword: "#{keyword}"), %(keyword: "#{to}"))
|
|
309
|
+
end.join
|
|
310
|
+
source = source.sub(block, updated) if updated != block
|
|
311
|
+
end
|
|
312
|
+
File.write(candidate, source) if source != original
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# From `value_object "ArgumentSeed"` to ITS OWN closing `end` — see
|
|
317
|
+
# `keyword_block`'s own comment for why the first bare `end` after
|
|
318
|
+
# the opener is already the right one, now that `member` rows sit
|
|
319
|
+
# bare (S3, ADR 0025).
|
|
320
|
+
def argument_blocks(source) = seed_blocks(source, "ArgumentSeed")
|
|
321
|
+
def argument_block(source) = seed_block(source, "ArgumentSeed", required: true)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|