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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76f0a1325c41c7ff90c0e6a2b2952cd597ed7496103df0e6756ad197855da705
4
- data.tar.gz: 2bb8581b18beb14dec24dc2b61df053768ee8dea9378bf7d10fa43e5b74000c8
3
+ metadata.gz: 4da894ee2a8bbae62bfd0c91d741184cb544ab52c7daa172cc4d29d6760b0e88
4
+ data.tar.gz: 57a1e3127acc18cfae45d00e8d68d980ca5884dea25c0edd616fff96816b4c02
5
5
  SHA512:
6
- metadata.gz: 845c312c4126618fa265248d19b3a515c47d854c830616d03d1254c9f42df4a6ce1b263fa77b431b1eaa6d64a58e8a7684dbc98d79a8fe590c5c3d20bde366d2
7
- data.tar.gz: 6de47b842e40351d514f84b3af1d4b2bd4e5e2145f1f9c329521e251bc9a36842cdde9f41b1c1edbff306b231777b91c5d189ea5b79098453a45de0dde1c639e
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
- Guides, presets and onboarding material for
4
- [enola](https://github.com/enola-labs/enola), the architecture-graph tool,
5
- written for Ruby and Rails developers first.
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
- Ready-to-use templates, presets, skills and instructions: how to use enola on
8
- a Rails app, on Ember, as a constraints gate in CI, and through hooks and
9
- agent skills. Consumed by [enola-rb](https://github.com/misabegovic/enola-rb),
10
- the gem that runs enola.
13
+ ```bash
14
+ gem install enola-guides
15
+ gem contents enola-guides
16
+ ```
11
17
 
12
- Placeholder release. No content yet.
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.
@@ -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.