hecks 3.0.5 → 3.1.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 (150) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +24 -12
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  5. data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
  6. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
  7. data/lib/hecks/bluebook/hexagon.rb +45 -25
  8. data/lib/hecks/bluebook/model_check.rb +7 -7
  9. data/lib/hecks/chapters.rb +31 -3
  10. data/lib/hecks/cli/check_era.rb +1 -1
  11. data/lib/hecks/cli/deploy_template_diff.rb +1 -1
  12. data/lib/hecks/cli/doc_coverage.rb +1 -1
  13. data/lib/hecks/cli/domain_stub.rb +128 -0
  14. data/lib/hecks/cli/interview_draft.rb +246 -0
  15. data/lib/hecks/cli/model_check.rb +1 -1
  16. data/lib/hecks/cli/project_cli.rb +51 -29
  17. data/lib/hecks/cli/reference.rb +2 -1
  18. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  19. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  20. data/lib/hecks/cli/spec_example.rb +1 -1
  21. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  22. data/lib/hecks/corpus.rb +11 -8
  23. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  24. data/lib/hecks/deploy/oidc.json +5 -0
  25. data/lib/hecks/doc/reference.rb +4 -3
  26. data/lib/hecks/doors/cli_door.rb +13 -13
  27. data/lib/hecks/doors/cli_runner.rb +52 -32
  28. data/lib/hecks/doors/launcher_options.rb +5 -5
  29. data/lib/hecks/doors/mcp_door.rb +40 -18
  30. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  31. data/lib/hecks/doors.rb +1 -1
  32. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  33. data/lib/hecks/forms/banking_presentation.rb +1 -1
  34. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  35. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  36. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  37. data/lib/hecks/framework.rb +1 -1
  38. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  39. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  40. data/lib/hecks/fuzzing/differential.rb +4 -2
  41. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  42. data/lib/hecks/fuzzing/form_census.rb +2 -2
  43. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  44. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  45. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  46. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  47. data/lib/hecks/gate/stages.yml +5 -5
  48. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  49. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  50. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  51. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  52. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  53. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  54. data/lib/hecks/hecks/codebase.bluebook +4 -3
  55. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  56. data/lib/hecks/hecks/custodian.bluebook +45 -3
  57. data/lib/hecks/hecks/hecks.hecksagon +10 -4
  58. data/lib/hecks/hecks/hecks.world +6 -6
  59. data/lib/hecks/hecks/site.behaviors +17 -0
  60. data/lib/hecks/hecks/site.world +10 -0
  61. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  62. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  63. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  64. data/lib/hecks/projections/deploy/box/settings.rb +248 -0
  65. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
  66. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
  67. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  68. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
  69. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  70. data/lib/hecks/projections/deploy/box.rb +387 -0
  71. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  72. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  73. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  74. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  75. data/lib/hecks/projections/diagrams.rb +2 -2
  76. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  77. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  78. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  79. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  80. data/lib/hecks/projections/site/edge.rb +136 -0
  81. data/lib/hecks/projections/site/regions.rb +44 -0
  82. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  83. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  84. data/lib/hecks/projections/site/table/checks.rb +124 -0
  85. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  86. data/lib/hecks/projections/site/table.rb +98 -0
  87. data/lib/hecks/projections.rb +2 -0
  88. data/lib/hecks/projector/cli_projector.rb +122 -104
  89. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  90. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  91. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  92. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  93. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  94. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  95. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  96. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  97. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  98. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  99. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  100. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  101. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  102. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  103. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  104. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  105. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  106. data/lib/hecks/runtime/registry/verification.rb +15 -29
  107. data/lib/hecks/runtime/registry.rb +14 -20
  108. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  109. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  110. data/lib/hecks/rust_build/domain_name.rb +26 -0
  111. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  112. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  113. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  114. data/lib/hecks/rust_build/project_rust.rb +57 -24
  115. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  116. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  117. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  118. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  119. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  120. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  121. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  122. data/lib/hecks/storehouse.rb +37 -7
  123. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  124. data/lib/hecks/three_zero/forms.yml +88 -88
  125. data/lib/hecks/tools/ci_gates.rb +1 -1
  126. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  127. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  128. data/lib/hecks/tools/sequence_script.rb +2 -2
  129. data/lib/hecks/tools/site_routes.rb +152 -0
  130. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  131. data/lib/hecks/tools.rb +1 -0
  132. data/lib/hecks/version.rb +1 -1
  133. data/rust/build/src/cargo_sync.rs +1 -1
  134. data/rust/build/src/pipeline.rs +23 -16
  135. data/rust/build/src/resolve.rs +39 -6
  136. data/rust/host/HECKS_RELEASE +1 -1
  137. data/rust/host/src/api.rs +1 -1
  138. data/rust/host/src/dispatch.rs +87 -7
  139. data/rust/host/src/needs.rs +111 -0
  140. data/rust/parser/src/keywords.rs +7 -6
  141. data/rust/parser/src/main.rs +4 -3
  142. data/rust/parser/src/parse/chapter.rs +1 -1
  143. data/rust/parser/src/parse/hecksagon.rs +25 -10
  144. data/rust/project/mutations.rb +4 -30
  145. data/rust/project/naming.rb +8 -28
  146. data/rust/project/write_if_changed.rb +4 -80
  147. data/rust/project_rust_pipeline.rb +16 -12
  148. data/rust/src/kernel/cli.rs +2 -0
  149. data/rust/src/kernel/needs.rs +74 -1
  150. metadata +37 -1
@@ -0,0 +1,205 @@
1
+ # The Site chapter: the route table a client declares for its website, and the command that
2
+ # projects it into the TypeScript the site reads.
3
+ #
4
+ # A client declares its routes as `member` rows of a `value_object "Route"` in its own chapter (the
5
+ # Route aggregate below names every field a row may carry and the closed sets its values come from),
6
+ # attaches this chapter, and runs `hecks site site_projection.project_site`. The projection (`:site_routes_ts`)
7
+ # writes one `routes.ts` the site's middleware, navigation, sitemap, robots file and CMS all import,
8
+ # so a route is declared once. The Route aggregate's value objects are the one place the closed
9
+ # vocabularies live: the projector reads them from here, so a row naming a cache class that is
10
+ # not a member is refused with the members listed.
11
+ #
12
+ # The same command projects the CDN: a project that adds Edge, EdgePolicy, EdgeOrigin and EdgeRule rows
13
+ # to its chapter has the CloudFront behaviours and the listener rules written into the marked regions
14
+ # of the template it owns (the `:site_cdn` projection). Those rows are the template's facts, so only the
15
+ # closed sets they draw on, cache class and origin, are named here.
16
+ #
17
+ # Each run records that it was requested before the SiteToolchain port is asked, and what came of
18
+ # it after. No command declares a role: the chapter boots standing alone.
19
+ Hecks.bluebook "Site" do
20
+ vision "Declare a site's routes once and project them into the TypeScript its pages, navigation and CMS read."
21
+
22
+ aggregate "Route" do
23
+ description "The shape of one row of a site's route table: where a path is served from, who may reach it, how it is cached, and where it appears in the navigation."
24
+
25
+ identified_by :path
26
+
27
+ attribute :path, Path
28
+ attribute :kind, Kind
29
+ attribute :render, Render
30
+ attribute :auth, Auth
31
+ attribute :cache, CacheClass, optional: true
32
+ attribute :origin, Origin
33
+ attribute :source, Source, optional: true
34
+ attribute :http_method, HttpMethod, optional: true
35
+ attribute :preview, Preview, optional: true
36
+
37
+ # `/blog/:slug.html` names a parameter; a trailing `/*` matches everything beneath a prefix. A
38
+ # row with a `command:` or `query:` source may leave its path out, and the projection derives it
39
+ # from the forms scheme (`/Chapter/Aggregate/Verb`).
40
+ value_object "Path" do
41
+ attribute :value, String, pattern: '\A(/[^ \t\n\r]*)?\z'
42
+ end
43
+
44
+ # What the route is: a `page` the site renders, an `endpoint` that answers data, a `redirect` to
45
+ # another path, a `rewrite` that serves another path's page under this one, or a `proxy` to
46
+ # another origin.
47
+ value_object "Kind" do
48
+ attribute :value, String, one_of: ["page", "endpoint", "redirect", "rewrite", "proxy"]
49
+ end
50
+
51
+ # When a page's HTML is made: at build time (`prerender`) or on each request (`ssr`).
52
+ value_object "Render" do
53
+ attribute :value, String, one_of: ["prerender", "ssr"]
54
+ end
55
+
56
+ # Who may reach the route: anyone (`public`), an admin session (`admin`), or the holder of a
57
+ # signed link (`signed`).
58
+ value_object "Auth" do
59
+ attribute :value, String, one_of: ["public", "admin", "signed"]
60
+ end
61
+
62
+ # How long a CDN may keep a route's response. `no_store` is never cached, `immutable` is cached
63
+ # for as long as the CDN allows. A row that names none gets `no_store` for an admin or signed
64
+ # route or an endpoint, `immutable` from the `assets` origin, and `page` otherwise.
65
+ value_object "CacheClass" do
66
+ attribute :value, String, one_of: ["page", "home", "static", "media", "immutable", "no_store"]
67
+ end
68
+
69
+ # Which server answers: the `website`, the `cms`, the `domain` (a Hecks host) or the `assets`
70
+ # store.
71
+ value_object "Origin" do
72
+ attribute :value, String, one_of: ["website", "cms", "domain", "assets"]
73
+ end
74
+
75
+ # `public` previews a route at its own URL; `draft` previews it under the preview prefix, where
76
+ # unpublished content is visible to an admin.
77
+ value_object "Preview" do
78
+ attribute :value, String, one_of: ["public", "draft"]
79
+ end
80
+
81
+ # The HTTP verbs a route answers.
82
+ value_object "HttpMethod" do
83
+ attribute :value, String, one_of: ["GET", "POST", "PUT", "PATCH", "DELETE"]
84
+ end
85
+
86
+ # Where a page's content comes from, one of `none`, `global:<slug>` (a CMS global),
87
+ # `collection:<name>` (a CMS collection, whose rows fill the path's parameter),
88
+ # `command:<Chapter>.<Aggregate>.<Command>` or `query:<Chapter>.<Aggregate>.<Query>`. The last
89
+ # two are checked against the chapters the project attaches.
90
+ value_object "Source" do
91
+ attribute :value, String, pattern: '\A(none|(global|collection):[a-z0-9_-]+|(command|query):[A-Za-z0-9_]+\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+)\z'
92
+ end
93
+ end
94
+
95
+ aggregate "SiteProjection" do
96
+ description "One projection of a site's route table into its routes.ts, or the check that the file on disk is current."
97
+
98
+ attribute :domain, DomainPath
99
+ attribute :run, RunKey
100
+ attribute :out, Location, optional: true
101
+ attribute :check, Switch, optional: true
102
+ attribute :output, Report, optional: true
103
+ attribute :refusal, Report, optional: true
104
+ identified_by :run
105
+
106
+ value_object "RunKey" do
107
+ attribute :value, String, pattern: '[^ \t\n\r]'
108
+
109
+ invariant("a run is named") { !value.to_s.empty? }
110
+ end
111
+
112
+ # A project directory, whose `bluebook/` holds the chapter that declares the route table.
113
+ value_object "DomainPath" do
114
+ attribute :value, String, pattern: '[^\t\n\r]'
115
+
116
+ invariant("a project is named by its path") { !value.to_s.empty? }
117
+ end
118
+
119
+ # Where routes.ts is written, when not beside the project's bluebook.
120
+ value_object "Location" do
121
+ attribute :value, String, pattern: '[^\t\n\r]'
122
+ end
123
+
124
+ value_object "Switch" do
125
+ attribute :value, Boolean
126
+ end
127
+
128
+ value_object "Report" do
129
+ attribute :value, String
130
+ end
131
+
132
+ lifecycle :status, default: "requested" do
133
+ transition "Complete" => "projected", from: "requested"
134
+ transition "Fault" => "faulted", from: "requested"
135
+ end
136
+
137
+ # `hecks site site_projection.project_site <project> [out=<dir>] [--check]`. With `check` nothing is written: a
138
+ # routes.ts that differs from the table is faulted, naming the file, so a CI job fails on drift.
139
+ command "ProjectSite" do
140
+ goal "Project a site's route table into routes.ts, or with check only compare it with the file on disk"
141
+
142
+ attribute :domain, DomainPath
143
+ attribute :run, RunKey
144
+ attribute :out, Location, optional: true
145
+ attribute :check, Switch, optional: true
146
+
147
+ sets :domain
148
+ sets :run
149
+ sets :out
150
+ sets :check
151
+
152
+ emits "SiteProjectionRequested"
153
+ end
154
+
155
+ command "Complete" do
156
+ goal "Record that routes.ts was written, or found current, and where"
157
+
158
+ reference_to SiteProjection
159
+ attribute :output, Report
160
+
161
+ sets :output
162
+
163
+ emits "SiteProjected"
164
+ end
165
+
166
+ command "Fault" do
167
+ goal "Record that routes.ts could not be projected, or was out of date, and why"
168
+
169
+ reference_to SiteProjection
170
+ attribute :refusal, Report
171
+
172
+ sets :refusal
173
+
174
+ emits "SiteFaulted"
175
+ end
176
+
177
+ query "Outcome" do
178
+ description "What one projection did: its status, and the files written or the reason it stopped."
179
+ attribute :run, RunKey
180
+ where(run: :run)
181
+ order_by :run
182
+ end
183
+
184
+ query "Faulted" do
185
+ description "Every projection that could not write routes.ts, or found it out of date."
186
+ where(status: "faulted")
187
+ order_by :run
188
+ end
189
+ end
190
+
191
+ policy "ProjectWhenRequested" do
192
+ on "SiteProjection.SiteProjectionRequested"
193
+ trigger SiteProjection::SiteToolchain::Project, with: { run: :run }
194
+ end
195
+
196
+ policy "RecordTheProjection" do
197
+ on "SiteProjection.SiteAnswered"
198
+ trigger SiteProjection::Complete, with: { output: :output }
199
+ end
200
+
201
+ policy "RecordTheFaultedProjection" do
202
+ on "SiteProjection.SiteRefused"
203
+ trigger SiteProjection::Fault, with: { refusal: :refusal }
204
+ end
205
+ end
@@ -0,0 +1,5 @@
1
+ # The Site domain, bound so it boots standing alone. The SiteToolchain port every attaching
2
+ # hecksagon shares is in site.ports.hecksagon.
3
+ Hecks.hecksagon "Site" do
4
+ persisted_by "Memory"
5
+ end
@@ -0,0 +1,15 @@
1
+ # The port Site declares, for every hecksagon that attaches the chapter (`Chapters` loads this file
2
+ # with it). Each projection asks the SiteToolchain port, which the Hecks domain binds
3
+ # (lib/hecks/hecks/adapters/site_toolchain.rb): the outside a projection reaches. The ask is
4
+ # answered with the record's own arguments or refused with a reason. The port has to be declared
5
+ # even where no adapter binds it, so ProjectWhenRequested has a target and model_check's
6
+ # deaf-policy check can see it.
7
+ Hecks.hecksagon "Site" do
8
+ Site::SiteProjection.port "SiteToolchain" do
9
+ asks "Project", to: SiteProjection do
10
+ attribute :run, RunKey
11
+ answers "SiteAnswered"
12
+ refuses "SiteRefused"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,336 @@
1
+ # SME: learn a business from the people who know it (ADR 0088).
2
+ #
3
+ # An interview is the record of one conversation with a subject matter expert about a domain: what
4
+ # was asked, what was said, and what a developer then accepted as established. It is development
5
+ # time work, so this chapter ships in the gem but is not attached to the Hecks domain: nothing
6
+ # boots it except the command that runs an interview.
7
+ Hecks.bluebook "SME" do
8
+ vision "Learn a business from the people who know it: an interview with a subject matter expert, kept as a record of what was asked, what was said, and what was accepted as established."
9
+ supporting
10
+
11
+ aggregate "Interview" do
12
+ description "One sitting with a subject matter expert about a domain: planned, held, and concluded, with every question and answer kept in order and every proposed finding accepted or rejected by name."
13
+
14
+ identified_by :reference
15
+
16
+ attribute :reference, Reference
17
+ attribute :subject, Subject
18
+ attribute :expert, Expert
19
+ attribute :exchanges, list_of(Exchange)
20
+
21
+ attribute :thing_findings, list_of(ThingFinding)
22
+ attribute :action_findings, list_of(ActionFinding)
23
+ attribute :rule_findings, list_of(RuleFinding)
24
+
25
+ # Held as counts because a rule cannot look inside a list: `Conclude` needs to know that at least
26
+ # one thing and one action were accepted, and each acceptance adds one here (see the policies).
27
+ attribute :accepted_things, Tally
28
+ attribute :accepted_actions, Tally
29
+
30
+ value_object "Reference" do
31
+ attribute :value, String, pattern: '[^ \t\n\r]'
32
+ end
33
+
34
+ # The domain being discovered, spelled as its bluebook will spell it. Many interviews, with
35
+ # different experts on different days, name the same subject; the domain is not modelled here.
36
+ value_object "Subject" do
37
+ attribute :value, String, pattern: '[^ \t\n\r]'
38
+ end
39
+
40
+ # Who is being interviewed.
41
+ value_object "Expert" do
42
+ attribute :value, String, pattern: '[^\t\n\r]'
43
+ end
44
+
45
+ value_object "Question" do
46
+ attribute :value, String, pattern: '[^\t\n\r]'
47
+ end
48
+
49
+ # The expert's answer in their own words.
50
+ value_object "Answer" do
51
+ attribute :value, String, pattern: '[^\t\n\r]'
52
+ end
53
+
54
+ # A label for grouping exchanges ("lending rules"); optional on every exchange.
55
+ value_object "Topic" do
56
+ attribute :value, String, pattern: '[^\t\n\r]'
57
+ end
58
+
59
+ value_object "Exchange" do
60
+ attribute :question, String, pattern: '[^\t\n\r]'
61
+ attribute :answer, String, pattern: '[^\t\n\r]'
62
+ attribute :topic, String, optional: true
63
+ end
64
+
65
+ # The number of the exchange a finding rests on, counted from one in the order they were recorded.
66
+ value_object "Source" do
67
+ attribute :value, Integer
68
+ invariant("a finding cites an exchange by its number, from one") { value.positive? }
69
+ end
70
+
71
+ value_object "FindingNumber" do
72
+ attribute :value, Integer
73
+ invariant("a finding is numbered from one") { value.positive? }
74
+ end
75
+
76
+ value_object "Name" do
77
+ attribute :value, String, pattern: '[^ \t\n\r]'
78
+ end
79
+
80
+ # How many findings of a kind were accepted.
81
+ value_object "Tally" do
82
+ attribute :value, Integer, default: 0
83
+ invariant("a tally is not negative") { !value.negative? }
84
+ end
85
+
86
+ value_object "Statement" do
87
+ attribute :value, String, pattern: '[^\t\n\r]'
88
+ end
89
+
90
+ # Whether an action brings its thing into being, as opposed to happening to one that exists.
91
+ value_object "Creates" do
92
+ attribute :value, Boolean
93
+ end
94
+
95
+ # A thing the domain keeps track of, and the field that identifies one: an aggregate to be.
96
+ entity "ThingFinding" do
97
+ description "A thing the expert described, proposed as an aggregate, with the field that identifies one."
98
+
99
+ identified_by :number
100
+
101
+ attribute :number, FindingNumber
102
+ attribute :name, Name
103
+ attribute :identifier, Name
104
+ attribute :source, Source
105
+
106
+ lifecycle :status, default: "proposed" do
107
+ transition "AcceptThing" => "accepted", from: "proposed"
108
+ transition "RejectThing" => "rejected", from: "proposed"
109
+ end
110
+
111
+ command "AcceptThing", from: "proposed" do
112
+ goal "Establish that the thing is real"
113
+
114
+ attribute :number, FindingNumber
115
+
116
+ emits "ThingAccepted"
117
+ end
118
+
119
+ command "RejectThing", from: "proposed" do
120
+ goal "Set the proposed thing aside"
121
+
122
+ attribute :number, FindingNumber
123
+
124
+ emits "ThingRejected"
125
+ end
126
+ end
127
+
128
+ # Something that happens to a thing, and what it announces: a command and its event to be.
129
+ entity "ActionFinding" do
130
+ description "Something the expert said happens to a thing, proposed as a command and the event it announces."
131
+
132
+ identified_by :number
133
+
134
+ attribute :number, FindingNumber
135
+ attribute :name, Name
136
+ attribute :thing, Name
137
+ attribute :event, Name
138
+ attribute :creates, Creates, optional: true
139
+ attribute :source, Source
140
+
141
+ lifecycle :status, default: "proposed" do
142
+ transition "AcceptAction" => "accepted", from: "proposed"
143
+ transition "RejectAction" => "rejected", from: "proposed"
144
+ end
145
+
146
+ command "AcceptAction", from: "proposed" do
147
+ goal "Establish that the action is real"
148
+
149
+ attribute :number, FindingNumber
150
+
151
+ emits "ActionAccepted"
152
+ end
153
+
154
+ command "RejectAction", from: "proposed" do
155
+ goal "Set the proposed action aside"
156
+
157
+ attribute :number, FindingNumber
158
+
159
+ emits "ActionRejected"
160
+ end
161
+ end
162
+
163
+ # A rule is kept as the expert's sentence, and never turned into code from prose (ADR 0088).
164
+ entity "RuleFinding" do
165
+ description "A rule in the expert's words, kept as text for a developer to turn into a given."
166
+
167
+ identified_by :number
168
+
169
+ attribute :number, FindingNumber
170
+ attribute :statement, Statement
171
+ attribute :source, Source
172
+
173
+ lifecycle :status, default: "proposed" do
174
+ transition "AcceptRule" => "accepted", from: "proposed"
175
+ transition "RejectRule" => "rejected", from: "proposed"
176
+ end
177
+
178
+ command "AcceptRule", from: "proposed" do
179
+ goal "Establish that the rule is real"
180
+
181
+ attribute :number, FindingNumber
182
+
183
+ emits "RuleAccepted"
184
+ end
185
+
186
+ command "RejectRule", from: "proposed" do
187
+ goal "Set the proposed rule aside"
188
+
189
+ attribute :number, FindingNumber
190
+
191
+ emits "RuleRejected"
192
+ end
193
+ end
194
+
195
+ lifecycle :status, default: "planned" do
196
+ transition "Begin" => "underway", from: "planned"
197
+ transition "Conclude" => "concluded", from: "underway"
198
+ transition "Cancel" => "cancelled", from: ["planned", "underway"]
199
+ end
200
+
201
+ command "Plan" do
202
+ goal "Arrange an interview with an expert about a domain"
203
+
204
+ attribute :reference, Reference
205
+ attribute :subject, Subject
206
+ attribute :expert, Expert
207
+
208
+ emits "InterviewPlanned"
209
+ end
210
+
211
+ command "Begin" do
212
+ goal "Sit down with the expert"
213
+
214
+ reference_to Interview
215
+
216
+ emits "InterviewBegun"
217
+ end
218
+
219
+ command "Record" do
220
+ goal "Keep a question and the expert's answer to it"
221
+
222
+ reference_to Interview
223
+ attribute :question, Question
224
+ attribute :answer, Answer
225
+ attribute :topic, Topic, optional: true
226
+
227
+ given("answers are recorded only while the interview is underway") { status == "underway" }
228
+
229
+ sets :exchanges, append: { question: :question, answer: :answer, topic: :topic }
230
+
231
+ emits "ExchangeRecorded"
232
+ end
233
+
234
+ command "ProposeThing" do
235
+ goal "Propose a thing the expert described as an aggregate"
236
+
237
+ reference_to Interview
238
+ attribute :number, FindingNumber
239
+ attribute :name, Name
240
+ attribute :identifier, Name
241
+ attribute :source, Source
242
+
243
+ given("findings are proposed only while the interview is underway") { status == "underway" }
244
+ given("a finding cites an exchange that was recorded") { source.value <= exchanges.size }
245
+
246
+ sets :thing_findings, append: { number: :number, name: :name, identifier: :identifier, source: :source }
247
+
248
+ emits "ThingProposed"
249
+ end
250
+
251
+ command "ProposeAction" do
252
+ goal "Propose something that happens to a thing as a command and its event"
253
+
254
+ reference_to Interview
255
+ attribute :number, FindingNumber
256
+ attribute :name, Name
257
+ attribute :thing, Name
258
+ attribute :event, Name
259
+ attribute :creates, Creates, optional: true
260
+ attribute :source, Source
261
+
262
+ given("findings are proposed only while the interview is underway") { status == "underway" }
263
+ given("a finding cites an exchange that was recorded") { source.value <= exchanges.size }
264
+
265
+ sets :action_findings, append: { number: :number, name: :name, thing: :thing, event: :event, creates: :creates, source: :source }
266
+
267
+ emits "ActionProposed"
268
+ end
269
+
270
+ command "ProposeRule" do
271
+ goal "Keep a rule in the expert's words"
272
+
273
+ reference_to Interview
274
+ attribute :number, FindingNumber
275
+ attribute :statement, Statement
276
+ attribute :source, Source
277
+
278
+ given("findings are proposed only while the interview is underway") { status == "underway" }
279
+ given("a finding cites an exchange that was recorded") { source.value <= exchanges.size }
280
+
281
+ sets :rule_findings, append: { number: :number, statement: :statement, source: :source }
282
+
283
+ emits "RuleProposed"
284
+ end
285
+
286
+ command "CountThing" do
287
+ goal "Count one more accepted thing"
288
+
289
+ reference_to Interview
290
+
291
+ sets :accepted_things, increment: { value: 1 }
292
+
293
+ emits "ThingCounted"
294
+ end
295
+
296
+ command "CountAction" do
297
+ goal "Count one more accepted action"
298
+
299
+ reference_to Interview
300
+
301
+ sets :accepted_actions, increment: { value: 1 }
302
+
303
+ emits "ActionCounted"
304
+ end
305
+
306
+ command "Conclude" do
307
+ goal "End the interview with something established to show for it"
308
+
309
+ reference_to Interview
310
+
311
+ given("an interview that recorded nothing has not happened") { exchanges.size.positive? }
312
+ given("a thing must be accepted before the interview ends") { accepted_things.value.positive? }
313
+ given("an action must be accepted before the interview ends") { accepted_actions.value.positive? }
314
+
315
+ emits "InterviewConcluded"
316
+ end
317
+
318
+ command "Cancel" do
319
+ goal "Call the interview off"
320
+
321
+ reference_to Interview
322
+
323
+ emits "InterviewCancelled"
324
+ end
325
+ end
326
+
327
+ policy "CountAcceptedThing" do
328
+ on "Interview.ThingAccepted"
329
+ trigger Interview::CountThing, with: { interview: :reference }
330
+ end
331
+
332
+ policy "CountAcceptedAction" do
333
+ on "Interview.ActionAccepted"
334
+ trigger Interview::CountAction, with: { interview: :reference }
335
+ end
336
+ end
@@ -0,0 +1,5 @@
1
+ # An interview lives for the length of one session, so it is kept in memory. What outlasts it is the
2
+ # Markdown record written when it concludes (ADR 0088), not a database.
3
+ Hecks.hecksagon "SME" do
4
+ SME::Interview.persisted_by("Memory")
5
+ end
@@ -1,3 +1,4 @@
1
+ require "digest"
1
2
  require "json"
2
3
  require "fileutils"
3
4
  require "time"
@@ -59,7 +60,7 @@ module Hecks
59
60
  # and query never drift from what a human typing hecks run sees.
60
61
  # :nodoc:
61
62
  def resolve!(cli, name, asking:)
62
- pool = asking ? cli[:questions] : cli[:verbs]
63
+ pool = asking ? cli[:questions] : cli[:commands]
63
64
  key = cli[:names][asking ? :question : :command][name]
64
65
  spec = pool[key]
65
66
  return spec if spec
@@ -68,6 +69,35 @@ module Hecks
68
69
  raise Runtime::NotFound, "no such #{asking ? 'query' : 'command'}: #{name.inspect} — known: #{known}"
69
70
  end
70
71
 
72
+ # A fingerprint of a domain directory: every file's relative path, size and modification time.
73
+ # Equal fingerprints mean no file was added, removed, resized or rewritten between two looks.
74
+ #
75
+ # @param path [String] the domain directory
76
+ # @return [String] a hex SHA-256
77
+ def fingerprint(path)
78
+ files = Dir.glob(File.join(path, "**", "*"), File::FNM_DOTMATCH).sort.select { |file| File.file?(file) }
79
+ state = files.map do |file|
80
+ stat = File.stat(file)
81
+ "#{file.delete_prefix(path)}\0#{stat.size}\0#{stat.mtime.to_f}"
82
+ end
83
+ Digest::SHA256.hexdigest(state.join("\n"))
84
+ end
85
+
86
+ # The qualified verb each command name resolves to, by the alias map `dispatch` resolves
87
+ # through; nil for a name that resolves to no command.
88
+ #
89
+ # @param runtime [Runtime::Dispatcher] the booted domain
90
+ # @param names [Array<String, nil>] short or qualified command names
91
+ # @return [Array<String, nil>] one verb or nil per name, in order
92
+ def verbs_for(runtime, names)
93
+ cli = Projector.call(:cli, bluebook: bluebook_for(runtime), options: { program: "mcp" })
94
+ names.map do |name|
95
+ resolve!(cli, name.to_s, asking: false)[:command]
96
+ rescue Runtime::NotFound
97
+ nil
98
+ end
99
+ end
100
+
71
101
  # Required on dispatch/query/state: what makes an audit row legible later.
72
102
  # :nodoc:
73
103
  def require_summary!(summary)
@@ -109,7 +139,7 @@ module Hecks
109
139
  return unless spec[:role_gated] && role.nil?
110
140
 
111
141
  raise Runtime::Unauthorized,
112
- "#{spec[:verb]} requires role: #{spec[:role].inspect} — this command is role-gated and no caller " \
142
+ "#{spec[:command]} requires role: #{spec[:role].inspect} — this command is role-gated and no caller " \
113
143
  "(role:/actor_id:) is bound; dispatching it unbound is refused, not silently unchecked"
114
144
  end
115
145
 
@@ -192,12 +222,12 @@ module Hecks
192
222
  result = with_caller(role, actor_id) do
193
223
  dry_run ? dry_run_outcome(runtime, spec, envelope, summary: summary) : real_dispatch(runtime, spec, envelope, summary)
194
224
  end
195
- result.merge(verb: spec[:verb])
225
+ result.merge(verb: spec[:command])
196
226
  end
197
227
 
198
228
  # :nodoc:
199
229
  def real_dispatch(runtime, spec, envelope, summary)
200
- result = runtime.dispatch_flat(spec[:verb], envelope)
230
+ result = runtime.dispatch_flat(spec[:command], envelope)
201
231
  ok(summary: summary,
202
232
  id: result.id,
203
233
  state: result.state.nil? ? nil : Doors::JsonDoor.materialize(result.state),
@@ -207,7 +237,7 @@ module Hecks
207
237
  # :nodoc:
208
238
  def dry_run_outcome(runtime, spec, envelope, summary:)
209
239
  flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
210
- runtime.dry_run?(spec[:verb], **flat)
240
+ runtime.dry_run?(spec[:command], **flat)
211
241
  ok(summary: summary, would_succeed: true)
212
242
  rescue Runtime::WiringError, *Runtime::DOMAIN_REFUSALS => e
213
243
  ok(summary: summary, would_succeed: false, error: e.message)
@@ -268,9 +298,9 @@ module Hecks
268
298
  valid_caller!(role, actor_id)
269
299
  cli = Projector.call(:cli, bluebook: bluebook, options: { program: "mcp" })
270
300
  spec = resolve!(cli, question, asking: true)
271
- rows = with_caller(role, actor_id) { runtime.query(spec[:verb], **Doors::JsonDoor.deep_symbolize(args)) }
301
+ rows = with_caller(role, actor_id) { runtime.query(spec[:command], **Doors::JsonDoor.deep_symbolize(args)) }
272
302
 
273
- ok(summary: summary, rows: rows.map { |row| Doors::JsonDoor.materialize(row) }).merge(verb: spec[:verb])
303
+ ok(summary: summary, rows: rows.map { |row| Doors::JsonDoor.materialize(row) }).merge(verb: spec[:command])
274
304
  end
275
305
 
276
306
  # Reads one aggregate's stored records directly, bypassing any declared
@@ -1,4 +1,4 @@
1
- # Its own bounded context, booted centrally like `Deploy` and never attached via `uses_framework`:
1
+ # Its own bounded context, booted centrally like `Deploy` and never attached via `attaches`:
2
2
  # an attached Tenant would be a single-row copy in every tenant's boot, not one cross-tenant list.
3
3
  Hecks.bluebook "Tenancy" do
4
4
  vision "Which tenants exist, which realm and storage each one owns, and whether each is currently allowed to run, is one queryable, historical fact — not scattered across a world-file overlay and a validation-only command."