hecks 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +12 -13
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +3 -3
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +1 -1
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/verification.rb +11 -11
- data/lib/hecks/runtime/registry.rb +8 -4
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +6 -1
- metadata +28 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require_relative "commands"
|
|
2
|
+
require_relative "git"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Release
|
|
6
|
+
class Runner
|
|
7
|
+
# Makes sure the release tag `vX.Y.Z` is an annotated tag on the release
|
|
8
|
+
# commit and is on origin, creating and pushing it only when it is missing.
|
|
9
|
+
class Tagger
|
|
10
|
+
# @param git [Git] the repository the release is cut from
|
|
11
|
+
# @param console [Console] asks before the tag is created or pushed
|
|
12
|
+
# @param dry_run [Boolean] report what would happen and change nothing
|
|
13
|
+
def initialize(git:, console:, dry_run:)
|
|
14
|
+
@git = git
|
|
15
|
+
@console = console
|
|
16
|
+
@dry_run = dry_run
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Brings the tag to the state described above, saying what it did.
|
|
20
|
+
#
|
|
21
|
+
# @param facts [Preflight::Facts] the version and commit being released
|
|
22
|
+
# @return [Boolean] false when the person declined the question, true otherwise
|
|
23
|
+
# @raise [Refusal] if the tag already points at another commit, locally or on origin
|
|
24
|
+
# @raise [CommandFailed] if creating or pushing the tag fails
|
|
25
|
+
def ensure!(facts)
|
|
26
|
+
tag = "v#{facts.version}"
|
|
27
|
+
local = local_commit(tag)
|
|
28
|
+
remote = remote_commit(tag)
|
|
29
|
+
refuse_elsewhere(tag, facts.sha, local: local, remote: remote)
|
|
30
|
+
return true.tap { already_there(tag) } if remote
|
|
31
|
+
return true.tap { preview(tag, facts.sha, local) } if @dry_run
|
|
32
|
+
return false unless @console.confirm?(question(tag, facts.sha, local))
|
|
33
|
+
|
|
34
|
+
create(tag, facts.sha) unless local
|
|
35
|
+
push(tag)
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def already_there(tag)
|
|
42
|
+
@console.say("Tag #{tag} is already on origin at the release commit; skipping.")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def preview(tag, sha, local)
|
|
46
|
+
@console.say("Would create annotated tag #{tag} at #{sha[0, 7]}.") unless local
|
|
47
|
+
@console.say("Would push #{tag} to origin.")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def question(tag, sha, local)
|
|
51
|
+
verb = local ? "Push" : "Create and push"
|
|
52
|
+
"#{verb} annotated tag #{tag} at #{sha[0, 7]} to origin?"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def create(tag, sha)
|
|
56
|
+
@git.run!("tag", "-a", tag, "-m", "Release #{tag.delete_prefix('v')}", sha)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def push(tag)
|
|
60
|
+
@git.run!("push", "origin", tag)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def refuse_elsewhere(tag, sha, local:, remote:)
|
|
64
|
+
{ "locally" => local, "on origin" => remote }.each do |where, commit|
|
|
65
|
+
next if commit.nil? || commit == sha
|
|
66
|
+
|
|
67
|
+
raise Refusal, "tag #{tag} already points at #{commit[0, 7]} #{where}, not at the release commit " \
|
|
68
|
+
"#{sha[0, 7]}; if that tag was never released, delete it (git tag -d #{tag}; " \
|
|
69
|
+
"git push origin :refs/tags/#{tag}) and re-run, otherwise main has moved since the release " \
|
|
70
|
+
"and the missing package must be published from a checkout of #{tag}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def local_commit(tag)
|
|
75
|
+
result = @git.capture("rev-parse", "-q", "--verify", "refs/tags/#{tag}^{commit}")
|
|
76
|
+
result.success? ? result.stdout.strip : nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def remote_commit(tag)
|
|
80
|
+
listing = @git.read("ls-remote", "--tags", "origin", "refs/tags/#{tag}", "refs/tags/#{tag}^{}")
|
|
81
|
+
shas = listing.lines.to_h { |line| line.split.then { |sha, ref| [ref, sha] } }
|
|
82
|
+
shas.fetch("refs/tags/#{tag}^{}") { shas["refs/tags/#{tag}"] }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
require_relative "runner/commands"
|
|
2
|
+
require_relative "runner/console"
|
|
3
|
+
require_relative "runner/git"
|
|
4
|
+
require_relative "runner/preflight"
|
|
5
|
+
require_relative "runner/published"
|
|
6
|
+
require_relative "runner/tagger"
|
|
7
|
+
require_relative "runner/gem_publisher"
|
|
8
|
+
require_relative "runner/npm_publisher"
|
|
9
|
+
require_relative "runner/ci_publisher"
|
|
10
|
+
|
|
11
|
+
module Hecks
|
|
12
|
+
module Release
|
|
13
|
+
# Performs the release once the release PR has merged: tags the merge
|
|
14
|
+
# commit, publishes the gem to rubygems.org and gets the JavaScript client
|
|
15
|
+
# published to npm.
|
|
16
|
+
#
|
|
17
|
+
# ## Why it exists
|
|
18
|
+
#
|
|
19
|
+
# A release is four steps that have to happen in one order, and only
|
|
20
|
+
# the contributing guide remembered it. This runs them in that order, refuses to
|
|
21
|
+
# start from a checkout that is not the merged `main`, and can be run again:
|
|
22
|
+
# it asks the registries what is already published and skips it, so a run
|
|
23
|
+
# that died between the gem and the package resumes at the package.
|
|
24
|
+
#
|
|
25
|
+
# ## Steps
|
|
26
|
+
#
|
|
27
|
+
# 1. Preflight: tools installed, on a clean `main` equal to `origin/main`,
|
|
28
|
+
# the gem and the client at one version, the changelog naming it.
|
|
29
|
+
# 2. Published state: which of the gem and the package are already out.
|
|
30
|
+
# 3. Tag: an annotated `vX.Y.Z` on the release commit, pushed to origin.
|
|
31
|
+
# 4. Gem: `bin/release_gem`, unchanged.
|
|
32
|
+
# 5. npm, one of two ways:
|
|
33
|
+
# - by default CI publishes: the tag triggers `publish-client.yml`, which
|
|
34
|
+
# uses npm trusted publishing, and this waits for the version to appear
|
|
35
|
+
# (`--no-wait` skips the wait);
|
|
36
|
+
# - with `--npm-local` this publishes from the machine, with a
|
|
37
|
+
# bypass-two-factor token 1Password holds: the way to make the first
|
|
38
|
+
# publish, before trusted publishing can be configured, and the fallback
|
|
39
|
+
# when CI is down.
|
|
40
|
+
#
|
|
41
|
+
# ## Injectable commands and time
|
|
42
|
+
#
|
|
43
|
+
# Every process is started through `commands`, any object answering
|
|
44
|
+
# `capture(*argv, env:, chdir:)` and `run!(*argv, env:, chdir:)` like
|
|
45
|
+
# {Commands}, so a spec drives the whole release with a recorder. The wait for
|
|
46
|
+
# CI takes its clock and its pause as callables for the same reason.
|
|
47
|
+
class Runner
|
|
48
|
+
# Which steps to run and how.
|
|
49
|
+
class Options
|
|
50
|
+
# @return [Boolean] run every check and build, but tag, push and publish nothing
|
|
51
|
+
attr_reader :dry_run
|
|
52
|
+
|
|
53
|
+
# @param dry_run [Boolean] run every check and build, but tag, push and publish nothing
|
|
54
|
+
# @param gem_only [Boolean] publish the gem, not the package
|
|
55
|
+
# @param npm_only [Boolean] do the npm step only, not the gem: wait for CI's publish, or
|
|
56
|
+
# with `npm_local` publish from here
|
|
57
|
+
# @param npm_local [Boolean] publish the package from this machine, not by CI
|
|
58
|
+
# @param no_wait [Boolean] do not wait for CI's publish to reach npm
|
|
59
|
+
# @param yes [Boolean] answer every confirmation yes
|
|
60
|
+
# @raise [ArgumentError] if the flags contradict each other
|
|
61
|
+
def initialize(dry_run: false, gem_only: false, npm_only: false, npm_local: false, no_wait: false, yes: false)
|
|
62
|
+
check_flags(gem_only, npm_only, npm_local, no_wait)
|
|
63
|
+
|
|
64
|
+
@dry_run = dry_run
|
|
65
|
+
@gem_only = gem_only
|
|
66
|
+
@npm_only = npm_only
|
|
67
|
+
@npm_local = npm_local
|
|
68
|
+
@no_wait = no_wait
|
|
69
|
+
@yes = yes
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Says whether confirmations are answered automatically.
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] true under `--yes`
|
|
75
|
+
def yes?
|
|
76
|
+
@yes
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Says whether the gem step is in scope.
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean] true unless `--npm-only`
|
|
82
|
+
def gem?
|
|
83
|
+
!@npm_only
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Says whether the npm step is in scope.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean] true unless `--gem-only`
|
|
89
|
+
def npm?
|
|
90
|
+
!@gem_only
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Says whether the package is published from this machine.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] true under `--npm-local`
|
|
96
|
+
def npm_local?
|
|
97
|
+
@npm_local
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Says whether to wait for CI's publish to reach npm.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] true unless `--no-wait`
|
|
103
|
+
def wait?
|
|
104
|
+
!@no_wait
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def check_flags(gem_only, npm_only, npm_local, no_wait)
|
|
110
|
+
raise ArgumentError, "--gem-only and --npm-only cannot be combined" if gem_only && npm_only
|
|
111
|
+
raise ArgumentError, "--gem-only and --npm-local cannot be combined" if gem_only && npm_local
|
|
112
|
+
return unless no_wait && npm_local
|
|
113
|
+
|
|
114
|
+
raise ArgumentError, "--no-wait applies to CI's publish, so it cannot be combined with --npm-local"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param root [String] the repository root
|
|
119
|
+
# @param options [Options] which steps to run and how
|
|
120
|
+
# @param commands [#capture, #run!] starts every process the release runs
|
|
121
|
+
# @param input [IO] where confirmations are read from
|
|
122
|
+
# @param out [IO] where progress is written
|
|
123
|
+
# @param err [IO] where refusals and failures are written
|
|
124
|
+
# @param pause [#call] sleeps for a number of seconds while waiting for CI
|
|
125
|
+
# @param now [#call] answers the current time in seconds, monotonically, while waiting for CI
|
|
126
|
+
def initialize(root:, options: Options.new, commands: Commands.new, input: $stdin, out: $stdout, err: $stderr,
|
|
127
|
+
pause: ->(seconds) { sleep(seconds) }, now: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
128
|
+
@root = root
|
|
129
|
+
@options = options
|
|
130
|
+
@commands = commands
|
|
131
|
+
@console = Console.new(input: input, out: out, err: err, assume_yes: options.yes?)
|
|
132
|
+
@git = Git.new(root: root, commands: commands)
|
|
133
|
+
@published = Published.new(root: root, commands: commands)
|
|
134
|
+
@ci = CiPublisher.new(root: root, published: @published, console: @console, pause: pause, now: now)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Runs the release.
|
|
138
|
+
#
|
|
139
|
+
# @return [Integer] 0 when the release finished or had nothing to do, 1 when a check refused,
|
|
140
|
+
# a step failed, CI's publish did not arrive in time or a confirmation was declined
|
|
141
|
+
def call
|
|
142
|
+
facts = Preflight.new(root: @root, commands: @commands, git: @git, tools: tools).check!
|
|
143
|
+
pending = pending_steps(facts.version)
|
|
144
|
+
require_workflow(pending)
|
|
145
|
+
return declined unless Tagger.new(git: @git, console: @console, dry_run: @options.dry_run).ensure!(facts)
|
|
146
|
+
|
|
147
|
+
publish(facts, pending)
|
|
148
|
+
rescue Refusal, CommandFailed => e
|
|
149
|
+
@console.warn(e.message)
|
|
150
|
+
1
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def tools
|
|
156
|
+
list = %w[git curl]
|
|
157
|
+
list << "gem" if @options.gem?
|
|
158
|
+
list << "npm" if @options.npm?
|
|
159
|
+
list << "op" if !@options.dry_run && (@options.gem? || @options.npm_local?)
|
|
160
|
+
list
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def pending_steps(version)
|
|
164
|
+
[
|
|
165
|
+
(:gem if @options.gem? && !done?("hecks #{version} is already on rubygems.org", @published.gem?(version))),
|
|
166
|
+
(:npm if @options.npm? && !done?("@hecks/client #{version} is already on npm", @published.npm?(version)))
|
|
167
|
+
].compact
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def done?(message, published)
|
|
171
|
+
@console.say("#{message}; skipping.") if published
|
|
172
|
+
published
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def ci_publishes?(pending)
|
|
176
|
+
pending.include?(:npm) && !@options.npm_local?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def require_workflow(pending)
|
|
180
|
+
return unless ci_publishes?(pending) && !@ci.workflow?
|
|
181
|
+
|
|
182
|
+
raise Refusal, "#{CiPublisher::WORKFLOW} is not in this checkout, so CI cannot publish @hecks/client; " \
|
|
183
|
+
"publish it from here with --npm-local (the first publish, before trusted publishing is set up)"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def publish(facts, pending)
|
|
187
|
+
if pending.empty?
|
|
188
|
+
@console.say("Nothing to publish for #{facts.version}.")
|
|
189
|
+
return 0
|
|
190
|
+
end
|
|
191
|
+
return declined unless @options.dry_run || publish_confirmed?(facts.version, pending)
|
|
192
|
+
|
|
193
|
+
run_steps(facts.version, pending)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def publish_confirmed?(version, pending)
|
|
197
|
+
targets = []
|
|
198
|
+
targets << "hecks #{version} to rubygems.org" if pending.include?(:gem)
|
|
199
|
+
targets << "@hecks/client #{version} to npm" if pending.include?(:npm) && @options.npm_local?
|
|
200
|
+
return true if targets.empty?
|
|
201
|
+
|
|
202
|
+
suffix = ci_publishes?(pending) ? " (CI then publishes @hecks/client from the tag)" : ""
|
|
203
|
+
@console.confirm?("Publish #{targets.join(' and ')}#{suffix}? This cannot be undone.")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def run_steps(version, pending)
|
|
207
|
+
dry_run = @options.dry_run
|
|
208
|
+
publish_gem(version, dry_run) if pending.include?(:gem)
|
|
209
|
+
return 1 if pending.include?(:npm) && !publish_npm!(version, dry_run, ci_publishes?(pending))
|
|
210
|
+
|
|
211
|
+
@console.say(dry_run ? "Dry run complete; nothing was tagged, pushed or published." : "Released hecks #{version}.")
|
|
212
|
+
0
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def publish_gem(version, dry_run)
|
|
216
|
+
GemPublisher.new(root: @root, commands: @commands, console: @console).publish!(version, dry_run: dry_run)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def publish_npm!(version, dry_run, via_ci)
|
|
220
|
+
return wait_for_ci!(version, dry_run) if via_ci
|
|
221
|
+
|
|
222
|
+
NpmPublisher.new(root: @root, commands: @commands, console: @console).publish!(version, dry_run: dry_run)
|
|
223
|
+
true
|
|
224
|
+
rescue CommandFailed => e
|
|
225
|
+
@console.warn("npm publish failed: #{e.message}")
|
|
226
|
+
unless dry_run
|
|
227
|
+
@console.warn("Finish the release with: bin/release --npm-only --npm-local (published steps are not repeated)")
|
|
228
|
+
end
|
|
229
|
+
raise
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def wait_for_ci!(version, dry_run)
|
|
233
|
+
arrived = @ci.wait!(version, dry_run: dry_run, wait: @options.wait?)
|
|
234
|
+
@console.warn(CiPublisher.timeout_hint(version)) unless arrived
|
|
235
|
+
arrived
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def declined
|
|
239
|
+
@console.warn("Aborted; nothing was published.")
|
|
240
|
+
1
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -140,7 +140,7 @@ module Hecks
|
|
|
140
140
|
.each do |(aggregate, verb), candidates|
|
|
141
141
|
# Bounded chapters wrap in their own module (`Domain::Aggregate`)
|
|
142
142
|
# so two BCs can both declare `Person` without colliding on
|
|
143
|
-
# Object::Person. Folder-spread files of the
|
|
143
|
+
# Object::Person. Folder-spread files of the same chapter still
|
|
144
144
|
# get the shortcut — they are not BCs.
|
|
145
145
|
next if bounded_chapter?(candidates)
|
|
146
146
|
|
|
@@ -106,7 +106,7 @@ module Hecks
|
|
|
106
106
|
ctx.plan = DependencyPlanning::Analyzer.call(aggregate: aggregate, command: command)
|
|
107
107
|
# **Resolved here, once, before hydration** — `Registry#repository`
|
|
108
108
|
# memoizes, so this and `step_hydrate`'s own read of `ctx.repository`
|
|
109
|
-
# (no second fetch there
|
|
109
|
+
# (no second fetch there) always name the same instance;
|
|
110
110
|
# the isolation decision below (lock vs. CAS+retry) needs the
|
|
111
111
|
# repository's capabilities before a single step runs.
|
|
112
112
|
ctx.repository = @registry.repository(domain, aggregate)
|
|
@@ -154,7 +154,7 @@ module Hecks
|
|
|
154
154
|
|
|
155
155
|
def step_hydrate(ctx)
|
|
156
156
|
# `ctx.repository` is resolved once, in `#call`, before the
|
|
157
|
-
# isolation decision (lock vs. CAS+retry) — not here
|
|
157
|
+
# isolation decision (lock vs. CAS+retry) — not here.
|
|
158
158
|
ctx.strategy = ctx.plan.strategy_for(capabilities: ctx.repository.capabilities)
|
|
159
159
|
ctx.instance = step(:hydrate) do
|
|
160
160
|
if ctx.plan.complete_state? && ctx.plan.state_independent?
|
|
@@ -543,7 +543,7 @@ module Hecks
|
|
|
543
543
|
# Transitional compatibility for live source that has not yet
|
|
544
544
|
# acquired explicit effects, same as it always was — deliberately
|
|
545
545
|
# isolated from the normal routing and planning path so
|
|
546
|
-
# `reference_to`
|
|
546
|
+
# `reference_to` does not choose how a migrated command hydrates
|
|
547
547
|
# or persists. A real, future Wave 8 removes this once that wider
|
|
548
548
|
# inventory is empty, not before.
|
|
549
549
|
#
|
|
@@ -163,7 +163,7 @@ module Hecks
|
|
|
163
163
|
def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil, correction: {})
|
|
164
164
|
state = GuardState.new(subject)
|
|
165
165
|
# A rule may only read within its own aggregate boundary (S12,
|
|
166
|
-
# ADR 0025) — `subject`'s own stored references are
|
|
166
|
+
# ADR 0025) — `subject`'s own stored references are not
|
|
167
167
|
# dereferenced here at all: a `projects :customer_status, from:
|
|
168
168
|
# :"customer.status"` field is just `subject`'s own
|
|
169
169
|
# field, a regular stored attribute, already present in
|
|
@@ -375,7 +375,7 @@ module Hecks
|
|
|
375
375
|
def enforce_ensures(subject, command, args, old:, domain:, parent: nil, correction: {})
|
|
376
376
|
state = GuardState.new(subject)
|
|
377
377
|
# S12, ADR 0025 — same boundary reasoning as enforce_givens
|
|
378
|
-
# above: `subject`'s own stored references are
|
|
378
|
+
# above: `subject`'s own stored references are not
|
|
379
379
|
# dereferenced here; a `projects`-maintained field is already
|
|
380
380
|
# part of `state`. `command`/`args` still dereferences — a
|
|
381
381
|
# fresh reference-typed argument stays in bounds.
|
|
@@ -89,7 +89,7 @@ module Hecks
|
|
|
89
89
|
# matches by the element's own minted identity string directly instead
|
|
90
90
|
# (`element_identity`, below) — the routing envelope already resolved
|
|
91
91
|
# which element it means, so re-deriving `wants` from `args` would be
|
|
92
|
-
# redundant at best and wrong if `args`
|
|
92
|
+
# redundant at best and wrong if `args` does not carry that identity
|
|
93
93
|
# at all.
|
|
94
94
|
# Locate, then copy-before-mutate, in that order — see the "one
|
|
95
95
|
# level deeper" comment below on why the copy has to happen exactly
|
|
@@ -251,7 +251,7 @@ module Hecks
|
|
|
251
251
|
|
|
252
252
|
def step_hydrate_parent(ctx)
|
|
253
253
|
# `ctx.repository` is resolved once, in `#call`, before the
|
|
254
|
-
# isolation decision — not here
|
|
254
|
+
# isolation decision — not here.
|
|
255
255
|
ctx.instance = step(:hydrate_parent) do
|
|
256
256
|
parent(ctx.repository, ctx.aggregate, ctx.entity_name, ctx.command_name, ctx.args, ctx.route)
|
|
257
257
|
end
|
|
@@ -12,7 +12,7 @@ module Hecks
|
|
|
12
12
|
# vs the flat facts hash `dispatch_flat` carries, a port operation's
|
|
13
13
|
# reference attribute
|
|
14
14
|
# lifted into `to:`). `Runtime::Routing.envelope`/`.payload` delegate here
|
|
15
|
-
# and
|
|
15
|
+
# and hold none of that logic themselves.
|
|
16
16
|
#
|
|
17
17
|
# `facts` maps a fact name to exactly one of:
|
|
18
18
|
#
|
data/lib/hecks/runtime/loader.rb
CHANGED
|
@@ -177,7 +177,7 @@ module Hecks
|
|
|
177
177
|
# (conservative — see `SagaPersistence#rehydrate_sagas!`'s own
|
|
178
178
|
# comment).
|
|
179
179
|
#
|
|
180
|
-
# ADR 0033 — this loader
|
|
180
|
+
# ADR 0033 — this loader does not name `EraCheck`, or any other
|
|
181
181
|
# era-specific class, at all. Every loaded persistence plugin
|
|
182
182
|
# (`Ports::Persistence.each_plugin` — nothing here if nothing was
|
|
183
183
|
# ever `require`d, or brought in by a bound adapter, see
|
data/lib/hecks/runtime/outbox.rb
CHANGED
|
@@ -398,7 +398,7 @@ module Hecks
|
|
|
398
398
|
# A DOMAIN_REFUSAL never reaches here — PolicyInterpreter and
|
|
399
399
|
# SagaInterpreter both rescue it as a recorded, undelivered
|
|
400
400
|
# reaction. Anything that does reach here is a defect in the
|
|
401
|
-
# relay's own path (a consumer that
|
|
401
|
+
# relay's own path (a consumer that does not exist, an
|
|
402
402
|
# adapter that raised outside the interpreters' own rescue).
|
|
403
403
|
repository.outbox_settle(row.id, status: "failed", error: "#{e.class}: #{e.message}")
|
|
404
404
|
row.status = "failed"
|
|
@@ -271,9 +271,9 @@ module Hecks
|
|
|
271
271
|
# aggregate, never one of its entities). An entity target's own identity
|
|
272
272
|
# is resolved entirely separately, from `args` (`build`'s own `entity_
|
|
273
273
|
# identities` loop) — this method is never consulted for it, so
|
|
274
|
-
# answering for an entity target does not "invent an entity identity"
|
|
275
|
-
#
|
|
276
|
-
#
|
|
274
|
+
# answering for an entity target does not "invent an entity identity",
|
|
275
|
+
# just as answering for a plain one invents none; it is
|
|
276
|
+
# the one thing this method supplies. `build`'s own
|
|
277
277
|
# explicit branch (below) already applies this correctly either way
|
|
278
278
|
# (`aggregate_identity ||= inherited_receiver`, entity identities read
|
|
279
279
|
# from `args` regardless) — the caller that actually needed a guard
|
|
@@ -33,6 +33,8 @@ module Hecks
|
|
|
33
33
|
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
34
34
|
# @raise [ArgumentError] if `group_by` or `median` names a field its target
|
|
35
35
|
# aggregate does not declare
|
|
36
|
+
# @raise [Runtime::InvariantViolation] if two rows reach the same full `group_by`
|
|
37
|
+
# key path and that path does not cover the grouped aggregate's whole identity
|
|
36
38
|
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
37
39
|
def call(domain, model, args)
|
|
38
40
|
project(domain, model, args)
|
|
@@ -148,7 +150,8 @@ module Hecks
|
|
|
148
150
|
reduced_head = aggregation_target(model, bluebook)
|
|
149
151
|
[heads.each_with_object({}) do |(as, value), out|
|
|
150
152
|
out[as] = if grouped_head && as == grouped_head[:as]
|
|
151
|
-
nest(value.map { |record| Value.materialize_unwrapped(row(record)) }, model.group_by_fields
|
|
153
|
+
nest(value.map { |record| Value.materialize_unwrapped(row(record)) }, model.group_by_fields,
|
|
154
|
+
collision_check(model, bluebook, grouped_head))
|
|
152
155
|
elsif reduced_head && as == reduced_head[:as] && model.count?
|
|
153
156
|
value.length
|
|
154
157
|
elsif reduced_head && as == reduced_head[:as]
|
|
@@ -267,28 +270,47 @@ module Hecks
|
|
|
267
270
|
target
|
|
268
271
|
end
|
|
269
272
|
|
|
273
|
+
# The read model whose `group_by` leaves must each hold one row, or nil
|
|
274
|
+
# when its key path covers the grouped aggregate's whole identity and so
|
|
275
|
+
# cannot collide (ADR 0061, decision D1). Decided from the declaration,
|
|
276
|
+
# never from the rows.
|
|
277
|
+
def collision_check(model, bluebook, grouped_head)
|
|
278
|
+
model.groups_by_identity?(bluebook.aggregate(grouped_head[:aggregate])) ? nil : model
|
|
279
|
+
end
|
|
280
|
+
|
|
270
281
|
# One level of nesting per field, in `group_by`'s own declared
|
|
271
282
|
# order — the leaf is the row with every grouped field removed
|
|
272
|
-
# (already spent, as the keys that reached it).
|
|
273
|
-
# `
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
|
|
283
|
+
# (already spent, as the keys that reached it). A leaf holds one row:
|
|
284
|
+
# when `checked` names a read model and two rows reach the same full
|
|
285
|
+
# key path, this refuses rather than keep one of them (ADR 0061,
|
|
286
|
+
# decision D1). An array-of-rows leaf would change the output shape
|
|
287
|
+
# of every shipped `group_by`, so it is not offered here.
|
|
288
|
+
#
|
|
289
|
+
# Groups are walked in first-occurrence order and depth first, so the
|
|
290
|
+
# collision reported is the first one reached; the Rust kernel's
|
|
291
|
+
# `read_model::nest` walks in the same order.
|
|
292
|
+
def nest(rows, fields, checked, reached = [])
|
|
280
293
|
field, *rest = fields
|
|
281
|
-
rows.group_by { |row| row[field] }.
|
|
294
|
+
rows.group_by { |row| row[field] }.to_h do |key, group|
|
|
282
295
|
# Strip only the field just grouped by, not the whole remaining
|
|
283
296
|
# list — `rest`'s own fields have to survive into the recursive
|
|
284
|
-
# call below, or the next level groups by a key that's already
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
rest.empty? ? stripped.first : nest(stripped, rest)
|
|
297
|
+
# call below, or the next level groups by a key that's already gone.
|
|
298
|
+
stripped = group.map { |row| row.reject { |name, _| name == field } }
|
|
299
|
+
path = reached + [[field, key]]
|
|
300
|
+
[key, rest.empty? ? leaf(stripped, checked, path) : nest(stripped, rest, checked, path)]
|
|
289
301
|
end
|
|
290
302
|
end
|
|
291
303
|
|
|
304
|
+
def leaf(rows, checked, path)
|
|
305
|
+
return rows.first unless checked && rows.size > 1
|
|
306
|
+
|
|
307
|
+
raise InvariantViolation,
|
|
308
|
+
RefusalWording.render_site("InvariantViolation", "group_by_collision",
|
|
309
|
+
read_model: checked.name, fields: checked.group_by_fields,
|
|
310
|
+
ids: rows.map { |row| row[:id] },
|
|
311
|
+
key: path.map { |field, value| "#{field} = #{value}" }.join(", "))
|
|
312
|
+
end
|
|
313
|
+
|
|
292
314
|
# `count`/`median`'s own declared target — the same single
|
|
293
315
|
# many-side head `group_by_target` resolves, for the same reason
|
|
294
316
|
# (`seal_aggregation` already refuses zero or several many-side
|
|
@@ -45,7 +45,7 @@ module Hecks
|
|
|
45
45
|
# Options, Hecks::Runtime::ReferenceHop::Folded] `declared` unchanged when it has no
|
|
46
46
|
# hop clauses; otherwise a `Folded` wrapper whose `#wheres` replaces each hop clause
|
|
47
47
|
# with its folded `in` clause
|
|
48
|
-
# @raise [Runtime::WiringError] if a hop's target
|
|
48
|
+
# @raise [Runtime::WiringError] if a hop's target does not resolve (see `fold`)
|
|
49
49
|
def apply(declared, args, registry:, domain:, aggregate:)
|
|
50
50
|
hopped, local = declared.wheres.partition { |clause| QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes) }
|
|
51
51
|
return declared if hopped.empty?
|
|
@@ -65,7 +65,7 @@ module Hecks
|
|
|
65
65
|
# @param aggregate [Bluebook::Aggregate] the aggregate `clause` is declared against
|
|
66
66
|
# @return [QuerySpecification::Common::WhereClause] a synthetic `in` clause on the hop
|
|
67
67
|
# attribute's name, whose value is every id the inner clause admits on the target
|
|
68
|
-
# @raise [Runtime::WiringError] if the hop's target aggregate
|
|
68
|
+
# @raise [Runtime::WiringError] if the hop's target aggregate does not resolve
|
|
69
69
|
def fold(clause, args, registry:, domain:, aggregate:)
|
|
70
70
|
step = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
|
|
71
71
|
hop, rest = step
|
|
@@ -74,7 +74,7 @@ module Hecks
|
|
|
74
74
|
# this exact clause before the domain ever booted. Held anyway:
|
|
75
75
|
# an unresolvable target folding silently to "matches
|
|
76
76
|
# everything" is precisely the failure shape this whole feature
|
|
77
|
-
# exists to close, and a hop that
|
|
77
|
+
# exists to close, and a hop that cannot resolve (a
|
|
78
78
|
# target aggregate unloaded after boot, say) is a real fact
|
|
79
79
|
# worth a real error, not a query that quietly stops filtering.
|
|
80
80
|
unless hop&.target
|
|
@@ -106,7 +106,7 @@ module Hecks
|
|
|
106
106
|
# @param registry [Runtime::Registry] the booted registry to resolve `target`'s
|
|
107
107
|
# repository from
|
|
108
108
|
# @return [Array<String>] every distinct id the inner clause(s) admit on `target`
|
|
109
|
-
# @raise [Runtime::WiringError] if a hop nested inside `wheres`
|
|
109
|
+
# @raise [Runtime::WiringError] if a hop nested inside `wheres` does not resolve
|
|
110
110
|
def matching_ids(domain, target, wheres, args, registry:)
|
|
111
111
|
spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
|
|
112
112
|
registry: registry, domain: domain, aggregate: target)
|
|
@@ -272,7 +272,7 @@ module Hecks
|
|
|
272
272
|
# types the consumer inlines. `uses_embryonaut_bluebook` only loads
|
|
273
273
|
# the package's `.bluebook` files — persistence, Governance, and the
|
|
274
274
|
# anti-corruption layer (`translates` field mapping) live on a named
|
|
275
|
-
# sibling hecksagon the
|
|
275
|
+
# sibling hecksagon the consumer must declare. Without that hexagon
|
|
276
276
|
# the package has no wiring of its own, and cross-context field
|
|
277
277
|
# mapping has nowhere to be written. Breaking in 2.0: attaching a
|
|
278
278
|
# vendored chapter without `Hecks.hecksagon "PackageName"` refuses
|
|
@@ -311,7 +311,7 @@ module Hecks
|
|
|
311
311
|
# An explicit `bounded` mark on a consumer chapter always needs an
|
|
312
312
|
# ACL — at least one `translates` block. Any field can be mapped;
|
|
313
313
|
# the BC does not list which. `uses_framework` / `uses_embryonaut_bluebook`
|
|
314
|
-
# mark the
|
|
314
|
+
# mark the attached chapter bounded (module wrap, no Object shortcut)
|
|
315
315
|
# and require the sibling hecksagon above; they do not require a
|
|
316
316
|
# `translates` on that sibling unless the consumer also wrote `bounded`.
|
|
317
317
|
# rust/host Google sign-in reads `ir.json`'s `membership` and
|
|
@@ -393,22 +393,22 @@ module Hecks
|
|
|
393
393
|
bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
|
|
394
394
|
end
|
|
395
395
|
|
|
396
|
-
#
|
|
396
|
+
# Two unrelated packages, one chapter name by coincidence — the
|
|
397
397
|
# real risk `Registry#bluebook_sources` exists to catch (found
|
|
398
398
|
# live: a stale `vendor/hecksagain` fork's own copy of Governance/
|
|
399
399
|
# Identity/Deploy, still reachable on 4 consuming apps' own load
|
|
400
400
|
# paths alongside the real gem). `BluebookBuilder.build`'s own
|
|
401
|
-
# accumulation (several files declaring the
|
|
402
|
-
#
|
|
401
|
+
# accumulation (several files declaring the same chapter name on
|
|
402
|
+
# purpose — `lib/hecks/language/bluebook/*.bluebook` all open
|
|
403
403
|
# `Hecks.bluebook "Bluebook"`) is never touched here — that merge
|
|
404
|
-
# stays unconditional, checked only
|
|
404
|
+
# stays unconditional, checked only after every file has loaded,
|
|
405
405
|
# the same "check the merged final result once" timing
|
|
406
406
|
# `refuse_ungoverned_roles!` already uses and for the same reason
|
|
407
407
|
# (a check against an incomplete load can never see the real
|
|
408
408
|
# shape). What distinguishes intentional accumulation from
|
|
409
|
-
# coincidence is
|
|
409
|
+
# coincidence is package root, not file identity: files a real
|
|
410
410
|
# gemspec or a `vendor/` boundary already treats as one unit are
|
|
411
|
-
# expected to share a name; files from two
|
|
411
|
+
# expected to share a name; files from two different roots never
|
|
412
412
|
# legitimately do.
|
|
413
413
|
def refuse_cross_package_bluebook_merge!
|
|
414
414
|
@bluebook_sources.each do |name, paths|
|
|
@@ -424,9 +424,9 @@ module Hecks
|
|
|
424
424
|
end
|
|
425
425
|
end
|
|
426
426
|
|
|
427
|
-
#
|
|
428
|
-
# gemspec (this
|
|
429
|
-
# it), or a bare `vendor/` path component, treated as its
|
|
427
|
+
# The nearest boundary a path already belongs to — a real
|
|
428
|
+
# gemspec (this is a package, whatever depends on it or vendors
|
|
429
|
+
# it), or a bare `vendor/` path component, treated as its own
|
|
430
430
|
# root regardless of what gemspec might sit above it: vendored
|
|
431
431
|
# code should never be considered "the same package" as whatever
|
|
432
432
|
# it's vendored into, even when nothing else marks the boundary.
|