hecks 3.2.0 → 3.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.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/check_era.rb +3 -39
- data/lib/hecks/cli/interview_draft.rb +86 -31
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +179 -0
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door.rb +16 -2
- data/lib/hecks/doors/mcp_door_scope.rb +45 -5
- data/lib/hecks/doors/mcp_guide_cache.rb +75 -0
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +17 -1
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +10 -2
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/rust_workspace.rb +1 -1
- data/lib/hecks/hecks/adapters/site_toolchain.rb +88 -11
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +220 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -3
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +40 -6
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +2 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +28 -4
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/edge/behaviours.rb +5 -3
- data/lib/hecks/projections/site/edge/checks.rb +31 -5
- data/lib/hecks/projections/site/edge.rb +25 -11
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/projections/site/routes_ts.rb +50 -27
- data/lib/hecks/projections/site/site_cdn.rb +7 -4
- data/lib/hecks/projections/site/table/checks.rb +53 -7
- data/lib/hecks/projections/site/table/link_builder.rb +64 -0
- data/lib/hecks/projections/site/table/row_builder.rb +21 -6
- data/lib/hecks/projections/site/table.rb +16 -5
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +51 -17
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +7 -140
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +80 -14
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/storehouse.rb +35 -1
- data/lib/hecks/tools/comment_style.rb +4 -2
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +2 -4
- data/lib/hecks/tools/site_routes.rb +69 -28
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +152 -1
- data/lib/hecks.rb +1 -0
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +31 -16
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +9 -20
- data/rust/codegen/src/attr.rs +1 -1
- data/rust/codegen/src/bridging.rs +8 -8
- data/rust/codegen/src/commands.rs +1 -1
- data/rust/codegen/src/constraints.rs +1 -1
- data/rust/codegen/src/dependency_planning.rs +1 -1
- data/rust/codegen/src/domain_generator.rs +2 -2
- data/rust/codegen/src/exemplar.rs +1 -1
- data/rust/codegen/src/expr_emitter.rs +1 -1
- data/rust/codegen/src/fielded.rs +1 -1
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/json_codec.rs +1 -1
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +23 -13
- data/rust/codegen/src/mutations.rs +1 -1
- data/rust/codegen/src/naming.rs +5 -3
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/codegen/src/ports.rs +1 -1
- data/rust/codegen/src/queries.rs +3 -3
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/read_models.rs +2 -2
- data/rust/codegen/src/reference_specs.rs +1 -1
- data/rust/codegen/src/registry.rs +4 -4
- data/rust/codegen/src/shared.rs +1 -1
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/codegen/src/skip_reason.rs +1 -1
- data/rust/codegen/src/types.rs +4 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +193 -29
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/exemplar/commands.rs +1 -1
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/fielded.rs +1 -1
- data/rust/src/exemplar/json.rs +1 -1
- data/rust/src/exemplar/mod.rs +1 -1
- data/rust/src/exemplar/mutations.rs +1 -1
- data/rust/src/exemplar/queries.rs +1 -1
- data/rust/src/exemplar/reactions.rs +1 -1
- data/rust/src/exemplar/read_models.rs +1 -1
- data/rust/src/exemplar/registry.rs +1 -1
- data/rust/src/exemplar/types.rs +1 -1
- data/rust/src/kernel/cli.rs +18 -8
- data/rust/src/kernel/needs.rs +104 -9
- data/rust/src/kernel/read_model.rs +1 -1
- data/rust/src/lib.rs +1 -1
- metadata +24 -30
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
- data/rust/project/bridging.rb +0 -257
- data/rust/project/commands.rb +0 -877
- data/rust/project/constraints.rb +0 -86
- data/rust/project/dependency_planning.rb +0 -178
- data/rust/project/domain_generator.rb +0 -983
- data/rust/project/exemplar.rb +0 -148
- data/rust/project/expr_emitter.rb +0 -72
- data/rust/project/fielded.rb +0 -141
- data/rust/project/json_codec.rb +0 -641
- data/rust/project/mutations.rb +0 -695
- data/rust/project/naming.rb +0 -223
- data/rust/project/ports.rb +0 -65
- data/rust/project/queries.rb +0 -493
- data/rust/project/reactions.rb +0 -317
- data/rust/project/read_models.rb +0 -690
- data/rust/project/reference_specs.rb +0 -27
- data/rust/project/registry.rb +0 -396
- data/rust/project/skip_reason.rb +0 -29
- data/rust/project/types.rb +0 -225
- data/rust/project/write_if_changed.rb +0 -7
- data/rust/project.rb +0 -30
- data/rust/project_rust_pipeline.rb +0 -391
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Quality control: every check writes its expectation down before it looks at the answer.
|
|
2
2
|
# A bug is logged with the failing test that proves it, or it is not logged.
|
|
3
3
|
|
|
4
|
-
# Tunable dials for the QA loop: values in qa/settings.yml,
|
|
4
|
+
# Tunable dials for the QA loop: values in qa/settings.yml, what each is in the `QaDial` rows of the
|
|
5
|
+
# Vocabulary chapter.
|
|
5
6
|
# `require`, not `require_relative`: isolated fuzz boots load a copy of this file from a tmpdir.
|
|
6
7
|
require "hecks/fuzzing/qa_settings"
|
|
7
8
|
|
|
@@ -11,111 +12,15 @@ unless defined?(QualityControlDials)
|
|
|
11
12
|
# boots. HECKS_QA_SETTINGS_PATH lets a subprocess test read a different settings file.
|
|
12
13
|
settings = Hecks::Fuzzing::QaSettings.load(ENV.fetch("HECKS_QA_SETTINGS_PATH", Hecks::Fuzzing::QaSettings::DEFAULT_PATH))
|
|
13
14
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
PR_CAP_PER_DAY = settings.pr_cap_per_day
|
|
20
|
-
|
|
21
|
-
# Fuzz depth by `Target.clean_streak`, in three discrete tiers, read by
|
|
22
|
-
# `SweepDepth.for_streak`. The last row (`upto: Float::INFINITY`) is the ceiling.
|
|
23
|
-
WIDENING_TIERS = settings.widening_tiers
|
|
24
|
-
|
|
25
|
-
# How many `hecks quality_control query sweep.run <target>` children `--all` keeps running at once.
|
|
26
|
-
SWEEP_MAX_PARALLEL = settings.sweep_max_parallel
|
|
27
|
-
|
|
28
|
-
# How long the `/loop hecks_qa` caller waits for a tick's report before assuming it stalled.
|
|
29
|
-
LIVENESS_FALLBACK_SECONDS = settings.liveness_fallback_seconds
|
|
15
|
+
# Each dial of qa/settings.yml as a constant named for it: `PR_CAP_PER_DAY` is `pr_cap_per_day`.
|
|
16
|
+
# What each is and takes is declared once, as its `QaDial` row in the Vocabulary chapter.
|
|
17
|
+
Hecks::Vocabulary.rows("QaDial").each do |dial|
|
|
18
|
+
const_set(dial.fetch("name").upcase, settings.public_send(dial.fetch("name")))
|
|
19
|
+
end
|
|
30
20
|
|
|
31
21
|
# The identity the loop runs as. `WaivedBy` repeats this literal (invariants read literals,
|
|
32
22
|
# not constants); `spec/quality_control_spec.rb` pins the two equal.
|
|
33
23
|
AUTOMATED_ENGINEER = "qa_sweep"
|
|
34
|
-
|
|
35
|
-
# How `hecks quality_control patch.open` opens a PR: not a draft, queued for auto-merge, on a branch that starts
|
|
36
|
-
# with BRANCH_PREFIX.
|
|
37
|
-
DRAFT_ONLY = settings.draft_only
|
|
38
|
-
AUTO_MERGE = settings.auto_merge
|
|
39
|
-
BRANCH_PREFIX = settings.branch_prefix
|
|
40
|
-
|
|
41
|
-
# Fraction (0.0..1.0) of generated command steps mutated adversarially; 0 turns the layer off.
|
|
42
|
-
# Higher finds refusal-ordering splits sooner but reaches deep state less often.
|
|
43
|
-
ADVERSARIAL_FRACTION = settings.adversarial_fraction
|
|
44
|
-
|
|
45
|
-
# Coverage-guided generation across one sweep's seeds (`Hecks::Fuzzing::CoverageCampaign`).
|
|
46
|
-
GUIDED_GENERATION = settings.guided_generation
|
|
47
|
-
|
|
48
|
-
# Fraction (0.0..1.0) of seeds that start from a prefix of an earlier seed's new coverage.
|
|
49
|
-
# Off by default: measured, it trades breadth for depth (banking reached 1.41x the deep-state
|
|
50
|
-
# tuples, chess lost 0.12x of all tuples), while favoring rare verbs alone never lost.
|
|
51
|
-
CORPUS_SPLICE_PROBABILITY = settings.corpus_splice_probability
|
|
52
|
-
|
|
53
|
-
# How many of the least-hit verbs the next seed weights up; 0 turns the lever off.
|
|
54
|
-
FAVOR_RARE_VERBS = settings.favor_rare_verbs
|
|
55
|
-
|
|
56
|
-
# Whether `hecks quality_control query sweep.run` also runs the self-consistency pass (cold rehydration, replay
|
|
57
|
-
# idempotency, value-object JSON round trip) on the same generated sequence.
|
|
58
|
-
SELF_CONSISTENCY_CHECKS = settings.self_consistency_checks
|
|
59
|
-
|
|
60
|
-
# Candidate replays `hecks quality_control query sweep.run` may spend shrinking one surprised check; 0 turns shrinking
|
|
61
|
-
# off. `concurrency` is never shrunk: a race does not reproduce on demand.
|
|
62
|
-
SHRINK_BUDGET = settings.shrink_budget
|
|
63
|
-
|
|
64
|
-
# Seconds of staleness that one point of yield score is worth in `RotationPriority.pick`.
|
|
65
|
-
# Zero would make yield invisible, so pick a number that lets it matter.
|
|
66
|
-
YIELD_WEIGHT_SECONDS = settings.yield_weight_seconds
|
|
67
|
-
|
|
68
|
-
# Percent of the prior yield score kept at each release, so recent findings dominate.
|
|
69
|
-
# Applied by the caller: the mutation language cannot express "decay, then add" in `Release`.
|
|
70
|
-
YIELD_DECAY_PERCENT = settings.yield_decay_percent
|
|
71
|
-
|
|
72
|
-
# Once a waiting target is this stale, `RotationPriority.pick` takes it whatever its yield
|
|
73
|
-
# score, so a quiet target cannot starve.
|
|
74
|
-
ROTATION_STALE_FLOOR_SECONDS = settings.rotation_stale_floor_seconds
|
|
75
|
-
|
|
76
|
-
# Ceiling on `--seeds` for `--persistence-parity`, which pays for real Postgres per dispatch.
|
|
77
|
-
# `hecks quality_control query sweep.run` clamps down, never up, and prints a note.
|
|
78
|
-
PERSISTENCE_PARITY_SEED_CAP = settings.persistence_parity_seed_cap
|
|
79
|
-
|
|
80
|
-
# Ceiling on `--seeds` for the concurrency mode, which needs two disposable schemas per seed.
|
|
81
|
-
CONCURRENCY_SEED_CAP = settings.concurrency_seed_cap
|
|
82
|
-
|
|
83
|
-
# The two adapters each adapter-parity mode compares, by mode name; a new pairing is a new
|
|
84
|
-
# entry. `adapter_parity_sqlite` needs no `database:`/`schema:` because sqlite has no
|
|
85
|
-
# disposable database.
|
|
86
|
-
ADAPTER_PARITY_PAIRS = settings.adapter_parity_pairs
|
|
87
|
-
|
|
88
|
-
# Which comparison modes `hecks quality_control query sweep.run` may run: the enabled ones intersected with what
|
|
89
|
-
# `TargetCapabilities.infer(path)` finds in the target. `--modes a,b,c` overrides for one run.
|
|
90
|
-
#
|
|
91
|
-
# `wasm_front` and `adapter_parity_postgres` are off because nothing runs them; the others are
|
|
92
|
-
# built and off because their I/O cost across the rotation is unmeasured. `hecks quality_control query sweep.run`
|
|
93
|
-
# refuses to start when a mode outside `TargetCapabilities::RUNNABLE_MODES` is enabled.
|
|
94
|
-
MODES = settings.modes
|
|
95
|
-
|
|
96
|
-
# How often a generated role-gated command step carries a drawn caller (`role:`/`actor_id:`);
|
|
97
|
-
# 0 turns the draw off.
|
|
98
|
-
ROLE_DRAW_PROBABILITY = settings.role_draw_probability
|
|
99
|
-
|
|
100
|
-
# Fraction of generated command steps offered as a dry run instead of a real dispatch; 0 is
|
|
101
|
-
# off. Kept low: a dry run creates no state for later steps to act on.
|
|
102
|
-
DRY_RUN_FRACTION = settings.dry_run_fraction
|
|
103
|
-
|
|
104
|
-
# How many generated domains one `hecks quality_control sweep.tick` checks after the rotation sweep; 0 skips the
|
|
105
|
-
# step.
|
|
106
|
-
GENERATED_DOMAINS_PER_TICK = settings.generated_domains_per_tick
|
|
107
|
-
|
|
108
|
-
# Whether generated domains are also compared against a compiled Rust binary built in a scratch
|
|
109
|
-
# copy.
|
|
110
|
-
GENERATED_DOMAINS_RUST = settings.generated_domains_rust
|
|
111
|
-
|
|
112
|
-
# Generated sequences run per generated domain.
|
|
113
|
-
GENERATED_DOMAIN_SEEDS = settings.generated_domain_seeds
|
|
114
|
-
|
|
115
|
-
# Construct families a skipped query verb may belong to. A skip inside the list is logged Held,
|
|
116
|
-
# one outside it Surprised. Shrink-only: spec/fuzzing/rust_gap_manifest_spec.rb fails when a
|
|
117
|
-
# family here is declared by no committed manifest.
|
|
118
|
-
STRUCTURAL_REFUSAL_BOUNDARY = settings.structural_refusal_boundary
|
|
119
24
|
end
|
|
120
25
|
end
|
|
121
26
|
|
|
@@ -1726,8 +1631,7 @@ Hecks.bluebook "QualityControl" do
|
|
|
1726
1631
|
attribute :branch, PatchBranch
|
|
1727
1632
|
attribute :commit, CommitRef
|
|
1728
1633
|
attribute :title, PatchTitle
|
|
1729
|
-
#
|
|
1730
|
-
# epoch, which the cap never counts.
|
|
1634
|
+
# When the PR was opened, in epoch seconds. `default: 0` hydrates older rows at the epoch.
|
|
1731
1635
|
attribute :opened_at, Instant
|
|
1732
1636
|
|
|
1733
1637
|
value_object "Instant" do
|
|
@@ -1775,8 +1679,7 @@ Hecks.bluebook "QualityControl" do
|
|
|
1775
1679
|
# the record is written by the process that made it true. `now` is that script's clock read.
|
|
1776
1680
|
# The script asks `dry_run?` of this command before it opens anything, so the two `given`s are
|
|
1777
1681
|
# the rules for opening a PR at all: the branch is one this practice recognises as its own, and
|
|
1778
|
-
# the bug it fixes is fixed. The prefix
|
|
1779
|
-
# repeats `AUTOMATED_ENGINEER`; `spec/quality_control_spec.rb` pins the two equal.
|
|
1682
|
+
# the bug it fixes is fixed. The prefix is declared here, once.
|
|
1780
1683
|
command "Open" do
|
|
1781
1684
|
role "QA engineer"
|
|
1782
1685
|
goal "Record that a fix has been opened as a pull request, the moment its number, branch and commit are already known"
|
|
@@ -1828,21 +1731,78 @@ Hecks.bluebook "QualityControl" do
|
|
|
1828
1731
|
order_by :number
|
|
1829
1732
|
end
|
|
1830
1733
|
|
|
1831
|
-
# `hecks quality_control patch.open` compares this and `Improvement.OpenedSince` counts, from local midnight,
|
|
1832
|
-
# against `PR_CAP_PER_DAY`. `gte` on a bound argument, the `Account.AtOrAbove` shape.
|
|
1833
|
-
query "OpenedSince" do
|
|
1834
|
-
description "Every patch opened at or after one instant, in number order — what today's count against PR_CAP_PER_DAY is read from."
|
|
1835
|
-
attribute :since, Instant
|
|
1836
|
-
where(:"opened_at.value" => { gte: :since })
|
|
1837
|
-
order_by :number
|
|
1838
|
-
end
|
|
1839
|
-
|
|
1840
1734
|
query "All" do
|
|
1841
1735
|
description "Every patch ever opened — what a runner reads back to reconcile against GitHub's own list."
|
|
1842
1736
|
order_by :number
|
|
1843
1737
|
end
|
|
1844
1738
|
end
|
|
1845
1739
|
|
|
1740
|
+
# How many pull requests the practice has opened on one UTC day, and the rule that the day's cap
|
|
1741
|
+
# is not exceeded. A rule across many records belongs to an aggregate that owns it (ADR 0081): the
|
|
1742
|
+
# cap is a `given` on one record, not a count over `Patch` and `Improvement`, and a record saves
|
|
1743
|
+
# atomically, so two runs cannot both take the last slot. Identified by the day, so a new day is
|
|
1744
|
+
# a new record. `hecks quality_control patch.open` asks `Today` for the day's record, opens it
|
|
1745
|
+
# with `Open` when there is none, and takes a slot with `Take` once a PR is open.
|
|
1746
|
+
aggregate "DailyQuota" do
|
|
1747
|
+
description "One UTC day's count of the pull requests the practice has opened, and the rule that the day's cap is not exceeded."
|
|
1748
|
+
|
|
1749
|
+
identified_by :today
|
|
1750
|
+
|
|
1751
|
+
# Whole days since the epoch, the fact the runtime calls `today`.
|
|
1752
|
+
attribute :today, Day
|
|
1753
|
+
attribute :used, SlotsUsed
|
|
1754
|
+
|
|
1755
|
+
value_object "Day" do
|
|
1756
|
+
attribute :value, Integer, default: 0
|
|
1757
|
+
invariant("a day is not before the epoch") { !value.negative? }
|
|
1758
|
+
end
|
|
1759
|
+
|
|
1760
|
+
value_object "SlotsUsed" do
|
|
1761
|
+
attribute :value, Integer, default: 0
|
|
1762
|
+
invariant("a count of pull requests is not negative") { !value.negative? }
|
|
1763
|
+
end
|
|
1764
|
+
|
|
1765
|
+
value_object "SlotCap" do
|
|
1766
|
+
attribute :value, Integer
|
|
1767
|
+
invariant("a cap is not negative") { !value.negative? }
|
|
1768
|
+
end
|
|
1769
|
+
|
|
1770
|
+
command "Open" do
|
|
1771
|
+
role "QA engineer"
|
|
1772
|
+
goal "Open the day the clock reads, with no slot taken yet"
|
|
1773
|
+
|
|
1774
|
+
attribute :today, Day
|
|
1775
|
+
needs :today
|
|
1776
|
+
|
|
1777
|
+
sets :today
|
|
1778
|
+
|
|
1779
|
+
emits "QuotaOpened"
|
|
1780
|
+
end
|
|
1781
|
+
|
|
1782
|
+
# `cap` is the dial's value, `PR_CAP_PER_DAY`; 0 is uncapped. The script asks this as a dry
|
|
1783
|
+
# run before it opens a PR, so a spent day refuses before anything is opened.
|
|
1784
|
+
command "Take" do
|
|
1785
|
+
role "QA engineer"
|
|
1786
|
+
goal "Take one of the day's slots for a pull request, refused once the day's cap is spent"
|
|
1787
|
+
|
|
1788
|
+
reference_to DailyQuota
|
|
1789
|
+
attribute :cap, SlotCap
|
|
1790
|
+
|
|
1791
|
+
given("the day's cap is not spent") { cap.value == 0 || used.value < cap.value }
|
|
1792
|
+
|
|
1793
|
+
sets :used, increment: { value: 1 }
|
|
1794
|
+
|
|
1795
|
+
emits "SlotTaken"
|
|
1796
|
+
end
|
|
1797
|
+
|
|
1798
|
+
query "Today" do
|
|
1799
|
+
description "The quota of the day the clock reads: nothing before the day's first pull request."
|
|
1800
|
+
attribute :today, Day
|
|
1801
|
+
needs :today
|
|
1802
|
+
where(:"today.value" => :today)
|
|
1803
|
+
end
|
|
1804
|
+
end
|
|
1805
|
+
|
|
1846
1806
|
# A PR for deliberate, non-bugfix work: a sibling of `Patch`, because `Bug` means "proven wrong"
|
|
1847
1807
|
# and `Patch.Open` keeps its required `reference_to Bug`.
|
|
1848
1808
|
# Born `opened`, with no investigating state: the work was decided before any code moved.
|
|
@@ -1864,7 +1824,7 @@ Hecks.bluebook "QualityControl" do
|
|
|
1864
1824
|
attribute :branch, ImprovementBranch
|
|
1865
1825
|
attribute :commit, CommitRef
|
|
1866
1826
|
attribute :title, ImprovementTitle
|
|
1867
|
-
# See `Patch.opened_at
|
|
1827
|
+
# See `Patch.opened_at`.
|
|
1868
1828
|
attribute :opened_at, Instant
|
|
1869
1829
|
|
|
1870
1830
|
value_object "Instant" do
|
|
@@ -1997,14 +1957,6 @@ Hecks.bluebook "QualityControl" do
|
|
|
1997
1957
|
order_by :number
|
|
1998
1958
|
end
|
|
1999
1959
|
|
|
2000
|
-
# `Patch.OpenedSince`'s twin: the cap counts both kinds of PR.
|
|
2001
|
-
query "OpenedSince" do
|
|
2002
|
-
description "Every improvement opened at or after one instant, in number order — the other half of today's count against PR_CAP_PER_DAY."
|
|
2003
|
-
attribute :since, Instant
|
|
2004
|
-
where(:"opened_at.value" => { gte: :since })
|
|
2005
|
-
order_by :number
|
|
2006
|
-
end
|
|
2007
|
-
|
|
2008
1960
|
query "All" do
|
|
2009
1961
|
description "Every improvement ever landed — what a runner reads back to reconcile against GitHub's own list."
|
|
2010
1962
|
order_by :number
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require_relative "value"
|
|
2
2
|
require_relative "aggregate_lock"
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "needs"
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Runtime
|
|
@@ -14,11 +14,6 @@ module Hecks
|
|
|
14
14
|
interpreter.singleton_class.attr_accessor :trace
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# What answers each outside fact a command may `needs` (ADR 0081). A fact is answered once,
|
|
18
|
-
# before any given runs, and the answer rides in the command's own arguments, so the event
|
|
19
|
-
# records it and a replay re-dispatches the recorded value rather than asking again.
|
|
20
|
-
NEED_ANSWERS = { now: ->(registry) { Ports::Clock.now(registry) } }.freeze
|
|
21
|
-
|
|
22
17
|
private
|
|
23
18
|
|
|
24
19
|
# Fills each outside fact the command `needs` and the caller left out. A value the caller
|
|
@@ -32,13 +27,8 @@ module Hecks
|
|
|
32
27
|
fill_defaults(command, fill_needs(command, args))
|
|
33
28
|
end
|
|
34
29
|
|
|
35
|
-
# Answers each fact the command `needs` that the caller left out.
|
|
36
|
-
def fill_needs(command, args)
|
|
37
|
-
missing = command.needs.reject { |fact| args.key?(fact) || args.key?(fact.to_s) }
|
|
38
|
-
return args if missing.empty?
|
|
39
|
-
|
|
40
|
-
args.merge(missing.to_h { |fact| [fact, need_value(command, fact)] })
|
|
41
|
-
end
|
|
30
|
+
# Answers each fact the command `needs` that the caller left out (see `Needs`).
|
|
31
|
+
def fill_needs(command, args) = Needs.fill(command, args, registry: @registry)
|
|
42
32
|
|
|
43
33
|
# Gives each argument the caller left out the default its attribute declares
|
|
44
34
|
# (`attribute :runs, Count, default: 30`), so a declared default holds on every way in and
|
|
@@ -56,15 +46,6 @@ module Hecks
|
|
|
56
46
|
args.merge(absent.to_h { |attribute| [attribute.name.to_sym, attribute.default] })
|
|
57
47
|
end
|
|
58
48
|
|
|
59
|
-
# The answer to one fact, in the shape the command's argument of that name takes: a bare
|
|
60
|
-
# Integer for an Integer attribute, else the one-field value object the language declares
|
|
61
|
-
# for an instant.
|
|
62
|
-
def need_value(command, fact)
|
|
63
|
-
answer = NEED_ANSWERS.fetch(fact).call(@registry)
|
|
64
|
-
attribute = command.attributes.find { |held| held.name.to_s == fact.to_s }
|
|
65
|
-
attribute&.type.to_s == "Integer" ? answer : { value: answer }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
49
|
# Logged after the step's work, so trace order is completion order.
|
|
69
50
|
def step(name)
|
|
70
51
|
result = yield
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative "../ports/clock"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Runtime
|
|
5
|
+
# Answers the outside facts a command or a query `needs` (ADR 0081). A fact is answered once,
|
|
6
|
+
# before any given or filter reads the arguments, and the answer rides in the declaration's own
|
|
7
|
+
# arguments, so an event records it and a replay re-dispatches the recorded value rather than
|
|
8
|
+
# asking again.
|
|
9
|
+
module Needs
|
|
10
|
+
# What answers each fact a declaration may need: `now` is the clock port's reading in epoch
|
|
11
|
+
# seconds, `today` the day it falls in, whole days since the epoch in UTC.
|
|
12
|
+
ANSWERS = {
|
|
13
|
+
now: ->(registry) { Ports::Clock.now(registry) },
|
|
14
|
+
today: ->(registry) { Ports::Clock.now(registry) / 86_400 }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Fills each outside fact the declaration `needs` and the caller left out. A value the caller
|
|
20
|
+
# supplied is kept, so a test or a back-fill can name its own time.
|
|
21
|
+
#
|
|
22
|
+
# @param declaring [#needs, #attributes] the command or query being run
|
|
23
|
+
# @param args [Hash{Symbol => Object}] the arguments the caller passed
|
|
24
|
+
# @param registry [Runtime::Registry] where the clock port is bound
|
|
25
|
+
# @return [Hash{Symbol => Object}] `args`, with each missing needed fact answered
|
|
26
|
+
# @raise [Runtime::WiringError] when the port that answers a fact is not bound exactly once
|
|
27
|
+
def fill(declaring, args, registry:)
|
|
28
|
+
missing = declaring.needs.reject { |fact| args.key?(fact) || args.key?(fact.to_s) }
|
|
29
|
+
return args if missing.empty?
|
|
30
|
+
|
|
31
|
+
args.merge(missing.to_h { |fact| [fact, value(declaring, fact, registry)] })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The answer to one fact, in the shape the declaration's argument of that name takes: a bare
|
|
35
|
+
# Integer for an Integer attribute, else the one-field value object the language declares.
|
|
36
|
+
#
|
|
37
|
+
# @param declaring [#attributes] the command or query
|
|
38
|
+
# @param fact [Symbol] a key of `ANSWERS`
|
|
39
|
+
# @param registry [Runtime::Registry] where the clock port is bound
|
|
40
|
+
# @return [Integer, Hash{Symbol => Integer}] the answer, shaped for its argument
|
|
41
|
+
def value(declaring, fact, registry)
|
|
42
|
+
answer = ANSWERS.fetch(fact).call(registry)
|
|
43
|
+
attribute = declaring.attributes.find { |held| held.name.to_s == fact.to_s }
|
|
44
|
+
attribute&.type.to_s == "Integer" ? answer : { value: answer }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -5,6 +5,7 @@ require_relative "../ports/query/ordering"
|
|
|
5
5
|
require_relative "../query_specification/field_path"
|
|
6
6
|
require_relative "../query_specification/common/comparison"
|
|
7
7
|
require_relative "errors"
|
|
8
|
+
require_relative "needs"
|
|
8
9
|
require_relative "reference_hop"
|
|
9
10
|
require_relative "refusal_wording"
|
|
10
11
|
require_relative "tenant_scope"
|
|
@@ -203,6 +204,7 @@ module Hecks
|
|
|
203
204
|
def entity_rows(domain, aggregate, dotted, args)
|
|
204
205
|
entity_name, query_name = Naming.split_dotted(dotted)
|
|
205
206
|
entity, declared, list_attr = resolve_entity_query(aggregate, entity_name, query_name)
|
|
207
|
+
args = Needs.fill(declared, args, registry: @registry)
|
|
206
208
|
declared = TenantScope.apply(declared, args)
|
|
207
209
|
|
|
208
210
|
parent_key = Naming.reference_key(aggregate.hecks_name)
|
|
@@ -282,6 +284,7 @@ module Hecks
|
|
|
282
284
|
# from field defaults and only refuses when a field has none. `null_vo_argument!`
|
|
283
285
|
# refuses like any other wrong-shaped value. Command arguments are untouched.
|
|
284
286
|
def normalize_args(aggregate, declared, args)
|
|
287
|
+
args = Needs.fill(declared, args, registry: @registry)
|
|
285
288
|
declared.attributes.each_with_object(args.dup) do |attribute, normalized|
|
|
286
289
|
next unless normalized.key?(attribute.name)
|
|
287
290
|
|
|
@@ -408,9 +408,9 @@ module Hecks
|
|
|
408
408
|
File.expand_path(path.to_s).start_with?("#{GEM_CHAPTER_DIR}/")
|
|
409
409
|
end
|
|
410
410
|
|
|
411
|
-
# Two packages can share a chapter name by coincidence (
|
|
412
|
-
#
|
|
413
|
-
#
|
|
411
|
+
# Two packages can share a chapter name by coincidence (a stale vendored
|
|
412
|
+
# fork of Governance/Identity/Deploy still on an app's load paths).
|
|
413
|
+
# Intentional accumulation (several files declaring the same chapter on
|
|
414
414
|
# purpose) is distinguished by package root, not file identity, and is left
|
|
415
415
|
# untouched here — checked once, after every file has loaded.
|
|
416
416
|
def refuse_cross_package_bluebook_merge!
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require_relative "../rust_build"
|
|
6
|
+
require_relative "wasm"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module RustBuild
|
|
10
|
+
# Builds the Rust host binary for a domain and stages it with the domain's module and IR, the
|
|
11
|
+
# three files a container image copies in.
|
|
12
|
+
#
|
|
13
|
+
# Host.call(["path/to/domain", "--target=aarch64-unknown-linux-gnu", "--stage=build/host"])
|
|
14
|
+
#
|
|
15
|
+
# The host is compiled from the workspace this release carries (`rust/host`), so the binary is
|
|
16
|
+
# the one the installed gem was released with; nothing is fetched. The module and IR come from
|
|
17
|
+
# `Wasm`. `CARGO_TARGET_DIR` names where Cargo writes (the copy's `target/` for a client), so
|
|
18
|
+
# a second build recompiles only what changed. Stages `<domain>-host`, `<domain>.wasm` and
|
|
19
|
+
# `<domain>.ir.json` in `--stage` (default `.hecks/host/<target>/` in the working directory).
|
|
20
|
+
module Host
|
|
21
|
+
# The host crate's binary.
|
|
22
|
+
BINARY = "bootstrap"
|
|
23
|
+
|
|
24
|
+
# The toolchain Cargo runs under, as `Wasm` builds with.
|
|
25
|
+
TOOLCHAIN = "stable"
|
|
26
|
+
|
|
27
|
+
# A Rust target triple: `<arch>-<vendor>-<os>`, with an optional `-<env>`.
|
|
28
|
+
TRIPLE = /\A[a-z0-9_]+(-[a-z0-9_.]+){2,3}\z/
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @param argv [Array<String>] the domain's directory, then optional `--target=<triple>` and
|
|
33
|
+
# `--stage=<directory>`
|
|
34
|
+
# @return [Integer] the exit status
|
|
35
|
+
# @raise [Failure] when the toolchain, the target or a step is missing or fails
|
|
36
|
+
def call(argv)
|
|
37
|
+
domain = argv.find { |arg| !arg.start_with?("--") } or
|
|
38
|
+
raise Failure, "usage: hecks build_host <domain> [target=<triple>] [stage=<directory>]"
|
|
39
|
+
target = flag(argv, "target") || host_triple
|
|
40
|
+
require_buildable!(target)
|
|
41
|
+
stage = flag(argv, "stage") || File.join(Dir.pwd, ".hecks", "host", target)
|
|
42
|
+
|
|
43
|
+
status = Wasm.call([domain])
|
|
44
|
+
raise Failure, "project_wasm failed for #{domain}" unless status.zero?
|
|
45
|
+
|
|
46
|
+
binary = compile(target)
|
|
47
|
+
publish(binary, File.basename(domain.chomp("/")), stage)
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param argv [Array<String>] a tool's arguments
|
|
52
|
+
# @param name [String] a flag's name
|
|
53
|
+
# @return [String, nil] the value of `--<name>=<value>`, nil when absent
|
|
54
|
+
def flag(argv, name)
|
|
55
|
+
argv.filter_map { |arg| arg.delete_prefix("--#{name}=") if arg.start_with?("--#{name}=") }.last
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [String] the triple the Rust toolchain builds for by default
|
|
59
|
+
# @raise [Failure] when there is no Rust toolchain
|
|
60
|
+
def host_triple
|
|
61
|
+
require_rustup!
|
|
62
|
+
query("rustup", "run", TOOLCHAIN, "rustc", "-vV").to_s[/^host: (\S+)/, 1] or
|
|
63
|
+
raise Failure, "rustup has no #{TOOLCHAIN} toolchain. Install it once with:\n\n " \
|
|
64
|
+
"rustup toolchain install #{TOOLCHAIN}\n\nthen re-run the build."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def require_buildable!(target)
|
|
68
|
+
raise Failure, "#{target} is not a Rust target triple (for example aarch64-unknown-linux-gnu)" unless
|
|
69
|
+
TRIPLE.match?(target)
|
|
70
|
+
if target.start_with?("wasm")
|
|
71
|
+
raise Failure, "the host is a server binary; #{target} is a wasm target. Build the module with " \
|
|
72
|
+
"build.build_wasm, or name a native target"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
require_rustup!
|
|
76
|
+
installed = query("rustup", "target", "list", "--installed", "--toolchain", TOOLCHAIN).to_s.split
|
|
77
|
+
return if installed.include?(target) || target == host_triple
|
|
78
|
+
|
|
79
|
+
raise Failure, <<~MSG
|
|
80
|
+
#{target} isn't installed for the #{TOOLCHAIN} toolchain. Install it once with:
|
|
81
|
+
|
|
82
|
+
rustup target add #{target} --toolchain #{TOOLCHAIN}
|
|
83
|
+
|
|
84
|
+
then re-run the build.
|
|
85
|
+
MSG
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def require_rustup!
|
|
89
|
+
return if query("rustup", "--version")
|
|
90
|
+
|
|
91
|
+
raise Failure, "rustup isn't installed. Install the Rust toolchain from https://rustup.rs, " \
|
|
92
|
+
"then re-run the build."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# `rustup run`, not bare `cargo`, as `Wasm` does. A cross target links with the
|
|
96
|
+
# `<arch>-linux-gnu-gcc` on PATH unless `CARGO_TARGET_<TRIPLE>_LINKER` already names a linker.
|
|
97
|
+
def compile(target)
|
|
98
|
+
host_dir = File.join(RustBuild.rust_dir, "host")
|
|
99
|
+
target_dir = ENV.fetch("CARGO_TARGET_DIR", File.join(host_dir, "target"))
|
|
100
|
+
puts "== cargo build --release --target #{target} --bin #{BINARY} (rust/host) =="
|
|
101
|
+
env = { "CARGO_TARGET_DIR" => target_dir }.merge(linker_for(target))
|
|
102
|
+
RustBuild.command!("rustup", "run", TOOLCHAIN, "cargo", "build", "--release", "--target", target,
|
|
103
|
+
"--bin", BINARY, chdir: host_dir, env: env)
|
|
104
|
+
File.join(target_dir, target, "release", BINARY)
|
|
105
|
+
rescue Failure => e
|
|
106
|
+
raise Failure, "#{e.message}#{cross_hint(target)}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def linker_for(target)
|
|
110
|
+
variable = "CARGO_TARGET_#{target.upcase.tr('-.', '__')}_LINKER"
|
|
111
|
+
return {} if ENV.key?(variable) || target == host_triple
|
|
112
|
+
|
|
113
|
+
candidate = "#{target.split('-').first}-linux-gnu-gcc"
|
|
114
|
+
target.end_with?("linux-gnu") && query(candidate, "--version") ? { variable => candidate } : {}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def cross_hint(target)
|
|
118
|
+
return "" if target == host_triple
|
|
119
|
+
|
|
120
|
+
"\n#{target} differs from this machine's #{host_triple}, so linking needs a linker for it: " \
|
|
121
|
+
"put a cross compiler such as #{target.split('-').first}-linux-gnu-gcc on PATH, or set " \
|
|
122
|
+
"CARGO_TARGET_#{target.upcase.tr('-.', '__')}_LINKER"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def publish(binary, name, stage)
|
|
126
|
+
dist = File.join(RustBuild.rust_dir, "dist")
|
|
127
|
+
files = { binary => "#{name}-host", File.join(dist, "#{name}.wasm") => "#{name}.wasm",
|
|
128
|
+
File.join(dist, "#{name}.ir.json") => "#{name}.ir.json" }
|
|
129
|
+
files.each_key do |path|
|
|
130
|
+
raise Failure, "the build left no #{path}" unless File.file?(path)
|
|
131
|
+
end
|
|
132
|
+
FileUtils.mkdir_p(stage)
|
|
133
|
+
files.each { |from, to| FileUtils.cp(from, File.join(stage, to)) }
|
|
134
|
+
File.chmod(0o755, File.join(stage, "#{name}-host"))
|
|
135
|
+
puts "staged in #{stage}: #{files.values.join(', ')}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [String, nil] a program's combined output, nil when it is missing or fails
|
|
139
|
+
def query(*command)
|
|
140
|
+
out, status = Open3.capture2e(*command)
|
|
141
|
+
status.success? ? out : nil
|
|
142
|
+
rescue SystemCallError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -5,12 +5,14 @@ require_relative "../rust_build"
|
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module RustBuild
|
|
8
|
-
# The stdin a compiled domain binary reads: the steps to run, plus the
|
|
9
|
-
#
|
|
8
|
+
# The stdin a compiled domain binary reads: the steps to run, plus what the host would hand its
|
|
9
|
+
# kernel from the domain's IR.
|
|
10
10
|
#
|
|
11
|
-
# The kernel has no IR of its own, so
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# The kernel has no IR of its own, so the facts a declaration reaches it with arrive as
|
|
12
|
+
# top-level tables the Rust host builds from the domain's IR: `"defaults"`, a command's declared
|
|
13
|
+
# argument defaults (`{ "Domain::Aggregate.Command" => { "attribute" => value } }`); `"needs"`,
|
|
14
|
+
# the outside facts a command needs; and `"query_needs"`, the same for a query (kept apart: a
|
|
15
|
+
# command and a query may share a qualified name). A harness that sends only `"steps"` would
|
|
14
16
|
# leave the kernel refusing an argument Ruby fills, so every conformance caller builds its
|
|
15
17
|
# input here.
|
|
16
18
|
module KernelInput
|
|
@@ -18,11 +20,10 @@ module Hecks
|
|
|
18
20
|
|
|
19
21
|
# @param domain_path [String] the domain's directory; its basename names the generated module
|
|
20
22
|
# @param steps [Array<Hash>] the steps to run
|
|
21
|
-
# @return [Hash{String => Object}] the input;
|
|
23
|
+
# @return [Hash{String => Object}] the input; each table is there only when it has an entry
|
|
22
24
|
def build(domain_path, steps)
|
|
23
25
|
input = { "steps" => steps }
|
|
24
|
-
|
|
25
|
-
input["defaults"] = defaults unless defaults.empty?
|
|
26
|
+
tables_for(domain_path).each { |name, table| input[name] = table unless table.empty? }
|
|
26
27
|
input
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -30,19 +31,52 @@ module Hecks
|
|
|
30
31
|
# @return [String] the input as JSON, ready for the binary's stdin
|
|
31
32
|
def json(domain_path, steps) = JSON.generate(build(domain_path, steps))
|
|
32
33
|
|
|
33
|
-
# Each command's declared defaults, read from the domain's
|
|
34
|
+
# Each command's declared defaults, entity commands included, read from the domain's
|
|
35
|
+
# generated IR. An entity's commands sit under `Domain::Aggregate.Entity[.Entity].Command`.
|
|
34
36
|
#
|
|
35
37
|
# @param domain_path [String] the domain's directory
|
|
36
38
|
# @return [Hash{String => Hash{String => Object}}] empty when the IR is absent or has none
|
|
37
|
-
def defaults_for(domain_path)
|
|
38
|
-
|
|
39
|
+
def defaults_for(domain_path) = tables_for(domain_path).fetch("defaults")
|
|
40
|
+
|
|
41
|
+
# The three tables of one domain: `"defaults"`, `"needs"` and `"query_needs"`.
|
|
42
|
+
#
|
|
43
|
+
# @param domain_path [String] the domain's directory
|
|
44
|
+
# @return [Hash{String => Hash}] each table, empty when the IR is absent or declares none
|
|
45
|
+
def tables_for(domain_path)
|
|
46
|
+
tables = { "defaults" => {}, "needs" => {}, "query_needs" => {} }
|
|
47
|
+
ir = generated_ir(domain_path) or return tables
|
|
39
48
|
domain = ir["name"].to_s
|
|
40
|
-
Array(ir["aggregates"]).
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
Array(ir["aggregates"]).each { |aggregate| collect(aggregate, "#{domain}::#{aggregate['name']}", tables) }
|
|
50
|
+
tables
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Adds what `node`'s commands and queries declare, then what each entity nested in it does,
|
|
54
|
+
# under `prefix`.
|
|
55
|
+
#
|
|
56
|
+
# @param node [Hash] an aggregate or entity of the IR
|
|
57
|
+
# @param prefix [String] the verb path down to `node`
|
|
58
|
+
# @param tables [Hash] filled in place
|
|
59
|
+
# @return [void]
|
|
60
|
+
def collect(node, prefix, tables)
|
|
61
|
+
Array(node["commands"]).each do |command|
|
|
62
|
+
verb = "#{prefix}.#{command['name']}"
|
|
63
|
+
held = Array(command["attributes"]).reject { |attribute| attribute["default"].nil? }
|
|
64
|
+
.to_h { |attribute| [attribute["name"], attribute["default"]] }
|
|
65
|
+
tables["defaults"][verb] = held unless held.empty?
|
|
66
|
+
tables["needs"][verb] = needs_of(command) unless Array(command["needs"]).empty?
|
|
67
|
+
end
|
|
68
|
+
Array(node["queries"]).each do |query|
|
|
69
|
+
tables["query_needs"]["#{prefix}.#{query['name']}"] = needs_of(query) unless Array(query["needs"]).empty?
|
|
70
|
+
end
|
|
71
|
+
Array(node["entities"]).each { |entity| collect(entity, "#{prefix}.#{entity['name']}", tables) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param declaration [Hash] a command or query of the IR that declares `needs`
|
|
75
|
+
# @return [Array<Hash>] each needed fact with the type of the argument of the same name
|
|
76
|
+
def needs_of(declaration)
|
|
77
|
+
Array(declaration["needs"]).map do |need|
|
|
78
|
+
argument = Array(declaration["attributes"]).find { |attribute| attribute["name"] == need["fact"] }
|
|
79
|
+
{ "fact" => need["fact"], "type" => argument ? argument["type"].to_s : "" }
|
|
46
80
|
end
|
|
47
81
|
end
|
|
48
82
|
|