hecks 3.10.0 → 3.12.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 +158 -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 +70 -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/newcomer.rb +50 -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/read_models.rb +1 -1
- 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 +4 -15
- data/lib/hecks/bluebook/assembly/scalars.rb +39 -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 +24 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
- 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 +2 -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 +34 -4
- 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 +33 -7
- 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 +133 -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 +13 -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.behaviors +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.behaviors +19 -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 +63 -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/projection_files/expression_tables.rb +7 -2
- data/lib/hecks/projections/deploy/box/compose.rb +6 -4
- data/lib/hecks/projections/deploy/box/edge.rb +62 -0
- data/lib/hecks/projections/deploy/box/logging.rb +99 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
- data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
- data/lib/hecks/projections/deploy/box/settings.rb +6 -2
- data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +32 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
- data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
- data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box.rb +15 -3
- 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/projections/site/site_root/workflow.rb +5 -0
- data/lib/hecks/projector/cli_audience.rb +22 -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/cli_projector/usage.rb +15 -4
- 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/host.rb +11 -4
- data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
- 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/ci_gate_decision.rb +35 -0
- data/lib/hecks/tools/ci_gates.rb +20 -1
- 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/files.rb +8 -0
- data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
- data/lib/hecks/tools/lanes/rows.rb +6 -0
- data/lib/hecks/tools/lanes.rb +1 -0
- data/lib/hecks/tools/oidc_manifests.rb +4 -5
- data/lib/hecks/tools/regeneration_run.rb +11 -4
- data/lib/hecks/tools/release_only.rb +111 -0
- 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 +19 -5
- 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/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
- data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
- data/prebuilt/verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.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/extension.rs +13 -1
- data/rust/host/src/lib.rs +1 -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/platform_signin/attack_tests.rs +1100 -0
- data/rust/host/src/platform_signin/config.rs +512 -0
- data/rust/host/src/platform_signin/cookie.rs +237 -0
- data/rust/host/src/platform_signin/extension.rs +190 -0
- data/rust/host/src/platform_signin/fetcher.rs +322 -0
- data/rust/host/src/platform_signin/handler.rs +251 -0
- data/rust/host/src/platform_signin/handler_tests.rs +554 -0
- data/rust/host/src/platform_signin/jti.rs +196 -0
- data/rust/host/src/platform_signin/jwks.rs +542 -0
- data/rust/host/src/platform_signin/jws.rs +249 -0
- data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
- data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
- data/rust/host/src/platform_signin/mod.rs +75 -0
- data/rust/host/src/platform_signin/mode_tests.rs +382 -0
- data/rust/host/src/platform_signin/pages.rs +204 -0
- data/rust/host/src/platform_signin/query.rs +233 -0
- data/rust/host/src/platform_signin/session.rs +151 -0
- data/rust/host/src/platform_signin/settings.rs +385 -0
- data/rust/host/src/platform_signin/strict_json.rs +351 -0
- data/rust/host/src/platform_signin/test_support.rs +235 -0
- data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
- data/rust/host/src/platform_signin/verify.rs +604 -0
- data/rust/host/src/platform_signin/wiring_support.rs +319 -0
- 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 +53 -9
- 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 +34 -17
- 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 +178 -38
- data/lib/hecks/doors/cli_door/nesting.rb +0 -34
- data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
- data/lib/hecks/doors/cli_door.rb +0 -124
- data/lib/hecks/doors/cli_runner/answers.rb +0 -138
- data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
- data/lib/hecks/doors/cli_runner/settling.rb +0 -115
- data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
- data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
- data/lib/hecks/doors/cli_runner.rb +0 -66
- data/lib/hecks/doors/command_request.rb +0 -119
- data/lib/hecks/doors/handle.rb +0 -208
- data/lib/hecks/doors/json_door.rb +0 -132
- data/lib/hecks/doors/launcher_options.rb +0 -247
- data/lib/hecks/doors/mcp_door/properties.rb +0 -47
- data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
- data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
- data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
- data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
- data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
- data/lib/hecks/doors/mcp_door.rb +0 -156
- data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
- data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
- data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
- data/lib/hecks/doors/mcp_door_scope.rb +0 -201
- data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
- data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
- data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
- data/lib/hecks/doors/ruby_door.rb +0 -80
- data/lib/hecks/doors/usage_cache.rb +0 -139
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_widget.js.tmpl +0 -282
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/media_picker.js.tmpl +0 -109
- data/prebuilt/c28d6d9db14eea3e17161e9d9c262efee30b256cf97693a7cfd552bce24b2f7f.marshal +0 -0
- data/prebuilt/cb375cba61ca0dbe8ddb9739c306510fe05e5f4b7a9b6955caff8224c63ea0e0.marshal +0 -0
- data/prebuilt/verdicts-d6d584ad43b5f3cebf88aafbbdbf024c38574acf18e06adffbc555f7786b1c2f.json +0 -1
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// A list of mixed blocks (or of one kind of group, with no kind) as an ordered list of cards. Each
|
|
4
|
+
// card is one block: its kind as a badge, one line that says what it holds, tools to move it up
|
|
5
|
+
// and down and remove it, and, when open, only the slots its kind uses (see block_list.js). The
|
|
6
|
+
// list is plain markup that posts the same dotted-path fields every value object does, so a
|
|
7
|
+
// browser with no script still edits what it has; browser/blocks.js adds adding, removing,
|
|
8
|
+
// reordering and opening cards. A card holds its repeated parts as groups of cards one level in.
|
|
9
|
+
//
|
|
10
|
+
// A new card is made from a `<template>` the page carries per kind, written with the index left
|
|
11
|
+
// as `ZZI0ZZ` (and `ZZI1ZZ` one level in), so a new card is what the server would have drawn.
|
|
12
|
+
// A group's members show all their slots, each required as its value object declares, within the
|
|
13
|
+
// limits the holder's rules give (see `Limits` in the generator).
|
|
14
|
+
import type { Aggregate, Attr, BlockList } from "../schema.ts";
|
|
15
|
+
import { esc } from "./html.ts";
|
|
16
|
+
import { icon } from "./icons.ts";
|
|
17
|
+
import { filled, kindOf, lacking, slotsOf, summaryOf } from "./block_list.js";
|
|
18
|
+
import { label, limitsNote, pluralOf, singular } from "./words.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A refused value object: where it is in the command's arguments (`path`) and in the form (`name`, the
|
|
22
|
+
* dotted name its fields share: `content.items.3` or `content.items.3.parts.1`), which of its
|
|
23
|
+
* parts are at fault, and why.
|
|
24
|
+
*/
|
|
25
|
+
export interface Problem {
|
|
26
|
+
path: (string | number)[];
|
|
27
|
+
name: string;
|
|
28
|
+
slots: string[];
|
|
29
|
+
message: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What the list needs from the form around it. */
|
|
33
|
+
export interface Rendering {
|
|
34
|
+
/** One part's control: its markup at `prefix + part.name`, flagged with `message` when it is the one a refusal names. */
|
|
35
|
+
slot: (part: Attr, prefix: string, value: unknown, message?: string, required?: boolean, rows?: boolean) => string;
|
|
36
|
+
/** Whether the list saves a draft by itself (see browser/autosave.js). */
|
|
37
|
+
draft: boolean;
|
|
38
|
+
/** The blocks a refused command named. */
|
|
39
|
+
problems: Problem[];
|
|
40
|
+
/** The one-line caption of the list: its label, with a mark when it is required. */
|
|
41
|
+
caption: string;
|
|
42
|
+
/** Whether a picture picker is on offer (`data-media`'s address), else the key is typed. */
|
|
43
|
+
media?: string;
|
|
44
|
+
/** What the domain said about the list as a whole (its size), when it did. */
|
|
45
|
+
message?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Block = Record<string, unknown>;
|
|
49
|
+
|
|
50
|
+
/** A part of a block as one kind shows it (see `slotsOf`). */
|
|
51
|
+
interface Slot {
|
|
52
|
+
part: Attr;
|
|
53
|
+
required: boolean;
|
|
54
|
+
mentioned: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The index placeholders a card's template is written with, by how deep the card is. */
|
|
58
|
+
const TOKEN = ["ZZI0ZZ", "ZZI1ZZ"];
|
|
59
|
+
|
|
60
|
+
const safe = (text: string): string => text.replace(/[^A-Za-z0-9]+/g, "-");
|
|
61
|
+
|
|
62
|
+
/** The control of the part that holds the kind: the kind as a hidden input, in the value object's own shape. */
|
|
63
|
+
function kindInput(agg: Aggregate, list: BlockList, parts: Attr[], prefix: string, kind: string): string {
|
|
64
|
+
const part = parts.find((candidate) => candidate.name === list.discriminator);
|
|
65
|
+
const inner = part?.kind === "object" ? (agg.valueObjects[part.type] ?? [])[0]?.name : undefined;
|
|
66
|
+
const name = `${prefix}${list.discriminator}${inner ? `.${inner}` : ""}`;
|
|
67
|
+
return `<input type="hidden" name="${esc(name)}" value="${esc(kind)}" data-kind-input>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tool = (action: string, glyph: string, text: string, what: string): string =>
|
|
71
|
+
`<button type="button" class="btn btn-ghost btn-square btn-sm" data-action="${action}" aria-label="${esc(`${text} ${what}`)}" title="${esc(`${text} ${what}`)}">${icon(glyph)}</button>`;
|
|
72
|
+
|
|
73
|
+
/** The tools of a card: shown by the script, which is what makes them work. */
|
|
74
|
+
function tools(noun: string, number: number | string, summary: string): string {
|
|
75
|
+
const what = `${noun} ${number}${summary ? `, ${summary}` : ""}`;
|
|
76
|
+
const move = [tool("up", "arrow-up", "Move up", what), tool("down", "arrow-down", "Move down", what)];
|
|
77
|
+
return `<div class="flex shrink-0 items-center" data-tools hidden>${[...move, tool("remove", "trash", "Remove", what)].join("")}</div>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface Place {
|
|
81
|
+
/** The list's name, then the index, as the path of the card's fields. */
|
|
82
|
+
path: string;
|
|
83
|
+
index: number | string;
|
|
84
|
+
depth: number;
|
|
85
|
+
noun: string;
|
|
86
|
+
/** The value object a card of this place holds one of. */
|
|
87
|
+
type: string;
|
|
88
|
+
list: BlockList;
|
|
89
|
+
parts: Attr[];
|
|
90
|
+
/** True in a list of one kind of group: what a part is required to be is what its value object declares. */
|
|
91
|
+
declared: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function badge(kind: string | null): string {
|
|
95
|
+
return kind ? `<span class="badge badge-soft badge-neutral shrink-0 font-semibold" data-kind-badge>${esc(label(kind))}</span>` : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The slots of a card, each as its control, with a picture's parts and a repeated part drawn as groups. */
|
|
99
|
+
function slotsOfCard(agg: Aggregate, place: Place, rendering: Rendering, block: Block, kind: string | null, flagged: Problem | undefined): string {
|
|
100
|
+
const prefix = `${place.path}.${place.index}.`;
|
|
101
|
+
const picture = place.list.pictures?.[place.type];
|
|
102
|
+
const plain = (part: Attr): Slot => ({ part, required: place.declared && !part.optional, mentioned: false });
|
|
103
|
+
const shown: Slot[] = kind === null ? place.parts.map(plain) : (slotsOf(place.list, place.parts, kind) as Slot[]);
|
|
104
|
+
const held = shown.filter((slot) => !(picture && (slot.part.name === picture.alt || slot.part.name === picture.caption)));
|
|
105
|
+
const draw = held.map(({ part, required, mentioned }) => {
|
|
106
|
+
const note = mentioned ? `<p class="-mt-3 mb-3 text-xs text-muted">A rule of this kind of ${esc(singular(place.noun))} reads this.</p>` : "";
|
|
107
|
+
const message = flagged?.slots.includes(part.name) ? flagged.message : undefined;
|
|
108
|
+
if (picture && part.name === picture.key) return pictureGroup(agg, place, rendering, block, picture, flagged, prefix);
|
|
109
|
+
if (place.depth === 0 && part.list && part.kind === "object") return group(agg, place, part, rendering, block, prefix, message);
|
|
110
|
+
const rows = place.depth >= 1 && part.list && part.kind === "object";
|
|
111
|
+
return `${rendering.slot(part, prefix, block[part.name], message, required, rows)}${note}`;
|
|
112
|
+
});
|
|
113
|
+
return draw.join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A picture's parts as one group: the key and the alt text and a button that opens the picker. */
|
|
117
|
+
function pictureGroup(agg: Aggregate, place: Place, rendering: Rendering, block: Block, picture: { key: string; alt: string; caption?: string }, flagged: Problem | undefined, prefix: string): string {
|
|
118
|
+
const find = (name: string): Attr | undefined => place.parts.find((part) => part.name === name);
|
|
119
|
+
const key = find(picture.key);
|
|
120
|
+
const alt = find(picture.alt);
|
|
121
|
+
const caption = picture.caption ? find(picture.caption) : undefined;
|
|
122
|
+
const control = (part: Attr | undefined): string => (part ? rendering.slot(part, prefix, block[part.name], flagged?.slots.includes(part.name) ? flagged.message : undefined) : "");
|
|
123
|
+
const pick = rendering.media ? `<button type="button" class="btn btn-sm w-fit" data-action="pick-picture" data-media="${esc(rendering.media)}">${icon("image")}<span>Choose a picture</span></button>` : "";
|
|
124
|
+
const thumb = `<img class="size-20 shrink-0 border border-base-300 object-cover" alt="" width="80" height="80" data-picture-thumb hidden>`;
|
|
125
|
+
return `<div class="my-4 grid gap-1 border border-base-300 p-3" data-picture data-key="${esc(prefix + picture.key)}" data-alt="${esc(prefix + picture.alt)}">
|
|
126
|
+
<p class="font-semibold">Picture</p>
|
|
127
|
+
<div class="flex flex-wrap items-start gap-3">${thumb}<div class="grid min-w-0 flex-1 gap-1 max-sm:basis-full">${control(key)}${control(alt)}${control(caption)}${pick}</div></div>
|
|
128
|
+
<p class="text-xs text-muted">A picture needs a description for people who cannot see it.</p>
|
|
129
|
+
</div>`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One card: its head and, in the panel, its slots. `kind` is null for a block with no kind (a repeated part). */
|
|
133
|
+
function card(agg: Aggregate, place: Place, rendering: Rendering, block: Block, kind: string | null): string {
|
|
134
|
+
const id = `blk-${safe(place.path)}-${place.index}`;
|
|
135
|
+
const flagged = rendering.problems.find((problem) => problem.name === `${place.path}.${place.index}`);
|
|
136
|
+
const picture = place.list.pictures?.[place.type];
|
|
137
|
+
const summary = kind === null && !place.list ? "" : String(summaryOf(place.list, place.parts, block, picture) ?? "");
|
|
138
|
+
const holds = rendering.problems.some((problem) => problem.name.startsWith(`${place.path}.${place.index}.`));
|
|
139
|
+
const open = flagged || holds ? "" : " hidden";
|
|
140
|
+
const attention = flagged ? `<span class="badge badge-soft badge-error shrink-0 gap-1 font-semibold">${icon("alert")}Needs attention</span>` : "";
|
|
141
|
+
const problem = flagged && flagged.slots.length === 0 ? `<p class="text-sm font-semibold text-error" id="${id}-problem">${esc(flagged.message)}</p>` : "";
|
|
142
|
+
const describe = flagged && flagged.slots.length === 0 ? ` aria-describedby="${id}-problem"` : "";
|
|
143
|
+
const number = typeof place.index === "number" ? place.index + 1 : place.index;
|
|
144
|
+
const kindField = kind === null ? "" : kindInput(agg, place.list, place.parts, `${place.path}.${place.index}.`, kind);
|
|
145
|
+
const toggle = `<button type="button" class="btn btn-ghost h-auto min-h-11 min-w-0 flex-1 flex-wrap justify-start gap-x-2 gap-y-0 py-2 text-left font-normal" data-action="toggle" aria-expanded="${flagged || holds ? "true" : "false"}" aria-controls="${id}-panel"${describe} hidden>${icon("chevron-right")}<span class="shrink-0 font-semibold" data-card-title>${esc(label(place.noun))} <span data-card-number>${number}</span></span>${badge(kind)}<span class="min-w-0 flex-1 truncate text-muted max-sm:basis-full max-sm:pl-7" data-card-summary>${esc(summary)}</span>${attention}</button>`;
|
|
146
|
+
const plain = `<span class="flex min-w-0 flex-1 flex-wrap items-center gap-2 py-2" data-card-static><span class="font-semibold">${esc(label(place.noun))} ${number}</span>${badge(kind)}<span class="min-w-0 text-muted">${esc(summary)}</span>${attention}</span>`;
|
|
147
|
+
const heading = `<h${place.depth + 3} class="m-0 flex min-w-0 flex-1 text-base">${plain}${toggle}</h${place.depth + 3}>`;
|
|
148
|
+
const valid = `${flagged ? ` data-invalid` : ""}${holds ? ` data-open` : ""}`;
|
|
149
|
+
const required = kind === null ? (place.declared ? place.parts.filter((part) => !part.optional && !part.list) : []) : [];
|
|
150
|
+
const requires = kind === null ? required.map((part) => part.name).join(" ") : (place.list.rules?.[kind]?.requires ?? []).join(" ");
|
|
151
|
+
const needs = requires ? ` data-requires="${esc(requires)}"` : "";
|
|
152
|
+
const said = picture && place.declared && place.depth === 0 ? ` data-summary-alt="${esc(picture.alt)}"` : "";
|
|
153
|
+
return `<li class="min-w-0 border border-base-300 bg-base-100" id="${id}" data-card data-index="${place.index}" data-kind="${esc(kind ?? "")}"${needs}${said}${valid}>
|
|
154
|
+
<div class="flex items-center gap-1 pl-2" data-card-head>${heading}${tools(place.noun, number, summary)}</div>
|
|
155
|
+
<div id="${id}-panel" class="border-t border-base-300 px-4 pb-4 [&>*:first-child]:mt-2" data-card-panel>
|
|
156
|
+
${problem}${kindField}
|
|
157
|
+
${slotsOfCard(agg, place, rendering, block, kind, flagged)}
|
|
158
|
+
</div>
|
|
159
|
+
</li>`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The blocks nested one level in a card (buttons, entries): a group of cards with one template. */
|
|
163
|
+
function group(agg: Aggregate, place: Place, part: Attr, rendering: Rendering, block: Block, prefix: string, problem?: string): string {
|
|
164
|
+
const parts = agg.valueObjects[part.type] ?? [];
|
|
165
|
+
const noun = singular(part.name.replace(/([^aeiou])ies$/, "$1y").replace(/s$/, ""));
|
|
166
|
+
const path = `${prefix}${part.name}`;
|
|
167
|
+
const held = Array.isArray(block[part.name]) ? (block[part.name] as Block[]) : [];
|
|
168
|
+
const limit = part.max ?? place.list.limits?.[part.name];
|
|
169
|
+
const pictures = { ...place.list.pictures, ...(part.picture ? { [part.type]: part.picture } : {}) };
|
|
170
|
+
const nested: BlockList = { discriminator: "", kinds: [], pictures };
|
|
171
|
+
const inside = (index: number | string, item: Block): string => card(agg, { path, index, depth: 1, noun, type: part.type, list: nested, parts, declared: place.declared }, rendering, item, null);
|
|
172
|
+
const cards = held.map((item, index) => inside(index, item));
|
|
173
|
+
const blank = inside(TOKEN[1], {});
|
|
174
|
+
const max = limit ? ` data-max="${limit}"` : "";
|
|
175
|
+
const required = place.declared ? !part.optional : place.list.rules?.[kindOf(place.list, block)]?.requires.includes(part.name);
|
|
176
|
+
const note = limitsNote(part.min, limit);
|
|
177
|
+
const need = `${required ? "" : ` <span class="ml-1 text-xs font-normal text-muted">optional</span>`}${note ? ` <span class="ml-1 text-xs font-normal text-muted">${esc(note)}</span>` : ""}`;
|
|
178
|
+
const bad = problem ? ` aria-invalid="true" aria-describedby="blk-${safe(path)}-problem"` : "";
|
|
179
|
+
const why = problem ? `<p class="clear-both text-sm font-semibold text-error" id="blk-${safe(path)}-problem">${esc(problem)}</p>` : "";
|
|
180
|
+
return `<fieldset class="mt-6 border-t border-base-300 pt-3" data-group data-path="${esc(path)}" data-noun="${esc(noun)}"${max} data-depth="1"${bad}>
|
|
181
|
+
<legend class="float-left mb-2 w-full text-base font-semibold">${esc(label(part.name))}${need}</legend>
|
|
182
|
+
<ol class="clear-both grid list-none grid-cols-[minmax(0,1fr)] gap-2 p-0" data-cards>
|
|
183
|
+
${cards.join("\n")}
|
|
184
|
+
</ol>
|
|
185
|
+
${why}
|
|
186
|
+
<template data-card-template data-kind="">${blank}</template>
|
|
187
|
+
<button type="button" class="btn btn-ghost btn-sm mt-2" data-action="add" hidden>${icon("plus")}<span>Add ${esc(noun)}</span></button>
|
|
188
|
+
</fieldset>`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** What a list of one kind of group is drawn from: a block list that has no kind, its limits and its member's picture. */
|
|
192
|
+
function plainList(attr: Attr): BlockList {
|
|
193
|
+
return { discriminator: "", kinds: [], pictures: attr.picture ? { [attr.type]: attr.picture } : undefined };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The add control and a template for each kind (or the one template, for a list that has no kinds). */
|
|
197
|
+
function adder(agg: Aggregate, attr: Attr, name: string, rendering: Rendering, noun: string): { control: string; templates: string } {
|
|
198
|
+
const list = attr.blockList ?? plainList(attr);
|
|
199
|
+
const parts = agg.valueObjects[attr.type] ?? [];
|
|
200
|
+
const place: Place = { path: name, index: TOKEN[0], depth: 0, noun, type: attr.type, list, parts, declared: !attr.blockList };
|
|
201
|
+
const button = `<button type="button" class="btn btn-neutral" data-action="add">${icon("plus")}<span>Add ${esc(singular(noun))}</span></button>`;
|
|
202
|
+
if (!attr.blockList) {
|
|
203
|
+
return { control: `<div class="mt-4 flex flex-wrap items-end gap-2" data-adder hidden>${button}</div>`, templates: `<template data-card-template data-kind="">${card(agg, place, rendering, {}, null)}</template>` };
|
|
204
|
+
}
|
|
205
|
+
const options = list.kinds.map((kind) => `<option value="${esc(kind)}">${esc(label(kind))}</option>`).join("");
|
|
206
|
+
const id = `blk-${safe(name)}-kind`;
|
|
207
|
+
const control = `<div class="mt-4 flex flex-wrap items-end gap-2" data-adder hidden>
|
|
208
|
+
<div class="grid gap-1"><label for="${id}" class="font-semibold">Kind of ${esc(singular(noun))} to add</label><select id="${id}" class="select" data-add-kind>${options}</select></div>
|
|
209
|
+
${button}
|
|
210
|
+
</div>`;
|
|
211
|
+
const templates = list.kinds.map((kind) => `<template data-card-template data-kind="${esc(kind)}">${card(agg, place, rendering, { [list.discriminator]: kindValue(agg, parts, list, kind) }, kind)}</template>`);
|
|
212
|
+
return { control, templates: templates.join("\n") };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A kind as the value the discriminator part holds: wrapped when the part is a one-part value object. */
|
|
216
|
+
function kindValue(agg: Aggregate, parts: Attr[], list: BlockList, kind: string): unknown {
|
|
217
|
+
const part = parts.find((candidate) => candidate.name === list.discriminator);
|
|
218
|
+
const inner = part?.kind === "object" ? (agg.valueObjects[part.type] ?? [])[0]?.name : undefined;
|
|
219
|
+
return inner ? { [inner]: kind } : kind;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** What the page says at the top of a refused list: how many blocks need attention and where each is. */
|
|
223
|
+
function summaryOfProblems(name: string, noun: string, problems: Problem[], titles: Map<string, string>): string {
|
|
224
|
+
if (problems.length === 0) return "";
|
|
225
|
+
const items = problems.map((problem) => {
|
|
226
|
+
const rest = problem.name.slice(name.length + 1).split(".");
|
|
227
|
+
const id = `blk-${safe(name)}-${rest[0]}`;
|
|
228
|
+
const where = rest.length > 2 ? `, ${esc(label(rest[1]))} ${Number(rest[2]) + 1}` : "";
|
|
229
|
+
return `<li><a href="#${id}">${esc(titles.get(rest[0]) ?? "")}${where}</a>: ${esc(problem.message)}</li>`;
|
|
230
|
+
});
|
|
231
|
+
const words = problems.length === 1 ? `1 ${esc(singular(noun))} needs attention` : `${problems.length} ${esc(pluralOf(singular(noun)))} need attention`;
|
|
232
|
+
return `<div class="alert alert-error alert-soft mb-4 block" role="alert" tabindex="-1" data-block-problems><p class="font-semibold">${words}</p><ul class="mt-1 list-disc pl-5">${items.join("")}</ul></div>`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** An attribute that is a list of mixed blocks, or of one kind of group, as its cards, its add control and its templates. */
|
|
236
|
+
export function blockList(agg: Aggregate, attr: Attr, name: string, value: unknown, rendering: Rendering): string {
|
|
237
|
+
const list = attr.blockList ?? plainList(attr);
|
|
238
|
+
const parts = agg.valueObjects[attr.type] ?? [];
|
|
239
|
+
const noun = singular(attr.type);
|
|
240
|
+
const plural = esc(pluralOf(noun));
|
|
241
|
+
const held = Array.isArray(value) ? (value as Block[]) : [];
|
|
242
|
+
const own = rendering.problems.filter((problem) => problem.name.startsWith(`${name}.`));
|
|
243
|
+
const place = (index: number): Place => ({ path: name, index, depth: 0, noun, type: attr.type, list, parts, declared: !attr.blockList });
|
|
244
|
+
const cards = held.map((block, index) => card(agg, place(index), rendering, block, kindOf(list, block) || null));
|
|
245
|
+
const titles = new Map(held.map((block, index) => [String(index), `${label(noun)} ${index + 1}${kindOf(list, block) ? ` (${label(kindOf(list, block))})` : ""}`]));
|
|
246
|
+
const adding = adder(agg, attr, name, rendering, noun);
|
|
247
|
+
const max = attr.max ? ` data-max="${attr.max}"` : "";
|
|
248
|
+
const draft = rendering.draft ? " data-draft" : "";
|
|
249
|
+
const id = `blk-${safe(name)}`;
|
|
250
|
+
const limits = limitsNote(attr.min, attr.max);
|
|
251
|
+
const help = `<p class="clear-both mb-3 text-sm text-muted" id="${id}-help">Open a ${esc(singular(noun))} to edit it. Move buttons change the order and Remove asks first. ${limits} Without scripts every ${esc(singular(noun))} stays open and cannot be added, moved or removed.</p>`;
|
|
252
|
+
const invalid = rendering.message ? ` aria-invalid="true"` : "";
|
|
253
|
+
const said = rendering.message ? `<p class="clear-both mb-3 text-sm font-semibold text-error" id="${id}-error">${esc(rendering.message)}</p>` : "";
|
|
254
|
+
const described = rendering.message ? `${id}-help ${id}-error` : `${id}-help`;
|
|
255
|
+
return `<fieldset class="mt-6 [&>legend+*]:clear-both" id="${id}" data-block-list data-group data-path="${esc(name)}" data-noun="${esc(noun)}" data-depth="0"${max}${draft}${invalid} aria-describedby="${described}">
|
|
256
|
+
<legend class="float-left mb-2 w-full border-t border-base-300 pt-4 text-lg font-semibold">${rendering.caption} <span class="text-sm font-normal text-muted" data-count>(${held.length})</span></legend>
|
|
257
|
+
${help}
|
|
258
|
+
${said}
|
|
259
|
+
${summaryOfProblems(name, noun, own, titles)}
|
|
260
|
+
<ol class="grid list-none grid-cols-[minmax(0,1fr)] gap-3 p-0" data-cards>
|
|
261
|
+
${cards.join("\n")}
|
|
262
|
+
</ol>
|
|
263
|
+
<p class="mt-2 text-sm text-muted" data-empty${held.length > 0 ? " hidden" : ""}>No ${plural} yet.</p>
|
|
264
|
+
${adding.control}
|
|
265
|
+
<p class="sr-only" role="status" aria-live="polite" data-block-say></p>
|
|
266
|
+
${adding.templates}
|
|
267
|
+
<dialog class="modal" id="${id}-remove" aria-labelledby="${id}-remove-title" data-remove-dialog>
|
|
268
|
+
<div class="modal-box"><h2 id="${id}-remove-title" class="text-xl" data-remove-title>Remove ${esc(singular(noun))}?</h2>
|
|
269
|
+
<p class="mt-2 text-muted">It is taken out of the list when the list is saved.</p>
|
|
270
|
+
<div class="modal-action"><button type="button" class="btn btn-ghost" data-action="remove-cancel">Keep it</button><button type="button" class="btn btn-error" data-action="remove-confirm">Remove</button></div></div>
|
|
271
|
+
</dialog>
|
|
272
|
+
</fieldset>`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { filled, lacking };
|
|
@@ -67,8 +67,11 @@ function imageHtml(block) {
|
|
|
67
67
|
return `<figure${attrs([["data-media-ref", block.media_ref ?? ""], ["data-alt", block.alt], ["data-caption", block.caption]])}>${note}${caption}</figure>`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* One block as HTML. An unknown kind is shown as a paragraph. `demote` moves a heading down that
|
|
72
|
+
* many levels (to at most 6), for a body shown inside a page that has its own headings above it.
|
|
73
|
+
*/
|
|
74
|
+
export function blockToHtml(block, demote = 0) {
|
|
72
75
|
const kind = block?.kind;
|
|
73
76
|
if (kind === "divider") return "<hr>";
|
|
74
77
|
if (kind === "image") return imageHtml(block);
|
|
@@ -76,14 +79,14 @@ export function blockToHtml(block) {
|
|
|
76
79
|
const tag = kind === "bullet_list" ? "ul" : "ol";
|
|
77
80
|
return `<${tag}${layout(block)}>${list(block.items).map(itemHtml).join("")}</${tag}>`;
|
|
78
81
|
}
|
|
79
|
-
const level = whole(block?.level, 1, 4) ?? 2;
|
|
82
|
+
const level = Math.min(6, (whole(block?.level, 1, 4) ?? 2) + demote);
|
|
80
83
|
const tag = kind === "heading" ? `h${level}` : kind === "quote" ? "blockquote" : "p";
|
|
81
84
|
return `<${tag}${layout(block ?? {})}>${spansToHtml(block?.spans)}</${tag}>`;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/** The body as HTML, one block after another; the same body gives the same text. */
|
|
85
|
-
export function bodyToHtml(body) {
|
|
86
|
-
return list(body?.blocks).map(blockToHtml).join("\n");
|
|
88
|
+
export function bodyToHtml(body, demote = 0) {
|
|
89
|
+
return list(body?.blocks).map((block) => blockToHtml(block, demote)).join("\n");
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
const present = (value) => value !== undefined && value !== null && value !== "";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
__BANNER__
|
|
2
|
+
|
|
3
|
+
// One instance: its values as a single document column, and beside it a margin panel with its
|
|
4
|
+
// lifecycle state and the commands that apply now. Values are shown by shape (a value object as its
|
|
5
|
+
// parts, a list of value objects as a small table, a rich-text body as typeset text). A command that
|
|
6
|
+
// asks for nothing and is safe runs from a button; one that asks for input opens its own page; one
|
|
7
|
+
// that is destructive asks first, in a native dialog (the page of the command is the same question
|
|
8
|
+
// for a browser with no script).
|
|
9
|
+
import { at } from "../names.ts";
|
|
10
|
+
import type { Aggregate, Command } from "../schema.ts";
|
|
11
|
+
import { available, needsPage } from "./availability.ts";
|
|
12
|
+
import type { Problem } from "./blocks.ts";
|
|
13
|
+
import { confirmDialog, dialogId } from "./dialogs.ts";
|
|
14
|
+
import { copyButton, esc, href, refusal, statusBadge } from "./html.ts";
|
|
15
|
+
import { relatedPanels } from "./related.ts";
|
|
16
|
+
import { schedulePanel } from "./schedule.ts";
|
|
17
|
+
import type { Tools } from "./preview.ts";
|
|
18
|
+
import { valueRows } from "./values.ts";
|
|
19
|
+
import type { World } from "./world.ts";
|
|
20
|
+
import { commandLabel, label, singular } from "./words.ts";
|
|
21
|
+
|
|
22
|
+
export { available, needsPage };
|
|
23
|
+
|
|
24
|
+
type State = Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
/** What a submitted command left behind: the refusal to show, and the values to put back. */
|
|
27
|
+
export interface Submitted {
|
|
28
|
+
command: string;
|
|
29
|
+
message: string | null;
|
|
30
|
+
values: State;
|
|
31
|
+
/** The attribute the refusal names, when it names one. */
|
|
32
|
+
field?: string;
|
|
33
|
+
/** The blocks of a list of mixed blocks the refusal is about (see blocks.ts). */
|
|
34
|
+
problems?: Problem[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function action(agg: Aggregate, id: string, command: Command, first: boolean): string {
|
|
38
|
+
const to = href(...at(agg), "id", id, command.name);
|
|
39
|
+
const text = esc(commandLabel(agg, command));
|
|
40
|
+
const tone = command.destructive ? "btn btn-error btn-outline lg:w-full" : first ? "btn btn-neutral lg:w-full" : "btn lg:w-full";
|
|
41
|
+
if (!needsPage(command)) return `<form method="post" action="${esc(to)}"><button type="submit" class="${tone}">${text}</button></form>`;
|
|
42
|
+
const confirm = command.destructive && command.attributes.length === 0 ? ` data-confirm="${dialogId(command)}"` : "";
|
|
43
|
+
return `<a class="${tone}" href="${esc(to)}"${confirm}>${text}</a>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dialogs(agg: Aggregate, id: string, commands: Command[]): string {
|
|
47
|
+
return commands
|
|
48
|
+
.filter((command) => command.destructive && command.attributes.length === 0)
|
|
49
|
+
.map((command) => confirmDialog(agg, command, href(...at(agg), "id", id, command.name), id))
|
|
50
|
+
.join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function panel(agg: Aggregate, id: string, values: State, commands: Command[]): string {
|
|
54
|
+
const status = agg.lifecycle ? `<div class="lg:pb-5"><h2 class="mb-2 text-sm">Status</h2><p>${statusBadge(agg, values[agg.lifecycle.field])}</p></div>` : "";
|
|
55
|
+
const rule = agg.lifecycle ? "lg:border-t lg:border-base-300 lg:pt-5" : "";
|
|
56
|
+
const buttons = commands.map((command, at) => action(agg, id, command, at === 0));
|
|
57
|
+
const list = commands.length === 0 ? `<p class="text-muted">Nothing to do with this ${esc(singular(agg.name))} right now.</p>` : `<div class="flex flex-wrap gap-2 lg:grid">${buttons.join("\n")}</div>`;
|
|
58
|
+
return `<aside class="order-first flex flex-wrap items-start gap-x-8 gap-y-4 border-b border-base-300 pb-5 lg:sticky lg:top-20 lg:order-none lg:block lg:border-b-0 lg:pb-0" aria-label="Status and actions">${status}<div class="${rule}"><h2 class="mb-2 text-sm">Actions</h2>${list}</div></aside>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The badge that says an edit is saved and not put live yet, when the aggregate keeps one beside its live content. */
|
|
62
|
+
function unpublished(agg: Aggregate, values: State): string {
|
|
63
|
+
const held = agg.drafts ? values[agg.drafts.attribute] : undefined;
|
|
64
|
+
const saved = held !== undefined && held !== null && !(Array.isArray(held) && held.length === 0);
|
|
65
|
+
return saved ? `<p class="mt-2"><span class="badge badge-soft badge-info">Changes not published yet</span></p>` : "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One instance: its values, its status, a button for each command that applies to it now, and the records and actions that go with it. */
|
|
69
|
+
export function detailPage(agg: Aggregate, id: string, values: State, shown: Submitted | null, world: World, preview: Tools): string {
|
|
70
|
+
const commands = agg.commands.filter((command) => command.on === agg.name && available(agg, command, values));
|
|
71
|
+
const here = href(...at(agg), "id", id);
|
|
72
|
+
const panels = [relatedPanels(agg, id, world, here), schedulePanel(agg, id, world, here)].filter((html) => html !== "");
|
|
73
|
+
return `<div class="mb-6 flex flex-wrap items-start justify-between gap-3">
|
|
74
|
+
<div><h1 class="flex items-center gap-2 break-all text-3xl">${esc(id)}${copyButton(id, "id")}</h1><p class="mt-1 text-muted">${esc(label(agg.name))}</p>${unpublished(agg, values)}</div>
|
|
75
|
+
${preview.button}
|
|
76
|
+
</div>
|
|
77
|
+
${refusal(shown?.message ?? null)}
|
|
78
|
+
<div class="grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_16rem]">
|
|
79
|
+
<div class="grid min-w-0 gap-6">
|
|
80
|
+
<section class="min-w-0 border border-base-300 bg-base-100 px-4 sm:px-6" aria-label="${esc(label(agg.name))}">
|
|
81
|
+
${valueRows(agg, agg.attributes, values)}
|
|
82
|
+
</section>
|
|
83
|
+
${panels.join("\n")}
|
|
84
|
+
</div>
|
|
85
|
+
${panel(agg, id, values, commands)}
|
|
86
|
+
</div>
|
|
87
|
+
${dialogs(agg, id, commands)}`;
|
|
88
|
+
}
|