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.
- checksums.yaml +4 -4
- data/exe/hecks +24 -12
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
- data/lib/hecks/bluebook/hexagon.rb +45 -25
- data/lib/hecks/bluebook/model_check.rb +7 -7
- data/lib/hecks/chapters.rb +31 -3
- data/lib/hecks/cli/check_era.rb +1 -1
- data/lib/hecks/cli/deploy_template_diff.rb +1 -1
- data/lib/hecks/cli/doc_coverage.rb +1 -1
- data/lib/hecks/cli/domain_stub.rb +128 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/model_check.rb +1 -1
- data/lib/hecks/cli/project_cli.rb +51 -29
- data/lib/hecks/cli/reference.rb +2 -1
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
- data/lib/hecks/cli/spec_example.rb +1 -1
- data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
- data/lib/hecks/corpus.rb +11 -8
- data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +4 -3
- data/lib/hecks/doors/cli_door.rb +13 -13
- data/lib/hecks/doors/cli_runner.rb +52 -32
- data/lib/hecks/doors/launcher_options.rb +5 -5
- data/lib/hecks/doors/mcp_door.rb +40 -18
- data/lib/hecks/doors/mcp_door_scope.rb +244 -24
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -2
- data/lib/hecks/forms/banking_presentation.rb +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
- data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
- data/lib/hecks/framework.rb +1 -1
- data/lib/hecks/fuzzing/combination_miner.rb +1 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
- data/lib/hecks/fuzzing/differential.rb +4 -2
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/form_census.rb +2 -2
- data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
- data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
- data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
- data/lib/hecks/gate/stages.yml +5 -5
- data/lib/hecks/grammar/grammar.hecksagon +2 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
- data/lib/hecks/hecks/adapters/host_http.rb +4 -3
- data/lib/hecks/hecks/adapters/local_files.rb +59 -0
- data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +45 -3
- data/lib/hecks/hecks/hecks.hecksagon +10 -4
- data/lib/hecks/hecks/hecks.world +6 -6
- data/lib/hecks/hecks/site.behaviors +17 -0
- data/lib/hecks/hecks/site.world +10 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
- data/lib/hecks/projections/deploy/box/settings.rb +248 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box.rb +387 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +11 -10
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
- data/lib/hecks/projections/diagrams.rb +2 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
- data/lib/hecks/projections/site/edge/checks.rb +137 -0
- data/lib/hecks/projections/site/edge/pattern.rb +50 -0
- data/lib/hecks/projections/site/edge/rules.rb +128 -0
- data/lib/hecks/projections/site/edge.rb +136 -0
- data/lib/hecks/projections/site/regions.rb +44 -0
- data/lib/hecks/projections/site/routes_ts.rb +284 -0
- data/lib/hecks/projections/site/site_cdn.rb +78 -0
- data/lib/hecks/projections/site/table/checks.rb +124 -0
- data/lib/hecks/projections/site/table/row_builder.rb +155 -0
- data/lib/hecks/projections/site/table.rb +98 -0
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/cli_projector.rb +122 -104
- data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
- data/lib/hecks/quality_control/adapters/agent.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
- data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
- data/lib/hecks/quality_control/quality_control.bluebook +18 -18
- data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
- data/lib/hecks/runtime/registry/verification.rb +15 -29
- data/lib/hecks/runtime/registry.rb +14 -20
- data/lib/hecks/rust_build/append_optionals.rb +79 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
- data/lib/hecks/rust_build/domain_name.rb +26 -0
- data/lib/hecks/rust_build/kernel_input.rb +58 -0
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
- data/lib/hecks/rust_build/project_rust.rb +57 -24
- data/lib/hecks/rust_build/rust_literal.rb +33 -0
- data/lib/hecks/rust_build/write_if_changed.rb +86 -0
- data/lib/hecks/site/bluebook/site.bluebook +205 -0
- data/lib/hecks/site/bluebook/site.hecksagon +5 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
- data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
- data/lib/hecks/storehouse.rb +37 -7
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
- data/lib/hecks/three_zero/forms.yml +88 -88
- data/lib/hecks/tools/ci_gates.rb +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +14 -6
- data/lib/hecks/tools/oidc_manifests.rb +2 -1
- data/lib/hecks/tools/sequence_script.rb +2 -2
- data/lib/hecks/tools/site_routes.rb +152 -0
- data/lib/hecks/tools/tenant_provisioning.rb +1 -1
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/pipeline.rs +23 -16
- data/rust/build/src/resolve.rs +39 -6
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +87 -7
- data/rust/host/src/needs.rs +111 -0
- data/rust/parser/src/keywords.rs +7 -6
- data/rust/parser/src/main.rs +4 -3
- data/rust/parser/src/parse/chapter.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +25 -10
- data/rust/project/mutations.rb +4 -30
- data/rust/project/naming.rb +8 -28
- data/rust/project/write_if_changed.rb +4 -80
- data/rust/project_rust_pipeline.rb +16 -12
- data/rust/src/kernel/cli.rs +2 -0
- data/rust/src/kernel/needs.rs +74 -1
- 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,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
|
data/lib/hecks/storehouse.rb
CHANGED
|
@@ -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[:
|
|
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[:
|
|
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[:
|
|
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[:
|
|
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[:
|
|
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[:
|
|
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[:
|
|
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 `
|
|
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."
|