hecks 2.9.0 → 3.0.1
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/exe/hecks +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +13 -4
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +15 -8
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
//! Delivers a `PendingCrossDomainReaction` to another domain's Lambda function,
|
|
2
|
+
//! mirroring `Adapters::Lambda::Client`'s invoke/dispatch shape.
|
|
3
|
+
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
|
|
6
|
+
// Decoupled from `aws_sdk_lambda`'s own response type so a mock never needs one.
|
|
7
|
+
pub struct InvokeOutcome {
|
|
8
|
+
pub body: Value,
|
|
9
|
+
pub function_error: bool,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// `&dyn`, not a generic — the call chain reaching this is already deep, and making
|
|
13
|
+
// every function in it generic over `L: LambdaInvoker` would be a much bigger diff.
|
|
14
|
+
#[async_trait::async_trait]
|
|
15
|
+
pub trait LambdaInvoker: Send + Sync {
|
|
16
|
+
async fn invoke(&self, function_name: &str, payload: &str) -> anyhow::Result<InvokeOutcome>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Mirrors Ruby's `"hecks-#{domain.downcase}"` — deployed stack names depend on
|
|
20
|
+
// this exact casing.
|
|
21
|
+
pub fn function_name_for(domain: &str) -> String {
|
|
22
|
+
format!("hecks-{}", domain.to_lowercase())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Mirrors `PolicyInterpreter#deliver`'s record shape, minus `on:` — this crate
|
|
26
|
+
// never sees the triggering event's own name, only its payload.
|
|
27
|
+
#[derive(Debug)]
|
|
28
|
+
pub struct CrossDomainDeliveryRecord {
|
|
29
|
+
pub policy: String,
|
|
30
|
+
pub target_domain: String,
|
|
31
|
+
pub target_verb: String,
|
|
32
|
+
pub delivered: bool,
|
|
33
|
+
pub reason: Option<String>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl CrossDomainDeliveryRecord {
|
|
37
|
+
pub fn to_json(&self) -> Value {
|
|
38
|
+
serde_json::json!({
|
|
39
|
+
"policy": self.policy,
|
|
40
|
+
"target_domain": self.target_domain,
|
|
41
|
+
"target_verb": self.target_verb,
|
|
42
|
+
"delivered": self.delivered,
|
|
43
|
+
"reason": self.reason,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A target-side refusal returns `delivered: false`; a genuine invoke fault
|
|
49
|
+
// (`function_error`, or a hard `Err` from `invoke`) returns `Err` instead.
|
|
50
|
+
pub async fn deliver<L: LambdaInvoker + ?Sized>(invoker: &L, reaction: &Value) -> anyhow::Result<CrossDomainDeliveryRecord> {
|
|
51
|
+
let policy = reaction.get("policy").and_then(Value::as_str).unwrap_or_default().to_string();
|
|
52
|
+
let target_domain = reaction.get("target_domain").and_then(Value::as_str).unwrap_or_default().to_string();
|
|
53
|
+
let target_verb = reaction.get("target_verb").and_then(Value::as_str).unwrap_or_default().to_string();
|
|
54
|
+
let payload = reaction.get("payload").cloned().unwrap_or_else(|| serde_json::json!({}));
|
|
55
|
+
|
|
56
|
+
let function_name = function_name_for(&target_domain);
|
|
57
|
+
// No `"role"` — a policy reaction is system-triggered, the same reasoning
|
|
58
|
+
// `orchestrate.rs`'s local branch already applies via `Caller.without`.
|
|
59
|
+
let request_payload = serde_json::json!({ "verb": target_verb, "args": payload }).to_string();
|
|
60
|
+
|
|
61
|
+
let outcome = invoker.invoke(&function_name, &request_payload).await?;
|
|
62
|
+
|
|
63
|
+
if outcome.function_error {
|
|
64
|
+
let message = outcome.body.get("errorMessage").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| outcome.body.to_string());
|
|
65
|
+
anyhow::bail!("Lambda {function_name} ({}): {message}", target_verb);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let target_side_refusal = outcome
|
|
69
|
+
.body
|
|
70
|
+
.get("refusals")
|
|
71
|
+
.and_then(Value::as_array)
|
|
72
|
+
.and_then(|refusals| refusals.iter().find(|r| r.get("verb").and_then(Value::as_str) == Some(target_verb.as_str())));
|
|
73
|
+
|
|
74
|
+
let Some(refusal) = target_side_refusal else {
|
|
75
|
+
return Ok(CrossDomainDeliveryRecord { policy, target_domain, target_verb, delivered: true, reason: None });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
Ok(CrossDomainDeliveryRecord {
|
|
79
|
+
policy,
|
|
80
|
+
target_domain,
|
|
81
|
+
target_verb,
|
|
82
|
+
delivered: false,
|
|
83
|
+
reason: refusal.get("error").and_then(Value::as_str).map(String::from),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Kept small — shares the invoking Lambda's own tight execution-time budget
|
|
88
|
+
// with every other cross-domain reaction still queued behind it.
|
|
89
|
+
pub const MAX_DELIVERY_ATTEMPTS: u32 = 3;
|
|
90
|
+
|
|
91
|
+
// Carries what `journal::record_dead_letter` needs; this file stays free of
|
|
92
|
+
// Postgres itself.
|
|
93
|
+
#[derive(Debug)]
|
|
94
|
+
pub struct DeliveryFailure {
|
|
95
|
+
pub policy: String,
|
|
96
|
+
pub target_domain: String,
|
|
97
|
+
pub target_verb: String,
|
|
98
|
+
pub payload: Value,
|
|
99
|
+
pub error: anyhow::Error,
|
|
100
|
+
pub attempts: u32,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Retries only a genuine invoke fault (`Err`); a clean `Ok(delivered: false)`
|
|
104
|
+
// refusal is a business outcome and is never retried.
|
|
105
|
+
pub async fn deliver_with_retry<L: LambdaInvoker + ?Sized>(
|
|
106
|
+
invoker: &L,
|
|
107
|
+
reaction: &Value,
|
|
108
|
+
) -> Result<CrossDomainDeliveryRecord, DeliveryFailure> {
|
|
109
|
+
let mut last_error = None;
|
|
110
|
+
|
|
111
|
+
for attempt in 1..=MAX_DELIVERY_ATTEMPTS {
|
|
112
|
+
match deliver(invoker, reaction).await {
|
|
113
|
+
Ok(record) => return Ok(record),
|
|
114
|
+
Err(error) => {
|
|
115
|
+
last_error = Some(error);
|
|
116
|
+
if attempt < MAX_DELIVERY_ATTEMPTS {
|
|
117
|
+
let backoff_ms = 100u64 * (1 << (attempt - 1));
|
|
118
|
+
tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Err(DeliveryFailure {
|
|
125
|
+
policy: reaction.get("policy").and_then(Value::as_str).unwrap_or_default().to_string(),
|
|
126
|
+
target_domain: reaction.get("target_domain").and_then(Value::as_str).unwrap_or_default().to_string(),
|
|
127
|
+
target_verb: reaction.get("target_verb").and_then(Value::as_str).unwrap_or_default().to_string(),
|
|
128
|
+
payload: reaction.get("payload").cloned().unwrap_or_else(|| serde_json::json!({})),
|
|
129
|
+
// Safe: this arm only runs after the loop above executed at least once,
|
|
130
|
+
// and every iteration's `Err` arm sets `last_error` before falling through.
|
|
131
|
+
error: last_error.unwrap(),
|
|
132
|
+
attempts: MAX_DELIVERY_ATTEMPTS,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Untestable here — needs a real deployed Lambda and live AWS credentials,
|
|
137
|
+
// neither of which this repository's test environment has.
|
|
138
|
+
pub struct AwsLambdaInvoker {
|
|
139
|
+
client: aws_sdk_lambda::Client,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
impl AwsLambdaInvoker {
|
|
143
|
+
// Explicit rustls+ring HTTP client — neither crate's own default feature
|
|
144
|
+
// actually wires in `ring` (see Cargo.toml's note on this dependency).
|
|
145
|
+
pub async fn from_env() -> Self {
|
|
146
|
+
let http_client = aws_smithy_http_client::Builder::new()
|
|
147
|
+
.tls_provider(aws_smithy_http_client::tls::Provider::Rustls(aws_smithy_http_client::tls::rustls_provider::CryptoMode::Ring))
|
|
148
|
+
.build_https();
|
|
149
|
+
let config = aws_config::defaults(aws_config::BehaviorVersion::latest()).http_client(http_client).load().await;
|
|
150
|
+
Self { client: aws_sdk_lambda::Client::new(&config) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Panics if invoked — asserts that same-domain-only test suites never
|
|
155
|
+
// trigger a cross-domain delivery.
|
|
156
|
+
#[cfg(test)]
|
|
157
|
+
pub struct NeverInvoker;
|
|
158
|
+
|
|
159
|
+
#[cfg(test)]
|
|
160
|
+
#[async_trait::async_trait]
|
|
161
|
+
impl LambdaInvoker for NeverInvoker {
|
|
162
|
+
async fn invoke(&self, function_name: &str, _payload: &str) -> anyhow::Result<InvokeOutcome> {
|
|
163
|
+
unreachable!("NeverInvoker: unexpected cross-domain Lambda invoke attempted against {function_name:?}")
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[async_trait::async_trait]
|
|
168
|
+
impl LambdaInvoker for AwsLambdaInvoker {
|
|
169
|
+
async fn invoke(&self, function_name: &str, payload: &str) -> anyhow::Result<InvokeOutcome> {
|
|
170
|
+
let response = self
|
|
171
|
+
.client
|
|
172
|
+
.invoke()
|
|
173
|
+
.function_name(function_name)
|
|
174
|
+
.payload(aws_sdk_lambda::primitives::Blob::new(payload.as_bytes()))
|
|
175
|
+
.send()
|
|
176
|
+
.await
|
|
177
|
+
.map_err(|e| anyhow::anyhow!("invoking Lambda {function_name}: {e:?}"))?;
|
|
178
|
+
|
|
179
|
+
let function_error = response.function_error().is_some();
|
|
180
|
+
let bytes = response.payload().map(|blob| blob.as_ref().to_vec()).unwrap_or_default();
|
|
181
|
+
// A non-JSON/empty body reads as `{}` rather than failing the call;
|
|
182
|
+
// `function_error` (checked above) is what actually signals a fault.
|
|
183
|
+
let body: Value = serde_json::from_slice(&bytes).unwrap_or_else(|_| serde_json::json!({}));
|
|
184
|
+
|
|
185
|
+
Ok(InvokeOutcome { body, function_error })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[cfg(test)]
|
|
190
|
+
mod tests {
|
|
191
|
+
use super::*;
|
|
192
|
+
use std::sync::Mutex;
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn function_name_matches_rubys_own_hecks_dash_domain_convention() {
|
|
196
|
+
assert_eq!(function_name_for("Compliance"), "hecks-compliance");
|
|
197
|
+
assert_eq!(function_name_for("Notifications"), "hecks-notifications");
|
|
198
|
+
// Ruby's `.downcase`, not snake_case — an internally-capitalized domain
|
|
199
|
+
// stays one lowercase word.
|
|
200
|
+
assert_eq!(function_name_for("Banking"), "hecks-banking");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Records each call and answers with whatever canned outcome the test configured.
|
|
204
|
+
struct MockLambdaInvoker {
|
|
205
|
+
response: Result<InvokeOutcome, String>,
|
|
206
|
+
calls: Mutex<Vec<(String, String)>>,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
impl MockLambdaInvoker {
|
|
210
|
+
fn answering(body: Value, function_error: bool) -> Self {
|
|
211
|
+
Self { response: Ok(InvokeOutcome { body, function_error }), calls: Mutex::new(Vec::new()) }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn failing(message: &str) -> Self {
|
|
215
|
+
Self { response: Err(message.to_string()), calls: Mutex::new(Vec::new()) }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[async_trait::async_trait]
|
|
220
|
+
impl LambdaInvoker for MockLambdaInvoker {
|
|
221
|
+
async fn invoke(&self, function_name: &str, payload: &str) -> anyhow::Result<InvokeOutcome> {
|
|
222
|
+
self.calls.lock().unwrap().push((function_name.to_string(), payload.to_string()));
|
|
223
|
+
match &self.response {
|
|
224
|
+
Ok(outcome) => Ok(InvokeOutcome { body: outcome.body.clone(), function_error: outcome.function_error }),
|
|
225
|
+
Err(message) => anyhow::bail!("{message}"),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fn reaction(target_domain: &str, target_verb: &str) -> Value {
|
|
231
|
+
serde_json::json!({
|
|
232
|
+
"policy": "ReviewOnFreeze",
|
|
233
|
+
"target_domain": target_domain,
|
|
234
|
+
"target_verb": target_verb,
|
|
235
|
+
"payload": { "number": { "value": "acct-1" } },
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[tokio::test]
|
|
240
|
+
async fn delivers_and_invokes_the_computed_function_name_with_the_verb_args_payload_shape() {
|
|
241
|
+
let invoker = MockLambdaInvoker::answering(serde_json::json!({ "refusals": [] }), false);
|
|
242
|
+
|
|
243
|
+
let record = deliver(&invoker, &reaction("Compliance", "Compliance::AccountFreezeReview.Open")).await.unwrap();
|
|
244
|
+
|
|
245
|
+
assert!(record.delivered);
|
|
246
|
+
assert_eq!(record.reason, None);
|
|
247
|
+
assert_eq!(record.target_domain, "Compliance");
|
|
248
|
+
|
|
249
|
+
let calls = invoker.calls.lock().unwrap();
|
|
250
|
+
assert_eq!(calls.len(), 1);
|
|
251
|
+
let (function_name, payload) = &calls[0];
|
|
252
|
+
assert_eq!(function_name, "hecks-compliance");
|
|
253
|
+
let sent: Value = serde_json::from_str(payload).unwrap();
|
|
254
|
+
assert_eq!(sent["verb"], "Compliance::AccountFreezeReview.Open");
|
|
255
|
+
assert_eq!(sent["args"]["number"]["value"], "acct-1");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[tokio::test]
|
|
259
|
+
async fn a_target_side_refusal_is_recorded_non_fatally_not_raised() {
|
|
260
|
+
let invoker = MockLambdaInvoker::answering(
|
|
261
|
+
serde_json::json!({ "refusals": [ { "verb": "Compliance::AccountFreezeReview.Open", "error": "already under review" } ] }),
|
|
262
|
+
false,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
let record = deliver(&invoker, &reaction("Compliance", "Compliance::AccountFreezeReview.Open")).await.unwrap();
|
|
266
|
+
|
|
267
|
+
assert!(!record.delivered);
|
|
268
|
+
assert_eq!(record.reason.as_deref(), Some("already under review"));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[tokio::test]
|
|
272
|
+
async fn a_refusal_for_a_different_verb_in_the_same_response_does_not_count_as_this_ones() {
|
|
273
|
+
// A response can legitimately carry refusals from other steps; only a
|
|
274
|
+
// refusal naming this verb counts as this delivery's outcome.
|
|
275
|
+
let invoker = MockLambdaInvoker::answering(
|
|
276
|
+
serde_json::json!({ "refusals": [ { "verb": "Compliance.SomethingElse", "error": "unrelated" } ] }),
|
|
277
|
+
false,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
let record = deliver(&invoker, &reaction("Compliance", "Compliance::AccountFreezeReview.Open")).await.unwrap();
|
|
281
|
+
|
|
282
|
+
assert!(record.delivered);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[tokio::test]
|
|
286
|
+
async fn a_function_error_response_is_a_hard_failure_not_a_delivery_record() {
|
|
287
|
+
// Mirrors Ruby's `raise WiringError` on `function_error` — not a domain
|
|
288
|
+
// refusal, so it propagates rather than becoming a `delivered: false` record.
|
|
289
|
+
let invoker = MockLambdaInvoker::answering(serde_json::json!({ "errorMessage": "unhandled panic" }), true);
|
|
290
|
+
|
|
291
|
+
let outcome = deliver(&invoker, &reaction("Compliance", "Compliance::AccountFreezeReview.Open")).await;
|
|
292
|
+
|
|
293
|
+
assert!(outcome.is_err(), "a function_error response must propagate as Err, not a delivered:false record");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[tokio::test]
|
|
297
|
+
async fn an_invoke_that_never_reaches_the_target_at_all_is_also_a_hard_failure() {
|
|
298
|
+
// The function doesn't exist, or the call never completes (network,
|
|
299
|
+
// throttling, AccessDenied) — never even reaches `Ok(InvokeOutcome)`.
|
|
300
|
+
let invoker = MockLambdaInvoker::failing("ResourceNotFoundException: function not found");
|
|
301
|
+
|
|
302
|
+
let outcome = deliver(&invoker, &reaction("Notifications", "Notifications.Send")).await;
|
|
303
|
+
|
|
304
|
+
assert!(outcome.is_err());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Fails the first `fail_count` calls, then succeeds — the shape of a
|
|
308
|
+
// transient throttle that clears before a retry limit is hit.
|
|
309
|
+
struct FlakyLambdaInvoker {
|
|
310
|
+
fail_count: std::sync::atomic::AtomicU32,
|
|
311
|
+
calls: Mutex<Vec<String>>,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
impl FlakyLambdaInvoker {
|
|
315
|
+
fn failing_then_succeeding(fail_count: u32) -> Self {
|
|
316
|
+
Self { fail_count: std::sync::atomic::AtomicU32::new(fail_count), calls: Mutex::new(Vec::new()) }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[async_trait::async_trait]
|
|
321
|
+
impl LambdaInvoker for FlakyLambdaInvoker {
|
|
322
|
+
async fn invoke(&self, function_name: &str, _payload: &str) -> anyhow::Result<InvokeOutcome> {
|
|
323
|
+
self.calls.lock().unwrap().push(function_name.to_string());
|
|
324
|
+
let remaining = self.fail_count.load(std::sync::atomic::Ordering::SeqCst);
|
|
325
|
+
if remaining > 0 {
|
|
326
|
+
self.fail_count.store(remaining - 1, std::sync::atomic::Ordering::SeqCst);
|
|
327
|
+
anyhow::bail!("ThrottlingException: rate exceeded");
|
|
328
|
+
}
|
|
329
|
+
Ok(InvokeOutcome { body: serde_json::json!({ "refusals": [] }), function_error: false })
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[tokio::test]
|
|
334
|
+
async fn deliver_with_retry_rides_out_a_transient_fault_that_clears_before_attempts_run_out() {
|
|
335
|
+
let invoker = FlakyLambdaInvoker::failing_then_succeeding(MAX_DELIVERY_ATTEMPTS - 1);
|
|
336
|
+
|
|
337
|
+
let record = deliver_with_retry(&invoker, &reaction("Compliance", "Compliance::AccountFreezeReview.Open"))
|
|
338
|
+
.await
|
|
339
|
+
.expect("should succeed once the fault clears, within MAX_DELIVERY_ATTEMPTS");
|
|
340
|
+
|
|
341
|
+
assert!(record.delivered);
|
|
342
|
+
assert_eq!(invoker.calls.lock().unwrap().len(), MAX_DELIVERY_ATTEMPTS as usize, "should have retried exactly up to the successful attempt");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[tokio::test]
|
|
346
|
+
async fn deliver_with_retry_never_retries_a_clean_domain_side_refusal() {
|
|
347
|
+
// A clean refusal is `Ok(delivered: false)`, a business decision — retrying
|
|
348
|
+
// it would not change what the target domain decided.
|
|
349
|
+
let invoker = MockLambdaInvoker::answering(
|
|
350
|
+
serde_json::json!({ "refusals": [ { "verb": "Compliance::AccountFreezeReview.Open", "error": "already under review" } ] }),
|
|
351
|
+
false,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
let record = deliver_with_retry(&invoker, &reaction("Compliance", "Compliance::AccountFreezeReview.Open"))
|
|
355
|
+
.await
|
|
356
|
+
.expect("a domain-side refusal is Ok, not a DeliveryFailure");
|
|
357
|
+
|
|
358
|
+
assert!(!record.delivered);
|
|
359
|
+
assert_eq!(invoker.calls.lock().unwrap().len(), 1, "a clean refusal should never be retried");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#[tokio::test]
|
|
363
|
+
async fn deliver_with_retry_gives_up_after_max_attempts_and_carries_everything_a_dead_letter_needs() {
|
|
364
|
+
let invoker = MockLambdaInvoker::failing("ResourceNotFoundException: function not found");
|
|
365
|
+
|
|
366
|
+
let failure = deliver_with_retry(&invoker, &reaction("Notifications", "Notifications.Send"))
|
|
367
|
+
.await
|
|
368
|
+
.expect_err("a persistent fault should exhaust every attempt and fail");
|
|
369
|
+
|
|
370
|
+
assert_eq!(failure.policy, "ReviewOnFreeze");
|
|
371
|
+
assert_eq!(failure.target_domain, "Notifications");
|
|
372
|
+
assert_eq!(failure.target_verb, "Notifications.Send");
|
|
373
|
+
assert_eq!(failure.payload, serde_json::json!({ "number": { "value": "acct-1" } }));
|
|
374
|
+
assert_eq!(failure.attempts, MAX_DELIVERY_ATTEMPTS);
|
|
375
|
+
assert!(format!("{:#}", failure.error).contains("ResourceNotFoundException"));
|
|
376
|
+
assert_eq!(invoker.calls.lock().unwrap().len(), MAX_DELIVERY_ATTEMPTS as usize);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Structured logging: one JSON object per line on stdout, shipped by the `awslogs` driver.
|
|
2
|
+
// Never put secrets, cookies, query strings or command facts in `fields`; log identifiers only.
|
|
3
|
+
|
|
4
|
+
use serde_json::{json, Map, Value};
|
|
5
|
+
use std::io::Write;
|
|
6
|
+
|
|
7
|
+
/// Logs an ordinary event.
|
|
8
|
+
pub fn info(msg: &str, fields: Value) {
|
|
9
|
+
emit("info", msg, fields);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Logs a failure worth an operator's attention.
|
|
13
|
+
pub fn error(msg: &str, fields: Value) {
|
|
14
|
+
emit("error", msg, fields);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// A boot phase in flight: logs `boot_phase` start and end lines, so a hang shows as a start
|
|
18
|
+
/// with no end. A failed phase never logs its end.
|
|
19
|
+
pub struct Phase {
|
|
20
|
+
name: &'static str,
|
|
21
|
+
fields: Value,
|
|
22
|
+
started: std::time::Instant,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Starts a boot phase with no extra fields.
|
|
26
|
+
pub fn phase(name: &'static str) -> Phase {
|
|
27
|
+
phase_with(name, json!({}))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Starts a boot phase and logs its `start` line.
|
|
31
|
+
pub fn phase_with(name: &'static str, fields: Value) -> Phase {
|
|
32
|
+
emit("info", "boot_phase", phase_fields(name, "start", &fields, None));
|
|
33
|
+
Phase { name, fields, started: std::time::Instant::now() }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl Phase {
|
|
37
|
+
/// Logs the `end` line and returns the elapsed milliseconds.
|
|
38
|
+
pub fn end(self) -> u64 {
|
|
39
|
+
self.end_with(json!({}))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Logs the `end` line with extra fields describing the outcome.
|
|
43
|
+
pub fn end_with(self, outcome: Value) -> u64 {
|
|
44
|
+
let elapsed_ms = self.started.elapsed().as_millis() as u64;
|
|
45
|
+
let mut fields = self.fields.clone();
|
|
46
|
+
if let (Value::Object(base), Value::Object(extra)) = (&mut fields, outcome) {
|
|
47
|
+
base.extend(extra);
|
|
48
|
+
}
|
|
49
|
+
emit("info", "boot_phase", phase_fields(self.name, "end", &fields, Some(elapsed_ms)));
|
|
50
|
+
elapsed_ms
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// The fields of one phase line: `phase`, `event`, `elapsed_ms` on an end line,
|
|
55
|
+
/// then the phase's own identifiers.
|
|
56
|
+
fn phase_fields(name: &str, event: &str, fields: &Value, elapsed_ms: Option<u64>) -> Value {
|
|
57
|
+
let mut object = Map::new();
|
|
58
|
+
object.insert("phase".to_string(), json!(name));
|
|
59
|
+
object.insert("event".to_string(), json!(event));
|
|
60
|
+
if let Some(ms) = elapsed_ms {
|
|
61
|
+
object.insert("elapsed_ms".to_string(), json!(ms));
|
|
62
|
+
}
|
|
63
|
+
if let Value::Object(fields) = fields {
|
|
64
|
+
for (key, value) in fields {
|
|
65
|
+
object.entry(key.clone()).or_insert(value.clone());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
Value::Object(object)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn emit(level: &str, msg: &str, fields: Value) {
|
|
72
|
+
let line = render(level, msg, fields);
|
|
73
|
+
// A closed stdout must never take a request down with it.
|
|
74
|
+
let _ = writeln!(std::io::stdout().lock(), "{line}");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// The line as text — `level` and `msg` first, then `fields` (an object;
|
|
78
|
+
/// anything else is ignored).
|
|
79
|
+
fn render(level: &str, msg: &str, fields: Value) -> String {
|
|
80
|
+
let mut object = Map::new();
|
|
81
|
+
object.insert("level".to_string(), json!(level));
|
|
82
|
+
object.insert("msg".to_string(), json!(msg));
|
|
83
|
+
if let Value::Object(fields) = fields {
|
|
84
|
+
for (key, value) in fields {
|
|
85
|
+
object.entry(key).or_insert(value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
Value::Object(object).to_string()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[cfg(test)]
|
|
92
|
+
mod tests {
|
|
93
|
+
use super::*;
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn renders_one_json_object_on_a_single_line() {
|
|
97
|
+
let line = render("info", "request", json!({"method": "GET", "status": 200}));
|
|
98
|
+
assert!(!line.contains('\n'));
|
|
99
|
+
let parsed: Value = serde_json::from_str(&line).unwrap();
|
|
100
|
+
assert_eq!(parsed["level"], "info");
|
|
101
|
+
assert_eq!(parsed["msg"], "request");
|
|
102
|
+
assert_eq!(parsed["method"], "GET");
|
|
103
|
+
assert_eq!(parsed["status"], 200);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn a_field_cannot_overwrite_level_or_msg() {
|
|
108
|
+
let line = render("error", "boom", json!({"level": "info", "msg": "fine"}));
|
|
109
|
+
let parsed: Value = serde_json::from_str(&line).unwrap();
|
|
110
|
+
assert_eq!(parsed["level"], "error");
|
|
111
|
+
assert_eq!(parsed["msg"], "boom");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn a_phase_line_names_the_phase_and_an_end_line_carries_elapsed_ms() {
|
|
116
|
+
let start = render("info", "boot_phase", phase_fields("audit_aggregate", "start", &json!({"aggregate": "registration"}), None));
|
|
117
|
+
let parsed: Value = serde_json::from_str(&start).unwrap();
|
|
118
|
+
assert_eq!(parsed["msg"], "boot_phase");
|
|
119
|
+
assert_eq!((parsed["phase"].as_str(), parsed["event"].as_str()), (Some("audit_aggregate"), Some("start")));
|
|
120
|
+
assert_eq!(parsed["aggregate"], "registration");
|
|
121
|
+
assert!(parsed.get("elapsed_ms").is_none(), "a start line has no duration yet");
|
|
122
|
+
|
|
123
|
+
let end = render("info", "boot_phase", phase_fields("audit_aggregate", "end", &json!({"aggregate": "registration"}), Some(42)));
|
|
124
|
+
let parsed: Value = serde_json::from_str(&end).unwrap();
|
|
125
|
+
assert_eq!((parsed["event"].as_str(), parsed["elapsed_ms"].as_u64()), (Some("end"), Some(42)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn a_phase_field_cannot_overwrite_the_phase_or_event() {
|
|
130
|
+
let fields = phase_fields("connect", "start", &json!({"phase": "other", "event": "end", "era": 7}), None);
|
|
131
|
+
assert_eq!((fields["phase"].as_str(), fields["event"].as_str(), fields["era"].as_u64()), (Some("connect"), Some("start"), Some(7)));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn ending_a_phase_reports_elapsed_time_and_merges_the_outcome() {
|
|
136
|
+
let phase = phase_with("mint_era", json!({"era": 7}));
|
|
137
|
+
std::thread::sleep(std::time::Duration::from_millis(15));
|
|
138
|
+
let elapsed = phase.end_with(json!({"aggregates": 5}));
|
|
139
|
+
assert!(elapsed >= 15, "{elapsed}");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn newlines_inside_a_value_stay_escaped() {
|
|
144
|
+
let line = render("error", "boom", json!({"error": "a\nb"}));
|
|
145
|
+
assert!(!line.contains('\n'));
|
|
146
|
+
}
|
|
147
|
+
}
|