hecks 2.6.0 → 2.8.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/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
data/lib/hecks/doc/reference.rb
DELETED
|
@@ -1,585 +0,0 @@
|
|
|
1
|
-
require_relative "../bluebook/meta_validator"
|
|
2
|
-
require_relative "../naming"
|
|
3
|
-
|
|
4
|
-
module Hecks
|
|
5
|
-
module Doc
|
|
6
|
-
# The DSL reference, projected from the language's own Syntax chapter
|
|
7
|
-
# — the same Keyword/Argument rows the conformance specs hold equal
|
|
8
|
-
# to the live builders. Nothing here is described twice: the tables
|
|
9
|
-
# come from the declaration, the prose is hand-written between
|
|
10
|
-
# markers the generator preserves, and the golden spec refuses a
|
|
11
|
-
# tree where the two have drifted.
|
|
12
|
-
#
|
|
13
|
-
# Regenerate with bin/reference. A new word arrives with a TODO
|
|
14
|
-
# sentinel; the coverage gate refuses to let an admitted word ship
|
|
15
|
-
# undocumented; prose for a word the language no longer declares is
|
|
16
|
-
# a hard error naming its orphans — deleting someone's writing is a
|
|
17
|
-
# human's decision.
|
|
18
|
-
module Reference
|
|
19
|
-
GENERATED_END = "<!-- generated:end -->".freeze
|
|
20
|
-
TODO_SENTINEL = "<!-- TODO: document this word -->".freeze
|
|
21
|
-
|
|
22
|
-
# A page's own hand-written opening, harvested under a key no word
|
|
23
|
-
# can ever collide with (words are strings off the Syntax chapter;
|
|
24
|
-
# this is a Symbol). It exists so a page can boot once — load a real
|
|
25
|
-
# corpus chapter, wire its hexagon — and have every word's example
|
|
26
|
-
# below run against that single boot, the way a guide's opening
|
|
27
|
-
# `ruby boot` block already does. Without it each word would have to
|
|
28
|
-
# stand up its own domain, and 105 invented chapters would collide
|
|
29
|
-
# on the facade constants `Surface.install` never uninstalls.
|
|
30
|
-
PREAMBLE = :preamble
|
|
31
|
-
|
|
32
|
-
module_function
|
|
33
|
-
|
|
34
|
-
# The marker opening one word's generated region.
|
|
35
|
-
#
|
|
36
|
-
# @param word [String, Symbol, #to_s] the word this section documents
|
|
37
|
-
# @return [String] the HTML comment marking that word's generated region open
|
|
38
|
-
def generated_begin(word) = "<!-- generated:begin word=#{word} -->"
|
|
39
|
-
|
|
40
|
-
# Keyed by region rather than by word — the same marker convention,
|
|
41
|
-
# used for the parts of a page that are not about one word: a
|
|
42
|
-
# page's generated lede here, README's generated indexes below.
|
|
43
|
-
#
|
|
44
|
-
# @param id [String, Symbol, #to_s] the region's id, such as `"page"` or `"tools"`
|
|
45
|
-
# @return [String] the HTML comment marking that region's generated region open
|
|
46
|
-
def region_begin(id) = "<!-- generated:begin id=#{id} -->"
|
|
47
|
-
|
|
48
|
-
# The language's own Syntax aggregate, read off the judged grammar chapter.
|
|
49
|
-
#
|
|
50
|
-
# @return [Bluebook::Aggregate, nil] the Syntax aggregate, or nil if the
|
|
51
|
-
# grammar's Bluebook chapter declares none by that name
|
|
52
|
-
def syntax
|
|
53
|
-
meta = Bluebook::MetaValidator.grammar_registry.bluebook("Bluebook")
|
|
54
|
-
meta.aggregates.find { |aggregate| aggregate.hecks_name == "Syntax" }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Reads one closed-set value object's declared members off the Syntax
|
|
58
|
-
# aggregate, as string-valued Hashes.
|
|
59
|
-
#
|
|
60
|
-
# @param name [String, Symbol, #to_s] the value object's `hecks_name`,
|
|
61
|
-
# such as `"Keyword"`
|
|
62
|
-
# @return [Array<Hash{Symbol => String}>] each declared member's fields,
|
|
63
|
-
# values stringified
|
|
64
|
-
def rows(name)
|
|
65
|
-
syntax.value_objects.find { |vo| vo.hecks_name == name }
|
|
66
|
-
.members.map { |row| row.to_h.transform_values(&:to_s) }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# S14, ADR 0026 — Keyword/Argument are genuine entities of Syntax
|
|
70
|
-
# now, dispatched (not merely declared) so their own `status`
|
|
71
|
-
# really is a lifecycle. `SyntaxBoot.call` discovers the static
|
|
72
|
-
# aggregate-local seed rows (`KeywordSeed`/`ArgumentSeed`), dispatches each
|
|
73
|
-
# one through the real admission/lifecycle door, and hands back the
|
|
74
|
-
# same shape `rows`, above, already produces — nothing below this
|
|
75
|
-
# needed to change.
|
|
76
|
-
#
|
|
77
|
-
# This module holds no separate `@keywords ||=` memo of its own —
|
|
78
|
-
# delegates straight through to `SyntaxBoot.call`'s own cache
|
|
79
|
-
# (keyed on the grammar registry's own chapter set — see its
|
|
80
|
-
# comment) rather than shadowing it. A second, local cache on top
|
|
81
|
-
# of that one would double-cache with no way to invalidate either
|
|
82
|
-
# half: whichever call happened to land first would lock in
|
|
83
|
-
# forever, even one caught mid-build missing every Paging-attached
|
|
84
|
-
# word (limit/offset/cursor/nulls).
|
|
85
|
-
#
|
|
86
|
-
# @return [Array<Hash{Symbol => String}>] every declared Keyword row
|
|
87
|
-
def keywords = Bluebook::MetaValidator::SyntaxBoot.call[:keywords]
|
|
88
|
-
|
|
89
|
-
# Every declared Argument row.
|
|
90
|
-
#
|
|
91
|
-
# @return [Array<Hash{Symbol => String}>] every declared Argument row
|
|
92
|
-
def arguments = Bluebook::MetaValidator::SyntaxBoot.call[:arguments]
|
|
93
|
-
|
|
94
|
-
# Reads a row's declared status, defaulting when it declared none.
|
|
95
|
-
#
|
|
96
|
-
# @param row [Hash{Symbol => String}] a Keyword or Argument row
|
|
97
|
-
# @return [String] the row's declared status, defaulting to `"admitted"`
|
|
98
|
-
# when it declared none
|
|
99
|
-
def status_of(row) = row[:status].to_s.empty? ? "admitted" : row[:status].to_s
|
|
100
|
-
|
|
101
|
-
# Whether a row is still current enough to appear in the reference.
|
|
102
|
-
#
|
|
103
|
-
# @param row [Hash{Symbol => String}] a Keyword or Argument row
|
|
104
|
-
# @return [Boolean] true if the row's status is `"admitted"` or `"deprecated"`
|
|
105
|
-
def live?(row) = %w[admitted deprecated].include?(status_of(row))
|
|
106
|
-
|
|
107
|
-
# Every distinct context a keyword is declared in.
|
|
108
|
-
#
|
|
109
|
-
# @return [Array<String>] every distinct context a keyword is declared in
|
|
110
|
-
def contexts = keywords.map { |row| row[:context] }.uniq
|
|
111
|
-
|
|
112
|
-
# Derives a context's reference page filename.
|
|
113
|
-
#
|
|
114
|
-
# @param context [String, Symbol, #to_s] a context name, such as `"File"`
|
|
115
|
-
# @return [String] the reference page's filename for that context
|
|
116
|
-
def page_name(context) = "#{Naming.snake(context)}.md"
|
|
117
|
-
|
|
118
|
-
# Every reference page, rendered fresh — prose carried over from
|
|
119
|
-
# the committed pages, new words seeded with the sentinel, orphaned
|
|
120
|
-
# prose refused.
|
|
121
|
-
#
|
|
122
|
-
# @param directory [String] path to the directory holding the committed
|
|
123
|
-
# reference pages, read for their hand-written prose
|
|
124
|
-
# @return [Hash{String => String}] every page's filename (plus `"index.md"`)
|
|
125
|
-
# mapped to its freshly rendered Markdown content
|
|
126
|
-
# @raise [RuntimeError] if a committed page carries prose for a word the
|
|
127
|
-
# language no longer declares in that context
|
|
128
|
-
def pages(directory)
|
|
129
|
-
contexts.each_with_object({}) do |context, pages|
|
|
130
|
-
path = File.join(directory, page_name(context))
|
|
131
|
-
prose = File.exist?(path) ? harvest(File.read(path)) : {}
|
|
132
|
-
pages[page_name(context)] = render_page(context, prose, path)
|
|
133
|
-
end.merge("index.md" => render_index)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# A word admitting two forms has two rows — syntax.bluebook's own
|
|
137
|
-
# stated rule, and `identified_by` (a block, or a bare argument and
|
|
138
|
-
# none) is the case that made it real again. One section per word all
|
|
139
|
-
# the same: the prose is the word's rather than the form's, and the
|
|
140
|
-
# argument rows join by (word, context) and so already cover every
|
|
141
|
-
# form. Grouped rather than rendered per row, or a reader would meet
|
|
142
|
-
# the same heading and the same paragraph twice.
|
|
143
|
-
#
|
|
144
|
-
# @param context [String] the context name, such as `"File"`
|
|
145
|
-
# @param prose [Hash{String, Symbol => String}] hand-written prose harvested
|
|
146
|
-
# from the committed page, keyed by word (or `PREAMBLE` for the page's lede)
|
|
147
|
-
# @param path [String] the page's file path, used only in the orphan-refusal
|
|
148
|
-
# message below
|
|
149
|
-
# @return [String] the page's full rendered Markdown
|
|
150
|
-
# @raise [RuntimeError] if `prose` carries a key for a word the language no
|
|
151
|
-
# longer declares in `context`
|
|
152
|
-
def render_page(context, prose, path)
|
|
153
|
-
words = keywords.select { |row| row[:context] == context }.group_by { |row| row[:word] }
|
|
154
|
-
orphans = prose.keys - words.keys - [PREAMBLE]
|
|
155
|
-
unless orphans.empty?
|
|
156
|
-
raise "#{path} carries prose for #{orphans.join(', ')}, which the language no longer " \
|
|
157
|
-
"declares in #{context} — deleting writing is a human's decision, so decide"
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
sections = words.map { |word, forms| render_word(forms, prose[word]) }
|
|
161
|
-
preamble = prose[PREAMBLE].to_s.strip
|
|
162
|
-
<<~PAGE
|
|
163
|
-
# #{context}
|
|
164
|
-
|
|
165
|
-
#{region_begin('page')}
|
|
166
|
-
#{context_lede(context)}
|
|
167
|
-
|
|
168
|
-
*The tables on this page are generated from the language's own
|
|
169
|
-
aggregate-local syntax tables (`lib/hecks/language/**/*.bluebook`)
|
|
170
|
-
by `bin/reference` — do not edit inside the markers. The prose
|
|
171
|
-
between them is hand-written and survives regeneration.*
|
|
172
|
-
#{GENERATED_END}
|
|
173
|
-
#{"\n#{preamble}\n" unless preamble.empty?}
|
|
174
|
-
#{sections.join("\n")}
|
|
175
|
-
PAGE
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# The one-line description of where a context's words are typed.
|
|
179
|
-
#
|
|
180
|
-
# @param context [String] the context name, such as `"File"` or `"Command"`
|
|
181
|
-
# @return [String] a sentence naming where words in this context are typed
|
|
182
|
-
def context_lede(context)
|
|
183
|
-
openers = keywords.select { |row| row[:opens] == context }
|
|
184
|
-
return "Words available at the top of a file." if context == "File"
|
|
185
|
-
return "Words available in the type position of an `attribute`." if context == "Type"
|
|
186
|
-
|
|
187
|
-
inside = openers.map { |row| "`#{row[:word]} do ... end`" }.uniq.join(" / ")
|
|
188
|
-
inside.empty? ? "Words available in the #{context} body." : "Words available inside #{inside}."
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# One spelling per form, everything else off the first row — the
|
|
192
|
-
# columns that differ between two forms of one word are `body` (which
|
|
193
|
-
# is what the spelling shows) and nothing else.
|
|
194
|
-
#
|
|
195
|
-
# @param forms [Array<Hash{Symbol => String}>] one word's Keyword rows, one
|
|
196
|
-
# per admitted form
|
|
197
|
-
# @param prose [String, nil] the word's hand-written prose, or nil if none
|
|
198
|
-
# was harvested
|
|
199
|
-
# @return [String] the word's rendered section, generated table plus prose
|
|
200
|
-
def render_word(forms, prose)
|
|
201
|
-
row = forms.first
|
|
202
|
-
table = argument_table(row)
|
|
203
|
-
facts = []
|
|
204
|
-
facts << "opens a `#{row[:opens]}` body" unless row[:opens].to_s.empty?
|
|
205
|
-
facts << "fills `#{row[:fills]}`" unless row[:fills].to_s.empty?
|
|
206
|
-
facts << "**status: #{status_of(row)}**" unless status_of(row) == "admitted"
|
|
207
|
-
facts << "was `#{row[:was]}`" unless row[:was].to_s.empty?
|
|
208
|
-
spellings = forms.map { |form| "`#{signature(form)}`" }.join(" / ")
|
|
209
|
-
|
|
210
|
-
<<~WORD
|
|
211
|
-
## #{row[:word]}
|
|
212
|
-
|
|
213
|
-
#{generated_begin(row[:word])}
|
|
214
|
-
#{spellings}#{" — #{facts.join(', ')}" unless facts.empty?}
|
|
215
|
-
#{table}#{GENERATED_END}
|
|
216
|
-
|
|
217
|
-
#{prose_or_sentinel(prose)}
|
|
218
|
-
WORD
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Falls back to the TODO sentinel when a word has no prose yet.
|
|
222
|
-
#
|
|
223
|
-
# @param prose [String, nil] a word's hand-written prose, or nil if none
|
|
224
|
-
# was harvested
|
|
225
|
-
# @return [String] `prose` stripped, or the TODO sentinel if it is blank
|
|
226
|
-
def prose_or_sentinel(prose)
|
|
227
|
-
text = prose.to_s.strip
|
|
228
|
-
text.empty? ? TODO_SENTINEL : text
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Finds the Argument rows declared for one Keyword row's word.
|
|
232
|
-
#
|
|
233
|
-
# @param row [Hash{Symbol => String}] a Keyword row
|
|
234
|
-
# @return [Array<Hash{Symbol => String}>] the Argument rows declared for
|
|
235
|
-
# this row's word, in this row's context
|
|
236
|
-
def word_arguments(row)
|
|
237
|
-
arguments.select { |arg| arg[:keyword] == row[:word] && arg[:context] == row[:context] }
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Builds the call spelling shown for one word.
|
|
241
|
-
#
|
|
242
|
-
# @param row [Hash{Symbol => String}] a Keyword row
|
|
243
|
-
# @return [String] the word's call spelling, positional arguments then
|
|
244
|
-
# named ones, with a trailing `do ... end` unless its body is `"none"`
|
|
245
|
-
def signature(row)
|
|
246
|
-
positional = word_arguments(row).reject { |arg| arg[:at].to_s.empty? }
|
|
247
|
-
.sort_by { |arg| arg[:at].to_i }
|
|
248
|
-
.map { |arg| arg[:fills].to_s.empty? ? arg[:kind] : arg[:fills] }
|
|
249
|
-
named = word_arguments(row).select { |arg| arg[:at].to_s.empty? }
|
|
250
|
-
.map { |arg| "#{arg[:named]}:" }
|
|
251
|
-
parts = positional + named
|
|
252
|
-
base = parts.empty? ? row[:word] : "#{row[:word]} #{parts.join(', ')}"
|
|
253
|
-
row[:body].to_s == "none" ? base : "#{base} do ... end"
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Renders one word's arguments as a Markdown table.
|
|
257
|
-
#
|
|
258
|
-
# @param row [Hash{Symbol => String}] a Keyword row
|
|
259
|
-
# @return [String] a Markdown table of the row's arguments, or `""` if it
|
|
260
|
-
# declares none
|
|
261
|
-
def argument_table(row)
|
|
262
|
-
args = word_arguments(row)
|
|
263
|
-
return "" if args.empty?
|
|
264
|
-
|
|
265
|
-
lines = ["", "| argument | kind | required | fills |", "|---|---|---|---|"]
|
|
266
|
-
args.each do |arg|
|
|
267
|
-
name = arg[:at].to_s.empty? ? "`#{arg[:named]}:`" : "positional #{arg[:at]}"
|
|
268
|
-
lines << "| #{name} | #{arg[:kind]} | #{arg[:required]} | #{arg[:fills]} |"
|
|
269
|
-
end
|
|
270
|
-
"#{lines.join("\n")}\n"
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# Renders the reference index page.
|
|
274
|
-
#
|
|
275
|
-
# @return [String] the reference index page's rendered Markdown, one
|
|
276
|
-
# linked entry per context
|
|
277
|
-
def render_index
|
|
278
|
-
listed = contexts.map do |context|
|
|
279
|
-
count = keywords.select { |row| row[:context] == context }.map { |row| row[:word] }.uniq.size
|
|
280
|
-
"- [#{context}](#{page_name(context)}) — #{count} #{count == 1 ? 'word' : 'words'}"
|
|
281
|
-
end
|
|
282
|
-
<<~INDEX
|
|
283
|
-
# The DSL reference
|
|
284
|
-
|
|
285
|
-
One page per context — the place in a file where a word may be
|
|
286
|
-
typed. Generated by `bin/reference` from the Syntax chapter;
|
|
287
|
-
the prose between the generated markers is hand-written and
|
|
288
|
-
survives regeneration.
|
|
289
|
-
|
|
290
|
-
#{listed.join("\n")}
|
|
291
|
-
INDEX
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# Prose keyed by word: everything between a section's generated
|
|
295
|
-
# region and the next `## ` heading (or end of file).
|
|
296
|
-
#
|
|
297
|
-
# Starts on `PREAMBLE` rather than nil so the text between the page's
|
|
298
|
-
# own generated lede and its first word heading is carried over too
|
|
299
|
-
# instead of being silently dropped. A page written before that
|
|
300
|
-
# region existed has no generated marker ahead of its first `## `,
|
|
301
|
-
# so nothing is collecting when that heading arrives and no empty
|
|
302
|
-
# preamble is invented — the older shape reads back unchanged.
|
|
303
|
-
# A single-pass line-scanning state machine (current/collecting/
|
|
304
|
-
# buffer/in_fence) — each branch mutates shared local state that
|
|
305
|
-
# carries into the next iteration, so splitting per branch would
|
|
306
|
-
# mean passing all four back and forth by reference every line.
|
|
307
|
-
#
|
|
308
|
-
# @param text [String] a committed reference page's full Markdown source
|
|
309
|
-
# @return [Hash{String, Symbol => String}] hand-written prose, keyed by
|
|
310
|
-
# word (or `PREAMBLE` for the page's lede); empty and TODO-sentinel-only
|
|
311
|
-
# entries are dropped
|
|
312
|
-
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
313
|
-
def harvest(text)
|
|
314
|
-
prose = {}
|
|
315
|
-
current = PREAMBLE
|
|
316
|
-
collecting = false
|
|
317
|
-
buffer = []
|
|
318
|
-
|
|
319
|
-
# A heading inside a fence is not a heading. `## something` is an
|
|
320
|
-
# ordinary Ruby comment, and now that every word's section carries
|
|
321
|
-
# runnable code, one written at the left margin would otherwise
|
|
322
|
-
# end that section mid-example and orphan the rest of it under a
|
|
323
|
-
# word the language never declared.
|
|
324
|
-
in_fence = false
|
|
325
|
-
|
|
326
|
-
text.each_line do |line|
|
|
327
|
-
in_fence = !in_fence if line.start_with?("```")
|
|
328
|
-
|
|
329
|
-
if !in_fence && (match = line.match(/\A## (\S+)\s*\z/))
|
|
330
|
-
prose[current] = buffer.join.strip if current && collecting
|
|
331
|
-
current = match[1]
|
|
332
|
-
collecting = false
|
|
333
|
-
buffer = []
|
|
334
|
-
elsif line.include?(GENERATED_END)
|
|
335
|
-
collecting = true
|
|
336
|
-
elsif collecting
|
|
337
|
-
buffer << line
|
|
338
|
-
end
|
|
339
|
-
end
|
|
340
|
-
prose[current] = buffer.join.strip if current && collecting
|
|
341
|
-
prose.reject { |_word, text_| text_.empty? || text_ == TODO_SENTINEL }
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
# Renders every reference page and writes each to `directory`.
|
|
345
|
-
#
|
|
346
|
-
# @param directory [String] path to the directory to write pages into,
|
|
347
|
-
# created if it does not exist
|
|
348
|
-
# @return [void]
|
|
349
|
-
# @raise [RuntimeError] if a committed page carries prose for a word the
|
|
350
|
-
# language no longer declares in that context
|
|
351
|
-
def write!(directory)
|
|
352
|
-
FileUtils.mkdir_p(directory)
|
|
353
|
-
pages(directory).each do |name, content|
|
|
354
|
-
File.write(File.join(directory, name), content)
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# README's own generated regions — the same marker convention as a
|
|
359
|
-
# reference page, keyed by region id instead of a word, so the
|
|
360
|
-
# index a reader lands on first can't drift from what actually
|
|
361
|
-
# exists on disk either.
|
|
362
|
-
#
|
|
363
|
-
# @param root [String] the repository root
|
|
364
|
-
# @return [Hash{String => String}] each region id mapped to its freshly
|
|
365
|
-
# rendered content
|
|
366
|
-
def readme_regions(root)
|
|
367
|
-
{
|
|
368
|
-
"guides" => guide_index(root),
|
|
369
|
-
"reference" => reference_index(root),
|
|
370
|
-
"tools" => tool_table(root),
|
|
371
|
-
"corpus" => corpus_roster(root),
|
|
372
|
-
"diagrams" => diagram_showcase(root)
|
|
373
|
-
}
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Lists every committed guide, linked and titled by its own heading.
|
|
377
|
-
#
|
|
378
|
-
# @param root [String] the repository root
|
|
379
|
-
# @return [String] a Markdown list linking every committed guide, titled
|
|
380
|
-
# by its own `# ` heading
|
|
381
|
-
def guide_index(root)
|
|
382
|
-
paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
|
|
383
|
-
.reject { |p| %w[AUTHORING.md].include?(File.basename(p)) }
|
|
384
|
-
lines = paths.map do |path|
|
|
385
|
-
heading = File.foreach(path).find { |line| line.start_with?("# ") }
|
|
386
|
-
title = heading ? heading.sub(/\A#\s*/, "").strip : File.basename(path)
|
|
387
|
-
"- [#{title}](docs/implemented/guides/#{File.basename(path)})"
|
|
388
|
-
end
|
|
389
|
-
lines.join("\n")
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
# Links the reference index, with its context count.
|
|
393
|
-
#
|
|
394
|
-
# @param _root [String] unused; kept for the shape `readme_regions` calls
|
|
395
|
-
# every region renderer with
|
|
396
|
-
# @return [String] one line linking the reference index, with its context count
|
|
397
|
-
def reference_index(_root)
|
|
398
|
-
count = contexts.size
|
|
399
|
-
"[The DSL reference](docs/implemented/reference/index.md) — #{count} contexts, generated from " \
|
|
400
|
-
"the aggregate-local tables under `lib/hecks/language/` and held to them by " \
|
|
401
|
-
"`spec/reference_golden_spec.rb`."
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
# Lists every `bin/` script that opens with a comment, one row each.
|
|
405
|
-
#
|
|
406
|
-
# @param root [String] the repository root
|
|
407
|
-
# @return [String] a Markdown table of every `bin/` script with an opening
|
|
408
|
-
# comment, one row each
|
|
409
|
-
def tool_table(root)
|
|
410
|
-
scripts = Dir.glob(File.join(root, "bin/*")).select { |p| File.file?(p) }.sort
|
|
411
|
-
rows = scripts.filter_map { |path| [path, tool_summary(path)] }.select { |_, desc| desc }
|
|
412
|
-
lines = ["| tool | |", "|---|---|"]
|
|
413
|
-
rows.each { |path, desc| lines << "| `bin/#{File.basename(path)}` | #{desc} |" }
|
|
414
|
-
lines.join("\n")
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
# The opening comment paragraph, not just the first line — a table
|
|
418
|
-
# cell that trails off mid-clause reads worse than one that runs a
|
|
419
|
-
# little long and says "...". A code-bearing comment (`field.name`,
|
|
420
|
-
# `pattern:`) makes naive sentence-splitting on "." or ":" cut in
|
|
421
|
-
# the wrong place, so this truncates on length alone.
|
|
422
|
-
#
|
|
423
|
-
# @param path [String] a `bin/` script's path
|
|
424
|
-
# @return [String, nil] its opening comment paragraph, truncated to 140
|
|
425
|
-
# characters, or nil if the script opens with no comment
|
|
426
|
-
def tool_summary(path)
|
|
427
|
-
comment_lines = []
|
|
428
|
-
started = false
|
|
429
|
-
File.foreach(path).first(10).each do |line|
|
|
430
|
-
if line.start_with?("#") && !line.start_with?("#!")
|
|
431
|
-
next if !started && line.strip == "#"
|
|
432
|
-
|
|
433
|
-
started = true
|
|
434
|
-
comment_lines << line.sub(/\A#\s?/, "").rstrip
|
|
435
|
-
elsif started
|
|
436
|
-
break
|
|
437
|
-
end
|
|
438
|
-
end
|
|
439
|
-
return nil if comment_lines.empty?
|
|
440
|
-
|
|
441
|
-
text = comment_lines.join(" ").squeeze(" ")
|
|
442
|
-
text.length > 140 ? "#{text[0, 137]}..." : text
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
# One real, committed file, read fresh — not re-derived from a boot
|
|
446
|
-
# (this module never requires `hecks/projections/diagrams`, and
|
|
447
|
-
# shouldn't just to draw one example). `docs/generated/diagrams/`
|
|
448
|
-
# is already held to the declaration by `spec/diagrams_spec.rb`'s
|
|
449
|
-
# own drift check; this just quotes its own output, so the two
|
|
450
|
-
# can't independently drift from each other either — a stale
|
|
451
|
-
# Order_lifecycle.mmd fails that spec long before this one runs.
|
|
452
|
-
#
|
|
453
|
-
# @param root [String] the repository root
|
|
454
|
-
# @return [String] a Markdown section showcasing the generated diagram
|
|
455
|
-
# tooling, quoting `examples/pizzas`'s own committed Order lifecycle diagram
|
|
456
|
-
def diagram_showcase(root)
|
|
457
|
-
lifecycle = File.read(File.join(root, "docs/generated/diagrams/pizzas/Order_lifecycle.mmd")).strip
|
|
458
|
-
<<~MARKDOWN.strip
|
|
459
|
-
`bin/project_diagrams` reads a booted domain's own declaration and draws it as Mermaid — nine kinds so far: `<Name>_lifecycle.mmd`, `relationships.mmd`, `dispatch.mmd`, `roles.mmd`, `ports.mmd`, `read_models.mmd`, `<Name>_surface.mmd` (what a command does, and what it writes), `<Name>_saga.mmd`, and `frameworks.mmd`. Nothing hand-drawn — the same reason a domain is data at all. Order's own lifecycle, straight off the bluebook above:
|
|
460
|
-
|
|
461
|
-
```mermaid
|
|
462
|
-
#{lifecycle}
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
The full set for every domain in this checkout — `examples/pizzas`, `examples/banking` — lives in [`docs/generated/diagrams/`](docs/generated/diagrams/), held to the declaration by `spec/diagrams_spec.rb` the same drift-refusing way this page is held to its own source.
|
|
466
|
-
MARKDOWN
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
# Lists every example domain, with its own declared vision.
|
|
470
|
-
#
|
|
471
|
-
# @param root [String] the repository root
|
|
472
|
-
# @return [String] a Markdown list of every example domain with a
|
|
473
|
-
# `.bluebook` file, each with its own declared `vision` text
|
|
474
|
-
def corpus_roster(root)
|
|
475
|
-
dirs = Dir.glob(File.join(root, "examples/*/"))
|
|
476
|
-
lines = dirs.filter_map do |dir|
|
|
477
|
-
name = File.basename(dir.chomp("/"))
|
|
478
|
-
bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook"))
|
|
479
|
-
bluebooks = Dir.glob(File.join(dir, "*.bluebook")) if bluebooks.empty?
|
|
480
|
-
next if bluebooks.empty?
|
|
481
|
-
|
|
482
|
-
vision = bluebooks.filter_map { |bluebook| File.read(bluebook)[/vision\s+"([^"]*)"/, 1] }.first
|
|
483
|
-
"- **#{name}** — #{vision}"
|
|
484
|
-
end
|
|
485
|
-
lines.join("\n")
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
# Replaces every generated region inside `text` with its freshly rendered
|
|
489
|
-
# content, leaving the hand-written parts of README untouched.
|
|
490
|
-
#
|
|
491
|
-
# @param root [String] the repository root
|
|
492
|
-
# @param text [String] the README's current full text
|
|
493
|
-
# @return [String] the README's text, with each generated region refreshed
|
|
494
|
-
def render_readme(root, text)
|
|
495
|
-
readme_regions(root).reduce(text) do |current, (id, content)|
|
|
496
|
-
pattern = /#{Regexp.escape(region_begin(id))}.*?#{Regexp.escape(GENERATED_END)}/m
|
|
497
|
-
current.sub(pattern) { "#{region_begin(id)}\n#{content}\n#{GENERATED_END}" }
|
|
498
|
-
end
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# Regenerates README's generated regions in place.
|
|
502
|
-
#
|
|
503
|
-
# @param root [String] the repository root
|
|
504
|
-
# @return [void]
|
|
505
|
-
def write_readme!(root)
|
|
506
|
-
path = File.join(root, "README.md")
|
|
507
|
-
File.write(path, render_readme(root, File.read(path)))
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
# An example a reader can see and the harness will actually run.
|
|
511
|
-
# `ruby skip` is display-only by the doctest harness's own rule, and
|
|
512
|
-
# a hidden `<!-- doctest:boot -->` block is setup rather than an
|
|
513
|
-
# example — a word whose only "example" is invisible or inert is a
|
|
514
|
-
# word still shipping on its prose alone, which is the thing this
|
|
515
|
-
# gate exists to refuse.
|
|
516
|
-
EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/
|
|
517
|
-
|
|
518
|
-
# Whether a word's prose carries a runnable example.
|
|
519
|
-
#
|
|
520
|
-
# @param prose [String, nil] a word's hand-written prose, or nil
|
|
521
|
-
# @return [Boolean] true if `prose` contains a runnable `ruby` or
|
|
522
|
-
# `ruby bluebook`/`ruby boot` fenced example
|
|
523
|
-
def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
|
|
524
|
-
|
|
525
|
-
# Every live word, paired with its prose. Both coverage gates ask a
|
|
526
|
-
# question about this same walk and differ only in what they ask of
|
|
527
|
-
# the prose, so they share it rather than each re-deriving the page
|
|
528
|
-
# set — the two are meant to move together, and one drifting past
|
|
529
|
-
# the other is how a word ends up counted documented by one and
|
|
530
|
-
# missing to the other.
|
|
531
|
-
#
|
|
532
|
-
# `harvest` already rejects empty prose and the TODO sentinel, so a
|
|
533
|
-
# word with nothing written for it arrives here with a nil.
|
|
534
|
-
#
|
|
535
|
-
# @param directory [String] path to the directory holding the committed
|
|
536
|
-
# reference pages
|
|
537
|
-
# @return [Array(String, String, String), Array(String, String, nil)]
|
|
538
|
-
# `[word, context, prose]` for every live (admitted or deprecated) word,
|
|
539
|
-
# `prose` nil if none was harvested
|
|
540
|
-
def live_words(directory)
|
|
541
|
-
rows = contexts.flat_map do |context|
|
|
542
|
-
path = File.join(directory, page_name(context))
|
|
543
|
-
prose = File.exist?(path) ? harvest(File.read(path)) : {}
|
|
544
|
-
keywords.select { |row| row[:context] == context && live?(row) }
|
|
545
|
-
.map { |row| [row[:word], context, prose[row[:word]]] }
|
|
546
|
-
end
|
|
547
|
-
rows.uniq { |word, context, _| [word, context] }
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
# Disambiguates a word by the context it is declared in.
|
|
551
|
-
#
|
|
552
|
-
# @param word [String] a word
|
|
553
|
-
# @param context [String] the context it is declared in
|
|
554
|
-
# @return [String] the word, disambiguated by its context
|
|
555
|
-
def name_of(word, context) = "#{word} (#{context})"
|
|
556
|
-
|
|
557
|
-
# The coverage gate's question: every live word with no prose yet.
|
|
558
|
-
#
|
|
559
|
-
# @param directory [String] path to the directory holding the committed
|
|
560
|
-
# reference pages
|
|
561
|
-
# @return [Array<String>] each undocumented live word, named by `name_of`
|
|
562
|
-
def undocumented(directory)
|
|
563
|
-
live_words(directory).reject { |_word, _context, prose| prose }
|
|
564
|
-
.map { |word, context, _| name_of(word, context) }
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
# The second coverage gate: prose is a declaration, and a
|
|
568
|
-
# declaration nothing runs cannot disagree with anything. A word
|
|
569
|
-
# documented only in sentences can go stale — or describe a word
|
|
570
|
-
# the runtime never wired at all, which this repository has already
|
|
571
|
-
# shipped twice (`read_model`'s where/order_by/limit/offset, and
|
|
572
|
-
# `role`/`goal` on a command). An example that runs is the only
|
|
573
|
-
# documentation that can go red.
|
|
574
|
-
#
|
|
575
|
-
# @param directory [String] path to the directory holding the committed
|
|
576
|
-
# reference pages
|
|
577
|
-
# @return [Array<String>] each live word with prose but no runnable
|
|
578
|
-
# example, named by `name_of`
|
|
579
|
-
def unexemplified(directory)
|
|
580
|
-
live_words(directory).reject { |_word, _context, prose| exemplified?(prose) }
|
|
581
|
-
.map { |word, context, _| name_of(word, context) }
|
|
582
|
-
end
|
|
583
|
-
end
|
|
584
|
-
end
|
|
585
|
-
end
|