hecks 3.9.1 → 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 +124 -2
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
- data/lib/hecks/deploy/oidc.json +16 -3
- 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/clearing.rb +51 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -0
- 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/site_toolchain/role_prober.rb +78 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +29 -4
- data/lib/hecks/hecks/adapters/terminal.rb +13 -11
- data/lib/hecks/hecks/adapters/world_clearing.rb +65 -0
- 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 +61 -18
- data/lib/hecks/hecks/hecks.world +11 -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/fargate/settings/tuning.rb +15 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +3 -1
- data/lib/hecks/projections/deploy/fargate/stack/readers.rb +1 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/shared.rb +5 -2
- data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +7 -4
- 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 +95 -0
- data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +77 -0
- 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 +85 -0
- 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/pretty.rb +40 -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 +161 -0
- data/lib/hecks/projections/site/cms_editor/setting.rb +130 -0
- data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +370 -0
- 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 +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/session.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/sso.ts.tmpl +20 -0
- 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 +133 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +29 -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 +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +177 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/multipart.ts.tmpl +53 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/sniff.ts.tmpl +94 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/storage.ts.tmpl +50 -0
- 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 +214 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/server.ts.tmpl +65 -0
- 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 +123 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_parse.js.tmpl +320 -0
- 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 +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +61 -0
- 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 +92 -0
- 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 +63 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +213 -0
- 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 +17 -0
- 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 +133 -0
- data/lib/hecks/projections/site/role_probe.rb +74 -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 +29 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -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/cli.rb +1 -1
- data/lib/hecks/tools/site_routes/editor_files.rb +45 -0
- data/lib/hecks/tools/site_routes.rb +14 -7
- 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 +9 -1
- 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/bridging.rs +1 -1
- data/rust/codegen/src/commands.rs +13 -8
- data/rust/codegen/src/constraints.rs +25 -0
- data/rust/codegen/src/domain_generator.rs +11 -9
- data/rust/codegen/src/expr_emitter.rs +13 -0
- data/rust/codegen/src/json_codec.rs +54 -15
- data/rust/codegen/src/main.rs +2 -1
- data/rust/codegen/src/mutations.rs +174 -4
- data/rust/codegen/src/naming.rs +30 -0
- 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 +73 -4
- data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
- data/rust/src/kernel/expression_operators/presence.rs +41 -3
- 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 +174 -36
- 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/prebuilt/0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal +0 -0
- data/prebuilt/6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal +0 -0
- data/prebuilt/verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json +0 -1
|
@@ -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));
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The domain's shape, read from its bluebook: every aggregate with its attributes, value objects,
|
|
4
|
+
// lifecycle, commands and queries. The pages, the forms and the input parser all read this one
|
|
5
|
+
// constant, so the editor can edit any aggregate; none of it is written by hand.
|
|
6
|
+
|
|
7
|
+
/** How an attribute's value is entered: `object` is a value object, the rest are single values. */
|
|
8
|
+
export type Kind = "text" | "integer" | "number" | "boolean" | "object" | "reference";
|
|
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
|
+
|
|
48
|
+
export interface Attr {
|
|
49
|
+
name: string;
|
|
50
|
+
/** The type as the bluebook spells it ("String", "Byline"). */
|
|
51
|
+
type: string;
|
|
52
|
+
kind: Kind;
|
|
53
|
+
optional: boolean;
|
|
54
|
+
/** True for `list_of`: the attribute holds a list of `type`. */
|
|
55
|
+
list: boolean;
|
|
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[];
|
|
65
|
+
/** The aggregate a `reference` points at. */
|
|
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 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface Transition {
|
|
82
|
+
verb: string;
|
|
83
|
+
from: string[];
|
|
84
|
+
to: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface Lifecycle {
|
|
88
|
+
field: string;
|
|
89
|
+
default: string;
|
|
90
|
+
transitions: Transition[];
|
|
91
|
+
/** Each state's badge tone: `neutral`, `ok`, `info`, `warn` or `danger`. */
|
|
92
|
+
tones: Record<string, string>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface Command {
|
|
96
|
+
name: string;
|
|
97
|
+
goal: string | null;
|
|
98
|
+
role: string | null;
|
|
99
|
+
/** True when the command makes a new instance; false when it acts on one (`reference_to`). */
|
|
100
|
+
creates: boolean;
|
|
101
|
+
/** The aggregate the command acts on, when it is not a creating command. */
|
|
102
|
+
on: string | null;
|
|
103
|
+
/** True when the editor asks for confirmation before running it (see the generator's `Destructive`). */
|
|
104
|
+
destructive: boolean;
|
|
105
|
+
attributes: Attr[];
|
|
106
|
+
/** The arguments that only clear a field and are not offered as inputs: sent as empty lists. */
|
|
107
|
+
empty?: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface Query {
|
|
111
|
+
name: string;
|
|
112
|
+
description: string | null;
|
|
113
|
+
attributes: Attr[];
|
|
114
|
+
}
|
|
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
|
+
|
|
141
|
+
export interface Aggregate {
|
|
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[];
|
|
147
|
+
description: string | null;
|
|
148
|
+
identity: string;
|
|
149
|
+
lifecycle: Lifecycle | null;
|
|
150
|
+
attributes: Attr[];
|
|
151
|
+
valueObjects: Record<string, Attr[]>;
|
|
152
|
+
commands: Command[];
|
|
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;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** How one part of a picture's record is sent: `wrap` is the part of a value object that holds the value. */
|
|
161
|
+
export interface MediaField {
|
|
162
|
+
name: string;
|
|
163
|
+
wrap: string | null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The aggregate that registers pictures, when the chapter has one: found by shape, never by name. */
|
|
167
|
+
export interface Media {
|
|
168
|
+
aggregate: string;
|
|
169
|
+
command: string;
|
|
170
|
+
fields: { key: MediaField; alt: MediaField; mime: MediaField; width?: MediaField; height?: MediaField };
|
|
171
|
+
/** The query that lists the pictures, or null when every instance is read. */
|
|
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;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface Schema {
|
|
205
|
+
/** The first chapter, which names the domain the host is asked for. */
|
|
206
|
+
domain: string;
|
|
207
|
+
/** Every chapter, in the order the navigation groups them; set when the editor spans several. */
|
|
208
|
+
chapters?: string[];
|
|
209
|
+
aggregates: Aggregate[];
|
|
210
|
+
media?: Media;
|
|
211
|
+
scheduling?: Scheduling;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const SCHEMA: Schema = __SCHEMA__;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The editor's HTTP server: node's own `http` module turning each request into a web `Request` for
|
|
4
|
+
// the handler in app.ts and writing its `Response` back. No framework. Start it with `npm start`;
|
|
5
|
+
// it listens on `PORT` (default 4400) on `HOST` (default 127.0.0.1), behind the site's edge, which
|
|
6
|
+
// routes the editor's base path here.
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
+
|
|
10
|
+
import { createApp } from "./app.ts";
|
|
11
|
+
import { EDITOR } from "./config.ts";
|
|
12
|
+
|
|
13
|
+
const MAX_BODY_BYTES = 1_000_000;
|
|
14
|
+
|
|
15
|
+
/** The room a picture upload's form framing takes beyond the picture's own size cap. */
|
|
16
|
+
const UPLOAD_OVERHEAD_BYTES = 65_536;
|
|
17
|
+
|
|
18
|
+
/** How much of a request's body is read: a picture upload may be as large as the row's size cap. */
|
|
19
|
+
function limitOf(req: IncomingMessage): number {
|
|
20
|
+
const path = (req.url ?? "").split("?")[0];
|
|
21
|
+
return req.method === "POST" && path === `${EDITOR.basePath}/media` ? EDITOR.mediaMaxBytes + UPLOAD_OVERHEAD_BYTES : MAX_BODY_BYTES;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function bodyOf(req: IncomingMessage): Promise<Buffer | undefined> {
|
|
25
|
+
if (req.method === "GET" || req.method === "HEAD") return undefined;
|
|
26
|
+
const limit = limitOf(req);
|
|
27
|
+
const chunks: Buffer[] = [];
|
|
28
|
+
let size = 0;
|
|
29
|
+
for await (const chunk of req) {
|
|
30
|
+
size += chunk.length;
|
|
31
|
+
if (size > limit) throw new RangeError("request body too large");
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
}
|
|
34
|
+
return Buffer.concat(chunks);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function requestOf(req: IncomingMessage): Promise<Request> {
|
|
38
|
+
const proto = String(req.headers["x-forwarded-proto"] ?? "http").split(",")[0].trim();
|
|
39
|
+
const headers = new Headers();
|
|
40
|
+
for (const [name, value] of Object.entries(req.headers)) if (value !== undefined) headers.set(name, String(value));
|
|
41
|
+
return new Request(`${proto}://${req.headers.host ?? "localhost"}${req.url ?? "/"}`, { method: req.method, headers, body: (await bodyOf(req)) as BodyInit | undefined });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function send(res: ServerResponse, response: Response): Promise<void> {
|
|
45
|
+
const headers: Record<string, string | string[]> = {};
|
|
46
|
+
response.headers.forEach((value, name) => {
|
|
47
|
+
if (name !== "set-cookie") headers[name] = value;
|
|
48
|
+
});
|
|
49
|
+
const cookies = response.headers.getSetCookie();
|
|
50
|
+
if (cookies.length > 0) headers["set-cookie"] = cookies;
|
|
51
|
+
res.writeHead(response.status, headers);
|
|
52
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const app = createApp();
|
|
56
|
+
|
|
57
|
+
createServer(async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
await send(res, await app(await requestOf(req)));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(err);
|
|
62
|
+
res.writeHead(err instanceof RangeError ? 413 : 500, { "Content-Type": "text/plain" });
|
|
63
|
+
res.end(err instanceof RangeError ? "Request too large." : "The editor failed.");
|
|
64
|
+
}
|
|
65
|
+
}).listen(Number(process.env.PORT || 4400), process.env.HOST || "127.0.0.1");
|
|
@@ -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;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// The rules of a block list that need no page: which slots a kind shows, the one line that stands
|
|
4
|
+
// for a block, whether a block is complete, and which block a refusal is about. Plain JavaScript
|
|
5
|
+
// over plain data, used by the server that renders the cards and by the script that edits them, so
|
|
6
|
+
// both answer the same way. A "list" here is an attribute's `blockList` (see schema.ts), a "block" is
|
|
7
|
+
// one value of the list's value object, and a "part" is one of that value object's attributes.
|
|
8
|
+
|
|
9
|
+
/** The discriminator's value in `block`, whether it is held as the plain text or as a one-part value object. */
|
|
10
|
+
export function kindOf(list, block) {
|
|
11
|
+
const held = block?.[list.discriminator];
|
|
12
|
+
if (held && typeof held === "object") return String(Object.values(held)[0] ?? "");
|
|
13
|
+
return held === undefined || held === null ? "" : String(held);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const present = (value) => {
|
|
17
|
+
if (value === undefined || value === null || value === "") return false;
|
|
18
|
+
if (Array.isArray(value)) return value.some(present);
|
|
19
|
+
if (typeof value === "object") return Object.values(value).some(present);
|
|
20
|
+
return true;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Whether a part holds anything: text, a picture's key, a body with a block, a list with a row. */
|
|
24
|
+
export const filled = present;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The parts a kind shows, each with whether the kind needs it: the parts it needs first, then those it
|
|
28
|
+
* uses, then those common to every kind, each in the value object's own order.
|
|
29
|
+
* With a declared table (`rules`) these are the parts a row names, the parts no row names (common to
|
|
30
|
+
* every kind) and a picture's alt text with its key; with none, every part is shown, and a part that
|
|
31
|
+
* an invariant of the kind reads is marked as needed.
|
|
32
|
+
*/
|
|
33
|
+
export function slotsOf(list, parts, kind) {
|
|
34
|
+
const rule = list.rules?.[kind];
|
|
35
|
+
const named = new Set(Object.values(list.rules ?? {}).flatMap((row) => [...row.requires, ...row.uses]));
|
|
36
|
+
const mentioned = new Set(list.mentions?.[kind] ?? []);
|
|
37
|
+
const uses = new Set(rule ? [...rule.requires, ...rule.uses] : []);
|
|
38
|
+
const has = (name) => parts.some((part) => part.name === name);
|
|
39
|
+
const picture = Object.values(list.pictures ?? {}).find((one) => has(one.key) && has(one.alt));
|
|
40
|
+
if (picture && uses.has(picture.key)) uses.add(picture.alt);
|
|
41
|
+
return parts
|
|
42
|
+
.filter((part) => part.name !== list.discriminator)
|
|
43
|
+
.filter((part) => !rule || uses.has(part.name) || !named.has(part.name))
|
|
44
|
+
.map((part) => ({ part, required: rule ? rule.requires.includes(part.name) : false, mentioned: !rule && mentioned.has(part.name) }))
|
|
45
|
+
.map((slot, at) => ({ slot, at, rank: slot.required ? 0 : uses.has(slot.part.name) || slot.mentioned ? 1 : 2 }))
|
|
46
|
+
.sort((a, b) => a.rank - b.rank || a.at - b.at)
|
|
47
|
+
.map((one) => one.slot);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** "1 entry", "3 entries": a list part's name, as a count of its rows. */
|
|
51
|
+
const countOf = (name, count) => {
|
|
52
|
+
const words = name.replace(/_/g, " ");
|
|
53
|
+
return `${count} ${count === 1 ? words.replace(/([^aeiou])ies$/, "$1y").replace(/s$/, "") : words}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const textOf = (value) => (value && typeof value === "object" ? String(Object.values(value)[0] ?? "") : String(value ?? ""));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The one line that stands for a block on its card: its first required text, else its longest text.
|
|
60
|
+
* A card that is itself a picture (`pictureOf`) in a list of one shape says the picture's description.
|
|
61
|
+
*/
|
|
62
|
+
export function summaryOf(list, parts, block, pictureOf) {
|
|
63
|
+
const kind = kindOf(list, block);
|
|
64
|
+
const slots = slotsOf(list, parts, kind).filter((slot) => slot.part.kind !== "object" && !slot.part.list && !slot.part.options);
|
|
65
|
+
const texts = slots.map((slot) => ({ ...slot, text: textOf(block?.[slot.part.name]).trim() })).filter((slot) => slot.text !== "");
|
|
66
|
+
const said = list.kinds.length === 0 && pictureOf ? texts.find((slot) => slot.part.name === pictureOf.alt) : undefined;
|
|
67
|
+
const first = said ?? texts.find((slot) => slot.required) ?? texts.reduce((best, slot) => (!best || slot.text.length > best.text.length ? slot : best), null);
|
|
68
|
+
if (first) return first.text.length > 80 ? `${first.text.slice(0, 79)}…` : first.text;
|
|
69
|
+
const counts = parts.filter((part) => part.list && filled(block?.[part.name])).map((part) => countOf(part.name, block[part.name].length));
|
|
70
|
+
return counts.join(", ");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What a block still lacks, as `{ slot, message }` for each: a part its kind requires that is empty,
|
|
75
|
+
* and a picture's alt text when it has a key. This is what the form can know before the domain is
|
|
76
|
+
* asked; the domain's own rules still decide.
|
|
77
|
+
*/
|
|
78
|
+
export function lacking(list, parts, block, pictureOf) {
|
|
79
|
+
const kind = kindOf(list, block);
|
|
80
|
+
const needs = slotsOf(list, parts, kind).filter((slot) => slot.required && !filled(block?.[slot.part.name]));
|
|
81
|
+
const missing = needs.map((slot) => ({ slot: slot.part.name, message: `${slot.part.name.replace(/_/g, " ")} is needed` }));
|
|
82
|
+
const picture = pictureOf?.key;
|
|
83
|
+
const alt = pictureOf?.alt;
|
|
84
|
+
if (picture && alt && filled(block?.[picture]) && !filled(block?.[alt])) missing.push({ slot: alt, message: "Describe the picture for people who cannot see it" });
|
|
85
|
+
return missing;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const normal = (value) => {
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
const items = value.map(normal).filter((item) => item !== undefined);
|
|
91
|
+
return items.length > 0 ? items : undefined;
|
|
92
|
+
}
|
|
93
|
+
if (value && typeof value === "object") {
|
|
94
|
+
const keys = Object.keys(value).sort();
|
|
95
|
+
if (keys.length === 1 && keys[0] === "value") return normal(value.value);
|
|
96
|
+
const made = Object.fromEntries(keys.map((key) => [key, normal(value[key])]).filter(([, held]) => held !== undefined));
|
|
97
|
+
return Object.keys(made).length > 0 ? made : undefined;
|
|
98
|
+
}
|
|
99
|
+
return value === null || value === "" || value === undefined ? undefined : value;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const sameShape = (a, b) => JSON.stringify(normal(a) ?? null) === JSON.stringify(normal(b) ?? null);
|
|
103
|
+
|
|
104
|
+
/** The words of a refusal: the value object it names, the rule's wording and the value it was given, when it echoes one. */
|
|
105
|
+
export function readRefusal(error) {
|
|
106
|
+
const found = /^(.+?) invariant violated — (.+?)(?: \(given (.*)\))?$/s.exec(String(error ?? ""));
|
|
107
|
+
if (!found) return null;
|
|
108
|
+
let given = null;
|
|
109
|
+
try {
|
|
110
|
+
given = found[3] ? JSON.parse(found[3]) : null;
|
|
111
|
+
} catch {
|
|
112
|
+
given = null;
|
|
113
|
+
}
|
|
114
|
+
return { type: found[1], rule: found[2], given };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sentence = (rule) => `${rule.charAt(0).toUpperCase()}${rule.slice(1)}.`;
|
|
118
|
+
|
|
119
|
+
/** Whether a value object of `type` is held, at any depth, by the value object named `from`. */
|
|
120
|
+
function holds(valueObjects, from, type, depth = 0) {
|
|
121
|
+
if (depth > 6) return false;
|
|
122
|
+
return (valueObjects[from] ?? []).some((part) => part.type === type || holds(valueObjects, part.type, type, depth + 1));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Every place in `args` (the arguments of the attributes `attrs`) a value object of `type` sits, as
|
|
127
|
+
* `{ path, name, attr }`: its path in the arguments, the dotted name of its fields in the form, and the
|
|
128
|
+
* attribute that holds it. A body holds the value objects of its tree, so a refusal about a block of it
|
|
129
|
+
* is the body's.
|
|
130
|
+
*/
|
|
131
|
+
function locate(type, attrs, args, valueObjects, path = [], prefix = "", depth = 0) {
|
|
132
|
+
const found = [];
|
|
133
|
+
if (depth > 6) return found;
|
|
134
|
+
for (const attr of attrs.filter((candidate) => candidate.kind === "object")) {
|
|
135
|
+
const held = args?.[attr.name];
|
|
136
|
+
const items = attr.list ? (Array.isArray(held) ? held : []) : held === undefined || held === null ? [] : [held];
|
|
137
|
+
items.forEach((item, at) => {
|
|
138
|
+
const here = attr.list ? [...path, attr.name, at] : [...path, attr.name];
|
|
139
|
+
const name = `${prefix}${attr.name}${attr.list ? `.${at}` : ""}`;
|
|
140
|
+
const within = attr.widget === "body" && holds(valueObjects, attr.type, type);
|
|
141
|
+
if (attr.type === type || within) found.push({ path: here, name, attr });
|
|
142
|
+
if (attr.widget !== "body") found.push(...locate(type, valueObjects[attr.type] ?? [], item, valueObjects, here, `${name}.`, depth + 1));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pick = (args, path) => path.reduce((value, step) => value?.[step], args);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The value objects a refused command's error is about, as `{ path, name, slots, message }` (`name` is
|
|
152
|
+
* the dotted name of the value object's fields in the form). The error is matched to the submitted
|
|
153
|
+
* value it echoes; with no echo it is the only value object of its type, else the blocks of a list
|
|
154
|
+
* whose kind the rule's wording names. A refusal that matches none gives nothing, and the page shows
|
|
155
|
+
* it as a whole. `slots` are the parts the value object lacks that it requires, else the parts the
|
|
156
|
+
* rule names.
|
|
157
|
+
*/
|
|
158
|
+
export function problemsOf(error, attrs, args, valueObjects) {
|
|
159
|
+
const read = readRefusal(error);
|
|
160
|
+
if (!read) return [];
|
|
161
|
+
const where = locate(read.type, attrs, args, valueObjects);
|
|
162
|
+
const exact = read.given ? where.filter((place) => sameShape(pick(args, place.path), read.given)) : [];
|
|
163
|
+
const worded = where.filter((place) => {
|
|
164
|
+
const kind = place.attr.blockList ? String(kindOf(place.attr.blockList, pick(args, place.path))) : "";
|
|
165
|
+
return kind !== "" && read.rule.includes(kind.replace(/_/g, " "));
|
|
166
|
+
});
|
|
167
|
+
const alone = !read.given && where.length === 1 ? where : [];
|
|
168
|
+
const chosen = [exact, worded, alone].find((list) => list.length > 0) ?? [];
|
|
169
|
+
return chosen.map((place) => ({ path: place.path, name: place.name, slots: slotsNamed(read, place, args, valueObjects), message: sentence(read.rule) }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The parts of `parts` that a value object must have and has not (and a picture's description when it has the file). */
|
|
173
|
+
function unmet(parts, block, picture) {
|
|
174
|
+
const missing = parts.filter((part) => !part.optional && !filled(block?.[part.name])).map((part) => part.name);
|
|
175
|
+
if (picture && filled(block?.[picture.key]) && !filled(block?.[picture.alt]) && !missing.includes(picture.alt)) missing.push(picture.alt);
|
|
176
|
+
return missing;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function slotsNamed(read, place, args, valueObjects) {
|
|
180
|
+
const parts = valueObjects[place.attr.type] ?? [];
|
|
181
|
+
const block = pick(args, place.path);
|
|
182
|
+
const list = place.attr.blockList;
|
|
183
|
+
const empty = list ? lacking(list, parts, block, list.pictures?.[place.attr.type]).map((one) => one.slot) : unmet(parts, block, place.attr.picture);
|
|
184
|
+
if (empty.length > 0) return empty;
|
|
185
|
+
return parts.filter((part) => new RegExp(`\\b${part.name.replace(/_/g, " ")}\\b`).test(read.rule)).map((part) => part.name);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether a member of a list holds anything a script has to make live: a rich-text body or a picture,
|
|
190
|
+
* at any depth (a list of its own among them). Such a member cannot be copied as plain inputs, so it is
|
|
191
|
+
* drawn as a card made from a template; a member of plain inputs, with rows of its own or none, stays
|
|
192
|
+
* a row.
|
|
193
|
+
*/
|
|
194
|
+
export function heavy(valueObjects, type, depth = 0) {
|
|
195
|
+
if (depth > 6) return false;
|
|
196
|
+
return (valueObjects[type] ?? []).some(
|
|
197
|
+
(part) => part.widget === "body" || part.picture !== undefined || (part.kind === "object" && heavy(valueObjects, part.type, depth + 1)),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** A name as an id spells it: each run of anything but letters and digits is one hyphen. */
|
|
202
|
+
export const safe = (text) => text.replace(/[^A-Za-z0-9]+/g, "-");
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* `text` with the index of the card at `path` changed from `was` to `at`, in the dotted spelling of a
|
|
206
|
+
* field's name (`list.3.heading`) and in the spelling of an id made from it (`f-list-3-heading`), and
|
|
207
|
+
* in what is nested in the card at any depth.
|
|
208
|
+
*/
|
|
209
|
+
export function retarget(text, path, was, at) {
|
|
210
|
+
const dotted = text.split(`${path}.${was}.`).join(`${path}.${at}.`);
|
|
211
|
+
const tail = new RegExp(`(?<![A-Za-z0-9])${safe(path)}-${was}(?![0-9])`, "g");
|
|
212
|
+
return dotted.replace(tail, `${safe(path)}-${at}`);
|
|
213
|
+
}
|