hecks 1.2.0 → 1.3.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/heki/journal.rb +57 -0
  3. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  4. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  5. data/lib/hecks/behaviors/expectations.rb +32 -4
  6. data/lib/hecks/bluebook/behaviour/domain_port.rb +24 -0
  7. data/lib/hecks/bluebook/meta_validator/judge.rb +25 -3
  8. data/lib/hecks/bluebook/model_check.rb +148 -17
  9. data/lib/hecks/forms/field_shape.rb +5 -3
  10. data/lib/hecks/fuzzing/concurrent_dispatch.rb +266 -0
  11. data/lib/hecks/fuzzing/era_boundary.rb +105 -0
  12. data/lib/hecks/fuzzing/form_census.rb +184 -0
  13. data/lib/hecks/fuzzing/isolated_boot.rb +198 -10
  14. data/lib/hecks/fuzzing/persistence_parity.rb +163 -0
  15. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  16. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +188 -13
  17. data/lib/hecks/fuzzing/properties/guards.rb +103 -0
  18. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +34 -1
  19. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  20. data/lib/hecks/fuzzing/properties.rb +23 -2
  21. data/lib/hecks/fuzzing/replay.rb +130 -20
  22. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  23. data/lib/hecks/fuzzing/self_consistency.rb +647 -0
  24. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  25. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +90 -26
  26. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +50 -4
  27. data/lib/hecks/fuzzing/sequence_generator/picker.rb +11 -0
  28. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +73 -9
  29. data/lib/hecks/fuzzing/sequence_generator.rb +47 -14
  30. data/lib/hecks/fuzzing/structural_skips.rb +146 -0
  31. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  32. data/lib/hecks/fuzzing/target_capabilities.rb +149 -0
  33. data/lib/hecks/fuzzing/value_generator.rb +55 -3
  34. data/lib/hecks/fuzzing.rb +6 -0
  35. data/lib/hecks/language/bluebook/vocabulary.bluebook +17 -2
  36. data/lib/hecks/naming.rb +70 -2
  37. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +50 -0
  38. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -0
  39. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +31 -0
  40. data/lib/hecks/ports/persistence/repository_factory.rb +8 -5
  41. data/lib/hecks/projections/glossary/html.rb +250 -0
  42. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  43. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  44. data/lib/hecks/projections/glossary/page.css +271 -0
  45. data/lib/hecks/projections/glossary/page.js +72 -0
  46. data/lib/hecks/projections/glossary/sections.rb +17 -0
  47. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  48. data/lib/hecks/projections/glossary.rb +214 -286
  49. data/lib/hecks/projector/narrate_projector.rb +4 -11
  50. data/lib/hecks/query_specification/common/comparison.rb +27 -1
  51. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +26 -8
  52. data/lib/hecks/runtime/command_rules/references.rb +75 -0
  53. data/lib/hecks/runtime/entity_element.rb +168 -16
  54. data/lib/hecks/runtime/entity_interpreter.rb +68 -3
  55. data/lib/hecks/runtime/query_interpreter.rb +66 -2
  56. data/lib/hecks/runtime/reaction_invocation.rb +70 -3
  57. data/lib/hecks/runtime/refusal_wording.rb +5 -2
  58. data/lib/hecks/runtime/registry.rb +12 -0
  59. data/lib/hecks/runtime/routing.rb +67 -2
  60. data/lib/hecks/runtime/saga_interpreter.rb +38 -1
  61. data/lib/hecks/runtime/value/coercion.rb +77 -115
  62. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  63. data/lib/hecks/runtime/value.rb +7 -2
  64. data/lib/hecks/version.rb +1 -1
  65. data/lib/hecks/vocabulary.rb +2 -1
  66. metadata +23 -2
@@ -0,0 +1,105 @@
1
+ require_relative "sections"
2
+ require_relative "sentences"
3
+ require_relative "mermaid"
4
+ require_relative "../statements"
5
+
6
+ module Hecks
7
+ module Projections
8
+ module Glossary
9
+ # THE DOCUMENT ITSELF — `glossary.md`, the one source the page is
10
+ # rendered from. Written in the handful of Markdown constructs
11
+ # GitHub renders as-is (headings, blockquotes, ```mermaid fences,
12
+ # lists, in-page links), so the file is a complete, readable
13
+ # glossary on its own before any page is built from it:
14
+ #
15
+ # # Banking — Glossary
16
+ # > vision
17
+ # lede
18
+ # ```mermaid the map
19
+ # ## Account
20
+ # > description
21
+ # Starts out open. Can be open, frozen, or closed.
22
+ # **How it fits** ```mermaid
23
+ # **How it moves** ```mermaid
24
+ # **Always true** - one sentence per declared fact
25
+ # ### Account number one ### per term, A to Z
26
+ # Made up of a value (text).
27
+ # Always true: an account number is present.
28
+ module Markdown
29
+ LEDE = "Every term %<name>s uses, in the words of the people who work in it — grouped under the thing " \
30
+ "each belongs to, and listed A to Z within it. This page is generated from the working " \
31
+ "specification, so it says what the system does today, not what anyone hoped it would do. " \
32
+ "If a sentence here reads wrong to you, the specification is wrong: say so.".freeze
33
+
34
+ STANDING = {
35
+ ROLES => "Who does what. A role is named once here rather than under every term it touches.",
36
+ READ_MODELS => "Questions answered across more than one of the things above.",
37
+ REACTIONS => "What happens on its own, in response to something this specification does not itself raise."
38
+ }.freeze
39
+
40
+ module_function
41
+
42
+ def title(bluebook) = "#{bluebook.name} — Glossary"
43
+
44
+ def render(document)
45
+ parts = [header(document.bluebook)]
46
+ parts += document.sections.map { |section| section_text(section, document) }
47
+ "#{parts.join("\n\n")}\n"
48
+ end
49
+
50
+ def header(bluebook)
51
+ parts = ["# #{title(bluebook)}"]
52
+ parts << "> #{bluebook.vision}" if bluebook.vision
53
+ parts << format(LEDE, name: bluebook.name)
54
+ parts << fence(Mermaid.map(bluebook))
55
+ parts.join("\n\n")
56
+ end
57
+
58
+ def section_text(section, document)
59
+ parts = ["## #{section.title}"]
60
+ parts += section.aggregate ? opening(section.aggregate, document.bluebook) : ["> #{STANDING[section.name]}"]
61
+ parts += section.terms.map { |entry| term_text(entry, document.index) }
62
+ parts.join("\n\n")
63
+ end
64
+
65
+ # What an aggregate IS, what it can be, how it fits and moves,
66
+ # and what is always true of it — before a single term.
67
+ def opening(aggregate, bluebook)
68
+ parts = []
69
+ parts << "> #{aggregate.description}" if aggregate.description
70
+ parts << Sentences.lifecycle_sentence(aggregate.lifecycle) if aggregate.lifecycle
71
+ parts << "**How it fits**" << fence(Mermaid.context(aggregate, bluebook))
72
+ parts << "**How it moves**" << fence(Mermaid.lifecycle(aggregate)) if aggregate.lifecycle
73
+ statements = always_true(aggregate, bluebook)
74
+ parts << "**Always true**" << statements.map { |statement| "- #{statement}" }.join("\n") unless statements.empty?
75
+ parts
76
+ end
77
+
78
+ # THE STATEMENTS PROJECTION'S OWN SENTENCES, SPOKEN. An invariant
79
+ # is the author's words and stays exactly so. A relationship
80
+ # sentence is built mechanically from construct names ("An
81
+ # ATMCard references an Account."), and a construct name is the
82
+ # one thing this page never shows as an identifier — so each is
83
+ # replaced with how it is said, whole-word, and nothing else in
84
+ # the sentence is touched.
85
+ def always_true(aggregate, bluebook)
86
+ names = bluebook.aggregates.flat_map { |other| [other.hecks_name, *other.entities.map(&:hecks_name)] }
87
+ [aggregate, *aggregate.entities].flat_map do |holder|
88
+ Statements.attribute_statements(holder).map { |sentence| spoken(sentence, names) } +
89
+ Statements.invariant_statements(holder)
90
+ end.uniq
91
+ end
92
+
93
+ def spoken(sentence, names)
94
+ names.reduce(sentence) { |text, name| text.gsub(/\b#{Regexp.escape(name)}\b/, Naming.words(name)) }
95
+ end
96
+
97
+ def term_text(entry, index)
98
+ ["### #{entry.headword}", *Sentences.paragraphs(entry, index)].join("\n\n")
99
+ end
100
+
101
+ def fence(source) = "```mermaid\n#{source}\n```"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,110 @@
1
+ require_relative "../../naming"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Glossary
6
+ # THE THREE PICTURES THE GLOSSARY DRAWS, each kept to the handful of
7
+ # things that matter — Evans' "three to five objects central to the
8
+ # issue at hand", never the whole model at once.
9
+ #
10
+ # map the aggregates and which points at which — one small
11
+ # graph at the top, for the reader who wants the shape
12
+ # of the domain before its words.
13
+ # context one aggregate, its entities, and the neighbours it
14
+ # points at or is pointed at by. Value objects are
15
+ # words in the text ("Made up of …"), not boxes here —
16
+ # Account alone has eight, and eight boxes say less
17
+ # than one sentence.
18
+ # lifecycle the states a thing can be in and what moves it.
19
+ #
20
+ # Every label is a person's spelling (`Naming.words`), and the
21
+ # edges read from the same declared facts `Projections::Diagrams`
22
+ # draws its own from (`reference?`, `type.target_name`, `name`,
23
+ # `lifecycle.transitions`) — a different picture of the same
24
+ # declaration, not a second declaration.
25
+ module Mermaid
26
+ module_function
27
+
28
+ def map(bluebook)
29
+ names = bluebook.aggregates.map(&:hecks_name)
30
+ lines = ["flowchart LR"]
31
+ names.each { |name| lines << node(name) }
32
+ bluebook.aggregates.each do |aggregate|
33
+ references(aggregate).each do |attribute|
34
+ target = attribute.type.target_name
35
+ lines << node(target) unless names.include?(target) || lines.include?(node(target))
36
+ lines << edge(aggregate.hecks_name, attribute, target)
37
+ end
38
+ end
39
+ lines.join("\n")
40
+ end
41
+
42
+ def context(aggregate, bluebook)
43
+ focus = aggregate.hecks_name
44
+ lines = ["flowchart LR", node(focus, focus: true)]
45
+ entities(aggregate).each do |attribute|
46
+ lines << node(attribute.type.to_s)
47
+ lines << edge(focus, attribute, attribute.type.to_s)
48
+ end
49
+ references(aggregate).each do |attribute|
50
+ lines << node(attribute.type.target_name)
51
+ lines << edge(focus, attribute, attribute.type.target_name)
52
+ end
53
+ pointing_at(aggregate, bluebook).each do |holder, attribute|
54
+ lines << node(holder.hecks_name)
55
+ lines << edge(holder.hecks_name, attribute, focus)
56
+ end
57
+ lines << " classDef focus stroke-width:3px"
58
+ lines.uniq.join("\n")
59
+ end
60
+
61
+ def lifecycle(holder)
62
+ lifecycle = holder.lifecycle
63
+ lines = ["stateDiagram-v2"]
64
+ states = ([lifecycle.default] + lifecycle.transitions.map { |_name, transition| transition.target }).uniq
65
+ states.select { |state| state.to_s.include?("_") }.each do |state|
66
+ lines << " state \"#{state.to_s.tr('_', ' ')}\" as #{state}"
67
+ end
68
+ lines << " [*] --> #{lifecycle.default}"
69
+ lifecycle.transitions.each do |name, transition|
70
+ Array(transition.from).each { |from| lines << " #{from} --> #{transition.target}: #{Naming.words(name)}" }
71
+ end
72
+ lines.join("\n")
73
+ end
74
+
75
+ # ── the facts drawn ──────────────────────────────────────────────
76
+
77
+ def references(holder) = holder.attributes.select(&:reference?)
78
+
79
+ # An entity is held as `list_of(LedgerEntry)` — the attribute
80
+ # whose element type names one of the aggregate's own entities.
81
+ def entities(aggregate)
82
+ names = aggregate.entities.map(&:hecks_name)
83
+ aggregate.attributes.select { |attribute| !attribute.reference? && names.include?(attribute.type.to_s) }
84
+ end
85
+
86
+ def pointing_at(aggregate, bluebook)
87
+ bluebook.aggregates.reject { |other| other.equal?(aggregate) }.flat_map do |other|
88
+ references(other).select { |attribute| attribute.type.target_name == aggregate.hecks_name }
89
+ .map { |attribute| [other, attribute] }
90
+ end
91
+ end
92
+
93
+ # ── the drawing ──────────────────────────────────────────────────
94
+
95
+ # A node id the page source can carry without an identifier in
96
+ # it — `n_atm_card`, never `n_ATMCard`.
97
+ def id(name) = "n_#{Naming.snake(name).gsub(/[^a-z0-9_]/, '_')}"
98
+
99
+ def node(name, focus: false)
100
+ line = " #{id(name)}[\"#{Naming.words(name)}\"]"
101
+ focus ? "#{line}:::focus" : line
102
+ end
103
+
104
+ def edge(from, attribute, to)
105
+ " #{id(from)} -->|\"#{Naming.words(attribute.name).downcase}\"| #{id(to)}"
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,271 @@
1
+ /* The glossary page. Ledger paper: faint blue horizontal rules between
2
+ entries, one red vertical margin rule down the navigation. The rules
3
+ carry information — the red marks the section in view, the blue
4
+ separates entries — and nothing else on the page is decoration. */
5
+
6
+ :root {
7
+ --paper: #f3f5f8;
8
+ --panel: #ffffff;
9
+ --ink: #1a2b4a;
10
+ --ink-soft: #4a5873;
11
+ --ink-faint: #8592a8;
12
+ --rule: #c9d3e3;
13
+ --red: #b0413e;
14
+ --red-wash: rgba(176, 65, 62, 0.09);
15
+ }
16
+
17
+ @media (prefers-color-scheme: dark) {
18
+ :root:not([data-theme="light"]) {
19
+ --paper: #0f1726;
20
+ --panel: #141e33;
21
+ --ink: #e3e7ee;
22
+ --ink-soft: #b4bdd0;
23
+ --ink-faint: #7f8ba6;
24
+ --rule: #2a3854;
25
+ --red: #d9635f;
26
+ --red-wash: rgba(217, 99, 95, 0.14);
27
+ }
28
+ }
29
+
30
+ :root[data-theme="dark"] {
31
+ --paper: #0f1726;
32
+ --panel: #141e33;
33
+ --ink: #e3e7ee;
34
+ --ink-soft: #b4bdd0;
35
+ --ink-faint: #7f8ba6;
36
+ --rule: #2a3854;
37
+ --red: #d9635f;
38
+ --red-wash: rgba(217, 99, 95, 0.14);
39
+ }
40
+
41
+ html { color-scheme: light dark; scroll-behavior: smooth; }
42
+
43
+ body {
44
+ margin: 0;
45
+ background: var(--paper);
46
+ color: var(--ink);
47
+ font-family: "Newsreader", Georgia, "Times New Roman", serif;
48
+ font-size: 17px;
49
+ line-height: 1.55;
50
+ font-variant-numeric: oldstyle-nums;
51
+ }
52
+
53
+ a { color: var(--red); }
54
+ :focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
55
+
56
+ .page {
57
+ display: grid;
58
+ grid-template-columns: 15.5rem minmax(0, 1fr);
59
+ min-height: 100vh;
60
+ }
61
+
62
+ /* ── the rail ───────────────────────────────────────────────────── */
63
+
64
+ .rail {
65
+ position: sticky;
66
+ top: 0;
67
+ height: 100vh;
68
+ box-sizing: border-box;
69
+ overflow-y: auto;
70
+ padding: 2.4rem 1.3rem 2rem 1.6rem;
71
+ border-right: 1px solid var(--rule);
72
+ font-family: "Instrument Sans", system-ui, -apple-system, sans-serif;
73
+ font-size: 0.92rem;
74
+ }
75
+
76
+ .rail .domain {
77
+ font-family: "Newsreader", Georgia, serif;
78
+ font-size: 1.55rem;
79
+ font-weight: 600;
80
+ line-height: 1.1;
81
+ margin: 0;
82
+ }
83
+
84
+ .rail .sub { color: var(--ink-faint); margin: 0.15rem 0 1.4rem; font-size: 0.82rem; }
85
+
86
+ .rail input {
87
+ width: 100%;
88
+ box-sizing: border-box;
89
+ font: inherit;
90
+ padding: 0.45rem 0.65rem;
91
+ border: 1px solid var(--rule);
92
+ border-radius: 4px;
93
+ background: var(--panel);
94
+ color: var(--ink);
95
+ }
96
+
97
+ .rail input::placeholder { color: var(--ink-faint); }
98
+
99
+ .rail .count { color: var(--ink-faint); font-size: 0.78rem; margin: 0.45rem 0 1.2rem; }
100
+
101
+ .rail ol {
102
+ list-style: none;
103
+ margin: 0;
104
+ padding: 0;
105
+ border-left: 2px solid var(--red);
106
+ }
107
+
108
+ .rail li a {
109
+ display: block;
110
+ padding: 0.28rem 0.85rem;
111
+ margin-left: -2px;
112
+ border-left: 2px solid transparent;
113
+ color: var(--ink-soft);
114
+ text-decoration: none;
115
+ }
116
+
117
+ .rail li a:hover { color: var(--ink); }
118
+
119
+ .rail li a[aria-current="true"] {
120
+ color: var(--ink);
121
+ font-weight: 600;
122
+ border-left-color: var(--ink);
123
+ }
124
+
125
+ /* ── the text column ────────────────────────────────────────────── */
126
+
127
+ main {
128
+ padding: 3.2rem 4rem 7rem 3.6rem;
129
+ max-width: 47rem;
130
+ }
131
+
132
+ header h1 {
133
+ font-size: 3rem;
134
+ font-weight: 500;
135
+ line-height: 1.02;
136
+ letter-spacing: -0.012em;
137
+ margin: 0 0 1.1rem;
138
+ }
139
+
140
+ .vision {
141
+ font-size: 1.28rem;
142
+ font-style: italic;
143
+ line-height: 1.45;
144
+ color: var(--ink-soft);
145
+ margin: 0 0 1.2rem;
146
+ }
147
+
148
+ .lede { color: var(--ink-soft); margin: 0 0 1.6rem; max-width: 62ch; }
149
+
150
+ figure { margin: 1.2rem 0 1.8rem; }
151
+
152
+ figcaption, .caption {
153
+ font-family: "Instrument Sans", system-ui, sans-serif;
154
+ font-size: 0.8rem;
155
+ color: var(--ink-faint);
156
+ margin: 0 0 0.4rem;
157
+ }
158
+
159
+ .caption { margin-top: 1.6rem; }
160
+
161
+ pre.mermaid {
162
+ margin: 0;
163
+ padding: 1rem 1.2rem;
164
+ background: var(--panel);
165
+ border: 1px solid var(--rule);
166
+ border-radius: 6px;
167
+ overflow-x: auto;
168
+ font-family: "Instrument Sans", system-ui, sans-serif;
169
+ font-size: 0.85rem;
170
+ line-height: 1.4;
171
+ color: var(--ink-soft);
172
+ }
173
+
174
+ pre.mermaid svg { display: block; max-width: 100%; height: auto; }
175
+
176
+ section { margin-top: 4.5rem; scroll-margin-top: 1.5rem; }
177
+
178
+ section h2 {
179
+ display: inline-block;
180
+ font-size: 2.1rem;
181
+ font-weight: 500;
182
+ line-height: 1.1;
183
+ margin: 0 0 0.5rem;
184
+ padding-bottom: 0.3rem;
185
+ border-bottom: 2px solid var(--red);
186
+ }
187
+
188
+ .about {
189
+ font-size: 1.18rem;
190
+ color: var(--ink-soft);
191
+ margin: 0.5rem 0 0.7rem;
192
+ max-width: 60ch;
193
+ }
194
+
195
+ section > p:not(.about):not(.caption) { margin: 0 0 0.8rem; max-width: 62ch; }
196
+
197
+ .rules {
198
+ list-style: none;
199
+ margin: 0 0 1.2rem;
200
+ padding: 0;
201
+ max-width: 62ch;
202
+ }
203
+
204
+ .rules li {
205
+ padding: 0.38rem 0;
206
+ border-bottom: 1px solid var(--rule);
207
+ }
208
+
209
+ .terms { margin-top: 1.8rem; border-top: 1px solid var(--rule); }
210
+
211
+ .term {
212
+ display: grid;
213
+ grid-template-columns: 12.5rem minmax(0, 1fr);
214
+ column-gap: 1.6rem;
215
+ padding: 0.9rem 0;
216
+ border-bottom: 1px solid var(--rule);
217
+ scroll-margin-top: 1.5rem;
218
+ }
219
+
220
+ .term h3 {
221
+ font-size: 1.08rem;
222
+ font-weight: 600;
223
+ line-height: 1.35;
224
+ margin: 0.05rem 0 0;
225
+ }
226
+
227
+ .term p { margin: 0 0 0.4rem; }
228
+ .term p:last-child { margin-bottom: 0; }
229
+
230
+ .term p.rule { color: var(--ink-soft); font-size: 0.95rem; }
231
+
232
+ .term a, .rules a {
233
+ color: var(--red);
234
+ font-style: italic;
235
+ text-decoration: underline;
236
+ text-decoration-thickness: 1px;
237
+ text-underline-offset: 2px;
238
+ }
239
+
240
+ /* A search narrows the page to the terms that match. */
241
+ section.searching figure,
242
+ section.searching .rules,
243
+ section.searching .caption,
244
+ section.searching .about,
245
+ section.searching > p { display: none; }
246
+
247
+ section.searching .terms { border-top: 0; margin-top: 0.8rem; }
248
+
249
+ /* Landing on a term from a link. */
250
+ .landed { animation: land 1.4s ease-out; }
251
+
252
+ @keyframes land {
253
+ from { background: var(--red-wash); }
254
+ to { background: transparent; }
255
+ }
256
+
257
+ @media (max-width: 760px) {
258
+ .page { grid-template-columns: 1fr; }
259
+ .rail { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--rule); }
260
+ .rail ol { display: flex; flex-wrap: wrap; gap: 0.2rem 0.6rem; border-left: 0; }
261
+ .rail li a { padding: 0.2rem 0.4rem; border-left: 0; }
262
+ .rail li a[aria-current="true"] { border-bottom: 2px solid var(--red); }
263
+ main { padding: 1.8rem 1.2rem 4rem; }
264
+ header h1 { font-size: 2.3rem; }
265
+ .term { grid-template-columns: 1fr; row-gap: 0.25rem; }
266
+ }
267
+
268
+ @media (prefers-reduced-motion: reduce) {
269
+ html { scroll-behavior: auto; }
270
+ .landed { animation: none; }
271
+ }
@@ -0,0 +1,72 @@
1
+ // The page's three behaviours: draw the diagrams, find a term, and
2
+ // keep the rail pointing at the section in view. Nothing here changes
3
+ // what the page says — that is all in the Markdown it was rendered from.
4
+ (function () {
5
+ var root = document.documentElement;
6
+ var dark = root.dataset.theme === "dark" ||
7
+ (root.dataset.theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
8
+
9
+ if (window.mermaid) {
10
+ mermaid.initialize({
11
+ startOnLoad: true,
12
+ theme: dark ? "dark" : "neutral",
13
+ fontFamily: "\"Instrument Sans\", system-ui, sans-serif",
14
+ flowchart: { curve: "basis", padding: 12 }
15
+ });
16
+ }
17
+
18
+ var input = document.querySelector(".rail input");
19
+ var count = document.querySelector(".rail .count");
20
+ var terms = Array.prototype.slice.call(document.querySelectorAll(".term"));
21
+ var sections = Array.prototype.slice.call(document.querySelectorAll("main section"));
22
+ var links = {};
23
+ Array.prototype.forEach.call(document.querySelectorAll(".rail a"), function (a) {
24
+ links[a.getAttribute("href").slice(1)] = a;
25
+ });
26
+
27
+ function filter() {
28
+ var query = input.value.trim().toLowerCase();
29
+ var shown = 0;
30
+ terms.forEach(function (term) {
31
+ var hit = !query || term.textContent.toLowerCase().indexOf(query) !== -1;
32
+ term.hidden = !hit;
33
+ if (hit) shown += 1;
34
+ });
35
+ sections.forEach(function (section) {
36
+ var any = Array.prototype.some.call(section.querySelectorAll(".term"), function (t) { return !t.hidden; });
37
+ section.hidden = !!query && !any;
38
+ section.classList.toggle("searching", !!query);
39
+ var link = links[section.id];
40
+ if (link) link.parentNode.hidden = section.hidden;
41
+ });
42
+ count.textContent = query ? shown + " of " + terms.length + " terms" : terms.length + " terms";
43
+ }
44
+
45
+ input.addEventListener("input", filter);
46
+ filter();
47
+
48
+ if ("IntersectionObserver" in window) {
49
+ var observer = new IntersectionObserver(function (entries) {
50
+ entries.forEach(function (entry) {
51
+ if (!entry.isIntersecting) return;
52
+ Object.keys(links).forEach(function (id) { links[id].removeAttribute("aria-current"); });
53
+ var link = links[entry.target.id];
54
+ if (link) link.setAttribute("aria-current", "true");
55
+ });
56
+ }, { rootMargin: "-8% 0px -82% 0px" });
57
+ sections.forEach(function (section) { observer.observe(section); });
58
+ }
59
+
60
+ // A link inside a definition lands on a term; a search in progress
61
+ // would hide it, so the search is cleared first.
62
+ document.addEventListener("click", function (event) {
63
+ var a = event.target.closest && event.target.closest("a[href^='#']");
64
+ if (!a) return;
65
+ var target = document.getElementById(a.getAttribute("href").slice(1));
66
+ if (!target) return;
67
+ if (input.value) { input.value = ""; filter(); }
68
+ target.classList.remove("landed");
69
+ void target.offsetWidth;
70
+ target.classList.add("landed");
71
+ });
72
+ })();
@@ -0,0 +1,17 @@
1
+ module Hecks
2
+ module Projections
3
+ module Glossary
4
+ # THE THREE SECTIONS NOTHING HOMES TO ONE AGGREGATE. In their own
5
+ # file because `Markdown` reads them and loads before the
6
+ # projector's own body does.
7
+ ROLES = "Roles".freeze
8
+ READ_MODELS = "Read models".freeze
9
+ # Where a term lands when the fact it hangs on is raised by nothing
10
+ # this chapter declares — pizzas' `PizzaPaymentReceived` comes in
11
+ # through a port, so the policy reacting to it homes to no
12
+ # aggregate. Shown, not dropped: a visible blank is how a reader
13
+ # learns the language has a gap there.
14
+ REACTIONS = "Reactions".freeze
15
+ end
16
+ end
17
+ end