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
|
@@ -2,16 +2,21 @@ __BANNER__
|
|
|
2
2
|
|
|
3
3
|
// Running one command and judging what came of it. The host answers HTTP 200 whether or not the
|
|
4
4
|
// domain refused, and its `refusals` list can carry entries replayed from history, so the outcome is
|
|
5
|
-
// judged by the state that comes back
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// judged by the state that comes back, never by what was sent: a command may set fields from
|
|
6
|
+
// arguments that are not fields (a clearing argument), so the arguments are no evidence. The command
|
|
7
|
+
// took effect when the instance's state changed, and for a lifecycle move when it reached the state
|
|
8
|
+
// the move names. When the state did not change at all (a repeated command, a move to the state it
|
|
9
|
+
// is already in) the answer's last refusal decides, and only if it names this verb.
|
|
8
10
|
import type { Answer, HostClient } from "@hecks/client";
|
|
9
11
|
import { DomainUnavailable } from "@hecks/client";
|
|
10
12
|
|
|
13
|
+
import { qualified } from "./names.ts";
|
|
11
14
|
import type { Aggregate, Command } from "./schema.ts";
|
|
15
|
+
import { problemsOf } from "./ui/block_list.js";
|
|
16
|
+
import type { Problem } from "./ui/blocks.ts";
|
|
12
17
|
|
|
13
18
|
export type State = Record<string, unknown>;
|
|
14
|
-
export type Outcome = { ok: true; id: string } | { ok: false; message: string };
|
|
19
|
+
export type Outcome = { ok: true; id: string } | { ok: false; message: string; field?: string; problems?: Problem[] };
|
|
15
20
|
|
|
16
21
|
/** The text an identity value holds: the part of a value object, or the plain value. */
|
|
17
22
|
export function leaf(value: unknown): string | null {
|
|
@@ -24,40 +29,86 @@ export const idOf = (agg: Aggregate, state: State | undefined): string | null =>
|
|
|
24
29
|
|
|
25
30
|
/** The state of one instance in an answer, or undefined when the answer holds none by that id. */
|
|
26
31
|
export function instanceIn(client: HostClient, answer: Answer, agg: Aggregate, id: string): State | undefined {
|
|
27
|
-
return client.instancesOf(answer, agg
|
|
32
|
+
return client.instancesOf(answer, qualified(agg)).find(([key]) => key === id)?.[1];
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/**
|
|
36
|
+
* A client that asks the host to read once and remembers the answer, for a request that only reads:
|
|
37
|
+
* the page and the navigation's counts then come from one read of the host.
|
|
38
|
+
*/
|
|
39
|
+
export function reading(client: HostClient): HostClient {
|
|
40
|
+
let held: Promise<Answer> | undefined;
|
|
41
|
+
return new Proxy(client, {
|
|
42
|
+
get(target, key) {
|
|
43
|
+
if (key === "read") return () => (held ??= target.read());
|
|
44
|
+
const member = Reflect.get(target, key, target);
|
|
45
|
+
return typeof member === "function" ? member.bind(target) : member;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
/**
|
|
51
|
+
* A value as it is compared: the keys of an object in order and each null left out, since one answer
|
|
52
|
+
* of a host may echo a slot that was never set as null where another leaves it out, in an order of its own.
|
|
53
|
+
*/
|
|
54
|
+
function canonical(value: unknown): unknown {
|
|
55
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
56
|
+
if (value === null || typeof value !== "object") return value;
|
|
57
|
+
const held = Object.entries(value as object).filter(([, part]) => part !== null);
|
|
58
|
+
return Object.fromEntries(held.sort(([a], [b]) => (a < b ? -1 : 1)).map(([key, part]) => [key, canonical(part)]));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sameState = (a: State | undefined, b: State | undefined): boolean => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
42
62
|
|
|
63
|
+
/** Whether the host's refusal names this aggregate's verb: by its chapter too when the editor spans several. */
|
|
43
64
|
function refusedLast(answer: Answer, agg: Aggregate, command: Command): boolean {
|
|
44
65
|
const last = (answer.refusals ?? []).at(-1);
|
|
45
|
-
|
|
66
|
+
if (last === undefined) return false;
|
|
67
|
+
const verb = String(last.verb ?? "");
|
|
68
|
+
return verb.replace(/^.*::/, "") === `${agg.name}.${command.name}` && (!agg.chapter || !verb.includes("::") || verb.startsWith(`${agg.chapter}::`));
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
/** Whether `after` shows the command took effect, given the instance as it was `before`. */
|
|
49
|
-
export function applied(agg: Aggregate, command: Command,
|
|
72
|
+
export function applied(agg: Aggregate, command: Command, before: State | undefined, after: State | undefined, answer: Answer): boolean {
|
|
50
73
|
if (!after) return false;
|
|
51
74
|
if (sameState(before, after)) return !refusedLast(answer, agg, command);
|
|
52
75
|
const move = agg.lifecycle?.transitions.find((transition) => transition.verb === command.name);
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
return move && agg.lifecycle ? after[agg.lifecycle.field] === move.to : true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const label = (name: string): string => name.replace(/_/g, " ");
|
|
80
|
+
|
|
81
|
+
/** The command's attribute a value object's name stands for, to name the offending field. */
|
|
82
|
+
function fieldOf(command: Command, type: string): string {
|
|
83
|
+
const held = command.attributes.find((attr) => attr.type === type);
|
|
84
|
+
return held ? label(held.name) : type;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The words for a refusal. The host words a given as `<Command> refused — <condition>`, which reads
|
|
89
|
+
* as though the condition were the reason, so it is shown as the condition that was not met; an
|
|
90
|
+
* invariant is shown as the field and the rule, without the value that offended (raw JSON).
|
|
91
|
+
*/
|
|
92
|
+
export function refusalText(refusal: { kind?: string; error?: string } | undefined, command: Command): string {
|
|
93
|
+
const error = refusal?.error ?? "";
|
|
94
|
+
const refused = /^.+? refused — (.+)$/s.exec(error);
|
|
95
|
+
if (refusal?.kind === "GivenNotMet" && refused) return `Not allowed unless ${refused[1]}.`;
|
|
96
|
+
if (refusal?.kind === "EnsuresNotMet" && refused) return `Not allowed: the result would break this rule: ${refused[1]}.`;
|
|
97
|
+
const invariant = /^(.+?) invariant violated — (.+?)(?: \(given .*\))?$/s.exec(error);
|
|
98
|
+
if (invariant) return `${fieldOf(command, invariant[1])}: ${invariant[2]}.`;
|
|
99
|
+
if (refusal?.kind === "InvariantViolation" && refused) return `${refused[1]}.`;
|
|
100
|
+
return error || `The domain did not apply ${command.name}.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The name of the command's attribute a refused invariant is about, so its input can be marked; undefined when none. */
|
|
104
|
+
export function refusedField(refusal: { kind?: string; error?: string } | undefined, command: Command): string | undefined {
|
|
105
|
+
const invariant = /^(.+?) invariant violated — /s.exec(refusal?.error ?? "");
|
|
106
|
+
return invariant ? command.attributes.find((attr) => attr.type === invariant[1])?.name : undefined;
|
|
55
107
|
}
|
|
56
108
|
|
|
57
109
|
/** The words for a command that changed nothing: the host's last refusal, or a plain statement. */
|
|
58
110
|
export function refusalMessage(answer: Answer, command: Command): string {
|
|
59
|
-
|
|
60
|
-
return last?.error ?? `The domain did not apply ${command.name}.`;
|
|
111
|
+
return refusalText((answer.refusals ?? []).at(-1), command);
|
|
61
112
|
}
|
|
62
113
|
|
|
63
114
|
/**
|
|
@@ -68,10 +119,13 @@ export async function runCommand(client: HostClient, agg: Aggregate, command: Co
|
|
|
68
119
|
try {
|
|
69
120
|
const id = to ?? idOf(agg, args);
|
|
70
121
|
const before = id ? instanceIn(client, await client.read(), agg, id) : undefined;
|
|
71
|
-
const answer = await client.dispatch(`${agg
|
|
122
|
+
const answer = await client.dispatch(`${qualified(agg)}.${command.name}`, args, to, command.role ?? undefined);
|
|
72
123
|
const after = id ? instanceIn(client, answer, agg, id) : undefined;
|
|
73
|
-
if (id && applied(agg, command,
|
|
74
|
-
|
|
124
|
+
if (id && applied(agg, command, before, after, answer)) return { ok: true, id };
|
|
125
|
+
const last = (answer.refusals ?? []).at(-1);
|
|
126
|
+
const problems: Problem[] = problemsOf(last?.error, command.attributes, args, agg.valueObjects);
|
|
127
|
+
const field = refusedField(last, command);
|
|
128
|
+
return { ok: false, message: refusalMessage(answer, command), field, problems };
|
|
75
129
|
} catch (err) {
|
|
76
130
|
if (err instanceof DomainUnavailable) return { ok: false, message: `The domain could not be reached: ${err.message}` };
|
|
77
131
|
throw err;
|
|
@@ -5,17 +5,23 @@ __BANNER__
|
|
|
5
5
|
|
|
6
6
|
export const EDITOR = {
|
|
7
7
|
title: __TITLE__,
|
|
8
|
+
brand: __BRAND__,
|
|
9
|
+
logo: __LOGO__,
|
|
8
10
|
basePath: __BASE_PATH__,
|
|
9
11
|
ssoPath: __SSO_PATH__,
|
|
10
12
|
login: __LOGIN__,
|
|
11
13
|
sessionCookie: __SESSION_COOKIE__,
|
|
12
14
|
sessionMaxAge: 28800,
|
|
15
|
+
flashMaxAge: 60,
|
|
13
16
|
hostCookie: __HOST_COOKIE__,
|
|
14
17
|
hostEnv: __HOST_ENV__,
|
|
15
18
|
hostDefault: __HOST_DEFAULT__,
|
|
16
19
|
roles: __ROLES__,
|
|
17
20
|
mediaDir: __MEDIA_DIR__,
|
|
18
21
|
mediaMaxBytes: __MEDIA_MAX_BYTES__,
|
|
22
|
+
pageSize: __PAGE_SIZE__,
|
|
23
|
+
/** Where a record is shown on the site, with `{key}`, `{kind}`, `{slug}` and `{id}`; null for no preview. */
|
|
24
|
+
preview: __PREVIEW__ as string | null,
|
|
19
25
|
accountPath: "/accounts/me",
|
|
20
26
|
membersPath: "/members",
|
|
21
27
|
rememberMs: 60000,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// A notice that survives one redirect: "Published." after a command succeeds and the person is sent
|
|
4
|
+
// to the page that shows the result. It is carried in a short-lived cookie that is signed (so a
|
|
5
|
+
// visitor cannot make the editor say something), holds only a kind and a sentence, and is read
|
|
6
|
+
// once: the page that shows it clears it in the same response. There is no server-side state.
|
|
7
|
+
// The sentence is escaped wherever it is shown; it is encoded as base64url so no character of it can
|
|
8
|
+
// break out of the cookie.
|
|
9
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
import { EDITOR } from "./config.ts";
|
|
12
|
+
|
|
13
|
+
export type FlashKind = "success" | "error" | "info";
|
|
14
|
+
|
|
15
|
+
export interface Flash {
|
|
16
|
+
kind: FlashKind;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SUFFIX = ":editor-flash";
|
|
21
|
+
const NAME = `${EDITOR.sessionCookie}_flash`;
|
|
22
|
+
const ATTRIBUTES = `Path=${EDITOR.basePath}; HttpOnly; SameSite=Lax`;
|
|
23
|
+
const KINDS: readonly string[] = ["success", "error", "info"];
|
|
24
|
+
|
|
25
|
+
const sign = (secret: string, payload: string): string => createHmac("sha256", secret + SUFFIX).update(payload).digest("base64url");
|
|
26
|
+
|
|
27
|
+
/** The `Set-Cookie` value that carries `flash` to the next page. */
|
|
28
|
+
export function flashCookie(flash: Flash, secret: string, now: () => number = Date.now): string {
|
|
29
|
+
const payload = Buffer.from(JSON.stringify({ k: flash.kind, m: flash.message.slice(0, 300), t: Math.floor(now() / 1000) })).toString("base64url");
|
|
30
|
+
const secure = process.env.NODE_ENV === "production" ? "; Secure" : "";
|
|
31
|
+
return `${NAME}=${payload}.${sign(secret, payload)}; ${ATTRIBUTES}; Max-Age=${EDITOR.flashMaxAge}${secure}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The `Set-Cookie` value that spends the notice. */
|
|
35
|
+
export const spentFlash = (): string => `${NAME}=; ${ATTRIBUTES}; Max-Age=0`;
|
|
36
|
+
|
|
37
|
+
/** The notice a `Cookie` header carries, or null when it has none, it was forged or it has expired. */
|
|
38
|
+
export function readFlash(header: string | null, secret: string, now: () => number = Date.now): Flash | null {
|
|
39
|
+
const raw = header?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${NAME}=`))?.slice(NAME.length + 1);
|
|
40
|
+
const [payload, signature] = raw?.split(".") ?? [];
|
|
41
|
+
if (!payload || !signature || !secret) return null;
|
|
42
|
+
const expected = Buffer.from(sign(secret, payload));
|
|
43
|
+
const given = Buffer.from(signature);
|
|
44
|
+
if (expected.length !== given.length || !timingSafeEqual(expected, given)) return null;
|
|
45
|
+
try {
|
|
46
|
+
const found = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
47
|
+
const fresh = typeof found.t === "number" && now() / 1000 - found.t <= EDITOR.flashMaxAge && now() / 1000 >= found.t - 5;
|
|
48
|
+
return fresh && KINDS.includes(found.k) && typeof found.m === "string" ? { kind: found.k, message: found.m } : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -4,7 +4,8 @@ __BANNER__
|
|
|
4
4
|
// posting to /dispatch from this machine. The host accepts that protocol from a loopback peer only,
|
|
5
5
|
// which is why this server, not the browser, talks to it; it authenticates each person itself
|
|
6
6
|
// (auth/) before any of these calls is made on their behalf.
|
|
7
|
-
import { HostClient } from "@hecks/client";
|
|
7
|
+
import { DomainRefusal, HostClient, rowsOf } from "@hecks/client";
|
|
8
|
+
import type { Answer } from "@hecks/client";
|
|
8
9
|
|
|
9
10
|
import { EDITOR } from "./config.ts";
|
|
10
11
|
import { SCHEMA } from "./schema.ts";
|
|
@@ -13,6 +14,8 @@ export interface HostOptions {
|
|
|
13
14
|
/** The host's address, when not read from the environment variable the Editor row names. */
|
|
14
15
|
url?: string;
|
|
15
16
|
fetch?: typeof fetch;
|
|
17
|
+
/** The domain to address, when it is not the chapter the editor edits (pictures kept in another chapter). */
|
|
18
|
+
domain?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/** The host's base URL, without a trailing slash. */
|
|
@@ -22,5 +25,19 @@ export function hostUrl(options: HostOptions = {}): string {
|
|
|
22
25
|
|
|
23
26
|
/** A client for the domain this editor edits. */
|
|
24
27
|
export function hostClient(options: HostOptions = {}): HostClient {
|
|
25
|
-
return new HostClient({ domain: SCHEMA.domain, url: hostUrl(options), fetch: options.fetch, timeoutMs: EDITOR.timeoutMs });
|
|
28
|
+
return new HostClient({ domain: options.domain ?? SCHEMA.domain, url: hostUrl(options), fetch: options.fetch, timeoutMs: EDITOR.timeoutMs });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The rows a query answered. A host that has no such query for its running domain (a declared
|
|
33
|
+
* query the Rust host does not generate) answers with no entry at all, which `rowsOf` reads as
|
|
34
|
+
* null; that is a refusal here, not an empty list, or a list page, a picker or the picture
|
|
35
|
+
* listing would show nothing while the records are there.
|
|
36
|
+
*
|
|
37
|
+
* @throws {DomainRefusal} when the host refused the question or did not answer it
|
|
38
|
+
*/
|
|
39
|
+
export function rowsAnswered(answer: Answer, name: string): Record<string, unknown>[] {
|
|
40
|
+
const rows = rowsOf(answer, name);
|
|
41
|
+
if (rows === null) throw new DomainRefusal("QueryNotAnswered", `The host did not answer ${name}.`);
|
|
42
|
+
return rows as Record<string, unknown>[];
|
|
26
43
|
}
|
|
@@ -11,12 +11,15 @@ __BANNER__
|
|
|
11
11
|
// the bytes, and a client-supplied filename is never used.
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
|
|
14
|
-
import { DomainRefusal
|
|
14
|
+
import { DomainRefusal } from "@hecks/client";
|
|
15
15
|
import type { HostClient } from "@hecks/client";
|
|
16
16
|
|
|
17
17
|
import { instanceIn, leaf, runCommand } from "../commands.ts";
|
|
18
18
|
import type { State } from "../commands.ts";
|
|
19
19
|
import { EDITOR } from "../config.ts";
|
|
20
|
+
import { hostClient, rowsAnswered } from "../host.ts";
|
|
21
|
+
import type { HostOptions } from "../host.ts";
|
|
22
|
+
import { mayEdit, qualified } from "../names.ts";
|
|
20
23
|
import { SCHEMA } from "../schema.ts";
|
|
21
24
|
import type { Aggregate, Media, MediaField } from "../schema.ts";
|
|
22
25
|
import { boundaryOf, parts } from "./multipart.ts";
|
|
@@ -76,7 +79,14 @@ function picture(storage: MediaStorage, media: Media, state: State) {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function aggregateOf(media: Media): Aggregate {
|
|
79
|
-
|
|
82
|
+
const own = (candidate: Aggregate): boolean => candidate.name === media.aggregate && (candidate.chapter ?? SCHEMA.domain) === (media.domain ?? SCHEMA.domain);
|
|
83
|
+
return media.definition ?? (SCHEMA.aggregates.find(own) as Aggregate);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether a person in `role` may use the pictures: not when the chapter that holds them limits its roles to others. */
|
|
87
|
+
export function mediaOpen(role: string | null): boolean {
|
|
88
|
+
const media = SCHEMA.media as Media;
|
|
89
|
+
return mayEdit(aggregateOf(media), role);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/** What was uploaded, or the refusal to answer with. */
|
|
@@ -113,9 +123,16 @@ async function upload(client: HostClient, storage: MediaStorage, request: Reques
|
|
|
113
123
|
|
|
114
124
|
async function stateOf(client: HostClient, media: Media): Promise<State[]> {
|
|
115
125
|
const agg = aggregateOf(media);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const everything = async () => client.instancesOf(await client.read(), qualified(agg)).map(([, state]) => state);
|
|
127
|
+
if (!media.listing) return everything();
|
|
128
|
+
const name = `${qualified(agg)}.${media.listing}`;
|
|
129
|
+
try {
|
|
130
|
+
return rowsAnswered(await client.query(name, {}), name) as State[];
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// The listing only narrows what is offered; a host that does not answer it still holds the pictures.
|
|
133
|
+
if (err instanceof DomainRefusal) return everything();
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
async function listing(client: HostClient, storage: MediaStorage, media: Media): Promise<Response> {
|
|
@@ -142,10 +159,16 @@ async function serve(storage: MediaStorage, key: string): Promise<Response> {
|
|
|
142
159
|
});
|
|
143
160
|
}
|
|
144
161
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
162
|
+
/**
|
|
163
|
+
* The handler of everything under `<base>/media`, given the path segments after `media`. When the
|
|
164
|
+
* pictures are kept in another chapter (`media.domain`), they are read and registered through a
|
|
165
|
+
* client for that chapter, not the editor's own.
|
|
166
|
+
*/
|
|
167
|
+
export function mediaHandler(storage: MediaStorage = diskStorage(), options: HostOptions = {}) {
|
|
147
168
|
const media = SCHEMA.media as Media;
|
|
148
|
-
|
|
169
|
+
const elsewhere = media.domain ? hostClient({ ...options, domain: media.domain }) : null;
|
|
170
|
+
return (editors: HostClient, request: Request, rest: string[]): Promise<Response> | Response => {
|
|
171
|
+
const client = elsewhere ?? editors;
|
|
149
172
|
if (rest.length === 0 && request.method === "POST") return upload(client, storage, request, media);
|
|
150
173
|
if (rest.length === 0 && request.method === "GET") return listing(client, storage, media);
|
|
151
174
|
if (rest.length === 1 && request.method === "GET") return serve(storage, rest[0]);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// How an aggregate is named in the three places it has a name: its address in the editor, the name
|
|
4
|
+
// the host knows it by, and the key the navigation and the counts hold it under. An editor of one
|
|
5
|
+
// chapter names an aggregate by its own name in all three. An editor of several chapters names it by
|
|
6
|
+
// its chapter as well, so two aggregates of one name in different chapters stay two.
|
|
7
|
+
import { SCHEMA } from "./schema.ts";
|
|
8
|
+
import type { Aggregate } from "./schema.ts";
|
|
9
|
+
|
|
10
|
+
/** The path segments that address an aggregate under the editor's base path. */
|
|
11
|
+
export const at = (agg: Aggregate): string[] => (agg.chapter ? [agg.chapter, agg.name] : [agg.name]);
|
|
12
|
+
|
|
13
|
+
/** The name the host knows an aggregate by: `Chapter::Aggregate` when the editor spans chapters. */
|
|
14
|
+
export const qualified = (agg: Aggregate): string => (agg.chapter ? `${agg.chapter}::${agg.name}` : agg.name);
|
|
15
|
+
|
|
16
|
+
/** What the navigation and the counts hold an aggregate under. */
|
|
17
|
+
export const keyOf = (agg: Aggregate): string => (agg.chapter ? `${agg.chapter}.${agg.name}` : agg.name);
|
|
18
|
+
|
|
19
|
+
/** The host's name for an aggregate of `chapter` (this editor's first chapter when none is named). */
|
|
20
|
+
export const hostName = (name: string, chapter?: string): string => `${chapter ?? SCHEMA.domain}::${name}`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The aggregate a path names, and the segments after it. With several chapters the path is
|
|
24
|
+
* `<Chapter>/<Aggregate>/...`, and with one it is `<Aggregate>/...`.
|
|
25
|
+
*/
|
|
26
|
+
export function locate(segments: string[]): { agg: Aggregate | undefined; rest: string[] } {
|
|
27
|
+
if (SCHEMA.chapters) {
|
|
28
|
+
const [chapter, name, ...rest] = segments;
|
|
29
|
+
return { agg: SCHEMA.aggregates.find((candidate) => candidate.chapter === chapter && candidate.name === name), rest };
|
|
30
|
+
}
|
|
31
|
+
const [name, ...rest] = segments;
|
|
32
|
+
return { agg: SCHEMA.aggregates.find((candidate) => candidate.name === name), rest };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether a person in `role` may edit `agg`: every role may, unless its chapter limits them. */
|
|
36
|
+
export const mayEdit = (agg: Aggregate, role: string | null): boolean => !agg.roles || (role !== null && agg.roles.includes(role));
|
|
@@ -7,6 +7,44 @@ __BANNER__
|
|
|
7
7
|
/** How an attribute's value is entered: `object` is a value object, the rest are single values. */
|
|
8
8
|
export type Kind = "text" | "integer" | "number" | "boolean" | "object" | "reference";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* What an attribute names: an instance of another aggregate, offered as a choice. `strict` pickers
|
|
12
|
+
* accept only a key that exists; the others suggest and accept any text. `listing` is the query of
|
|
13
|
+
* the target that lists what is on offer, or none to offer every instance. With `kinds`, the value is
|
|
14
|
+
* `<kind>:<key>`: the kind is chosen first, then the record of the aggregate that kind names.
|
|
15
|
+
*/
|
|
16
|
+
export interface Picker {
|
|
17
|
+
target?: string;
|
|
18
|
+
/** The target's chapter, when the editor spans several or the target is in another. */
|
|
19
|
+
chapter?: string;
|
|
20
|
+
/** The target's identity attribute. */
|
|
21
|
+
key?: string;
|
|
22
|
+
/** The attribute that names an instance to a person (its title, name or label, else its identity). */
|
|
23
|
+
label?: string;
|
|
24
|
+
listing?: string;
|
|
25
|
+
strict?: boolean;
|
|
26
|
+
kinds?: PickerKind[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One kind of a `<kind>:<key>` value, and the aggregate it names when it names one. */
|
|
30
|
+
export type PickerKind = Omit<Picker, "kinds"> & { kind: string };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A list of mixed blocks: a list of one value object whose `discriminator` part is one of `kinds`. With
|
|
34
|
+
* `rules`, each kind's `requires` (parts it must have) and `uses` (parts it may have) come from a
|
|
35
|
+
* declared table; a part no row names belongs to every kind. With none, every part is shown and
|
|
36
|
+
* `mentions` says which parts a kind's rules read. `limits` are the most each list part of the block
|
|
37
|
+
* may hold; `pictures` says, for a value object, which of its parts are a picture's key, alt text and caption.
|
|
38
|
+
*/
|
|
39
|
+
export interface BlockList {
|
|
40
|
+
discriminator: string;
|
|
41
|
+
kinds: string[];
|
|
42
|
+
rules?: Record<string, { requires: string[]; uses: string[] }>;
|
|
43
|
+
mentions?: Record<string, string[]>;
|
|
44
|
+
limits?: Record<string, number>;
|
|
45
|
+
pictures?: Record<string, { key: string; alt: string; caption?: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
export interface Attr {
|
|
11
49
|
name: string;
|
|
12
50
|
/** The type as the bluebook spells it ("String", "Byline"). */
|
|
@@ -15,10 +53,29 @@ export interface Attr {
|
|
|
15
53
|
optional: boolean;
|
|
16
54
|
/** True for `list_of`: the attribute holds a list of `type`. */
|
|
17
55
|
list: boolean;
|
|
18
|
-
/**
|
|
19
|
-
|
|
56
|
+
/**
|
|
57
|
+
* `body` for an attribute whose value object has the shape of a structured document (blocks of
|
|
58
|
+
* spans); `date` or `datetime` for whole seconds since 1970 that the names say are a moment.
|
|
59
|
+
*/
|
|
60
|
+
widget?: "body" | "date" | "datetime";
|
|
61
|
+
/** Set when the attribute names an instance of another aggregate of the editor. */
|
|
62
|
+
picker?: Picker;
|
|
63
|
+
/** The members of the closed set the attribute is one of, when its value object declares one. */
|
|
64
|
+
options?: string[];
|
|
20
65
|
/** The aggregate a `reference` points at. */
|
|
21
66
|
target?: string;
|
|
67
|
+
/** Set when the attribute is a list of mixed blocks (see `BlockList`). */
|
|
68
|
+
blockList?: BlockList;
|
|
69
|
+
/** The most rows the list may hold, when a rule of its holder (an invariant or a command's `given`) says. */
|
|
70
|
+
max?: number;
|
|
71
|
+
/** The fewest rows the list may hold, when a rule of its holder says. */
|
|
72
|
+
min?: number;
|
|
73
|
+
/** The most characters a text may hold, when a rule of its holder says. */
|
|
74
|
+
maxLength?: number;
|
|
75
|
+
/** True when `maxLength` is long enough for the text to be written in a box of several lines. */
|
|
76
|
+
multiline?: boolean;
|
|
77
|
+
/** Set when the value object is a picture: the names of its parts that hold the file, its description and its caption. */
|
|
78
|
+
picture?: { key: string; alt: string; caption?: string };
|
|
22
79
|
}
|
|
23
80
|
|
|
24
81
|
export interface Transition {
|
|
@@ -31,6 +88,8 @@ export interface Lifecycle {
|
|
|
31
88
|
field: string;
|
|
32
89
|
default: string;
|
|
33
90
|
transitions: Transition[];
|
|
91
|
+
/** Each state's badge tone: `neutral`, `ok`, `info`, `warn` or `danger`. */
|
|
92
|
+
tones: Record<string, string>;
|
|
34
93
|
}
|
|
35
94
|
|
|
36
95
|
export interface Command {
|
|
@@ -41,7 +100,11 @@ export interface Command {
|
|
|
41
100
|
creates: boolean;
|
|
42
101
|
/** The aggregate the command acts on, when it is not a creating command. */
|
|
43
102
|
on: string | null;
|
|
103
|
+
/** True when the editor asks for confirmation before running it (see the generator's `Destructive`). */
|
|
104
|
+
destructive: boolean;
|
|
44
105
|
attributes: Attr[];
|
|
106
|
+
/** The arguments that only clear a field and are not offered as inputs: sent as empty lists. */
|
|
107
|
+
empty?: string[];
|
|
45
108
|
}
|
|
46
109
|
|
|
47
110
|
export interface Query {
|
|
@@ -50,8 +113,37 @@ export interface Query {
|
|
|
50
113
|
attributes: Attr[];
|
|
51
114
|
}
|
|
52
115
|
|
|
116
|
+
/**
|
|
117
|
+
* A record of another aggregate that goes with this one's records, because its identity is a
|
|
118
|
+
* `<kind>:<slug>` key of this aggregate. `shape` says how to show it; `create` and `edit` name the
|
|
119
|
+
* commands that make and change it, when it has them.
|
|
120
|
+
*/
|
|
121
|
+
export interface Related {
|
|
122
|
+
aggregate: string;
|
|
123
|
+
chapter?: string;
|
|
124
|
+
/** The related aggregate's identity attribute, which the key fills. */
|
|
125
|
+
field: string;
|
|
126
|
+
shape: "document" | "gallery" | "metadata";
|
|
127
|
+
create?: string;
|
|
128
|
+
edit?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** An aggregate that keeps a draft of its content beside the live one, and the commands that handle it. */
|
|
132
|
+
export interface Drafts {
|
|
133
|
+
/** The draft attribute (`draft_body`, `draft_content`) and the live one it stands for (`body`, `content`). */
|
|
134
|
+
attribute: string;
|
|
135
|
+
live: string;
|
|
136
|
+
save: string;
|
|
137
|
+
publish?: string;
|
|
138
|
+
discard?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
53
141
|
export interface Aggregate {
|
|
54
142
|
name: string;
|
|
143
|
+
/** The chapter that declares the aggregate; set when the editor spans several. */
|
|
144
|
+
chapter?: string;
|
|
145
|
+
/** The roles that may edit it, when its chapter limits them; any admitted role otherwise. */
|
|
146
|
+
roles?: string[];
|
|
55
147
|
description: string | null;
|
|
56
148
|
identity: string;
|
|
57
149
|
lifecycle: Lifecycle | null;
|
|
@@ -59,6 +151,10 @@ export interface Aggregate {
|
|
|
59
151
|
valueObjects: Record<string, Attr[]>;
|
|
60
152
|
commands: Command[];
|
|
61
153
|
queries: Query[];
|
|
154
|
+
/** The aggregates keyed by this one's records, shown as panels on a record's page. */
|
|
155
|
+
related?: Related[];
|
|
156
|
+
/** The draft the aggregate keeps beside its live content, when it keeps one. */
|
|
157
|
+
drafts?: Drafts;
|
|
62
158
|
}
|
|
63
159
|
|
|
64
160
|
/** How one part of a picture's record is sent: `wrap` is the part of a value object that holds the value. */
|
|
@@ -74,12 +170,45 @@ export interface Media {
|
|
|
74
170
|
fields: { key: MediaField; alt: MediaField; mime: MediaField; width?: MediaField; height?: MediaField };
|
|
75
171
|
/** The query that lists the pictures, or null when every instance is read. */
|
|
76
172
|
listing: string | null;
|
|
173
|
+
/** The chapter that holds the picture aggregate, when it is not this editor's own chapter. */
|
|
174
|
+
domain?: string;
|
|
175
|
+
/** That aggregate, which is then not among the editor's own; see `domain`. */
|
|
176
|
+
definition?: Aggregate;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The aggregate whose records are actions to run later, found by shape: the attribute that names the
|
|
181
|
+
* record an action is for, the closed set of actions, the due time, and the commands. The editor only
|
|
182
|
+
* makes and changes these records; something else runs them.
|
|
183
|
+
*/
|
|
184
|
+
export interface Scheduling {
|
|
185
|
+
aggregate: string;
|
|
186
|
+
chapter?: string;
|
|
187
|
+
subject: string;
|
|
188
|
+
action: string;
|
|
189
|
+
actions: string[];
|
|
190
|
+
due: string;
|
|
191
|
+
/** The first state of a record: an action not yet run. */
|
|
192
|
+
pending: string;
|
|
193
|
+
schedule: string;
|
|
194
|
+
reschedule?: string;
|
|
195
|
+
cancel?: string;
|
|
196
|
+
/** The query that lists the actions of one subject, when there is one. */
|
|
197
|
+
query?: string;
|
|
198
|
+
/** The optional text a record may carry to say why it ended as it did. */
|
|
199
|
+
reason?: string;
|
|
200
|
+
/** True when the editor makes the record's identity, since it is not the subject. */
|
|
201
|
+
generated?: boolean;
|
|
77
202
|
}
|
|
78
203
|
|
|
79
204
|
export interface Schema {
|
|
205
|
+
/** The first chapter, which names the domain the host is asked for. */
|
|
80
206
|
domain: string;
|
|
207
|
+
/** Every chapter, in the order the navigation groups them; set when the editor spans several. */
|
|
208
|
+
chapters?: string[];
|
|
81
209
|
aggregates: Aggregate[];
|
|
82
210
|
media?: Media;
|
|
211
|
+
scheduling?: Scheduling;
|
|
83
212
|
}
|
|
84
213
|
|
|
85
214
|
export const SCHEMA: Schema = __SCHEMA__;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// Which commands apply to a record now, and which of them need a page of their own.
|
|
4
|
+
import type { Aggregate, Command } from "../schema.ts";
|
|
5
|
+
|
|
6
|
+
type State = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/** Whether a command applies to an instance in its present state: a lifecycle move only from its states. */
|
|
9
|
+
export function available(agg: Aggregate, command: Command, state: State): boolean {
|
|
10
|
+
const move = agg.lifecycle?.transitions.find((transition) => transition.verb === command.name);
|
|
11
|
+
return !move || !agg.lifecycle || move.from.includes(String(state[agg.lifecycle.field]));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** What a command needs asked first: it takes input, or it is destructive. */
|
|
15
|
+
export const needsPage = (command: Command): boolean => command.attributes.length > 0 || command.destructive;
|