hecks 3.10.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
- data/exe/hecks +4 -4
- data/lib/hecks/adapters/driven/d1.rb +2 -10
- data/lib/hecks/adapters/driven/heki.adapter +1 -1
- data/lib/hecks/adapters/driven/heki.rb +2 -12
- data/lib/hecks/adapters/driven/lambda.adapter +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +3 -18
- data/lib/hecks/adapters/driven/postgres.rb +4 -11
- data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
- data/lib/hecks/adapters/driven/sqlite.rb +2 -10
- data/lib/hecks/adapters/driving/admission.rb +46 -0
- data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
- data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
- data/lib/hecks/adapters/driving/cli.rb +127 -0
- data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
- data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
- data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
- data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
- data/lib/hecks/adapters/driving/command_request.rb +121 -0
- data/lib/hecks/adapters/driving/handle.rb +214 -0
- data/lib/hecks/adapters/driving/json.rb +136 -0
- data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
- data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
- data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
- data/lib/hecks/adapters/driving/mcp.rb +159 -0
- data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
- data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
- data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
- data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
- data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
- data/lib/hecks/adapters/driving/ruby.rb +83 -0
- data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
- data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
- data/lib/hecks/behaviors/expectations/boot.rb +1 -1
- data/lib/hecks/bench/ruby_runner.rb +1 -1
- data/lib/hecks/bluebook/ask_resolution.rb +130 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
- data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
- data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
- data/lib/hecks/bluebook/assembly/marks.rb +1 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/command.rb +13 -8
- data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
- data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
- data/lib/hecks/bluebook/capabilities.rb +91 -9
- data/lib/hecks/bluebook/domain_port.rb +16 -4
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
- data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
- data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
- data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
- data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
- data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
- data/lib/hecks/bluebook/expression/projection.json +36 -0
- data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
- data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
- data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
- data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
- data/lib/hecks/bluebook/expression/resolver.rb +20 -1
- data/lib/hecks/bluebook/hecksagon.rb +23 -3
- data/lib/hecks/bluebook/lifecycle.rb +22 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
- data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +12 -12
- data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
- data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
- data/lib/hecks/bluebook/model_check.rb +8 -2
- data/lib/hecks/bluebook/policy.rb +4 -2
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/bluebook/synthesizer.rb +2 -0
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/cli/command_table.rb +1 -1
- data/lib/hecks/cli/console.rb +2 -1
- data/lib/hecks/cli/document.rb +1 -1
- data/lib/hecks/cli/launcher_words.rb +4 -4
- data/lib/hecks/cli/mcp.rb +4 -4
- data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
- data/lib/hecks/cli/project_cli/templates.rb +5 -5
- data/lib/hecks/cli/project_cli.rb +9 -6
- data/lib/hecks/cli/run.rb +2 -2
- data/lib/hecks/cli/smoke_http/checks.rb +4 -1
- data/lib/hecks/cli/smoke_http/transport.rb +3 -3
- data/lib/hecks/cli/smoke_http.rb +3 -0
- data/lib/hecks/codemod.rb +5 -2
- data/lib/hecks/corpus/routes.rb +7 -0
- data/lib/hecks/corpus/rust.rb +3 -2
- data/lib/hecks/deploy/bluebook/deploy.bluebook +18 -2
- data/lib/hecks/doc/reference/rendering.rb +6 -3
- data/lib/hecks/forms/app/commands.rb +8 -7
- data/lib/hecks/forms/app/queries.rb +2 -1
- data/lib/hecks/forms/app.rb +1 -1
- data/lib/hecks/forms/record_renderer.rb +1 -1
- data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
- data/lib/hecks/fuzzing/failure_triage.rb +2 -1
- data/lib/hecks/fuzzing/mutation/run.rb +11 -4
- data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
- data/lib/hecks/fuzzing/properties/guards.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency.rb +6 -2
- data/lib/hecks/fuzzing/shrinker.rb +3 -1
- data/lib/hecks/fuzzing/value_generator.rb +19 -5
- data/lib/hecks/gate/stages.yml +4 -10
- data/lib/hecks/grammar/evolve.rb +1 -1
- data/lib/hecks/grammar/expression_operators.json +372 -0
- data/lib/hecks/grammar/operators.rb +3 -1
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
- data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
- data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
- data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
- data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
- data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
- data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
- data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
- data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
- data/lib/hecks/hecks/adapters/local_files.rb +2 -2
- data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
- data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
- data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
- data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
- data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
- data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
- data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
- data/lib/hecks/hecks/adapters/terminal.rb +13 -11
- data/lib/hecks/hecks/codebase.behaviors +13 -1
- data/lib/hecks/hecks/codebase.bluebook +239 -5
- data/lib/hecks/hecks/custodian.behaviors +24 -6
- data/lib/hecks/hecks/custodian.bluebook +6 -1227
- data/lib/hecks/hecks/deploy.behaviors +1 -1
- data/lib/hecks/hecks/deploy.world +1 -3
- data/lib/hecks/hecks/hecks.behaviors +1 -1
- data/lib/hecks/hecks/hecks.hecksagon +53 -18
- data/lib/hecks/hecks/hecks.world +10 -10
- data/lib/hecks/hecks/rehearsal.bluebook +477 -0
- data/lib/hecks/hecks/site.behaviors +1 -1
- data/lib/hecks/hecks/site.world +0 -1
- data/lib/hecks/hecks/tooling.bluebook +1238 -0
- data/lib/hecks/indifferent_key.rb +20 -0
- data/lib/hecks/language/adapter.bluebook +13 -3
- data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
- data/lib/hecks/language/bluebook/entity.bluebook +18 -0
- data/lib/hecks/language/bluebook/policy.bluebook +9 -1
- data/lib/hecks/language/bluebook/query.bluebook +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
- data/lib/hecks/language/oidc.json +10 -0
- data/lib/hecks/language/port.bluebook +4 -4
- data/lib/hecks/literal.rb +12 -3
- data/lib/hecks/ports/key_vault.rb +7 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/shared.rb +5 -2
- data/lib/hecks/projections/model/deviations.rb +2 -2
- data/lib/hecks/projections/model.rb +2 -2
- data/lib/hecks/projections/oidc.rb +13 -1
- data/lib/hecks/projections/shape.rb +5 -2
- data/lib/hecks/projections/site/cms_editor/attributes.rb +26 -6
- data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +14 -1
- data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
- data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
- data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
- data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
- data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
- data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
- data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
- data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
- data/lib/hecks/projections/site/cms_editor/media.rb +1 -1
- data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
- data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
- data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
- data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
- data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
- data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
- data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
- data/lib/hecks/projections/site/cms_editor/schema.rb +96 -19
- data/lib/hecks/projections/site/cms_editor/setting.rb +52 -9
- data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +18 -1
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +251 -57
- data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +32 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +78 -24
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +6 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +19 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +31 -8
- data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +131 -2
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +8 -5
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +229 -34
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +40 -81
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +29 -12
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +53 -17
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +178 -88
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
- data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +2 -1
- data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
- data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
- data/lib/hecks/projections/site/cms_editor.rb +49 -23
- data/lib/hecks/projections/site/role_probe.rb +2 -0
- data/lib/hecks/projector/cli_audience.rb +2 -1
- data/lib/hecks/projector/cli_projector/listing.rb +1 -1
- data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
- data/lib/hecks/projector/docs_projector/closing.rb +1 -1
- data/lib/hecks/projector/docs_projector.rb +1 -1
- data/lib/hecks/projector/exporter/bindings.rb +40 -10
- data/lib/hecks/projector/exporter/capability_values.rb +55 -0
- data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
- data/lib/hecks/projector/target.rb +11 -0
- data/lib/hecks/projector.rb +32 -5
- data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
- data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +6 -2
- data/lib/hecks/release/approval.rb +64 -0
- data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
- data/lib/hecks/router/namespace_installer.rb +16 -16
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/best_effort.rb +22 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
- data/lib/hecks/runtime/command_interpreter.rb +3 -2
- data/lib/hecks/runtime/command_rules/emission.rb +1 -1
- data/lib/hecks/runtime/dispatcher.rb +4 -4
- data/lib/hecks/runtime/entity_element/location.rb +1 -1
- data/lib/hecks/runtime/event.rb +5 -0
- data/lib/hecks/runtime/identity.rb +7 -6
- data/lib/hecks/runtime/interpreting.rb +2 -1
- data/lib/hecks/runtime/invocation_builder.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
- data/lib/hecks/runtime/loader.rb +10 -10
- data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
- data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
- data/lib/hecks/runtime/policy_interpreter.rb +6 -6
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +1 -1
- data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
- data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
- data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
- data/lib/hecks/runtime/registry/merging.rb +1 -1
- data/lib/hecks/runtime/registry/providers.rb +16 -0
- data/lib/hecks/runtime/registry/read_repository.rb +6 -4
- data/lib/hecks/runtime/registry/verification.rb +4 -1
- data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
- data/lib/hecks/runtime/registry.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
- data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter.rb +3 -3
- data/lib/hecks/runtime/tenant_check.rb +5 -4
- data/lib/hecks/runtime/value/coercion.rb +2 -2
- data/lib/hecks/runtime/value/validation.rb +16 -4
- data/lib/hecks/runtime.rb +9 -9
- data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
- data/lib/hecks/rust_build/wasm.rb +1 -1
- data/lib/hecks/rust_build/wasm_browser.rb +1 -1
- data/lib/hecks/rust_build.rb +11 -0
- data/lib/hecks/served_options.rb +53 -0
- data/lib/hecks/site/bluebook/site.bluebook +1 -0
- data/lib/hecks/storehouse/audit.rb +1 -1
- data/lib/hecks/storehouse/dispatching.rb +8 -6
- data/lib/hecks/storehouse/introspection.rb +2 -2
- data/lib/hecks/storehouse/querying.rb +6 -4
- data/lib/hecks/storehouse.rb +4 -4
- data/lib/hecks/tools/comment_style/source_file.rb +4 -1
- data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
- data/lib/hecks/tools/lanes/rows.rb +6 -0
- data/lib/hecks/tools/oidc_manifests.rb +4 -5
- data/lib/hecks/tools/regeneration_run.rb +11 -4
- data/lib/hecks/tools/site_routes/editor_files.rb +12 -5
- data/lib/hecks/tools/tenant_provisioning.rb +2 -2
- data/lib/hecks/tools/tools_doc.rb +4 -4
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +4 -4
- data/lib/hecks.rb +15 -15
- data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
- data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
- data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
- data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
- data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
- data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
- data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
- data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
- data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
- data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
- data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
- data/rust/.gitignore +6 -0
- data/rust/Cargo.toml +6 -0
- data/rust/build/src/binding_pass.rs +103 -0
- data/rust/build/src/cargo_sync.rs +6 -0
- data/rust/build/src/lineage_pass.rs +3 -3
- data/rust/build/src/main.rs +17 -5
- data/rust/build/src/pipeline.rs +48 -5
- data/rust/build/src/seams_pass.rs +123 -0
- data/rust/build/src/translation_pass.rs +167 -0
- data/rust/codegen/src/asks.rs +103 -0
- data/rust/codegen/src/commands.rs +13 -8
- data/rust/codegen/src/constraints.rs +25 -0
- data/rust/codegen/src/domain_generator.rs +2 -0
- data/rust/codegen/src/expr_emitter.rs +13 -0
- data/rust/codegen/src/json_codec.rs +54 -15
- data/rust/codegen/src/main.rs +1 -0
- data/rust/codegen/src/mutations.rs +128 -3
- data/rust/codegen/src/queries.rs +11 -2
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/types.rs +166 -8
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +4 -4
- data/rust/host/src/bin/validate_harness.rs +43 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
- data/rust/host/src/boundary_fuzz/ir.rs +1 -0
- data/rust/host/src/boundary_fuzz/server.rs +1 -1
- data/rust/host/src/checkout.rs +35 -19
- data/rust/host/src/commerce_ir.rs +408 -2
- data/rust/host/src/expr_json.rs +118 -0
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +27 -1
- data/rust/host/src/payments.rs +12 -4
- data/rust/host/src/query_step.rs +22 -4
- data/rust/host/src/reference_validate.rs +653 -108
- data/rust/host/src/resend.rs +2 -0
- data/rust/host/src/server.rs +19 -6
- data/rust/host/src/ui_schema.rs +132 -5
- data/rust/host/src/web/newsletter.rs +7 -8
- data/rust/host/src/web/newsletter_send.rs +38 -13
- data/rust/host/src/web/registrations.rs +229 -26
- data/rust/host/src/web.rs +3 -3
- data/rust/parser/src/emit.rs +38 -6
- data/rust/parser/src/expr/ast_json.rs +7 -1
- data/rust/parser/src/expr/resolver.rs +171 -6
- data/rust/parser/src/ir.rs +7 -0
- data/rust/parser/src/keywords.rs +24 -0
- data/rust/parser/src/main.rs +3 -0
- data/rust/parser/src/parse/aggregate.rs +2 -1
- data/rust/parser/src/parse/chapter.rs +2 -2
- data/rust/parser/src/parse/command.rs +2 -0
- data/rust/parser/src/parse/hecksagon.rs +5 -2
- data/rust/parser/src/parse/lifecycle.rs +81 -0
- data/rust/parser/src/parse/mod.rs +15 -1
- data/rust/parser/src/parse/policy.rs +13 -2
- data/rust/parser/src/parse/value_object.rs +13 -8
- data/rust/parser/src/ruby_value.rs +1 -1
- data/rust/src/exemplar/commands.rs +4 -4
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/json.rs +43 -1
- data/rust/src/kernel/cli.rs +7 -5
- data/rust/src/kernel/dispatch.rs +2 -2
- data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
- data/rust/src/kernel/expr.rs +66 -3
- data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
- data/rust/src/kernel/expression_operators/text.rs +26 -2
- data/rust/src/kernel/json.rs +15 -0
- data/rust/src/kernel/mod.rs +1 -1
- data/rust/src/kernel/orchestrate.rs +54 -5
- data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
- data/rust/src/kernel/pattern.rs +60 -0
- data/rust/src/kernel/reference_lookup.rs +90 -7
- metadata +140 -38
- data/lib/hecks/doors/cli_door/nesting.rb +0 -34
- data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
- data/lib/hecks/doors/cli_door.rb +0 -124
- data/lib/hecks/doors/cli_runner/answers.rb +0 -138
- data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
- data/lib/hecks/doors/cli_runner/settling.rb +0 -115
- data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
- data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
- data/lib/hecks/doors/cli_runner.rb +0 -66
- data/lib/hecks/doors/command_request.rb +0 -119
- data/lib/hecks/doors/handle.rb +0 -208
- data/lib/hecks/doors/json_door.rb +0 -132
- data/lib/hecks/doors/launcher_options.rb +0 -247
- data/lib/hecks/doors/mcp_door/properties.rb +0 -47
- data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
- data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
- data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
- data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
- data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
- data/lib/hecks/doors/mcp_door.rb +0 -156
- data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
- data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
- data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
- data/lib/hecks/doors/mcp_door_scope.rb +0 -201
- data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
- data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
- data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
- data/lib/hecks/doors/ruby_door.rb +0 -80
- data/lib/hecks/doors/usage_cache.rb +0 -139
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
- data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
- data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
- data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "targets"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Projections
|
|
7
|
+
module Site
|
|
8
|
+
module CmsEditor
|
|
9
|
+
# Which attributes name another aggregate's instance, found by the bluebook's own
|
|
10
|
+
# declarations, so the editor offers the instances in place of a text box. In this order:
|
|
11
|
+
# a `<kind>:<slug>` key by its pattern (see `KeyKinds`), each kind that is an aggregate's
|
|
12
|
+
# snake-case name offering that aggregate; a `reference_to` attribute whose target is an
|
|
13
|
+
# editor aggregate; a value object of one text part named `<Stem>Ref`, where `<Stem>` is
|
|
14
|
+
# the type of an editor aggregate's identity or its name; and `<Image|Picture|Photo|Media>
|
|
15
|
+
# Ref` with a picture aggregate, which suggests pictures and accepts any other text. A
|
|
16
|
+
# target is found once: the same chapter's, else the only one of its name; else none.
|
|
17
|
+
module Pickers
|
|
18
|
+
# The stems that name a picture, for the editor's picture aggregate.
|
|
19
|
+
PICTURE_STEMS = %w[Image Picture Photo Media].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# @param aggregates [Array<Hash{String => Object}>] the aggregates as `Schema` shapes them
|
|
24
|
+
# @param media [Hash{String => Object}, nil] the picture aggregate, as `Media` says
|
|
25
|
+
# @return [Array<Hash{String => Object}>] the aggregates, each attribute that names an
|
|
26
|
+
# instance carrying its `picker`
|
|
27
|
+
def apply(aggregates, media = nil)
|
|
28
|
+
targets = Targets.new(aggregates, media)
|
|
29
|
+
aggregates.map { |agg| annotate(agg, targets) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Hash{String => Object}] `agg` with a picker on each attribute that has one
|
|
33
|
+
def annotate(agg, targets)
|
|
34
|
+
mark = ->(list) { list.map { |attr| with_picker(attr, agg, targets) } }
|
|
35
|
+
agg.merge("attributes" => mark.call(agg["attributes"]), "valueObjects" => objects(agg, mark),
|
|
36
|
+
"commands" => marked(agg["commands"], mark), "queries" => marked(agg["queries"], mark))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Array<Hash>] the commands or queries, the attributes of each marked
|
|
40
|
+
def marked(items, mark) = items.map { |item| item.merge("attributes" => mark.call(item["attributes"])) }
|
|
41
|
+
|
|
42
|
+
# @return [Hash{String => Array}] the value objects, the parts of one of several parts
|
|
43
|
+
# marked; a value object of one part is entered as the attribute that names it
|
|
44
|
+
def objects(agg, mark)
|
|
45
|
+
agg["valueObjects"].transform_values do |parts|
|
|
46
|
+
parts.size == 1 ? parts.map { |part| part.except("keys") } : mark.call(parts)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Hash{String => Object}] `attr` with its `picker`, or as it was
|
|
51
|
+
def with_picker(attr, agg, targets)
|
|
52
|
+
picker = picker_for(attr, agg, targets)
|
|
53
|
+
picker ? attr.except("keys").merge("picker" => picker) : attr
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def picker_for(attr, agg, targets)
|
|
57
|
+
return kinds(attr["keys"], agg, targets) if attr["keys"]
|
|
58
|
+
return targets.named(attr["target"], agg) if attr["kind"] == "reference"
|
|
59
|
+
|
|
60
|
+
stem = reference_stem(attr, agg)
|
|
61
|
+
stem && (targets.keyed_by(stem, agg) || (targets.pictures if PICTURE_STEMS.include?(stem)))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Hash{String => Array}] one entry per kind, with the aggregate it names if any
|
|
65
|
+
def kinds(words, agg, targets)
|
|
66
|
+
{ "kinds" => words.map { |word| { "kind" => word, **(targets.kind(word, agg) || {}) } } }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [String, nil] `Stem` of a `StemRef` value object that has one plain text part
|
|
70
|
+
def reference_stem(attr, agg)
|
|
71
|
+
return nil unless plain_text_object?(attr, agg["valueObjects"][attr["type"]])
|
|
72
|
+
|
|
73
|
+
stem = attr["type"].delete_suffix("Ref")
|
|
74
|
+
stem unless stem.empty? || stem == attr["type"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [Boolean] whether `attr` is a value object whose one part is a single text
|
|
78
|
+
def plain_text_object?(attr, parts)
|
|
79
|
+
attr["kind"] == "object" && parts&.size == 1 && parts.first["kind"] == "text" && !parts.first["list"]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Projections
|
|
7
|
+
module Site
|
|
8
|
+
module CmsEditor
|
|
9
|
+
# The `Editor` row's `preview`: where a record is shown on the site, as a URL template the
|
|
10
|
+
# editor frames in a side drawer.
|
|
11
|
+
#
|
|
12
|
+
# A template is an `http://` or `https://` address, or a path that starts with one slash,
|
|
13
|
+
# with the placeholders `{key}`, `{kind}`, `{slug}` and `{id}` filled from a record's
|
|
14
|
+
# identity when the page is built. Anything else is refused, so a template cannot name a
|
|
15
|
+
# scheme that runs script (`javascript:`), a host written with credentials, or a
|
|
16
|
+
# placeholder the editor would leave unfilled. A placeholder may not stand in the origin
|
|
17
|
+
# (scheme, host and port), because the content security policy names that origin once, at
|
|
18
|
+
# build time, and allows no other frame.
|
|
19
|
+
module Preview
|
|
20
|
+
# The words a template may put in braces.
|
|
21
|
+
PLACEHOLDERS = %w[key kind slug id].freeze
|
|
22
|
+
|
|
23
|
+
# An absolute address: the origin, then whatever follows it.
|
|
24
|
+
ABSOLUTE = %r{\A(?<origin>https?://[^/?#\s]+)(?<rest>.*)\z}m
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# @param template [String] the row's `preview`, which may be empty
|
|
29
|
+
# @return [Array<String>] every reason the template is refused; empty when it is usable
|
|
30
|
+
def problems(template)
|
|
31
|
+
return [] if template.empty?
|
|
32
|
+
|
|
33
|
+
found = []
|
|
34
|
+
found << "must not hold whitespace" if template.match?(/\s/)
|
|
35
|
+
found.concat(shape_problems(template))
|
|
36
|
+
found.concat(placeholder_problems(template))
|
|
37
|
+
found.map { |problem| "preview #{template.inspect} #{problem}" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] what the content security policy's `frame-src` names for the
|
|
41
|
+
# template: its origin, `'self'` for a path, or nil for no preview
|
|
42
|
+
def frame_source(template)
|
|
43
|
+
return nil if template.empty?
|
|
44
|
+
|
|
45
|
+
found = ABSOLUTE.match(template)
|
|
46
|
+
found ? found[:origin] : "'self'"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Array<String>] the problems with the scheme, the host and the path
|
|
50
|
+
def shape_problems(template)
|
|
51
|
+
found = ABSOLUTE.match(template)
|
|
52
|
+
return path_problems(template) unless found
|
|
53
|
+
|
|
54
|
+
origin_problems(found[:origin])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Array<String>] the problems with a path template
|
|
58
|
+
def path_problems(template)
|
|
59
|
+
return ["must be an http(s) address or a path that starts with one slash"] unless template.start_with?("/")
|
|
60
|
+
return ["must not start with two slashes, which names another host"] if template.start_with?("//")
|
|
61
|
+
|
|
62
|
+
[]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Array<String>] the problems with the origin of an absolute template
|
|
66
|
+
def origin_problems(origin)
|
|
67
|
+
found = []
|
|
68
|
+
found << "must not name credentials in the address" if origin.include?("@")
|
|
69
|
+
found << "must not hold a placeholder in the scheme, host or port" if origin.include?("{")
|
|
70
|
+
found << "names no host" if !origin.include?("{") && URI.parse(origin).host.to_s.empty?
|
|
71
|
+
found
|
|
72
|
+
rescue URI::InvalidURIError
|
|
73
|
+
["is not a valid address"]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Array<String>] a problem for each unknown placeholder and each stray brace
|
|
77
|
+
def placeholder_problems(template)
|
|
78
|
+
unknown = template.scan(/\{([^{}]*)\}/).flatten.uniq - PLACEHOLDERS
|
|
79
|
+
found = unknown.map do |name|
|
|
80
|
+
"names the placeholder {#{name}}; the placeholders are #{PLACEHOLDERS.map do |word|
|
|
81
|
+
"{#{word}}"
|
|
82
|
+
end.join(", ")}"
|
|
83
|
+
end
|
|
84
|
+
found << "has a brace that is not part of a placeholder" if template.gsub(/\{[^{}]*\}/, "").match?(/[{}]/)
|
|
85
|
+
found
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Projections
|
|
5
|
+
module Site
|
|
6
|
+
module CmsEditor
|
|
7
|
+
# The records that belong to one record, found by the keys they carry. Aggregate `B` is
|
|
8
|
+
# related to `A` when `B`'s identity is a `<kind>:<slug>` key (see `KeyKinds`) whose
|
|
9
|
+
# kinds include `A`'s snake-case name, so `post:first-light` keys the `B` of the `Post`
|
|
10
|
+
# `first-light`.
|
|
11
|
+
#
|
|
12
|
+
# The rule reads the declared pattern and what the kind resolves to (see `Pickers`),
|
|
13
|
+
# never a name. A `B` is shown by its shape: a **document** (a rich-text body), a
|
|
14
|
+
# **gallery** (a list that names pictures) or **metadata** (anything else). Its creating
|
|
15
|
+
# command is the first that takes the identity; its editing command is the draft-saving
|
|
16
|
+
# command of a document that keeps drafts (see `Drafts`), else the first command on an
|
|
17
|
+
# existing record that is neither a lifecycle move nor destructive and takes the body or
|
|
18
|
+
# the pictures.
|
|
19
|
+
module Relations
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# @param aggregates [Array<Hash{String => Object}>] the aggregates as `Pickers` leaves
|
|
23
|
+
# them
|
|
24
|
+
# @param media [Hash{String => Object}, nil] the picture aggregate, as `Media` says
|
|
25
|
+
# @return [Array<Hash{String => Object}>] each aggregate, carrying `related` when other
|
|
26
|
+
# aggregates are keyed by it
|
|
27
|
+
def apply(aggregates, media = nil)
|
|
28
|
+
aggregates.map do |agg|
|
|
29
|
+
found = aggregates.reject { |other| other.equal?(agg) }.filter_map { |other| relation(other, agg, media) }
|
|
30
|
+
found.empty? ? agg : agg.merge("related" => found)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Hash{String => Object}, nil] how `other` is related to `agg`, or nil when it
|
|
35
|
+
# is not
|
|
36
|
+
def relation(other, agg, media)
|
|
37
|
+
key = other["attributes"].find { |attr| attr["name"] == other["identity"] }
|
|
38
|
+
return nil unless key && keyed_by?(key, agg)
|
|
39
|
+
|
|
40
|
+
shape, field = shape_of(other, media)
|
|
41
|
+
{ "aggregate" => other["name"], "chapter" => other["chapter"], "field" => key["name"], "shape" => shape,
|
|
42
|
+
"create" => creating(other, key), "edit" => editing(other, field) }.compact
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Boolean] whether `key` is declared a key of `<agg's snake name>:<slug>`
|
|
46
|
+
def keyed_by?(key, agg)
|
|
47
|
+
(key.dig("picker", "kinds") || []).any? { |kind| kind["target"] == agg["name"] && kind["chapter"] == agg["chapter"] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Array(String, String)] the shape, and the attribute that gives it
|
|
51
|
+
def shape_of(other, media)
|
|
52
|
+
body = other["attributes"].find { |attr| attr["widget"] == "body" && !attr["list"] }
|
|
53
|
+
return ["document", body["name"]] if body
|
|
54
|
+
|
|
55
|
+
pictures = pictures_of(other, media)
|
|
56
|
+
pictures ? ["gallery", pictures["name"]] : ["metadata", nil]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Hash{String => Object}, nil] the list attribute that names pictures
|
|
60
|
+
def pictures_of(other, media)
|
|
61
|
+
return nil unless media
|
|
62
|
+
|
|
63
|
+
other["attributes"].find { |attr| attr["list"] && attr.dig("picker", "target") == media["aggregate"] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [String, nil] the first creating command that takes the identity
|
|
67
|
+
def creating(other, key)
|
|
68
|
+
other["commands"].find do |command|
|
|
69
|
+
command["creates"] && command["attributes"].any? do |attr|
|
|
70
|
+
attr["name"] == key["name"]
|
|
71
|
+
end
|
|
72
|
+
end&.fetch("name")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [String, nil] the command that changes the record's body, pictures or fields
|
|
76
|
+
def editing(other, field)
|
|
77
|
+
return other.dig("drafts", "save") if other["drafts"] && field == other.dig("drafts", "live")
|
|
78
|
+
|
|
79
|
+
moves = (other.dig("lifecycle", "transitions") || []).map { |move| move["verb"] }
|
|
80
|
+
other["commands"].find { |command| edits?(command, field, moves) }&.fetch("name")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def edits?(command, field, moves)
|
|
84
|
+
names = command["attributes"].map { |attr| attr["name"] }
|
|
85
|
+
!command["creates"] && !command["destructive"] && !moves.include?(command["name"]) &&
|
|
86
|
+
!names.empty? && (field.nil? || names.include?(field))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Projections
|
|
5
|
+
module Site
|
|
6
|
+
module CmsEditor
|
|
7
|
+
# What the `Editor` row's settings for many chapters and long lists must satisfy: chapters
|
|
8
|
+
# named once each, roles given for a chapter the editor edits, and a page size a list can
|
|
9
|
+
# hold. A chapter's name is part of an address, so it cannot be spelled as one of the
|
|
10
|
+
# editor's own paths.
|
|
11
|
+
module ScaleChecks
|
|
12
|
+
# The most instances a list may show to a page.
|
|
13
|
+
MAX_PAGE = 1000
|
|
14
|
+
|
|
15
|
+
# The first segments of the editor's own addresses, which no chapter may be named.
|
|
16
|
+
RESERVED = %w[assets media logout api scheduled].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @param row [Hash{Symbol => String}] the row, defaults filled
|
|
21
|
+
# @return [Array<String>] every problem with `chapter`, `chapter_roles` and `page_size`
|
|
22
|
+
def problems(row)
|
|
23
|
+
chapters = row[:chapter].split(",").map(&:strip)
|
|
24
|
+
[*chapter_problems(chapters), *role_problems(row[:chapter_roles], chapters), *page_size_problems(row[:page_size])]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Array<String>] the problems with an empty, repeated or reserved chapter name
|
|
28
|
+
def chapter_problems(chapters)
|
|
29
|
+
found = []
|
|
30
|
+
found << "chapter names no chapter" if chapters.reject(&:empty?).empty?
|
|
31
|
+
repeated = chapters.find { |name| chapters.count(name) > 1 }
|
|
32
|
+
found << "chapter names #{repeated.inspect} twice" if repeated
|
|
33
|
+
[*found, *reserved_problems(chapters)]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Array<String>] a problem for each chapter spelled as an address of the editor's
|
|
37
|
+
def reserved_problems(chapters)
|
|
38
|
+
return [] if chapters.size < 2
|
|
39
|
+
|
|
40
|
+
(chapters & RESERVED).map do |name|
|
|
41
|
+
"chapter #{name.inspect} is the first part of an address the editor serves; name it another way"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Array<String>] the problems with `Chapter=Role,Role;Chapter=Role` entries
|
|
46
|
+
def role_problems(text, chapters)
|
|
47
|
+
text.split(";").map(&:strip).reject(&:empty?).flat_map do |entry|
|
|
48
|
+
name, roles = entry.split("=", 2)
|
|
49
|
+
next ["chapter_roles #{entry.inspect} must read Chapter=Role,Role"] if roles.to_s.strip.empty?
|
|
50
|
+
next [] if chapters.include?(name.strip)
|
|
51
|
+
|
|
52
|
+
["chapter_roles names #{name.strip.inspect}, which is not a chapter of this editor"]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Array<String>] the problem with a page size that is not a whole 1 to 1000
|
|
57
|
+
def page_size_problems(size)
|
|
58
|
+
return [] if size.match?(/\A[1-9]\d{0,3}\z/) && size.to_i <= MAX_PAGE
|
|
59
|
+
|
|
60
|
+
["page_size #{size.inspect} must be a whole number from 1 to #{MAX_PAGE}"]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Projections
|
|
5
|
+
module Site
|
|
6
|
+
module CmsEditor
|
|
7
|
+
# An aggregate whose records are actions to run later, found by shape. The editor makes
|
|
8
|
+
# and changes the records and never runs an action: a runner elsewhere does.
|
|
9
|
+
#
|
|
10
|
+
# It has a **subject** (a plain `<kind>:<slug>` key, see `KeyKinds`), an **action** from
|
|
11
|
+
# a closed set (`one_of`), a **due time** that is a moment (see `Moment`), a
|
|
12
|
+
# **lifecycle** whose first state moves to at least two states that move nowhere, and a
|
|
13
|
+
# **creating command** that takes the three. The command that reschedules takes the due
|
|
14
|
+
# time; the one that cancels is a destructive move from the first state to a final one;
|
|
15
|
+
# the reason is the first optional plain text left over; the query takes the subject
|
|
16
|
+
# alone. The first such aggregate is the one.
|
|
17
|
+
module Scheduling
|
|
18
|
+
# What a scheduling aggregate's attributes are called in its record: `subject`,
|
|
19
|
+
# `action`, `due`.
|
|
20
|
+
ROLES = %w[subject action due].freeze
|
|
21
|
+
|
|
22
|
+
# The widgets of an integer that is a moment.
|
|
23
|
+
MOMENTS = %w[date datetime].freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# @param aggregates [Array<Hash{String => Object}>] the aggregates as `Pickers` leaves
|
|
28
|
+
# them
|
|
29
|
+
# @return [Hash{String => Object}, nil] the scheduling aggregate and its parts; nil when
|
|
30
|
+
# none
|
|
31
|
+
def read(aggregates)
|
|
32
|
+
aggregates.lazy.filter_map { |agg| shape(agg) }.first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Hash{String => Object}, nil] `agg` as the scheduling aggregate, or nil
|
|
36
|
+
def shape(agg)
|
|
37
|
+
parts = parts_of(agg)
|
|
38
|
+
schedule = parts && endings(agg["lifecycle"]).size > 1 && creating(agg, parts)
|
|
39
|
+
return nil unless schedule
|
|
40
|
+
|
|
41
|
+
{ "aggregate" => agg["name"], "chapter" => agg["chapter"], **parts, "pending" => agg.dig("lifecycle", "default"),
|
|
42
|
+
"schedule" => schedule["name"], **optional(agg, parts, schedule) }.compact
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [Hash{String => Object}, nil] the subject, action, actions and due time, or nil
|
|
46
|
+
def parts_of(agg)
|
|
47
|
+
return nil unless agg["lifecycle"]
|
|
48
|
+
|
|
49
|
+
plain = agg["attributes"].reject { |attr| attr["list"] }
|
|
50
|
+
found = [subject_of(agg, plain), plain.find { |attr| attr["options"] }, plain.find { |attr| moment?(attr) }]
|
|
51
|
+
describe(*found) if found.all?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Boolean] whether the attribute is an integer that is a moment
|
|
55
|
+
def moment?(attr) = MOMENTS.include?(attr["widget"])
|
|
56
|
+
|
|
57
|
+
# @return [Hash{String => Object}] the three attributes' names, and the action's members
|
|
58
|
+
def describe(subject, action, due)
|
|
59
|
+
{ "subject" => subject["name"], "action" => action["name"], "actions" => action["options"], "due" => due["name"] }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Hash{String => Object}, nil] the attribute that is a key of a kind, the
|
|
63
|
+
# identity last
|
|
64
|
+
def subject_of(agg, plain)
|
|
65
|
+
plain.select { |attr| attr.dig("picker", "kinds") }.min_by { |attr| attr["name"] == agg["identity"] ? 1 : 0 }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Array<String>] the final states the first state moves to: states nothing
|
|
69
|
+
# moves out of
|
|
70
|
+
def endings(lifecycle)
|
|
71
|
+
moves = lifecycle["transitions"]
|
|
72
|
+
first = moves.select { |move| move["from"].include?(lifecycle["default"]) }.map { |move| move["to"] }.uniq
|
|
73
|
+
first - moves.flat_map { |move| move["from"] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Hash{String => Object}, nil] the creating command that takes the subject,
|
|
77
|
+
# action and due time
|
|
78
|
+
def creating(agg, parts)
|
|
79
|
+
agg["commands"].find { |cmd| cmd["creates"] && takes?(cmd, parts.values_at(*ROLES)) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] whether the command takes every one of the named attributes
|
|
83
|
+
def takes?(cmd, names) = (names - cmd["attributes"].map { |attr| attr["name"] }).empty?
|
|
84
|
+
|
|
85
|
+
# @return [Hash{String => Object}] the commands, query and reason that may be missing
|
|
86
|
+
def optional(agg, parts, schedule)
|
|
87
|
+
{ "reschedule" => rescheduling(agg, parts), "cancel" => cancelling(agg), "query" => querying(agg, parts),
|
|
88
|
+
"reason" => reason(agg, parts), "generated" => generated?(agg, parts, schedule) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [String, nil] the command on a record, not a lifecycle move, that takes the
|
|
92
|
+
# due time
|
|
93
|
+
def rescheduling(agg, parts)
|
|
94
|
+
agg["commands"].find { |cmd| !cmd["creates"] && !move?(agg, cmd) && takes?(cmd, [parts["due"]]) }&.fetch("name")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [String, nil] the destructive lifecycle move from the first state to a final
|
|
98
|
+
# one
|
|
99
|
+
def cancelling(agg)
|
|
100
|
+
agg["commands"].find { |cmd| cmd["destructive"] && ends?(agg, cmd) }&.fetch("name")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @return [String, nil] the query whose one argument is the subject
|
|
104
|
+
def querying(agg, parts)
|
|
105
|
+
agg["queries"].find { |query| query["attributes"].map { |attr| attr["name"] } == [parts["subject"]] }&.fetch("name")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [Boolean] whether the command is a lifecycle move
|
|
109
|
+
def move?(agg, cmd) = agg["lifecycle"]["transitions"].any? { |move| move["verb"] == cmd["name"] }
|
|
110
|
+
|
|
111
|
+
# @return [Boolean] whether the command moves a first-state record to a final state
|
|
112
|
+
def ends?(agg, cmd)
|
|
113
|
+
lifecycle = agg["lifecycle"]
|
|
114
|
+
final = endings(lifecycle)
|
|
115
|
+
lifecycle["transitions"].any? do |move|
|
|
116
|
+
move["verb"] == cmd["name"] && move["from"].include?(lifecycle["default"]) && final.include?(move["to"])
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [String, nil] the first optional plain text attribute no other part uses
|
|
121
|
+
def reason(agg, parts)
|
|
122
|
+
used = [agg["identity"], *parts.values_at(*ROLES)]
|
|
123
|
+
spare = agg["attributes"].find { |attr| attr["optional"] && !used.include?(attr["name"]) && plain_text?(agg, attr) }
|
|
124
|
+
spare&.fetch("name")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [Boolean] whether the attribute is one plain text, alone or as a value
|
|
128
|
+
# object's only part
|
|
129
|
+
def plain_text?(agg, attr)
|
|
130
|
+
return false if %w[list picker options widget].any? { |key| attr[key] }
|
|
131
|
+
|
|
132
|
+
parts = agg["valueObjects"][attr["type"]]
|
|
133
|
+
parts ? lone_text?(parts) : attr["kind"] == "text"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Boolean] whether the parts are one plain text
|
|
137
|
+
def lone_text?(parts) = parts.size == 1 && parts.first["kind"] == "text" && !parts.first["list"]
|
|
138
|
+
|
|
139
|
+
# @return [true, nil] whether the creating command takes an identity that is not the
|
|
140
|
+
# subject,
|
|
141
|
+
# which the editor then makes itself
|
|
142
|
+
def generated?(agg, parts, schedule)
|
|
143
|
+
taken = schedule["attributes"].any? { |attr| attr["name"] == agg["identity"] }
|
|
144
|
+
true if taken && agg["identity"] != parts["subject"]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "attributes"
|
|
4
|
+
require_relative "blocks"
|
|
5
|
+
require_relative "clearing"
|
|
6
|
+
require_relative "composition"
|
|
7
|
+
require_relative "destructive"
|
|
8
|
+
require_relative "drafts"
|
|
9
|
+
require_relative "limits"
|
|
4
10
|
require_relative "media"
|
|
11
|
+
require_relative "pickers"
|
|
12
|
+
require_relative "relations"
|
|
13
|
+
require_relative "scheduling"
|
|
5
14
|
|
|
6
15
|
module Hecks
|
|
7
16
|
module Projections
|
|
@@ -18,17 +27,55 @@ module Hecks
|
|
|
18
27
|
# A query that `returns` a value object is answered outside the domain and is left out: the
|
|
19
28
|
# host refuses it, so a list cannot be built from it.
|
|
20
29
|
class Schema
|
|
21
|
-
# @param
|
|
22
|
-
#
|
|
23
|
-
# @
|
|
24
|
-
# @
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
# @param chapters [Bluebook::Chapter, Array<Bluebook::Chapter>] the chapter, or the
|
|
31
|
+
# chapters in the order the row names them; the first names the domain addressed
|
|
32
|
+
# @param skip [Array<String>] aggregates to leave out: `Name`, or `Chapter::Name`
|
|
33
|
+
# @param pictures [Bluebook::Chapter, nil] another chapter whose picture aggregate the
|
|
34
|
+
# editor's pictures use, in place of any in `chapters`
|
|
35
|
+
# @param roles [Hash{String => Array<String>}] the roles each chapter is limited to
|
|
36
|
+
# @return [Hash{String => Object}] the domain's name and its aggregates; `chapters` too
|
|
37
|
+
# when there are several
|
|
38
|
+
# @raise [ArgumentError] when no aggregate is left, or `pictures` has no picture aggregate
|
|
39
|
+
def self.read(chapters, skip: [], pictures: nil, roles: {})
|
|
40
|
+
list = chapters.is_a?(Array) ? chapters : [chapters]
|
|
41
|
+
shaped = Composition.aggregates(list, skip: skip, roles: roles) { |agg| new(agg).to_h }
|
|
42
|
+
raise ArgumentError, "the #{list.map(&:name).join(", ")} chapter has no aggregate to edit" if shaped.empty?
|
|
43
|
+
|
|
44
|
+
media = pictures ? elsewhere(pictures) : within(shaped, list.first.name)
|
|
45
|
+
assemble(list, Relations.apply(Pickers.apply(shaped, media), media), media)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Hash{String => Object}] the schema: the domain, the chapters when several, the
|
|
49
|
+
# aggregates, and the picture and scheduling aggregates when there are such
|
|
50
|
+
def self.assemble(list, aggregates, media)
|
|
51
|
+
chapters = list.size > 1 ? { "chapters" => list.map(&:name) } : {}
|
|
52
|
+
scheduling = Scheduling.read(aggregates)
|
|
53
|
+
{ "domain" => list.first.name, **chapters, "aggregates" => aggregates, **(media ? { "media" => media } : {}),
|
|
54
|
+
**(scheduling ? { "scheduling" => scheduling } : {}) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The picture aggregate among the editor's own, with the chapter that holds it when that
|
|
58
|
+
# is not the first, since the host is addressed by chapter.
|
|
59
|
+
#
|
|
60
|
+
# @return [Hash{String => Object}, nil] the picture aggregate, as `Media` describes it
|
|
61
|
+
def self.within(shaped, first)
|
|
62
|
+
found = Media.read(shaped)
|
|
63
|
+
chapter = found&.delete("chapter")
|
|
64
|
+
chapter && chapter != first ? found.merge("domain" => chapter) : found
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The picture aggregate of another chapter: the same record as one in the editor's own
|
|
68
|
+
# chapter, with the chapter's name and the aggregate itself, since the editor's own
|
|
69
|
+
# aggregates do not hold it.
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash{String => Object}] the picture aggregate, as `Media` describes it
|
|
72
|
+
# @raise [ArgumentError] when the chapter has no aggregate that registers pictures
|
|
73
|
+
def self.elsewhere(chapter)
|
|
74
|
+
shaped = chapter.aggregates.map { |agg| new(agg).to_h }
|
|
75
|
+
found = Media.read(shaped)
|
|
76
|
+
raise ArgumentError, "the #{chapter.name} chapter has no aggregate that registers pictures" unless found
|
|
77
|
+
|
|
78
|
+
found.merge("domain" => chapter.name, "definition" => shaped.find { |agg| agg["name"] == found["aggregate"] })
|
|
32
79
|
end
|
|
33
80
|
|
|
34
81
|
# @param aggregate [Bluebook::Aggregate] one aggregate of the chapter
|
|
@@ -39,28 +86,51 @@ module Hecks
|
|
|
39
86
|
|
|
40
87
|
# @return [Hash{String => Object}] the aggregate as the editor reads it
|
|
41
88
|
def to_h
|
|
89
|
+
held = aggregate_attributes
|
|
90
|
+
drafts = Drafts.of(@agg, held)
|
|
42
91
|
{ "name" => @agg.hecks_name, "description" => @agg.description, "identity" => @agg.identified_by.to_s,
|
|
43
|
-
"lifecycle" => lifecycle, "attributes" =>
|
|
44
|
-
"
|
|
45
|
-
"
|
|
92
|
+
"lifecycle" => lifecycle, "attributes" => held, "valueObjects" => value_objects,
|
|
93
|
+
"commands" => @agg.commands.map { |command| command(command) }, "queries" => queries,
|
|
94
|
+
**(drafts ? { "drafts" => drafts } : {}) }
|
|
46
95
|
end
|
|
47
96
|
|
|
48
97
|
private
|
|
49
98
|
|
|
50
99
|
def lifecycle_field = @agg.lifecycle&.field
|
|
51
100
|
|
|
52
|
-
|
|
101
|
+
# The aggregate's own attributes, bounded by the aggregate's invariants (see `Limits`).
|
|
102
|
+
def aggregate_attributes
|
|
103
|
+
bounded(attributes(@agg.attributes.reject { |a| a.name == lifecycle_field }), @agg.invariants)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def attributes(list) = list.map { |attribute| limited(@attributes.of(attribute)) }
|
|
107
|
+
|
|
108
|
+
# `list` with what the holder's rules say about the most each may hold (see `Limits`).
|
|
109
|
+
def bounded(list, rules) = Limits.apply(list, rules)
|
|
110
|
+
|
|
111
|
+
# `attr` with the most its block list may hold, when a command's `given` says.
|
|
112
|
+
def limited(attr)
|
|
113
|
+
most = attr.key?("blockList") ? Blocks.maximum(@agg.commands, attr["name"]) : nil
|
|
114
|
+
most ? attr.merge("max" => most) : attr
|
|
115
|
+
end
|
|
53
116
|
|
|
54
117
|
def value_objects
|
|
55
|
-
@agg.value_objects.to_h
|
|
118
|
+
@agg.value_objects.to_h do |object|
|
|
119
|
+
[object.hecks_name, bounded(attributes(object.attributes), object.invariants)]
|
|
120
|
+
end
|
|
56
121
|
end
|
|
57
122
|
|
|
58
123
|
def lifecycle
|
|
124
|
+
@lifecycle ||= shaped_lifecycle
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def shaped_lifecycle
|
|
59
128
|
lifecycle = @agg.lifecycle
|
|
60
129
|
return nil unless lifecycle
|
|
61
130
|
|
|
62
|
-
{ "field" => lifecycle.field.to_s, "default" => lifecycle.default,
|
|
63
|
-
|
|
131
|
+
shaped = { "field" => lifecycle.field.to_s, "default" => lifecycle.default,
|
|
132
|
+
"transitions" => lifecycle.transitions.map { |verb, move| transition(verb, move) } }
|
|
133
|
+
shaped.merge("tones" => Destructive.tones(shaped))
|
|
64
134
|
end
|
|
65
135
|
|
|
66
136
|
def transition(verb, move)
|
|
@@ -69,9 +139,16 @@ module Hecks
|
|
|
69
139
|
|
|
70
140
|
def command(command)
|
|
71
141
|
{ "name" => command.hecks_name, "goal" => command.goal, "role" => command.role, "creates" => command.creates?,
|
|
72
|
-
"on" => command.creates? ? nil : command.references.to_s,
|
|
142
|
+
"on" => command.creates? ? nil : command.references.to_s,
|
|
143
|
+
"destructive" => Destructive.command?(command.hecks_name, lifecycle),
|
|
144
|
+
"attributes" => bounded(attributes(command.attributes), command.givens) }
|
|
145
|
+
.then { |shaped| clearing(shaped, command) }
|
|
73
146
|
end
|
|
74
147
|
|
|
148
|
+
# Leaves the arguments that only clear (see `Clearing`) out of the command's form, and
|
|
149
|
+
# names those that are lists as `empty`, for the editor to send as empty lists.
|
|
150
|
+
def clearing(shaped, command) = Clearing.apply(shaped, Clearing.of(command, @agg))
|
|
151
|
+
|
|
75
152
|
def queries
|
|
76
153
|
@agg.queries.reject(&:returns).map do |query|
|
|
77
154
|
{ "name" => query.name, "description" => query.description, "attributes" => attributes(query.attributes) }
|