hecks 2.7.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/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- 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/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/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
- 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.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +12 -13
- 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/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
- 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/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 +12 -6
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +3 -3
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- 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/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 +1 -1
- 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/verification.rb +11 -11
- data/lib/hecks/runtime/registry.rb +8 -4
- data/lib/hecks/runtime/saga_interpreter.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/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +6 -1
- metadata +28 -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 -658
- 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/grammar/evolve.rb
DELETED
|
@@ -1,524 +0,0 @@
|
|
|
1
|
-
module Hecks
|
|
2
|
-
module Grammar
|
|
3
|
-
# The file surgery under bin/evolve: reading and rewriting the
|
|
4
|
-
# aggregate-local KeywordSeed/ArgumentSeed rows as text, so a
|
|
5
|
-
# proposed word enters the table exactly as a hand would write it
|
|
6
|
-
# and an admitted one loses its ceremony (an absent status reads as
|
|
7
|
-
# admitted — the grown-column convention).
|
|
8
|
-
#
|
|
9
|
-
# Text, not IR, on purpose: the syntax table is source, its comments
|
|
10
|
-
# and grouping are part of the declaration, and a rewrite that
|
|
11
|
-
# round-tripped it through the IR would flatten both. Everything
|
|
12
|
-
# here touches only bare `member ` lines inside Keyword's own body
|
|
13
|
-
# (S3, ADR 0025 — no `one_of do ... end` wrapper anymore) and leaves
|
|
14
|
-
# every other byte alone.
|
|
15
|
-
module Evolve
|
|
16
|
-
class Refusal < StandardError; end
|
|
17
|
-
|
|
18
|
-
module_function
|
|
19
|
-
|
|
20
|
-
# bin/evolve's own `--name value` flag reader. Only consumes the
|
|
21
|
-
# next argv element as the value when that element doesn't itself
|
|
22
|
-
# look like a flag — otherwise `--foo --bar` would swallow `--bar`
|
|
23
|
-
# as `--foo`'s value (and `--bar` would then never be seen at
|
|
24
|
-
# all), and a value-less `--foo` at the end of argv would bypass
|
|
25
|
-
# whatever default `foo` promised instead of falling back to it.
|
|
26
|
-
# @param argv [Array<String>] the command-line argument list to scan
|
|
27
|
-
# @param name [String] the flag's name, without its leading `--`
|
|
28
|
-
# @param default [String, nil] value to use when the flag is absent or has no value
|
|
29
|
-
# @return [String, nil] the flag's value, or `default`
|
|
30
|
-
def option(argv, name, default = nil)
|
|
31
|
-
index = argv.index("--#{name}")
|
|
32
|
-
return default unless index
|
|
33
|
-
|
|
34
|
-
value = argv[index + 1]
|
|
35
|
-
value.nil? || value.start_with?("-") ? default : value
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# The snapshot/restore ceremony bin/evolve wraps every mutating
|
|
39
|
-
# command in: read every `paths` file, run the block, and put
|
|
40
|
-
# every file back exactly as it was if the block raises partway
|
|
41
|
-
# through — `rename`'s keyword-row write followed by its
|
|
42
|
-
# argument cascade across several files (`cascade_argument_rename`
|
|
43
|
-
# above) included, not only a declared gate failing after the
|
|
44
|
-
# block has already returned cleanly (bin/evolve's own `guarded`
|
|
45
|
-
# still handles that half on its own, since it depends on running
|
|
46
|
-
# the gate specs, not on anything this method knows about). A
|
|
47
|
-
# clean return leaves the snapshots unused; the caller decides
|
|
48
|
-
# from there whether the tree stands.
|
|
49
|
-
#
|
|
50
|
-
# @param paths [Array<String>] files to snapshot before running the block
|
|
51
|
-
# @yield the mutating work to run, restored from snapshot if it raises
|
|
52
|
-
# @return [Object] the block's result
|
|
53
|
-
# @raise [StandardError] re-raises whatever the block raised, after restoring
|
|
54
|
-
# every snapshotted file
|
|
55
|
-
def restore_on_raise(paths)
|
|
56
|
-
snapshots = paths.to_h { |path| [path, File.read(path)] }
|
|
57
|
-
yield
|
|
58
|
-
rescue StandardError
|
|
59
|
-
snapshots.each { |path, content| File.write(path, content) }
|
|
60
|
-
raise
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Every syntax-table file declaring a `KeywordSeed` or `ArgumentSeed`
|
|
64
|
-
# value object.
|
|
65
|
-
#
|
|
66
|
-
# @return [Array<String>] matching `.bluebook` file paths
|
|
67
|
-
def syntax_paths
|
|
68
|
-
Dir.glob(File.expand_path("../language/**/*.bluebook", __dir__)).select do |path|
|
|
69
|
-
source = File.read(path)
|
|
70
|
-
source.include?('value_object "KeywordSeed"') || source.include?('value_object "ArgumentSeed"')
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Kept as a narrow compatibility door for callers deliberately doing
|
|
75
|
-
# single-file surgery. Normal operation uses `syntax_paths` and discovers
|
|
76
|
-
# the owning concept from the row itself.
|
|
77
|
-
#
|
|
78
|
-
# @return [String, nil] the first syntax-table path, or nil when there are none
|
|
79
|
-
def syntax_path = syntax_paths.first
|
|
80
|
-
|
|
81
|
-
# Resolves the file(s) a call should search or write.
|
|
82
|
-
#
|
|
83
|
-
# @param path [String, nil] an explicit single file, or nil for every syntax path
|
|
84
|
-
# @return [Array<String>] `[path]` when given, else `syntax_paths`
|
|
85
|
-
def paths_for(path) = path ? Array(path) : syntax_paths
|
|
86
|
-
|
|
87
|
-
# The Keyword one_of's member rows, parsed leniently off the text —
|
|
88
|
-
# enough to know each row's (word, context, status), which is all
|
|
89
|
-
# the tool ever asks.
|
|
90
|
-
#
|
|
91
|
-
# @param path [String, nil] an explicit single file to read, or nil for every
|
|
92
|
-
# syntax path
|
|
93
|
-
# @return [Array<Hash>] one Hash per member row, with `:word`, `:context`,
|
|
94
|
-
# `:status` (`"admitted"` when unspelled), and `:was` (nil unless renamed)
|
|
95
|
-
# @raise [Refusal] if `path` is given and declares no `KeywordSeed` value object
|
|
96
|
-
def keyword_rows(path = nil)
|
|
97
|
-
paths_for(path).flat_map do |candidate|
|
|
98
|
-
blocks = seed_blocks(File.read(candidate), "KeywordSeed")
|
|
99
|
-
raise Refusal, "source declares no KeywordSeed value object" if path && blocks.empty?
|
|
100
|
-
|
|
101
|
-
blocks.flat_map do |block|
|
|
102
|
-
block.scan(/^\s*member (.+)$/).map do |(cells)|
|
|
103
|
-
row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
|
|
104
|
-
{ word: row["word"], context: row["context"],
|
|
105
|
-
status: row.fetch("status", "admitted"), was: row["was"] }
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Declares a new, proposed keyword row in the syntax table that owns
|
|
112
|
-
# `context` (or `opens`'s own aggregate, for a `File`-context word).
|
|
113
|
-
#
|
|
114
|
-
# @param word [String] the keyword's spelling
|
|
115
|
-
# @param context [String] the grammar context the word is declared in
|
|
116
|
-
# @param body [String] the keyword's body shape; `"none"` by default
|
|
117
|
-
# @param inner [String] the keyword's inner shape, if any
|
|
118
|
-
# @param opens [String] the aggregate concept a `File`-context word opens
|
|
119
|
-
# @param fills [String] the field the keyword fills, if any
|
|
120
|
-
# @param path [String, nil] an explicit single file to search/write, or nil to
|
|
121
|
-
# search every syntax path
|
|
122
|
-
# @return [void]
|
|
123
|
-
# @raise [Refusal] if `context`.`word` is already declared, or no syntax table
|
|
124
|
-
# owns `context`
|
|
125
|
-
def propose(word:, context:, body: "none", inner: "", opens: "", fills: "", path: nil)
|
|
126
|
-
if keyword_rows(path).any? do |row|
|
|
127
|
-
row[:word] == word && row[:context] == context
|
|
128
|
-
end
|
|
129
|
-
raise Refusal,
|
|
130
|
-
"#{context}.#{word} is already declared — one row per (word, context, form)"
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
path = owner_path(context: context, word: word, opens: opens, paths: paths_for(path))
|
|
134
|
-
source = File.read(path)
|
|
135
|
-
block = keyword_blocks(source).find { |candidate| candidate.include?(%(context: "#{context}")) } || keyword_block(source)
|
|
136
|
-
indent = block[/^(\s*)member /, 1] || " "
|
|
137
|
-
row = %(#{indent}member word: "#{word}", context: "#{context}", body: "#{body}", ) +
|
|
138
|
-
%(inner: "#{inner}", opens: "#{opens}", fills: "#{fills}", status: "proposed"\n)
|
|
139
|
-
|
|
140
|
-
# At the end of the one_of — grouping by context is a courtesy of
|
|
141
|
-
# the hand; a proposed row sits at the bottom until admission,
|
|
142
|
-
# when whoever admits it may move it home.
|
|
143
|
-
closing = block.rindex(/^\s*end\s*$/)
|
|
144
|
-
updated = block[0...closing] + row + block[closing..]
|
|
145
|
-
File.write(path, source.sub(block, updated))
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Rewrites a declared keyword row's `status:` cell in place.
|
|
149
|
-
#
|
|
150
|
-
# @param word [String] the keyword's spelling
|
|
151
|
-
# @param context [String] the grammar context the word is declared in
|
|
152
|
-
# @param to [String] the new status: `"proposed"`, `"admitted"`, `"deprecated"`,
|
|
153
|
-
# or `"retired"`
|
|
154
|
-
# @param path [String, nil] an explicit single file to search/write, or nil to
|
|
155
|
-
# search every syntax path
|
|
156
|
-
# @return [void]
|
|
157
|
-
# @raise [Refusal] if `to` is not one of the four stations, or `context`.`word`
|
|
158
|
-
# is not declared
|
|
159
|
-
def set_status(word:, context:, to:, path: nil)
|
|
160
|
-
raise Refusal, "#{to.inspect} is not a station a word's life admits" unless %w[proposed admitted deprecated
|
|
161
|
-
retired].include?(to)
|
|
162
|
-
|
|
163
|
-
path = path_holding_keyword(word, context, paths_for(path))
|
|
164
|
-
source = File.read(path)
|
|
165
|
-
block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
|
|
166
|
-
rows = block.lines.select { |line| member_row?(line, word, context) }
|
|
167
|
-
raise Refusal, "#{context}.#{word} is not declared" if rows.empty?
|
|
168
|
-
|
|
169
|
-
updated = block.lines.map do |line|
|
|
170
|
-
next line unless member_row?(line, word, context)
|
|
171
|
-
|
|
172
|
-
stripped = line.sub(/,\s*status: "[^"]*"/, "")
|
|
173
|
-
# Admitted is the default and stays unspelled — only a word
|
|
174
|
-
# entering or leaving the language carries its status.
|
|
175
|
-
to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
|
|
176
|
-
end.join
|
|
177
|
-
|
|
178
|
-
File.write(path, source.sub(block, updated))
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# A rename respells the row's word and holds the old spelling in
|
|
182
|
-
# `was:` — one hop only. Renaming an already-renamed word refuses
|
|
183
|
-
# until the language grows real eras for its own words; renaming
|
|
184
|
-
# onto a spelling the context already declares refuses too. The
|
|
185
|
-
# word's Argument rows follow it — row-aware, not a blind
|
|
186
|
-
# substitution (see `cascade_argument_rename`).
|
|
187
|
-
#
|
|
188
|
-
# @param word [String] the keyword's current spelling
|
|
189
|
-
# @param context [String] the grammar context the word is declared in
|
|
190
|
-
# @param to [String] the keyword's new spelling
|
|
191
|
-
# @param path [String, nil] an explicit single file to search/write, or nil to
|
|
192
|
-
# search every syntax path
|
|
193
|
-
# @return [void]
|
|
194
|
-
# @raise [Refusal] if `context`.`word` is not declared, was already renamed once,
|
|
195
|
-
# or `to` is already declared in `context`
|
|
196
|
-
def rename(word:, context:, to:, path: nil)
|
|
197
|
-
row = keyword_rows(path).find { |r| r[:word] == word && r[:context] == context }
|
|
198
|
-
raise Refusal, "#{context}.#{word} is not declared" unless row
|
|
199
|
-
raise Refusal, "#{context}.#{word} was already #{row[:was]} — one rename hop, then eras" if row[:was]
|
|
200
|
-
if keyword_rows(path).any? do |r|
|
|
201
|
-
r[:word] == to && r[:context] == context
|
|
202
|
-
end
|
|
203
|
-
raise Refusal,
|
|
204
|
-
"#{context}.#{to} is already declared — a rename cannot land on a living word"
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
paths = paths_for(path)
|
|
208
|
-
path = path_holding_keyword(word, context, paths)
|
|
209
|
-
source = File.read(path)
|
|
210
|
-
block = keyword_blocks(source).find { |candidate| candidate.lines.any? { |line| member_row?(line, word, context) } }
|
|
211
|
-
updated = block.lines.map do |line|
|
|
212
|
-
next line unless member_row?(line, word, context)
|
|
213
|
-
|
|
214
|
-
line.sub(%(word: "#{word}"), %(word: "#{to}"))
|
|
215
|
-
.sub(/\n\z/, %(, was: "#{word}"\n))
|
|
216
|
-
end.join
|
|
217
|
-
source = source.sub(block, updated)
|
|
218
|
-
File.write(path, source)
|
|
219
|
-
|
|
220
|
-
cascade_argument_rename(keyword: word, context: context, to: to, path: paths)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Tells whether `line` is a KeywordSeed member row for `(word, context)`.
|
|
224
|
-
#
|
|
225
|
-
# @param line [String] one raw source line
|
|
226
|
-
# @param word [String] the keyword's spelling
|
|
227
|
-
# @param context [String] the grammar context
|
|
228
|
-
# @return [Boolean]
|
|
229
|
-
def member_row?(line, word, context)
|
|
230
|
-
line =~ /^\s*member / && line.include?(%(word: "#{word}")) && line.include?(%(context: "#{context}"))
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Finds which syntax-table path declares a keyword row.
|
|
234
|
-
#
|
|
235
|
-
# @param word [String] the keyword's spelling
|
|
236
|
-
# @param context [String] the grammar context
|
|
237
|
-
# @param paths [Array<String>] candidate syntax-table paths to search
|
|
238
|
-
# @return [String] the path whose KeywordSeed declares `(word, context)`
|
|
239
|
-
# @raise [Refusal] if no path in `paths` declares that row
|
|
240
|
-
def path_holding_keyword(word, context, paths = syntax_paths)
|
|
241
|
-
paths.find do |candidate|
|
|
242
|
-
keyword_blocks(File.read(candidate)).any? { |block| block.lines.any? { |line| member_row?(line, word, context) } }
|
|
243
|
-
end || raise(Refusal, "#{context}.#{word} is not declared")
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Finds which syntax-table path declares an argument row.
|
|
247
|
-
#
|
|
248
|
-
# @param keyword [String] the argument's owning keyword
|
|
249
|
-
# @param context [String] the grammar context
|
|
250
|
-
# @param at [String] the argument's positional slot, `""` for a named-only argument
|
|
251
|
-
# @param named [String] the argument's keyword name, `""` for a positional-only argument
|
|
252
|
-
# @param paths [Array<String>] candidate syntax-table paths to search
|
|
253
|
-
# @return [String] the path whose ArgumentSeed declares this row
|
|
254
|
-
# @raise [Refusal] if no path in `paths` declares that row
|
|
255
|
-
def path_holding_argument(keyword, context, at, named, paths = syntax_paths)
|
|
256
|
-
paths.find do |candidate|
|
|
257
|
-
argument_blocks(File.read(candidate)).any? do |block|
|
|
258
|
-
block.lines.any? do |line|
|
|
259
|
-
argument_row?(line, keyword, context, at, named)
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
end || raise(Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not declared")
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# A new row belongs wherever that context's existing rows live. File is
|
|
266
|
-
# intentionally wider than one aggregate; for a new entry point, `opens`
|
|
267
|
-
# identifies the aggregate concept whose file should own it.
|
|
268
|
-
#
|
|
269
|
-
# @param context [String] the grammar context a new row is being added to
|
|
270
|
-
# @param word [String] the word being added, used only in the refusal message
|
|
271
|
-
# @param opens [String] the aggregate concept a `File`-context word opens
|
|
272
|
-
# @param paths [Array<String>] candidate syntax-table paths to search
|
|
273
|
-
# @return [String] the path that should own the new row
|
|
274
|
-
# @raise [Refusal] if no candidate path declares an existing row for `context`
|
|
275
|
-
# (and, for a `File` context with `opens` given, no path declares that aggregate)
|
|
276
|
-
def owner_path(context:, word:, opens: "", paths: syntax_paths)
|
|
277
|
-
if context == "File" && !opens.to_s.empty?
|
|
278
|
-
aggregate_path = paths.find { |candidate| File.read(candidate).match?(/^\s*aggregate "#{Regexp.escape(opens)}" do$/) }
|
|
279
|
-
return aggregate_path if aggregate_path
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
paths.find do |candidate|
|
|
283
|
-
source = File.read(candidate)
|
|
284
|
-
%w[KeywordSeed ArgumentSeed].any? do |seed|
|
|
285
|
-
seed_blocks(source, seed).any? { |block| block.include?(%(context: "#{context}")) }
|
|
286
|
-
end
|
|
287
|
-
end || raise(Refusal, "no aggregate-local syntax table owns context #{context.inspect} for #{word}")
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
# Every `value_object "<name>"` block's full source text, from its
|
|
291
|
-
# opener to its closing `end`.
|
|
292
|
-
#
|
|
293
|
-
# @param source [String] a `.bluebook` file's source text
|
|
294
|
-
# @param name [String] the value object's name, such as `"KeywordSeed"`
|
|
295
|
-
# @return [Array<String>] each matching block's raw source, including the
|
|
296
|
-
# opener and closing `end` lines
|
|
297
|
-
def seed_blocks(source, name)
|
|
298
|
-
opener = /^([ \t]*)value_object "#{Regexp.escape(name)}" do$/
|
|
299
|
-
source.to_enum(:scan, opener).map do
|
|
300
|
-
match = Regexp.last_match
|
|
301
|
-
start = match.begin(0)
|
|
302
|
-
indent = match[1]
|
|
303
|
-
closing = source.index(/^#{Regexp.escape(indent)}end\s*$/, match.end(0))
|
|
304
|
-
closing = source.index(/\n/, closing) + 1
|
|
305
|
-
source[start...closing]
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
# The first `value_object "<name>"` block's source text.
|
|
310
|
-
#
|
|
311
|
-
# @param source [String] a `.bluebook` file's source text
|
|
312
|
-
# @param name [String] the value object's name, such as `"KeywordSeed"`
|
|
313
|
-
# @param required [Boolean] whether a missing block should raise instead of
|
|
314
|
-
# returning nil
|
|
315
|
-
# @return [String, nil] the block's raw source, or nil when absent and not
|
|
316
|
-
# `required`
|
|
317
|
-
# @raise [Refusal] if `required` and `source` declares no such value object
|
|
318
|
-
def seed_block(source, name, required: false)
|
|
319
|
-
block = seed_blocks(source, name).first
|
|
320
|
-
unless block
|
|
321
|
-
raise Refusal, "source declares no #{name} value object" if required
|
|
322
|
-
|
|
323
|
-
return
|
|
324
|
-
end
|
|
325
|
-
block
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# From `value_object "KeywordSeed"` to its own closing `end` — `member`
|
|
329
|
-
# rows sit bare now (S3, ADR 0025 — the `one_of do ... end` wrapper
|
|
330
|
-
# is gone), so the first bare `end` line after the opener already
|
|
331
|
-
# is the value object's own, the same fact the original one_of-
|
|
332
|
-
# nested version of this method leaned on (nothing else nested
|
|
333
|
-
# inside it either, before or after).
|
|
334
|
-
# @param source [String] a `.bluebook` file's source text
|
|
335
|
-
# @return [Array<String>] each KeywordSeed block's raw source
|
|
336
|
-
def keyword_blocks(source) = seed_blocks(source, "KeywordSeed")
|
|
337
|
-
|
|
338
|
-
# The first KeywordSeed block's source text.
|
|
339
|
-
#
|
|
340
|
-
# @param source [String] a `.bluebook` file's source text
|
|
341
|
-
# @return [String] the first KeywordSeed block's raw source
|
|
342
|
-
# @raise [Refusal] if `source` declares no KeywordSeed value object
|
|
343
|
-
def keyword_block(source) = seed_block(source, "KeywordSeed", required: true)
|
|
344
|
-
|
|
345
|
-
# ── the Argument rows — a word's own arguments, at last with tooling
|
|
346
|
-
# of their own rather than the rename-only cascade above. A word may
|
|
347
|
-
# carry several argument rows (one per position, one per named
|
|
348
|
-
# kwarg), so identity here is the full (keyword, context, at, named)
|
|
349
|
-
# tuple, not the two-field key a Keyword row answers to.
|
|
350
|
-
|
|
351
|
-
# The ArgumentSeed's member rows, parsed leniently off the text.
|
|
352
|
-
#
|
|
353
|
-
# @param path [String, nil] an explicit single file to read, or nil for every
|
|
354
|
-
# syntax path
|
|
355
|
-
# @return [Array<Hash>] one Hash per member row, with `:keyword`, `:context`,
|
|
356
|
-
# `:at`, `:named`, `:kind`, `:required`, `:fills`, and `:status`
|
|
357
|
-
# (`"admitted"` when unspelled)
|
|
358
|
-
# @raise [Refusal] if `path` is given and declares no `ArgumentSeed` value object
|
|
359
|
-
def argument_rows(path = nil)
|
|
360
|
-
paths_for(path).flat_map do |candidate|
|
|
361
|
-
blocks = seed_blocks(File.read(candidate), "ArgumentSeed")
|
|
362
|
-
raise Refusal, "source declares no ArgumentSeed value object" if path && blocks.empty?
|
|
363
|
-
|
|
364
|
-
blocks.flat_map do |block|
|
|
365
|
-
block.scan(/^\s*member (.+)$/).map do |(cells)|
|
|
366
|
-
row = cells.scan(/(\w+): "((?:[^"\\]|\\.)*)"/).to_h
|
|
367
|
-
{ keyword: row["keyword"], context: row["context"], at: row["at"].to_s,
|
|
368
|
-
named: row["named"].to_s, kind: row["kind"], required: row["required"],
|
|
369
|
-
fills: row["fills"].to_s, status: row.fetch("status", "admitted") }
|
|
370
|
-
end
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
# `pairs_shape` — for a `pairs` argument that fills one field with a
|
|
376
|
-
# whole key/value list rather than naming a field per pair (the
|
|
377
|
-
# shape `Handler.dispatch`'s own `with:` already carries). Without
|
|
378
|
-
# it, `spec/syntax_conformance_spec.rb` reads a pairs argument
|
|
379
|
-
# naming a single field as a row that "names a single field, which
|
|
380
|
-
# it cannot fill" — correctly, since the two shapes are genuinely
|
|
381
|
-
# different and only one of them can be checked the same way.
|
|
382
|
-
# @param keyword [String] the argument's owning keyword
|
|
383
|
-
# @param context [String] the grammar context
|
|
384
|
-
# @param kind [String] the argument's value kind
|
|
385
|
-
# @param required [String] `"true"` or `"false"`, as text like every other cell
|
|
386
|
-
# @param at [String] the argument's positional slot, `""` for a named-only argument
|
|
387
|
-
# @param named [String] the argument's keyword name, `""` for a positional-only argument
|
|
388
|
-
# @param fills [String] the field the argument fills, if any
|
|
389
|
-
# @param pairs_shape [String, nil] the shape a `pairs` argument's key/value list
|
|
390
|
-
# fills, or nil when this argument is not a `pairs` argument
|
|
391
|
-
# @param path [String, nil] an explicit single file to search/write, or nil to
|
|
392
|
-
# search every syntax path
|
|
393
|
-
# @return [void]
|
|
394
|
-
# @raise [Refusal] if this (keyword, context, at, named) row is already declared,
|
|
395
|
-
# or no syntax table owns `context`
|
|
396
|
-
def propose_argument(keyword:, context:, kind:, required: "false", at: "", named: "", fills: "",
|
|
397
|
-
pairs_shape: nil, path: nil)
|
|
398
|
-
if argument_rows(path).any? { |r| argument_identity(r) == [keyword, context, at, named] }
|
|
399
|
-
raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is " \
|
|
400
|
-
"already declared — one row per (keyword, context, at, named)"
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
path = owner_path(context: context, word: keyword, paths: paths_for(path))
|
|
404
|
-
source = File.read(path)
|
|
405
|
-
block = argument_blocks(source).find do |candidate|
|
|
406
|
-
candidate.include?(%(context: "#{context}"))
|
|
407
|
-
end || argument_block(source)
|
|
408
|
-
indent = block[/^(\s*)member /, 1] || " "
|
|
409
|
-
shape = pairs_shape.to_s.empty? ? "" : %(pairs_shape: "#{pairs_shape}", )
|
|
410
|
-
row = %(#{indent}member keyword: "#{keyword}", context: "#{context}", at: "#{at}", ) +
|
|
411
|
-
%(named: "#{named}", kind: "#{kind}", required: "#{required}", fills: "#{fills}", ) +
|
|
412
|
-
shape + %(status: "proposed"\n)
|
|
413
|
-
|
|
414
|
-
closing = block.rindex(/^\s*end\s*$/)
|
|
415
|
-
updated = block[0...closing] + row + block[closing..]
|
|
416
|
-
File.write(path, source.sub(block, updated))
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
# Rewrites a declared argument row's `status:` cell in place.
|
|
420
|
-
#
|
|
421
|
-
# @param keyword [String] the argument's owning keyword
|
|
422
|
-
# @param context [String] the grammar context
|
|
423
|
-
# @param to [String] the new status: `"proposed"`, `"admitted"`, `"deprecated"`,
|
|
424
|
-
# or `"retired"`
|
|
425
|
-
# @param at [String] the argument's positional slot, `""` for a named-only argument
|
|
426
|
-
# @param named [String] the argument's keyword name, `""` for a positional-only argument
|
|
427
|
-
# @param path [String, nil] an explicit single file to search/write, or nil to
|
|
428
|
-
# search every syntax path
|
|
429
|
-
# @return [void]
|
|
430
|
-
# @raise [Refusal] if `to` is not one of the four stations, or the row is not declared
|
|
431
|
-
def set_argument_status(keyword:, context:, to:, at: "", named: "", path: nil)
|
|
432
|
-
raise Refusal, "#{to.inspect} is not a station an argument's life admits" unless %w[proposed admitted deprecated
|
|
433
|
-
retired].include?(to)
|
|
434
|
-
|
|
435
|
-
path = path_holding_argument(keyword, context, at, named, paths_for(path))
|
|
436
|
-
source = File.read(path)
|
|
437
|
-
block = argument_blocks(source).find do |candidate|
|
|
438
|
-
candidate.lines.any? do |line|
|
|
439
|
-
argument_row?(line, keyword, context, at, named)
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
rows = block.lines.select { |line| argument_row?(line, keyword, context, at, named) }
|
|
443
|
-
if rows.empty?
|
|
444
|
-
raise Refusal, "#{context}.#{keyword}'s argument at #{at.inspect}/named #{named.inspect} is not " \
|
|
445
|
-
"declared"
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
updated = block.lines.map do |line|
|
|
449
|
-
next line unless argument_row?(line, keyword, context, at, named)
|
|
450
|
-
|
|
451
|
-
stripped = line.sub(/,\s*status: "[^"]*"/, "")
|
|
452
|
-
to == "admitted" ? stripped : stripped.sub(/\n\z/, %(, status: "#{to}"\n))
|
|
453
|
-
end.join
|
|
454
|
-
|
|
455
|
-
File.write(path, source.sub(block, updated))
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
# Tells whether `line` is an ArgumentSeed member row for this
|
|
459
|
-
# (keyword, context, at, named) tuple.
|
|
460
|
-
#
|
|
461
|
-
# @param line [String] one raw source line
|
|
462
|
-
# @param keyword [String] the argument's owning keyword
|
|
463
|
-
# @param context [String] the grammar context
|
|
464
|
-
# @param at [String] the argument's positional slot
|
|
465
|
-
# @param named [String] the argument's keyword name
|
|
466
|
-
# @return [Boolean]
|
|
467
|
-
def argument_row?(line, keyword, context, at, named)
|
|
468
|
-
line =~ /^\s*member / &&
|
|
469
|
-
line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}")) &&
|
|
470
|
-
line.include?(%(at: "#{at}")) && line.include?(%(named: "#{named}"))
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
# The identity tuple an argument row is keyed by.
|
|
474
|
-
#
|
|
475
|
-
# @param row [Hash] an argument row, as `argument_rows` returns one
|
|
476
|
-
# @return [Array(String, String, String, String)] the row's `(keyword, context,
|
|
477
|
-
# at, named)` identity tuple
|
|
478
|
-
def argument_identity(row) = [row[:keyword], row[:context], row[:at], row[:named]]
|
|
479
|
-
|
|
480
|
-
# The rename cascade, row-aware — only the rows that actually belong
|
|
481
|
-
# to the renamed word, spelling updated in place, rather than a
|
|
482
|
-
# blind `gsub` on every `keyword: "word",` substring in the file
|
|
483
|
-
# (which a coincidentally-matching row elsewhere could have
|
|
484
|
-
# corrupted, and which read nothing before writing).
|
|
485
|
-
# @param keyword [String] the keyword whose argument rows follow its rename
|
|
486
|
-
# @param context [String] the grammar context
|
|
487
|
-
# @param to [String] the keyword's new spelling
|
|
488
|
-
# @param path [String, nil] an explicit single file to search/write, or nil to
|
|
489
|
-
# search every syntax path
|
|
490
|
-
# @return [void]
|
|
491
|
-
def cascade_argument_rename(keyword:, context:, to:, path: nil)
|
|
492
|
-
paths_for(path).each do |candidate|
|
|
493
|
-
source = File.read(candidate)
|
|
494
|
-
original = source
|
|
495
|
-
argument_blocks(source).each do |block|
|
|
496
|
-
updated = block.lines.map do |line|
|
|
497
|
-
next line unless line =~ /^\s*member / &&
|
|
498
|
-
line.include?(%(keyword: "#{keyword}")) && line.include?(%(context: "#{context}"))
|
|
499
|
-
|
|
500
|
-
line.sub(%(keyword: "#{keyword}"), %(keyword: "#{to}"))
|
|
501
|
-
end.join
|
|
502
|
-
source = source.sub(block, updated) if updated != block
|
|
503
|
-
end
|
|
504
|
-
File.write(candidate, source) if source != original
|
|
505
|
-
end
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
# From `value_object "ArgumentSeed"` to its own closing `end` — see
|
|
509
|
-
# `keyword_block`'s own comment for why the first bare `end` after
|
|
510
|
-
# the opener is already the right one, now that `member` rows sit
|
|
511
|
-
# bare (S3, ADR 0025).
|
|
512
|
-
# @param source [String] a `.bluebook` file's source text
|
|
513
|
-
# @return [Array<String>] each ArgumentSeed block's raw source
|
|
514
|
-
def argument_blocks(source) = seed_blocks(source, "ArgumentSeed")
|
|
515
|
-
|
|
516
|
-
# The first ArgumentSeed block's source text.
|
|
517
|
-
#
|
|
518
|
-
# @param source [String] a `.bluebook` file's source text
|
|
519
|
-
# @return [String] the first ArgumentSeed block's raw source
|
|
520
|
-
# @raise [Refusal] if `source` declares no ArgumentSeed value object
|
|
521
|
-
def argument_block(source) = seed_block(source, "ArgumentSeed", required: true)
|
|
522
|
-
end
|
|
523
|
-
end
|
|
524
|
-
end
|