enola-guides 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +57 -8
- data/examples/ember-app/enola/constraints/ember.yaml +10 -0
- data/examples/ember-app/enola/recipes/ember-conventions.yaml +83 -0
- data/examples/rails-shop/enola/constraints/laws.rb +93 -0
- data/examples/rails-shop/enola.yaml +9 -0
- data/guides/01-start-here.md +136 -0
- data/guides/02-rails.md +168 -0
- data/guides/03-ember.md +131 -0
- data/guides/04-writing-laws.md +108 -0
- data/guides/05-constraints-in-ci.md +67 -0
- data/guides/06-hooks-and-agents.md +74 -0
- data/guides/07-providers.md +90 -0
- data/guides/08-beyond-one-repo.md +99 -0
- data/lib/enola-guides.rb +1 -1
- data/recipes/README.md +52 -0
- data/recipes/ember-conventions.yaml +83 -0
- data/recipes/house-recipe-template.yaml +27 -0
- data/templates/constraints-starter.rb +30 -0
- data/templates/enola-check.sh +152 -0
- data/templates/enola-cluster.yaml +25 -0
- data/templates/enola-findings.rb +83 -0
- data/templates/skills/enola/SKILL.md +39 -0
- data/templates/skills/enola-grade-before-push/SKILL.md +21 -0
- data/templates/skills/enola-intent-first/SKILL.md +25 -0
- data/templates/skills/enola-laws/SKILL.md +35 -0
- metadata +27 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4da894ee2a8bbae62bfd0c91d741184cb544ab52c7daa172cc4d29d6760b0e88
|
|
4
|
+
data.tar.gz: 57a1e3127acc18cfae45d00e8d68d980ca5884dea25c0edd616fff96816b4c02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30fbd5e723192833b38e5e7108cdfab7dc48c8604a614c58a07c1d3c39c8fef2c3f4c941667fd67011fa93865d75891d0804479cbf79b909d3d42cad41c014c4
|
|
7
|
+
data.tar.gz: 8cb01c56994da025c7493b7c8e7ecfa9197b5f19a331c4722cbe9776e2a32d713514251ce1d6d0cae437b24707bf38b901682e0db101f20222281693833d5ba6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-08-22)
|
|
4
|
+
|
|
5
|
+
First content. Eight guides (start here, a Rails application, an Ember
|
|
6
|
+
application, writing laws, constraints in CI, hooks and agents,
|
|
7
|
+
providers, beyond one repository), two worked examples (a Rails shop in
|
|
8
|
+
the Ruby DSL with Prism and Rubydex as providers; an Ember app bound to
|
|
9
|
+
the conventions recipe), four agent skills (enola, enola-laws,
|
|
10
|
+
enola-grade-before-push, enola-intent-first), the CI check template with
|
|
11
|
+
its renderer, a cluster configuration, a constraints starter, and two
|
|
12
|
+
recipes. Written against the release of enola that carries the
|
|
13
|
+
constraints vocabulary (enola-labs/enola#247).
|
|
14
|
+
|
|
15
|
+
## 0.0.0
|
|
16
|
+
|
|
17
|
+
Placeholder to claim the name.
|
data/README.md
CHANGED
|
@@ -1,12 +1,61 @@
|
|
|
1
1
|
# enola-guides
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[enola](https://github.com/enola-labs/enola)
|
|
5
|
-
|
|
3
|
+
How to use what is landing in [enola](https://github.com/enola-labs/enola)
|
|
4
|
+
([enola-labs/enola#247](https://github.com/enola-labs/enola/pull/247)): a
|
|
5
|
+
Ruby DSL for writing architecture constraints, Prism and Rubydex as
|
|
6
|
+
providers that run together with enola's own extractors, recipes a team
|
|
7
|
+
reviews, laws that read the whole graph, and an MCP your agent uses during
|
|
8
|
+
a session. Guides, worked examples, skills and templates, packaged as a
|
|
9
|
+
gem so a team can pin the guidance it adopted. Rubyists first; consumed by
|
|
10
|
+
[enola-rb](https://github.com/misabegovic/enola-rb), the gem that runs
|
|
11
|
+
enola. Expanding as the practice does.
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
```bash
|
|
14
|
+
gem install enola-guides
|
|
15
|
+
gem contents enola-guides
|
|
16
|
+
```
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
## Start with an example
|
|
19
|
+
|
|
20
|
+
[`examples/rails-shop`](examples/rails-shop): a Rails application declaring
|
|
21
|
+
thirteen laws in the Ruby DSL, from "jobs never invoke controller code" to
|
|
22
|
+
"billing keeps to its own tables" and "a mutating action is authorized",
|
|
23
|
+
with Prism and Rubydex configured as providers beside the extractors.
|
|
24
|
+
[`examples/ember-app`](examples/ember-app): an Ember application bound to
|
|
25
|
+
the conventions recipe in one block.
|
|
26
|
+
|
|
27
|
+
## Guides
|
|
28
|
+
|
|
29
|
+
1. [Start here](guides/01-start-here.md): install, snapshot, baseline, a
|
|
30
|
+
graded change, the hooks, a first law. Ten minutes.
|
|
31
|
+
2. [A Rails application](guides/02-rails.md): what enola measures, the
|
|
32
|
+
Prism and Rubydex providers, the recipe, the laws a Rails team writes.
|
|
33
|
+
3. [An Ember application](guides/03-ember.md): the Ember facts, the
|
|
34
|
+
conventions as a recipe, an app inside a larger repository.
|
|
35
|
+
4. [Writing laws](guides/04-writing-laws.md): parts, the twenty-one
|
|
36
|
+
sentences, modes, dates, exemptions, what a refusal means.
|
|
37
|
+
5. [Constraints in CI](guides/05-constraints-in-ci.md): the check step.
|
|
38
|
+
6. [Hooks and agents](guides/06-hooks-and-agents.md): the session hooks,
|
|
39
|
+
the MCP tools, the skills.
|
|
40
|
+
7. [Providers](guides/07-providers.md): the seam, the Ruby pair, the
|
|
41
|
+
census, the runtime capture.
|
|
42
|
+
8. [Beyond one repository](guides/08-beyond-one-repo.md): the cluster, the
|
|
43
|
+
seams, the laws across them, the history.
|
|
44
|
+
|
|
45
|
+
## Skills
|
|
46
|
+
|
|
47
|
+
The skills under [`templates/skills/`](templates/skills) are the ones used
|
|
48
|
+
day to day with enola's MCP in an agent session: `enola` (ask the graph
|
|
49
|
+
before grepping, grade after changing), `enola-laws` (write a law as a
|
|
50
|
+
sentence with a reason, lint it, read what it verdicts),
|
|
51
|
+
`enola-grade-before-push` (the delta is the deliverable, a declined grade
|
|
52
|
+
is not a pass), `enola-intent-first` (read the governing decision before
|
|
53
|
+
the file moves). Copy a directory into your agent's skills folder.
|
|
54
|
+
|
|
55
|
+
## Templates and recipes
|
|
56
|
+
|
|
57
|
+
[`templates/`](templates): the CI check script and its comment renderer,
|
|
58
|
+
a cluster configuration, a constraints starter. [`recipes/`](recipes/README.md):
|
|
59
|
+
recipes a repository copies into its own `enola/recipes/` and binds in one
|
|
60
|
+
line, starting with the Ember conventions and a house-recipe template;
|
|
61
|
+
starting points the team reviews, never defaults the tool applies.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The Ember conventions bound in one block. The recipe is copied from
|
|
2
|
+
# enola-guides' recipes/ into this repository's enola/recipes/, where the
|
|
3
|
+
# team reviews it; the two optional roles are bound to this app's helpers.
|
|
4
|
+
use_recipe:
|
|
5
|
+
- recipe: ember-conventions
|
|
6
|
+
as: app
|
|
7
|
+
mode: ratchet
|
|
8
|
+
bind:
|
|
9
|
+
task-factories: { match: ["app/utils/**"], name_pattern: "*.trackedFunction" }
|
|
10
|
+
fixture-setup: { match: ["tests/helpers/**"] }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
recipe: ember-conventions
|
|
2
|
+
roles:
|
|
3
|
+
- name: components
|
|
4
|
+
match: ["app/components/**"]
|
|
5
|
+
- name: integration-tests
|
|
6
|
+
kind: test_ref
|
|
7
|
+
match: ["tests/integration/**"]
|
|
8
|
+
- name: actions
|
|
9
|
+
kind: symbol
|
|
10
|
+
where: { decorators: action }
|
|
11
|
+
- name: framework-controllers
|
|
12
|
+
kind: symbol
|
|
13
|
+
where: { superclass_module: "@ember/controller" }
|
|
14
|
+
- name: constructors
|
|
15
|
+
match: ["app/**"]
|
|
16
|
+
name_pattern: "*.constructor"
|
|
17
|
+
- name: fetchers
|
|
18
|
+
match: ["app/services/**"]
|
|
19
|
+
- name: legacy-accessor
|
|
20
|
+
match: ["app/utils/**"]
|
|
21
|
+
name_pattern: "*.get"
|
|
22
|
+
- name: argument-mutators
|
|
23
|
+
kind: symbol
|
|
24
|
+
where: { fields_written: args }
|
|
25
|
+
- name: element-modifiers
|
|
26
|
+
kind: symbol
|
|
27
|
+
match: ["app/components/**", "app/modifiers/**"]
|
|
28
|
+
- name: state-writing-members
|
|
29
|
+
kind: symbol
|
|
30
|
+
where: { fields_written: "<any>" }
|
|
31
|
+
- name: task-factories
|
|
32
|
+
optional: true
|
|
33
|
+
- name: fixture-setup
|
|
34
|
+
optional: true
|
|
35
|
+
rules:
|
|
36
|
+
- id: no-action-decorator
|
|
37
|
+
forbid_fact: actions
|
|
38
|
+
because: "An arrow function binds lexically and needs no decorator; the decorator predates it and the convention says to stop writing it."
|
|
39
|
+
|
|
40
|
+
- id: no-framework-controllers
|
|
41
|
+
forbid_fact: framework-controllers
|
|
42
|
+
because: "A route component replaces the controller and its template. Controllers are quirky and carry surprising singleton state, and the convention is to stop adding them."
|
|
43
|
+
|
|
44
|
+
- id: components-import-no-render-modifiers
|
|
45
|
+
forbid: components
|
|
46
|
+
to_name: ["@ember/render-modifiers*", "ember-render-helpers*"]
|
|
47
|
+
via: imports
|
|
48
|
+
because: "The render-helper form fires on a lifecycle rather than attaching to the element it acts on, and it has been discouraged for years. A named modifier attaches to the element and says what it acts on."
|
|
49
|
+
|
|
50
|
+
- id: constructors-do-not-fetch
|
|
51
|
+
forbid: constructors
|
|
52
|
+
to: fetchers
|
|
53
|
+
via: calls
|
|
54
|
+
because: "Rendering pulls data in as it is needed. A constructor that fetches is eager, runs before anything asks, and cannot be re-run when the arguments change."
|
|
55
|
+
|
|
56
|
+
- id: no-legacy-property-accessor
|
|
57
|
+
forbid: components
|
|
58
|
+
to: legacy-accessor
|
|
59
|
+
via: calls
|
|
60
|
+
because: "The proxy accessor reads through a layer that no longer exists on a resolved value, and it defeats type checking. Reach for the property."
|
|
61
|
+
|
|
62
|
+
- id: arguments-are-not-mutated
|
|
63
|
+
forbid_fact: argument-mutators
|
|
64
|
+
because: "Data flows down and actions flow up. A component that writes through its own arguments changes state it does not own, and the owner never learns of it. Pass a callback and let the owner make the change."
|
|
65
|
+
|
|
66
|
+
- id: a-modifier-takes-its-element
|
|
67
|
+
require: element-modifiers
|
|
68
|
+
when_edge_to: ["*.modifier"]
|
|
69
|
+
via: calls
|
|
70
|
+
must_prop_contain: { prop: takes_parameters, value: "yes" }
|
|
71
|
+
because: "A modifier is handed the element it is attached to. One that declares no parameter is not modifying anything; it is a side effect fired by render, and a getter or a tracked task says that honestly."
|
|
72
|
+
|
|
73
|
+
- id: derivations-do-not-write
|
|
74
|
+
forbid: state-writing-members
|
|
75
|
+
to: task-factories
|
|
76
|
+
via: calls
|
|
77
|
+
because: "A task is derived state and runs inside the render cycle. Writing the component's own state from one schedules another render off the render it is running in. Return the data and read it through the task's value instead."
|
|
78
|
+
|
|
79
|
+
- id: component-tests-carry-no-fixtures
|
|
80
|
+
forbid: integration-tests
|
|
81
|
+
to: fixture-setup
|
|
82
|
+
via: calls
|
|
83
|
+
because: "A fixture library sets up back-end state and has no place in a component test: if the component makes no request, build a front-end record, and if it does, let the acceptance layer answer it."
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# The Ruby DSL, end to end, for a Rails shop. Parsed by enola, never
|
|
2
|
+
# executed. Every sentence below compiles to a rule form the YAML also
|
|
3
|
+
# accepts; `enola constraints lint .` shows what each part resolved to.
|
|
4
|
+
Enola.architecture "shop" do
|
|
5
|
+
rails
|
|
6
|
+
|
|
7
|
+
part :billing, files: ["app/billing/**", "app/models/invoice.rb"]
|
|
8
|
+
part :orders, files: ["app/orders/**", "app/models/order.rb"], public: ["app/orders/public/**"]
|
|
9
|
+
part :records, ancestor: "ApplicationRecord"
|
|
10
|
+
part :mutating_actions, files: "app/controllers/**", handles: [:post, :put, :patch, :delete]
|
|
11
|
+
part :service_objects, files: "app/services/**", kind: :symbol, where: { symbol_kind: "class" }, owns: :methods
|
|
12
|
+
part :api, files: "config/**", kind: :route
|
|
13
|
+
part :tables, files: "app/models/**", kind: :storage
|
|
14
|
+
|
|
15
|
+
law "background jobs never invoke controller code" do
|
|
16
|
+
jobs.must_not_call controllers
|
|
17
|
+
why "rendering from a job goes through ApplicationController.renderer"
|
|
18
|
+
mode :ratchet
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
law "the request api stays in controllers" do
|
|
22
|
+
models.must_not_call "params", receiver: :none
|
|
23
|
+
why "params without a receiver is the controller's; a model reading it only works inside a request"
|
|
24
|
+
mode :ratchet
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
law "billing keeps to its own tables" do
|
|
28
|
+
billing.storage_must_stay_home
|
|
29
|
+
why "a part that writes another part's table owns a bug it cannot see"
|
|
30
|
+
mode :ratchet
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
law "orders are reached through their public surface" do
|
|
34
|
+
orders.stays_inside
|
|
35
|
+
why "the public directory is the contract; everything else may change"
|
|
36
|
+
mode :ratchet
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
law "a mutating action is authorized" do
|
|
40
|
+
mutating_actions.must_reach :policies
|
|
41
|
+
why "every write passes a policy before it touches a record"
|
|
42
|
+
mode :ratchet
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
law "a concern never reaches the class that includes it" do
|
|
46
|
+
concerns.must_not_reach_includers
|
|
47
|
+
why "a mixin that knows its includer is half a class in hiding"
|
|
48
|
+
mode :ratchet
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
law "jobs, models and mailers never depend on each other in a circle" do
|
|
52
|
+
jobs.must_not_cycle_with :models, :mailers
|
|
53
|
+
why "parts that reach each other in a circle cannot be taken apart"
|
|
54
|
+
mode :advisory
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
law "a service answers to call" do
|
|
58
|
+
service_objects.must_define :call
|
|
59
|
+
why "one door per service, so a caller never reads it to find the verb"
|
|
60
|
+
mode :ratchet
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
law "scopes come in pairs" do
|
|
64
|
+
records.names_must_match "with_*", requires: "without_*"
|
|
65
|
+
why "every with_ scope has a without_ sibling, or the query reads half a thought"
|
|
66
|
+
mode :advisory
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
law "the public api stays small" do
|
|
70
|
+
api.at_most 120
|
|
71
|
+
growth 5
|
|
72
|
+
why "a route is a promise; the count may grow, but only on purpose"
|
|
73
|
+
mode :ratchet
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
law "frames keep their query budget" do
|
|
77
|
+
controllers.must_keep_budget metric: :queries, max: 20
|
|
78
|
+
why "a frame past twenty queries is a page that will not scale; refused by name until a capture exists"
|
|
79
|
+
mode :advisory
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
law "billing is documented" do
|
|
83
|
+
billing.must_be_governed
|
|
84
|
+
since "2026-09-01"
|
|
85
|
+
why "code under a decision needs the page that decided it"
|
|
86
|
+
mode :ratchet
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
law "reach for an existing service" do
|
|
90
|
+
service_objects.advises "look in app/services before writing a new one"
|
|
91
|
+
why "duplication is a judgement no fact answers; advice where the edit happens is what can be offered honestly"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Providers run beside enola's own extractors at snapshot time and their
|
|
2
|
+
# facts land in the same graph. Prism needs a Ruby 3.3+ on PATH; Rubydex
|
|
3
|
+
# runs inside the binary after `enola providers fetch rubydex`.
|
|
4
|
+
providers:
|
|
5
|
+
- name: prism
|
|
6
|
+
command: ["ruby", "examples/providers/ruby/prism/enola_prism_provider.rb"]
|
|
7
|
+
expected_version: "0.1.0"
|
|
8
|
+
- name: rubydex
|
|
9
|
+
expected_version: "0.4.0"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Start here: the first useful answer in ten minutes
|
|
2
|
+
|
|
3
|
+
enola answers one question no linter can: **what did this change do to the
|
|
4
|
+
architecture?** It builds a graph of your code (modules, symbols, routes,
|
|
5
|
+
tables, and how they depend on each other), keeps that graph as a snapshot,
|
|
6
|
+
and grades a change by comparing the snapshot before it with the snapshot
|
|
7
|
+
after it. A dependency cycle that spans four files, a job that started
|
|
8
|
+
calling a controller, a table a second part began writing to: those are
|
|
9
|
+
invisible to tools that look at one file at a time, and they are what enola
|
|
10
|
+
reports.
|
|
11
|
+
|
|
12
|
+
This guide takes you from nothing to a graded change. Everything here is
|
|
13
|
+
written for the release that carries the constraints vocabulary (the one
|
|
14
|
+
this repository's recipes and templates target); if your `enola --version`
|
|
15
|
+
is older, upgrade first.
|
|
16
|
+
|
|
17
|
+
## 1. Install
|
|
18
|
+
|
|
19
|
+
No Go toolchain, no compiler, one binary:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh
|
|
23
|
+
enola --version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If your application is Ruby, fetch the Rubydex engine once so enola can
|
|
27
|
+
resolve constants and ancestry through Ruby's own rules (more in
|
|
28
|
+
[providers](07-providers.md)):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
enola providers fetch rubydex
|
|
32
|
+
enola providers list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 2. Look before you configure
|
|
36
|
+
|
|
37
|
+
One read-only command, nothing written:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
enola --explain .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It prints the architecture it detected (pattern, cycles, layer
|
|
44
|
+
violations), the coupling hotspots with their blast radius, and the code
|
|
45
|
+
health findings. Nothing here is a setting: enola detects every language
|
|
46
|
+
in the tree and indexes all of them into one graph.
|
|
47
|
+
|
|
48
|
+
## 3. Take the first snapshot
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
enola --generate .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This writes `.enola/` in the repository (add it to `.gitignore`): the
|
|
55
|
+
facts, the findings the explainers computed, and a receipt saying which
|
|
56
|
+
extractors and providers ran and what they skipped. Read the receipt's
|
|
57
|
+
census when something looks missing: a file enola did not parse is named
|
|
58
|
+
there, never silently dropped.
|
|
59
|
+
|
|
60
|
+
## 4. Pin a baseline, change something, grade it
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
enola baseline pin .
|
|
64
|
+
# ... edit code ...
|
|
65
|
+
enola --generate .
|
|
66
|
+
enola check .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`check` compares the current snapshot with the pinned one and prints what
|
|
70
|
+
the change did: findings introduced, findings resolved, edges added. It
|
|
71
|
+
exits 0 unless you tell it what fails the build:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
enola check -fail-on constraints,cycles,layers .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Two outcomes are not verdicts, and the output says so: **DECLINED** means
|
|
78
|
+
the two snapshots are not comparable (a different enola version, a
|
|
79
|
+
provider that ran on one side only, changed ignore globs), so re-pin the
|
|
80
|
+
baseline rather than reading it as a pass; **unevaluable** on a rule means
|
|
81
|
+
the snapshot could not answer that rule (no resolved ancestry, no runtime
|
|
82
|
+
capture, one repository where two are needed), named with its cause.
|
|
83
|
+
|
|
84
|
+
## 5. Tell your agents
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
enola install --hooks
|
|
88
|
+
enola doctor
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`install` writes enola's instructions into the files your coding agents
|
|
92
|
+
already read and, with `--hooks`, adds a session-start hook that restores
|
|
93
|
+
the snapshot and a session-end hook that grades the session's delta. It
|
|
94
|
+
previews every change and asks before writing; `enola uninstall` reverses
|
|
95
|
+
it byte for byte. `doctor` reports whether the hooks actually fired, which
|
|
96
|
+
version you run, and whether the Rubydex library is in place. Your agent
|
|
97
|
+
gets a map it can query instead of grepping; the [hooks and agents
|
|
98
|
+
guide](06-hooks-and-agents.md) says what to put in its skill.
|
|
99
|
+
|
|
100
|
+
## 6. Declare your first law
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
enola constraints init .
|
|
104
|
+
enola constraints lint .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`init` writes a starter declaration bound to the recipes whose roles your
|
|
108
|
+
tree has (controllers, models, jobs for a Rails app) and refuses to
|
|
109
|
+
overwrite one that exists. `lint` shows what each part resolved to. Then
|
|
110
|
+
write one law of your own in Ruby, in `enola/constraints/`:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
Enola.architecture "shop" do
|
|
114
|
+
rails
|
|
115
|
+
|
|
116
|
+
law "background jobs never invoke controller code" do
|
|
117
|
+
jobs.must_not_call controllers
|
|
118
|
+
why "rendering from a job goes through ApplicationController.renderer"
|
|
119
|
+
mode :ratchet
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`ratchet` reports what already breaks the law and fails only what a
|
|
125
|
+
change adds, which is the mode to start every law in. The [writing laws
|
|
126
|
+
guide](04-writing-laws.md) has the whole vocabulary.
|
|
127
|
+
|
|
128
|
+
## Where next
|
|
129
|
+
|
|
130
|
+
- [A Rails application](02-rails.md): what enola measures in one, the
|
|
131
|
+
providers that sharpen it, the recipe that names its parts.
|
|
132
|
+
- [An Ember application](03-ember.md): the same for the front end.
|
|
133
|
+
- [Constraints in CI](05-constraints-in-ci.md): the check step, with a
|
|
134
|
+
template you copy.
|
|
135
|
+
- [Beyond one repository](08-beyond-one-repo.md): the cluster, and the laws
|
|
136
|
+
that only make sense across it.
|
data/guides/02-rails.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# A Rails application
|
|
2
|
+
|
|
3
|
+
What enola sees in a Rails app, what the two Ruby providers add, and how
|
|
4
|
+
to go from the first snapshot to laws the team wrote.
|
|
5
|
+
|
|
6
|
+
## What the snapshot holds
|
|
7
|
+
|
|
8
|
+
From the Ruby extractor alone:
|
|
9
|
+
|
|
10
|
+
- **Symbols** for every class, module and method, with their calls,
|
|
11
|
+
constant references and one-level superclass.
|
|
12
|
+
- **Routes** from `config/routes.rb` and every engine's route file,
|
|
13
|
+
`resources` expanded, each route carrying its method and path and a
|
|
14
|
+
`handled_by` edge to the controller action that serves it.
|
|
15
|
+
- **Storage** facts for every `ApplicationRecord` descendant: the model,
|
|
16
|
+
its table (declared or derived), its columns and foreign keys when the
|
|
17
|
+
schema is present.
|
|
18
|
+
- **Associations** with their macro, target and whether the target was
|
|
19
|
+
declared (`class_name:`) or derived from the name.
|
|
20
|
+
- **Module edges** between directories, weighted by the symbol edges
|
|
21
|
+
behind them, which is what cycle detection reads.
|
|
22
|
+
|
|
23
|
+
Run `enola --explain .` and then `enola --generate .`; the receipt in
|
|
24
|
+
`.enola/receipt.json` lists what each extractor parsed and skipped.
|
|
25
|
+
|
|
26
|
+
## The two providers
|
|
27
|
+
|
|
28
|
+
Two things the extractor does not settle are settled by providers, which
|
|
29
|
+
enola runs at snapshot time beside its own extraction:
|
|
30
|
+
|
|
31
|
+
- **Prism** reads every file with Ruby's own parser and adds the call edges
|
|
32
|
+
a lexical walk can see. It needs a Ruby 3.3 or newer on PATH (Prism is a
|
|
33
|
+
default gem), nothing else:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
providers:
|
|
37
|
+
- name: prism
|
|
38
|
+
command: ["ruby", "/path/to/enola_prism_provider.rb"]
|
|
39
|
+
expected_version: "0.1.0"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Rubydex** resolves constants through nesting and inheritance, types the
|
|
43
|
+
receiver of a call, and linearises every class's ancestry with mixins in
|
|
44
|
+
order. It runs inside the enola binary against an engine library fetched
|
|
45
|
+
once, with no Ruby involved, and reads your bundle's gem paths when
|
|
46
|
+
`bundle` is on PATH so `ApplicationRecord` and `ViewComponent::Base`
|
|
47
|
+
resolve:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
enola providers fetch rubydex
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
providers:
|
|
55
|
+
- name: rubydex
|
|
56
|
+
expected_version: "0.4.0"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The configuration lives in `enola.yaml` (or `mcp-arch.yaml`) at the
|
|
60
|
+
repository root. With both providers a law can say "every class under
|
|
61
|
+
`ApplicationRecord`" and mean the resolved chain, not the text after `<`.
|
|
62
|
+
The [providers guide](07-providers.md) covers the census, the named skips,
|
|
63
|
+
and the runtime capture.
|
|
64
|
+
|
|
65
|
+
## Name the parts
|
|
66
|
+
|
|
67
|
+
`enola constraints init .` writes `enola/constraints/init.yaml` binding the
|
|
68
|
+
recipes whose roles your tree has. For a Rails app that is
|
|
69
|
+
`rails-conventions`:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
use_recipe:
|
|
73
|
+
- recipe: rails-conventions
|
|
74
|
+
as: rails
|
|
75
|
+
mode: ratchet
|
|
76
|
+
bind:
|
|
77
|
+
controllers: { match: ["app/controllers/**"] }
|
|
78
|
+
jobs: { match: ["app/jobs/**"] }
|
|
79
|
+
models: { match: ["app/models/**"] }
|
|
80
|
+
mailers: { match: ["app/mailers/**"] }
|
|
81
|
+
policies: { match: ["app/policies/**"] }
|
|
82
|
+
serializers: { match: ["app/serializers/**"] }
|
|
83
|
+
view-components: { match: ["app/components/**"] }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Two roles are optional and off until you bind them: `helpers` and
|
|
87
|
+
`services`. Binding them brings three more laws (services and models
|
|
88
|
+
never reach helpers; the controller's request API stays out of models and
|
|
89
|
+
services). `rails-strict` is the same set with the concerns law added and
|
|
90
|
+
nothing advisory. `vanilla-rails` is for an app that keeps Rails plain and
|
|
91
|
+
wants its extra directories to stay empty with a stated reason each.
|
|
92
|
+
|
|
93
|
+
`enola constraints lint .` shows every part with what it resolved to and
|
|
94
|
+
every rule a binding left unexpanded, which is how you check the tree
|
|
95
|
+
before the first verdict.
|
|
96
|
+
|
|
97
|
+
## The laws a Rails team writes
|
|
98
|
+
|
|
99
|
+
The recipe's laws, each with the reason it carries:
|
|
100
|
+
|
|
101
|
+
- jobs do not reach controllers (advisory, because the sanctioned path
|
|
102
|
+
through `ApplicationController.renderer` reads as a crossing);
|
|
103
|
+
- models do not reach controllers;
|
|
104
|
+
- policies only answer, never enqueue;
|
|
105
|
+
- mailers do not enqueue;
|
|
106
|
+
- serializers do not reach controllers;
|
|
107
|
+
- view components do not enqueue and do not reach controllers;
|
|
108
|
+
- maintenance tasks do not reach controllers;
|
|
109
|
+
- with `helpers` bound: services and models never reach helpers;
|
|
110
|
+
- with `services` bound: the request API (`params`, `session`, `render`,
|
|
111
|
+
`redirect_to`, `cookies`, `flash`) stays in controllers, stated as
|
|
112
|
+
receiver-less calls so `request.params` on a typed receiver is not a
|
|
113
|
+
breach.
|
|
114
|
+
|
|
115
|
+
Write your own beside them in Ruby, in any file under `enola/constraints/`:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
Enola.architecture "shop" do
|
|
119
|
+
rails
|
|
120
|
+
part :billing, files: ["app/billing/**", "app/models/invoice.rb"]
|
|
121
|
+
part :orders, files: ["app/orders/**", "app/models/order.rb"],
|
|
122
|
+
public: ["app/orders/public/**"]
|
|
123
|
+
|
|
124
|
+
law "billing keeps to its own tables" do
|
|
125
|
+
billing.storage_must_stay_home
|
|
126
|
+
why "a part that writes another part's table owns a bug it cannot see"
|
|
127
|
+
mode :ratchet
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
law "a mutating action is authorized" do
|
|
131
|
+
part :mutating_actions, files: "app/controllers/**",
|
|
132
|
+
handles: [:post, :put, :patch, :delete]
|
|
133
|
+
mutating_actions.must_reach :policies
|
|
134
|
+
why "every write passes a policy before it touches a record"
|
|
135
|
+
mode :ratchet
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The first law reads the storage facts: a member of `billing` reaching
|
|
141
|
+
`Order` is a breach that names the table `orders`, and the suggested cut
|
|
142
|
+
names the `orders` part's public member that already uses the model. The
|
|
143
|
+
second selects the code behind POST, PUT, PATCH and DELETE routes through
|
|
144
|
+
the `handled_by` edges and demands a call into the policies, so the law
|
|
145
|
+
follows the routes rather than a list of actions.
|
|
146
|
+
|
|
147
|
+
## Read a verdict
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
enola baseline pin .
|
|
151
|
+
# change something
|
|
152
|
+
enola --generate . && enola check .
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A constraint breach names the rule, the member, the edge and the reason,
|
|
156
|
+
leads its suggested actions with the smallest change the graph can see,
|
|
157
|
+
and says whether it is a decided-rule breach (exact membership over a
|
|
158
|
+
measured edge) or an advisory. `enola constraints explain app/jobs/x.rb`
|
|
159
|
+
answers what a file is a member of, by which selector, and which edges it
|
|
160
|
+
makes, which is the question to ask before arguing with a verdict.
|
|
161
|
+
|
|
162
|
+
## When the answer is "cannot evaluate"
|
|
163
|
+
|
|
164
|
+
A law over resolved ancestry without the Rubydex provider, a budget
|
|
165
|
+
without a runtime capture, a law across repositories in a one-repository
|
|
166
|
+
snapshot: each is reported as unevaluable with its cause and emits no
|
|
167
|
+
verdict. That is the design. A law nobody could measure must not read as
|
|
168
|
+
kept.
|