asgard 0.3.0 → 0.3.2
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/.envrc +5 -0
- data/.loki +19 -23
- data/.reek.yml +112 -0
- data/.rubocop.yml +6 -0
- data/Archspec.rb +24 -0
- data/CHANGELOG.md +98 -2
- data/CLAUDE.md +4 -7
- data/README.md +93 -2
- data/bin/asgard +1 -1
- data/docs/api.md +130 -10
- data/docs/changelog.md +28 -0
- data/docs/dependencies.md +83 -2
- data/docs/getting-started.md +4 -2
- data/docs/helpers.md +84 -0
- data/docs/index.md +3 -3
- data/docs/options.md +51 -7
- data/docs/task-files.md +2 -2
- data/examples/bad.loki +63 -0
- data/examples/server_subcommands.loki +31 -22
- data/gem_tasks.loki +51 -0
- data/git.loki +13 -0
- data/lib/asgard/base/dependency_graph.rb +124 -0
- data/lib/asgard/base/dispatch.rb +160 -0
- data/lib/asgard/base/registry.rb +38 -0
- data/lib/asgard/base/task_dsl.rb +65 -0
- data/lib/asgard/base.rb +44 -222
- data/lib/asgard/doctor/report.rb +73 -0
- data/lib/asgard/doctor/task_sections.rb +67 -0
- data/lib/asgard/doctor.rb +150 -0
- data/lib/asgard/kernel_methods.rb +12 -6
- data/lib/asgard/tasks.rb +18 -6
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +11 -1
- data/quality.loki +261 -0
- data/quality_rails.loki +46 -0
- data/xyzzy.loki +12 -0
- metadata +17 -17
- data/Rakefile +0 -101
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cec41525d2a749cb83bcd59a480fb4bbffa80e468ca4a35c453376a16c84640
|
|
4
|
+
data.tar.gz: 997da94f3b7bf4fadedaff8d78741ff2748b00fb7107b27402dc50cf73f897df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d189e34cd3733b41e436460367130fa19451bf3938b693f1c5663a1268fab41f87866063e2c0c3a185a69dfda238aa8a5e6a253de9bbac0e2c5e97e6c9d2fa6e
|
|
7
|
+
data.tar.gz: d6fcbee1e4302dce37d7ea39cf22019ae0322f3f200e4ac5421ac247d7facca205e38cdb1f905ab20781142472b0010c46ff966dbba83be918a7fbabd49bebbd
|
data/.envrc
CHANGED
data/.loki
CHANGED
|
@@ -2,33 +2,29 @@
|
|
|
2
2
|
# Asgard gem's own task file.
|
|
3
3
|
# Task is pre-defined by the gem — just reopen it to add tasks.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import "quality.loki"
|
|
6
|
+
import "quality_rails.loki" if defined?(Rails)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
8
|
+
import "gem_tasks.loki"
|
|
9
|
+
import "git.loki"
|
|
10
|
+
import "xyzzy.loki" # An example for the --doctor flag
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
12
|
+
class Tasks
|
|
13
|
+
@@project ||= "asgard".freeze
|
|
14
|
+
@@project_desc ||= "CLI-task runner"
|
|
17
15
|
|
|
18
|
-
desc "
|
|
19
|
-
def
|
|
20
|
-
sh "bundle exec rake build"
|
|
21
|
-
end
|
|
16
|
+
desc "xyzzy"
|
|
17
|
+
def xyzzy = puts "everything in Ruby metaprogramming is magic"
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def install
|
|
26
|
-
sh "bundle exec rake install"
|
|
19
|
+
helper(:project_version) do
|
|
20
|
+
@@project_version ||= File.read("lib/#{@@project}/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
27
21
|
end
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
default_task :quality
|
|
24
|
+
|
|
25
|
+
header <<~HEAD
|
|
26
|
+
Project: #{@@project} (v#{project_version}) - #{@@project_desc}
|
|
27
|
+
Root Dir: #{loki_up.parent}
|
|
28
|
+
Default task: #{default_task}
|
|
29
|
+
HEAD
|
|
34
30
|
end
|
data/.reek.yml
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
detectors:
|
|
3
|
+
|
|
4
|
+
# Disabled: conflicts with project no-comment coding style.
|
|
5
|
+
IrresponsibleModule:
|
|
6
|
+
enabled: false
|
|
7
|
+
|
|
8
|
+
# Disabled: bang methods without non-bang counterparts are common in internal APIs.
|
|
9
|
+
MissingSafeMethod:
|
|
10
|
+
enabled: false
|
|
11
|
+
|
|
12
|
+
# Disabled: explicit nil checks are idiomatic Ruby (nil? / unless nil, etc.).
|
|
13
|
+
NilCheck:
|
|
14
|
+
enabled: false
|
|
15
|
+
|
|
16
|
+
# Disabled: method size/complexity is already covered by Flog (the primary
|
|
17
|
+
# complexity gate) and RuboCop's Metrics/MethodLength.
|
|
18
|
+
TooManyStatements:
|
|
19
|
+
enabled: false
|
|
20
|
+
|
|
21
|
+
# Default is 4 — Doctor legitimately tracks several pieces of diagnostic state.
|
|
22
|
+
TooManyInstanceVariables:
|
|
23
|
+
max_instance_variables: 6
|
|
24
|
+
|
|
25
|
+
# Accept standard Ruby short-name conventions for limited-scope variables.
|
|
26
|
+
UncommunicativeVariableName:
|
|
27
|
+
accept:
|
|
28
|
+
- _ # intentionally ignored
|
|
29
|
+
- d # dep / dir / data
|
|
30
|
+
- e # rescue => e
|
|
31
|
+
- f # file / finding
|
|
32
|
+
- h # hash
|
|
33
|
+
- i # index
|
|
34
|
+
- k # key
|
|
35
|
+
- p # path
|
|
36
|
+
- t # task / thread
|
|
37
|
+
|
|
38
|
+
# Reviewed and accepted as false positives / intentional design, per method
|
|
39
|
+
# (generated with `reek --todo` and hand-curated — see project review notes).
|
|
40
|
+
# Unlike a count-based baseline, this is precise: it grandfathers exactly
|
|
41
|
+
# these smells at exactly these methods, so a genuinely new smell at a new
|
|
42
|
+
# or existing method still fails the gate even if it doesn't change the
|
|
43
|
+
# file's total warning count.
|
|
44
|
+
Attribute:
|
|
45
|
+
exclude:
|
|
46
|
+
- Asgard::Doctor::ImportTracer#doctor # needs to stay settable/resettable per doctor run
|
|
47
|
+
|
|
48
|
+
BooleanParameter:
|
|
49
|
+
exclude:
|
|
50
|
+
- Asgard::Base#help # must match Thor's positional super signature exactly
|
|
51
|
+
- Asgard::Shell#sh # silent: is the real public API contract
|
|
52
|
+
- Asgard::Shell#shebang # same
|
|
53
|
+
|
|
54
|
+
ControlParameter:
|
|
55
|
+
exclude:
|
|
56
|
+
- Asgard::Shell#sh
|
|
57
|
+
- Asgard::Shell#shebang
|
|
58
|
+
|
|
59
|
+
DuplicateMethodCall:
|
|
60
|
+
exclude:
|
|
61
|
+
- Asgard::Base::Dispatch#acquire_run_token # done.include?(target): check, then poll-until-true
|
|
62
|
+
- Asgard::Base::Dispatch#run_dep_group # errors.size: two branches of one guard
|
|
63
|
+
- Asgard::Doctor::Report#print_report # puts divider: intentional top+bottom rule
|
|
64
|
+
- Asgard::Doctor::Report#summary_line # errors.zero?: sequential guard clauses
|
|
65
|
+
- Asgard::Doctor#load_chain # Base.subclasses: before/after snapshot diff
|
|
66
|
+
- Kernel#import_up # "not found" warned from two independent branches
|
|
67
|
+
- Asgard#self.run! # Base.subclasses snapshot diff; e.message in separate rescues
|
|
68
|
+
- Asgard::Base#tree # say "\n": intentional blank line before and after the tree
|
|
69
|
+
|
|
70
|
+
FeatureEnvy:
|
|
71
|
+
exclude:
|
|
72
|
+
- Asgard::Base::Dispatch#acquire_run_token
|
|
73
|
+
- Asgard::Base::Dispatch#run_dep_group
|
|
74
|
+
- Asgard::Base::Dispatch#signal_done
|
|
75
|
+
- Asgard::Base::Registry#inherited # inherited(subclass) configuring subclass is the whole point
|
|
76
|
+
- Asgard::Base::DependencyGraph#_normalize_pending_deps # small transform of its own argument
|
|
77
|
+
- Asgard::Base::DependencyGraph#_call_dep_proc # rescue => e; e.class/e.message is inherent
|
|
78
|
+
- Asgard::Doctor::TaskSections#class_task_file_map
|
|
79
|
+
- Asgard::Doctor::TaskSections#relative_path
|
|
80
|
+
- Asgard::Doctor::TaskSections#task_status
|
|
81
|
+
- Asgard::Shell#shebang # Tempfile.create(&block) idiom
|
|
82
|
+
|
|
83
|
+
ManualDispatch:
|
|
84
|
+
exclude:
|
|
85
|
+
- Asgard::Base::DependencyGraph#_normalize_pending_deps # respond_to?(:call) distinguishes a Proc from an Array/Symbol — no polymorphic alternative over Ruby's own built-in types
|
|
86
|
+
- Asgard::Base::DependencyGraph#_resolve_lazy_deps! # same check
|
|
87
|
+
|
|
88
|
+
InstanceVariableAssumption:
|
|
89
|
+
exclude:
|
|
90
|
+
- Asgard::Base # class-level DSL state built up across method_added/inherited, not object init
|
|
91
|
+
- Asgard::Doctor # multi-phase report builder; @ivars set across report_markers/load_chain/etc in `run`
|
|
92
|
+
|
|
93
|
+
NestedIterators:
|
|
94
|
+
exclude:
|
|
95
|
+
- Asgard::Base::DependencyGraph#_build_dep_graph
|
|
96
|
+
- Asgard::Base::DependencyGraph#_check_dep_arities!
|
|
97
|
+
- Asgard::Base::TaskDSL#no_negate # inner block defines a method, isn't really iteration
|
|
98
|
+
- Asgard::Doctor::Report#print_task_sections
|
|
99
|
+
- Asgard::Doctor::TaskSections#class_task_file_map
|
|
100
|
+
- Asgard::Doctor::TaskSections#override_count
|
|
101
|
+
|
|
102
|
+
UtilityFunction:
|
|
103
|
+
exclude:
|
|
104
|
+
- Asgard::Base::DependencyGraph#_build_dep_graph # genuinely pure, but tightly coupled to this graph model
|
|
105
|
+
- Asgard::Base::Dispatch#dep_results # thread-local by design, not self — that's the whole point
|
|
106
|
+
- Asgard::Base::Dispatch#with_dep_results # same: stashes results on Thread.current, not self
|
|
107
|
+
|
|
108
|
+
exclude_paths:
|
|
109
|
+
- test
|
|
110
|
+
- docs
|
|
111
|
+
- coverage
|
|
112
|
+
- pkg
|
data/.rubocop.yml
CHANGED
|
@@ -6,6 +6,12 @@ AllCops:
|
|
|
6
6
|
- 'examples/**/*'
|
|
7
7
|
- 'vendor/**/*'
|
|
8
8
|
|
|
9
|
+
# Archspec.rb's capitalization is mandated by the archspec gem itself, not a
|
|
10
|
+
# style choice — it won't look for a snake_case config file.
|
|
11
|
+
Naming/FileName:
|
|
12
|
+
Exclude:
|
|
13
|
+
- 'Archspec.rb'
|
|
14
|
+
|
|
9
15
|
# ── Style: disabled cops ───────────────────────────────────────────────────
|
|
10
16
|
Style/StringLiterals:
|
|
11
17
|
Enabled: false
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source "lib/**/*.rb"
|
|
4
|
+
|
|
5
|
+
component :shell, in: "lib/asgard/shell.rb"
|
|
6
|
+
component :kernel_methods, in: "lib/asgard/kernel_methods.rb"
|
|
7
|
+
component :base, in: %w[lib/asgard/base.rb lib/asgard/base/**/*.rb]
|
|
8
|
+
component :tasks, in: "lib/asgard/tasks.rb"
|
|
9
|
+
component :doctor, in: %w[lib/asgard/doctor.rb lib/asgard/doctor/**/*.rb]
|
|
10
|
+
|
|
11
|
+
# The DSL engine must not depend upward on the classes built on top of it —
|
|
12
|
+
# Tasks and Doctor are consumers of Base, never the other way around.
|
|
13
|
+
base.cannot_use :tasks, :doctor
|
|
14
|
+
|
|
15
|
+
# Built-in/user tasks stay independent of the --doctor introspection feature.
|
|
16
|
+
tasks.cannot_use :doctor
|
|
17
|
+
|
|
18
|
+
# Shell (sh/shebang helpers, mixed into Base) and the Kernel additions (env,
|
|
19
|
+
# loki_up, import, ...) are leaf-level utilities — they must not depend on
|
|
20
|
+
# anything built on top of them.
|
|
21
|
+
shell.cannot_use :base, :tasks, :doctor
|
|
22
|
+
kernel_methods.cannot_use :base, :tasks, :doctor, :shell
|
|
23
|
+
|
|
24
|
+
no_cycles among: %i[base tasks doctor shell kernel_methods]
|
data/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,50 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.3.
|
|
8
|
+
## [0.3.2] - Unreleased
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **`helper(name, &block)` DSL method on `Asgard::Base`** — defines a method available in both class context (e.g. inside `header` or `footer`) and as a private instance method inside task bodies, with a single declaration. Eliminates the manual `def self.name` + `no_commands { private def name = self.class.name }` boilerplate. Supports positional arguments, keyword arguments, default values, and block arguments.
|
|
13
|
+
```ruby
|
|
14
|
+
class Tasks
|
|
15
|
+
@@project ||= "myapp".freeze
|
|
16
|
+
|
|
17
|
+
helper(:version) {
|
|
18
|
+
File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
header "#{@@project} v#{version}" # class context
|
|
22
|
+
|
|
23
|
+
desc "Show the current version"
|
|
24
|
+
def show_version = puts version # instance context
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **`header(text)` and `footer(text)` DSL methods on `Asgard::Base`** — attach static text to the general help output. `header` lines are printed above the commands list; `footer` lines are printed below the options block. Multiple calls accumulate: each `header` call appends a line, each `footer` call prepends a line, so content from later-loaded files naturally wraps around content from earlier files. Neither appears when `asgard help <command>` is called for per-command detail.
|
|
29
|
+
```ruby
|
|
30
|
+
class Tasks
|
|
31
|
+
header "my-project — build & release tasks"
|
|
32
|
+
footer "See https://example.com/docs for details"
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
- **`no_negate(*names)` DSL method on `Asgard::Base`** — suppresses the `[--no-name]` and `[--skip-name]` negation variants from help output for boolean class options where negation is meaningless. Call it after the `class_option` declaration:
|
|
36
|
+
```ruby
|
|
37
|
+
class_option :version, type: :boolean, default: false, desc: "Show version and exit"
|
|
38
|
+
no_negate :version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **`--version` reimplemented as a `class_option`** — the flag now appears in the "Options" section of `asgard help` alongside `--debug` and `--verbose`, rather than as a listed command. The `_version` method and its `map "--version" => :_version` registration have been removed. The actual early-exit behaviour still lives in `Asgard.run!` (before the `.loki` file is required), so `--version` works even when no `.loki` file exists. `no_negate :version` suppresses the spurious `[--no-version]` / `[--skip-version]` variants.
|
|
44
|
+
- **`loki_up` returns `Pathname` instead of `String`** — the return value is now a `Pathname` instance (or `nil` when not found). `Pathname` is accepted everywhere `loki_up`'s result is used: `import`, `dotenv`, `load`, and standard Ruby file methods all accept `Pathname` via `to_path` / `to_s`. Code that passes the result directly to those methods is unaffected; code that performs string operations on the path should call `.to_s` first.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Help output showed `tasks` prefix before every command** — `asgard help` displayed `asgard tasks build` instead of `asgard build`. The cause was a keyword-vs-positional mismatch in the `Asgard::Base#help` override: declaring `subcommand: false` as a keyword argument caused Ruby's `super` to forward it as the hash `{subcommand: false}`. Thor's `banner` method received this hash as the positional `subcommand` argument, treated it as truthy, and prepended the class namespace (`tasks`) to every command name. Fixed by changing the override signature to match Thor's positional signature: `def help(command = nil, subcommand = false)`.
|
|
49
|
+
|
|
50
|
+
### Added (continued)
|
|
51
|
+
|
|
12
52
|
- **`loki_up(name = ".loki")` Kernel method** — searches `Dir.pwd` and each ancestor directory for a file with the given name; returns the absolute path of the first match or `nil`. Available everywhere in Ruby (task bodies, `.loki` files, top-level code) as a `module_function` on `Kernel`.
|
|
13
53
|
- **`import(path)` Kernel method** — loads a `.loki` file (or a glob of `.loki` files) with `require`-like idempotency via `$LOADED_FEATURES`. Accepts a `String` or `Pathname`. Relative paths are resolved relative to the caller's file (like `require_relative`). Glob patterns (`*.loki`, `**/*.loki`) expand via `Dir.glob` and load all matches. Returns `true` if any file was newly loaded, `false` if all were already loaded or no glob matches were found. Raises `ArgumentError` if the path does not end with `.loki`.
|
|
14
54
|
- **`import_up(name = ".loki")` Kernel method** — combines `loki_up` and `import`. For exact names, finds the first ancestor directory containing that file and loads it. For glob names, finds the first ancestor directory containing any matching files and loads them all — stopping at that level rather than aggregating across multiple ancestors. Returns `false` if nothing is found.
|
|
@@ -16,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
56
|
- **`env(name, default = nil)` Kernel method** — fetches a system environment variable by symbol or string name, upcasing the key automatically. `env(:port, "3000")` returns `"3000"` when `PORT` is unset; `env(:api_key)` raises `KeyError` when `API_KEY` is missing and no default is provided. Accepts both `env(:port)` and `env("PORT")` forms. Cleaner than `ENV['PORT']` in task bodies.
|
|
17
57
|
- **Verbose/debug feedback for `import` and `import_up`** — when `verbose?` is true, each file loaded is printed to stderr. When `debug?` is true, already-loaded files are also reported (with an "already loaded" suffix), and `import_up` reports when a file is not found.
|
|
18
58
|
- **RuboCop lint gate** — RuboCop is now a first-class quality gate alongside tests and Flog. Added `rubocop` to the Gemfile, a `.rubocop.yml` tuned for this codebase (Ruby 3.2 target, relaxed `Metrics` thresholds consistent with Flog as the primary complexity gate, `examples/` excluded), and `rake rubocop` / `rake rubocop_fix` tasks backed by a `tmp/rubocop_cache` directory for fast re-runs.
|
|
19
|
-
- **Expanded `rake quality` task** — `quality` now runs three independent gates (tests + coverage, RuboCop, Flog) and
|
|
59
|
+
- **Expanded `rake quality` task** — `quality` now runs three independent gates (tests + coverage, RuboCop, Flog) in parallel using `depends_on [:test, :rubocop, :flog_check]`. Each gate captures its pass/fail result in an instance variable; output is suppressed on pass and filtered to failures only on fail, preventing interleaved output from concurrent subprocesses. A formatted pass/fail summary table is printed after all gates complete, so every failure is visible in a single run.
|
|
20
60
|
- **`rake flog_check` task** — replaces the bare `flog lib/` call with a structured task that enforces per-method thresholds (warn ≥20, fail ≥50), lists warnings and failures in separate sections, and exits non-zero only when the failure threshold is breached.
|
|
21
61
|
- **Single-argument `desc` shorthand** — `desc` now accepts one string (the description) with the usage string omitted. The usage defaults to the method name, eliminating the redundant first argument for the common case:
|
|
22
62
|
```ruby
|
|
@@ -63,6 +103,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
63
103
|
- **`loki_up` scope clarified in docs** — `docs/task-files.md` and `docs/api.md` now make explicit that `loki_up` locates any file by name, not just `.loki` files, with examples for `.env` and `VERSION`. The `dotenv loki_up(".env") || ".env"` pattern is shown as the canonical way to load a `.env` file from any subdirectory.
|
|
64
104
|
- **`examples/.loki`** — updated to use explicit `import "*.loki"` (sibling files) and `import "subdir/import_demo.loki"` (subdirectory file), with comments explaining `import`, `import_up`, and `loki_up`.
|
|
65
105
|
|
|
106
|
+
### Added (continued)
|
|
107
|
+
|
|
108
|
+
- **`--doctor` built-in CLI flag** — diagnoses `.loki` resolution, import chains, and task definitions for the current directory, then exits. Handled directly in `Asgard.run!` before the `.loki` file is loaded (same pattern as `--version`), so it keeps working in exactly the situations that would otherwise abort the whole process: a broken `.loki` file, a circular or undefined dependency, or a task silently redefined by a later `def`. Backed by the new `Asgard::Doctor` class. The report includes a "Tasks by file" listing: every command grouped by the file it's defined in, printed as `relative/path:line` so an editor can jump straight to it. A task name defined at more than one location gets every definition annotated inline — the earlier one(s) as `OVERRIDDEN by <file>:<line> — never callable`, the winning (last) one as `active — redefines <file>:<line>` — replacing the old flat "Tasks#x redefined" summary line with an annotation right where the problem is. See [API Reference](docs/api.md#asgarddoctor).
|
|
109
|
+
- **Flay and Reek quality gates** — `flay_check` checks for structural code duplication (mass ≥ 150); `reek` checks code smells. Both run as part of `quality` alongside `test`, `rubocop`, and `flog_check`. A `.reek.yml` tunes several detectors to this codebase's conventions (no doc-comment requirement, short variable names, disabled `TooManyStatements`, etc), plus per-method `exclude:` entries (generated with `reek --todo` and hand-curated) that grandfather specific reviewed smells at specific methods — precise enough that a genuinely new smell still fails the gate even at an already-reviewed method, unlike a per-file count.
|
|
110
|
+
- **`test_verbose` task** — runs the test suite with Minitest's verbose (`-v`) output.
|
|
111
|
+
- **Colorized quality gate summary** — `quality`'s final report now prints a green/red PASS/FAIL badge per gate plus a passed/failed tally, via a shared `print_quality_summary` helper.
|
|
112
|
+
- **`console` task** — opens an IRB console with the gem loaded (`bin/console` if present, otherwise `bundle exec irb`).
|
|
113
|
+
- **`git.loki`** — per-repo git tasks (`push`, `pull`, `fetch`), imported from `.loki`.
|
|
114
|
+
|
|
115
|
+
### Changed (continued)
|
|
116
|
+
|
|
117
|
+
- **`release` task** now prompts for confirmation (`Release asgard vX.Y.Z to RubyGems? [y/N]`) unless `-y`/`--yes` is passed, before tagging and pushing.
|
|
118
|
+
|
|
119
|
+
### Fixed (continued)
|
|
120
|
+
|
|
121
|
+
- **`bin/asgard` could silently run the wrong `asgard` version** — the executable did `require "asgard"`, which (without `bundle exec`) is resolved by RubyGems independently of where the script itself lives, so it could load a separately-installed gem version instead of this repo's own `lib/`. Changed to `require_relative "../lib/asgard"` so the executable always loads the library that ships alongside it, regardless of what else is installed.
|
|
122
|
+
|
|
123
|
+
### Added (continued 2)
|
|
124
|
+
|
|
125
|
+
- **`SKIP` and `WARN` quality-gate statuses** — alongside `PASS`/`FAIL`. Both are non-blocking (`quality` only aborts on `FAIL`); `SKIP` is for a required external tool that isn't installed, `WARN` is for a check that ran successfully but has non-blocking suggestions. `print_quality_summary` renders all four with distinct colored badges and a combined tally.
|
|
126
|
+
- **`typos_check` / `typos_fix` tasks** — spell-checking via the external `typos` CLI (`brew install typos-cli`, not a gem dependency). `typos_check` writes full findings to `typos_output.txt` and participates in `quality`. If `typos` isn't installed, the gate prints a one-line notice recommending `brew install typos-cli` and reports `SKIP` rather than failing.
|
|
127
|
+
- **`fasterer_check` task** — runs the `fasterer` gem (new dev dependency) against `lib/`, reporting performance-idiom suggestions as `WARN` (non-blocking) — these are suggestions on working code, not correctness problems.
|
|
128
|
+
- **`asgard tree` now shows the project header/footer** — `Base#tree` wraps Thor's built-in command tree the same way `Base#help` already wraps the command list, so both example outputs are consistent.
|
|
129
|
+
- Every quality gate (`test_check`, `rubocop_check`, `flog_check`, `flay_check`, `reek_check`, `typos_check`, `fasterer_check`, `bundler_audit_check`) now writes its full detailed output to a `<gate>_output.txt` file at the repo root (gitignored via `*_output.txt`) and prints only a one-line summary to stdout — full detail stays on disk without cluttering the terminal.
|
|
130
|
+
|
|
131
|
+
### Changed (continued 2)
|
|
132
|
+
|
|
133
|
+
- **`Asgard::Base` and `Asgard::Doctor` split into focused mixins** — `Asgard::Base` (31 methods) is now `Registry`, `DependencyGraph`, `TaskDSL`, and `Dispatch` (`lib/asgard/base/*.rb`), with `method_added`/`header`/`footer`/`help`/`tree` remaining directly on the class as the orchestrator. `Asgard::Doctor` (19 methods) is now `TaskSections` and `Report` (`lib/asgard/doctor/*.rb`), with the diagnostic flow (`run`, `report_markers`, `load_chain`, etc.) staying on the class itself. Purely a file-organization change — behavior, `asgard help`/`asgard --doctor` output, and the public API are unchanged. Drops Reek's `TooManyMethods`/`TooManyInstanceVariables` warnings on both classes to zero.
|
|
134
|
+
- **Reek grandfathering made precise** — replaced the per-file smell-count baseline (`.quality/reek_baseline.txt`, the `reek_baseline` task) with per-method, per-detector `exclude:` entries in `.reek.yml` itself, generated via `reek --todo` and hand-curated. Unlike a count, this still catches a genuinely new smell at an already-reviewed method, without relying on the file's total count staying the same. The `reek_baseline` and `ensure_quality_dir` tasks and the `.quality/` directory have been removed as no longer needed.
|
|
135
|
+
|
|
136
|
+
### Added (continued 3)
|
|
137
|
+
|
|
138
|
+
- **`depends_on` accepts a Proc/lambda in addition to a fixed list** — a sole callable defers resolution to `validate_deps!` (once, right after every `.loki` file has loaded) instead of resolving immediately when `depends_on` itself is evaluated. This solves the "load order matters" problem for a dependency list that can't be known upfront — e.g. "every task whose name ends in `_check`," discovered across several files including ones imported conditionally (`import "quality_rails.loki" if defined?(Rails)`). The Proc must return the same shape the splat form would receive (an array of stages, each a `Symbol` or `Array`); it's written directly in the class body, so it lexically captures that class as `self` and can call `all_commands` bare. A Proc that raises is re-raised as `Asgard::Error` naming the task it was declared for; a Proc that resolves to an undefined task or a cycle is still caught by the existing startup validation, since the resolved result is checked exactly like a plain array. See [Dynamic Dependencies](docs/dependencies.md#dynamic-dependencies-proc-form).
|
|
139
|
+
- **`bundler_audit_check` task** — runs `bundle-audit check --update` against `Gemfile.lock` (new dev dependency `bundler-audit`); reports `FAIL` on any known vulnerability, since this is a security gate, not a suggestion.
|
|
140
|
+
- **`quality_rails.loki`** — imported by `.loki` only when `Rails` is defined; currently ships `brakeman_check`, a Rails security-scan example. Needs no wiring into `quality`'s dependency list — `quality`'s `depends_on` Proc discovers it automatically once it's loaded.
|
|
141
|
+
|
|
142
|
+
### Changed (continued 3)
|
|
143
|
+
|
|
144
|
+
- **`test`, `rubocop`, `reek` renamed to `test_check`, `rubocop_check`, `reek_check`** — for consistency with the other gates, all of which already ended in `_check`. This convention is what makes automatic discovery possible: `quality`'s `depends_on` Proc finds every task whose name matches `_check\z` rather than naming them one by one.
|
|
145
|
+
|
|
146
|
+
### Removed (continued)
|
|
147
|
+
|
|
148
|
+
- **`dagwood` runtime dependency** — replaced by stdlib `TSort` for the one thing it was still doing (cycle detection); the parallel-execution plan itself was already derived directly from `depends_on`'s stage list, not from a rebuilt DAG. `_build_dep_graph` (dead code — its return value was already unused) is deleted along with the gemspec entry.
|
|
149
|
+
|
|
150
|
+
### Changed (continued 4)
|
|
151
|
+
|
|
152
|
+
- **`validate_deps!` cycle detection now uses stdlib `TSort`** instead of `Dagwood::DependencyGraph#order` — a private `Graph` `Struct` (`edges` member, `include TSort`) owns the task→dependency Hash and the traversal, raising `TSort::Cyclic` on a cycle exactly as before (converted to `Asgard::CircularDependencyError`). `run_deps_for` no longer round-trips through a rebuilt DAG on every dispatch — it iterates `_deps[target]`'s stage groups directly, which is already the parallel-execution plan `depends_on` built.
|
|
153
|
+
|
|
154
|
+
### Fixed (continued 2)
|
|
155
|
+
|
|
156
|
+
- **`quality.loki`'s parallel `*_check` tasks raced on shared instance state** — each `*_check` task wrote its pass/fail status to an ivar on `self` (`@test_result`, `@rubocop_result`, ...) from inside a `Thread.new` spawned by the same parallel-dependency group — an unsynchronized write across threads that MRI's GVL happens to hide today but would not on JRuby/TruffleRuby. Fixed at the framework level: `Dispatch#run_dep_group` now collects each task's own return value into a `Hash` (via `Thread#value`), `run_deps_for` merges per-stage results, and a new `dep_result`/`dep_results` instance API (backed by `Thread.current`, not `self`) hands them to the task body that depends on them. `quality.loki` and `quality_rails.loki`'s `*_check` tasks now return their status as a plain value instead of writing to an ivar; `quality` reads `dep_result(name)` instead of `instance_variable_get`.
|
|
157
|
+
|
|
158
|
+
### Added (continued 4)
|
|
159
|
+
|
|
160
|
+
- **`examples/bad.loki`** — a worked demonstration of the race the fix above addresses: 4 parallel workers read-modify-write a shared `@hits` counter directly (the anti-pattern `quality.loki` used to have), reliably losing updates. Kept as a contrast example for what `dep_result`/`dep_results` is for.
|
|
161
|
+
|
|
66
162
|
## [0.2.0] - 2026-05-29
|
|
67
163
|
|
|
68
164
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -16,7 +16,7 @@ bundle exec rake build # build .gem into pkg/
|
|
|
16
16
|
bundle exec rake install # install locally
|
|
17
17
|
|
|
18
18
|
# or use the gem's own .loki file:
|
|
19
|
-
asgard
|
|
19
|
+
asgard test_check
|
|
20
20
|
asgard quality
|
|
21
21
|
asgard release
|
|
22
22
|
```
|
|
@@ -72,16 +72,13 @@ depends_on :a, [:b, :c], :d # stages: [[:a], [:b, :c], [:d]]
|
|
|
72
72
|
|
|
73
73
|
**`invoke_command`** (Thor dispatch hook):
|
|
74
74
|
1. Atomically check `@_ran_tasks` Set (with `@_ran_mutex`); return early if already run
|
|
75
|
-
2.
|
|
76
|
-
3. For each
|
|
75
|
+
2. Look up `@_deps[target]` — already the parallel-group stage list `depends_on` built
|
|
76
|
+
3. For each stage group: spawn one thread per task, join; single-task groups run inline
|
|
77
77
|
4. Execute the target task
|
|
78
78
|
|
|
79
|
-
**`_build_dep_graph(stages)`** converts stages to a DAG hash:
|
|
80
|
-
- `[[:a], [:b, :c], [:d]]` → `{ a: [], b: [:a], c: [:a], d: [:b, :c] }`
|
|
81
|
-
|
|
82
79
|
### Dependency Resolution
|
|
83
80
|
|
|
84
|
-
|
|
81
|
+
`depends_on`'s stage list (`[[:a], [:b, :c], [:d]]`) *is* the parallel-execution plan — no separate graph library is needed to run it. Cycle detection is a separate concern, handled once in `validate_deps!` via stdlib `TSort` over the full `@_deps` graph (raises `TSort::Cyclic`, converted to `Asgard::CircularDependencyError`). The thread-safe deduplication (`_ran_tasks` Set + Mutex) ensures each task runs exactly once even when multiple tasks share a common dependency.
|
|
85
82
|
|
|
86
83
|
### Shell Helpers
|
|
87
84
|
|
data/README.md
CHANGED
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
- <strong>Concurrent Execution</strong> — parallel task groups run in native Ruby threads<br>
|
|
19
19
|
- <strong>Subcommands</strong> — group related tasks under a named namespace<br>
|
|
20
20
|
- <strong>Variables</strong> — shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
|
|
21
|
+
- <strong>`helper` DSL</strong> — define a method once, available in both class-level DSL calls (<code>header</code>) and inside task instance methods<br>
|
|
21
22
|
- <strong>Shell Helpers</strong> — <code>sh</code> for any shell command or heredoc; <code>shebang</code> for polyglot scripts<br>
|
|
22
23
|
- <strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code><br>
|
|
23
24
|
- <strong>Auto-Discovery</strong> — <code>.loki</code> root marker searched from CWD upward through parent directories<br>
|
|
24
25
|
- <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded via <code>import</code> from your <code>.loki</code><br>
|
|
25
|
-
- <strong>Built-in Flags</strong> — <code>--debug</code
|
|
26
|
+
- <strong>Built-in Flags</strong> — <code>--debug</code>, <code>--verbose</code>, <code>--version</code>, and <code>--doctor</code> built-in class options; header/footer DSL for static help text<br>
|
|
26
27
|
</td>
|
|
27
28
|
</tr>
|
|
28
29
|
</table>
|
|
@@ -220,6 +221,15 @@ asgard ci executes:
|
|
|
220
221
|
ci
|
|
221
222
|
```
|
|
222
223
|
|
|
224
|
+
`depends_on` also accepts a `Proc`/lambda instead of a fixed list, resolved once every `.loki` file has finished loading rather than immediately — useful when the list can't be known upfront, e.g. "every task whose name ends in `_check`," discovered across several files:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
depends_on -> { [all_commands.keys.grep(/_check\z/).map(&:to_sym)] }
|
|
228
|
+
def quality = puts "running every *_check task..."
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
See [Dependencies](https://madbomber.github.io/asgard/dependencies/#dynamic-dependencies-proc-form) for the full explanation.
|
|
232
|
+
|
|
223
233
|
---
|
|
224
234
|
|
|
225
235
|
## Variables
|
|
@@ -274,6 +284,40 @@ class Tasks
|
|
|
274
284
|
end
|
|
275
285
|
```
|
|
276
286
|
|
|
287
|
+
### The `helper` DSL method
|
|
288
|
+
|
|
289
|
+
Some values need to be available in both class context (e.g. inside `header`) and inside task instance methods. `helper` defines the method once in both contexts:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
class Tasks
|
|
293
|
+
@@project ||= "myapp".freeze
|
|
294
|
+
|
|
295
|
+
helper(:version) {
|
|
296
|
+
File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
header "#{@@project} v#{version}" # class context
|
|
300
|
+
|
|
301
|
+
desc "Show the current version"
|
|
302
|
+
def show_version
|
|
303
|
+
puts version # instance context
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Without `helper`, achieving this requires two separate definitions:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
def self.version = File.read(...).match(...)[1].freeze
|
|
312
|
+
no_commands { private def version = self.class.version }
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`helper` accepts positional arguments, keyword arguments, and blocks — any signature valid in a Ruby method definition:
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
helper(:tag) { |name, ver, prefix: "v"| "#{prefix}#{name}-#{ver}" }
|
|
319
|
+
```
|
|
320
|
+
|
|
277
321
|
Helpers can also be shared across multiple `.loki` files by extracting them into a plain Ruby file and loading it explicitly:
|
|
278
322
|
|
|
279
323
|
```ruby
|
|
@@ -299,6 +343,53 @@ end
|
|
|
299
343
|
|
|
300
344
|
---
|
|
301
345
|
|
|
346
|
+
## Help header and footer
|
|
347
|
+
|
|
348
|
+
Add static text above and below the command list in `asgard help` output:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
class Tasks
|
|
352
|
+
header "my-project — build & release tasks"
|
|
353
|
+
footer "See https://example.com/docs for details"
|
|
354
|
+
end
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Multiple calls accumulate. `header` appends each line (top to bottom); `footer` prepends each line (bottom to top), so content from a later-loaded `.loki` file sits closer to the commands:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# .loki
|
|
361
|
+
class Tasks
|
|
362
|
+
header "my-project"
|
|
363
|
+
footer "Maintainer: you@example.com"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
import "*.loki"
|
|
367
|
+
|
|
368
|
+
# deploy.loki
|
|
369
|
+
class Tasks
|
|
370
|
+
header " deploy targets: staging, production"
|
|
371
|
+
footer "See runbook at wiki/deploy"
|
|
372
|
+
end
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
my-project
|
|
377
|
+
deploy targets: staging, production
|
|
378
|
+
|
|
379
|
+
Commands:
|
|
380
|
+
...
|
|
381
|
+
|
|
382
|
+
Options:
|
|
383
|
+
...
|
|
384
|
+
|
|
385
|
+
See runbook at wiki/deploy
|
|
386
|
+
Maintainer: you@example.com
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Header and footer text is only shown for the general `asgard help` page, not for `asgard help <command>`.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
302
393
|
## Options shared across all tasks
|
|
303
394
|
|
|
304
395
|
`class_option` defines an option available to every task in the class:
|
|
@@ -566,7 +657,7 @@ end
|
|
|
566
657
|
| Method | Description |
|
|
567
658
|
|---|---|
|
|
568
659
|
| `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
|
|
569
|
-
| `Asgard.find_task_file` | Returns
|
|
660
|
+
| `Asgard.find_task_file` | Returns a `Pathname` to `.loki` searching from CWD upward, or `nil` |
|
|
570
661
|
|
|
571
662
|
`run!` handles its own errors — a missing `.loki`, a circular dependency, or a `depends_on` that names a task that doesn't exist all produce a clean one-line message and exit 1.
|
|
572
663
|
|
data/bin/asgard
CHANGED