enola-guides 0.0.0 → 0.1.1

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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +59 -8
  4. data/examples/ember-app/enola/constraints/ember.yaml +10 -0
  5. data/examples/ember-app/enola/recipes/ember-conventions.yaml +83 -0
  6. data/examples/laws/README.md +32 -0
  7. data/examples/laws/clean-rings/README.md +10 -0
  8. data/examples/laws/clean-rings/enola/constraints/clean.yaml +8 -0
  9. data/examples/laws/cluster-laws/README.md +9 -0
  10. data/examples/laws/cluster-laws/enola/constraints/cluster.rb +24 -0
  11. data/examples/laws/cqrs-queries-never-write/README.md +11 -0
  12. data/examples/laws/cqrs-queries-never-write/enola/constraints/cqrs.yaml +7 -0
  13. data/examples/laws/foreign-keys/README.md +10 -0
  14. data/examples/laws/foreign-keys/enola/constraints/foreign-keys.yaml +12 -0
  15. data/examples/laws/governed-code/README.md +11 -0
  16. data/examples/laws/governed-code/enola/constraints/governance.rb +8 -0
  17. data/examples/laws/instantiation/README.md +14 -0
  18. data/examples/laws/instantiation/enola/constraints/ceremony.yaml +11 -0
  19. data/examples/laws/maintenance-tasks/README.md +9 -0
  20. data/examples/laws/maintenance-tasks/enola/constraints/maintenance-tasks.rb +15 -0
  21. data/examples/laws/mixins-stay-independent/README.md +11 -0
  22. data/examples/laws/mixins-stay-independent/enola/constraints/concerns.yaml +9 -0
  23. data/examples/laws/naming-conventions/README.md +10 -0
  24. data/examples/laws/naming-conventions/enola/constraints/naming.yaml +5 -0
  25. data/examples/laws/one-door/README.md +8 -0
  26. data/examples/laws/one-door/enola/constraints/one-door.yaml +18 -0
  27. data/examples/laws/one-owner-per-table/README.md +12 -0
  28. data/examples/laws/one-owner-per-table/enola/constraints/tables.rb +8 -0
  29. data/examples/laws/paired-names/README.md +9 -0
  30. data/examples/laws/paired-names/enola/constraints/pairs.yaml +11 -0
  31. data/examples/laws/parts-never-cycle/README.md +12 -0
  32. data/examples/laws/parts-never-cycle/enola/constraints/cycles.yaml +12 -0
  33. data/examples/laws/protocol-any-of/README.md +10 -0
  34. data/examples/laws/protocol-any-of/enola/constraints/entry.yaml +8 -0
  35. data/examples/laws/public-surface/README.md +11 -0
  36. data/examples/laws/public-surface/enola/constraints/surface.yaml +10 -0
  37. data/examples/laws/public-tool-prefix/README.md +8 -0
  38. data/examples/laws/public-tool-prefix/enola/constraints/public-tools.rb +8 -0
  39. data/examples/laws/rails-forbidden-names/README.md +14 -0
  40. data/examples/laws/rails-forbidden-names/enola/constraints/naming.yaml +13 -0
  41. data/examples/laws/rails-mutations-are-authorized/README.md +11 -0
  42. data/examples/laws/rails-mutations-are-authorized/enola/constraints/authorization.rb +9 -0
  43. data/examples/laws/rails-request-boundaries/README.md +12 -0
  44. data/examples/laws/rails-request-boundaries/enola/constraints/rails.yaml +13 -0
  45. data/examples/laws/rails-strict-arrangement/README.md +10 -0
  46. data/examples/laws/rails-strict-arrangement/enola/constraints/strict.yaml +12 -0
  47. data/examples/laws/readers-read-the-store-only/README.md +7 -0
  48. data/examples/laws/readers-read-the-store-only/enola/constraints/readers.yaml +13 -0
  49. data/examples/laws/receiverless-request-api/README.md +10 -0
  50. data/examples/laws/receiverless-request-api/enola/constraints/bare.yaml +10 -0
  51. data/examples/laws/resolved-ancestry/README.md +15 -0
  52. data/examples/laws/resolved-ancestry/enola/constraints/records.yaml +11 -0
  53. data/examples/laws/routes-have-consumers/README.md +12 -0
  54. data/examples/laws/routes-have-consumers/enola/constraints/consumers.rb +8 -0
  55. data/examples/laws/storage-stays-home/README.md +12 -0
  56. data/examples/laws/storage-stays-home/enola/constraints/ownership.rb +9 -0
  57. data/examples/laws/vanilla-rails-stays-empty/README.md +10 -0
  58. data/examples/laws/vanilla-rails-stays-empty/enola/constraints/vanilla.yaml +11 -0
  59. data/examples/rails-shop/enola/constraints/laws.rb +93 -0
  60. data/examples/rails-shop/enola.yaml +9 -0
  61. data/guides/01-start-here.md +136 -0
  62. data/guides/02-rails.md +168 -0
  63. data/guides/03-ember.md +131 -0
  64. data/guides/04-writing-laws.md +108 -0
  65. data/guides/05-constraints-in-ci.md +67 -0
  66. data/guides/06-hooks-and-agents.md +74 -0
  67. data/guides/07-providers.md +90 -0
  68. data/guides/08-beyond-one-repo.md +99 -0
  69. data/lib/enola-guides.rb +1 -1
  70. data/recipes/README.md +52 -0
  71. data/recipes/ember-conventions.yaml +83 -0
  72. data/recipes/house-recipe-template.yaml +27 -0
  73. data/templates/constraints-starter.rb +30 -0
  74. data/templates/enola-check.sh +152 -0
  75. data/templates/enola-cluster.yaml +25 -0
  76. data/templates/enola-findings.rb +83 -0
  77. data/templates/skills/enola/SKILL.md +39 -0
  78. data/templates/skills/enola-grade-before-push/SKILL.md +21 -0
  79. data/templates/skills/enola-intent-first/SKILL.md +25 -0
  80. data/templates/skills/enola-laws/SKILL.md +35 -0
  81. metadata +80 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76f0a1325c41c7ff90c0e6a2b2952cd597ed7496103df0e6756ad197855da705
4
- data.tar.gz: 2bb8581b18beb14dec24dc2b61df053768ee8dea9378bf7d10fa43e5b74000c8
3
+ metadata.gz: 103217d2be760b0b582027a9119e6acc6fae874ffc1171f8e59df35ac5ffb09b
4
+ data.tar.gz: 65f13f8cd1c54cafec16aca3f50a80710dcc77df8f6e66077c54fb346afeaf3a
5
5
  SHA512:
6
- metadata.gz: 845c312c4126618fa265248d19b3a515c47d854c830616d03d1254c9f42df4a6ce1b263fa77b431b1eaa6d64a58e8a7684dbc98d79a8fe590c5c3d20bde366d2
7
- data.tar.gz: 6de47b842e40351d514f84b3af1d4b2bd4e5e2145f1f9c329521e251bc9a36842cdde9f41b1c1edbff306b231777b91c5d189ea5b79098453a45de0dde1c639e
6
+ metadata.gz: 9cac7aed45a1041aeaeb66c46040f165dfa4156b05d5c2c088890dd18912930b6924059b5f0b1bcc00c4379b7574e524ab8794a43c936a5e5915a4afccf7572c
7
+ data.tar.gz: fe230a7e9daac6c8c88662bdcce76f1be1ff8019992c48f57a95afb469fc4bad58bd02e6171d62603e796b80cd31938826d59a5c8010adada9fa116d5a16e13d
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-08-22)
4
+
5
+ The laws catalogue: `examples/laws/`, twenty-six complete declarations one
6
+ directory each, nineteen proven by enola's benchmark suite and seven
7
+ generalised from daily use (foreign keys, maintenance tasks, a public tool
8
+ prefix, one door to a schema, readers that read the store only, cluster
9
+ laws), each with a README and verified with `constraints lint`.
10
+
11
+ ## 0.1.0 (2026-08-22)
12
+
13
+ First content. Eight guides (start here, a Rails application, an Ember
14
+ application, writing laws, constraints in CI, hooks and agents,
15
+ providers, beyond one repository), two worked examples (a Rails shop in
16
+ the Ruby DSL with Prism and Rubydex as providers; an Ember app bound to
17
+ the conventions recipe), four agent skills (enola, enola-laws,
18
+ enola-grade-before-push, enola-intent-first), the CI check template with
19
+ its renderer, a cluster configuration, a constraints starter, and two
20
+ recipes. Written against the release of enola that carries the
21
+ constraints vocabulary (enola-labs/enola#247).
22
+
23
+ ## 0.0.0
24
+
25
+ Placeholder to claim the name.
data/README.md CHANGED
@@ -1,12 +1,63 @@
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. [`examples/laws`](examples/laws/README.md):
26
+ twenty-six laws, one directory each, from foreign keys on a tenant column
27
+ to parts that may not cycle, each a complete declaration you copy and run.
28
+
29
+ ## Guides
30
+
31
+ 1. [Start here](guides/01-start-here.md): install, snapshot, baseline, a
32
+ graded change, the hooks, a first law. Ten minutes.
33
+ 2. [A Rails application](guides/02-rails.md): what enola measures, the
34
+ Prism and Rubydex providers, the recipe, the laws a Rails team writes.
35
+ 3. [An Ember application](guides/03-ember.md): the Ember facts, the
36
+ conventions as a recipe, an app inside a larger repository.
37
+ 4. [Writing laws](guides/04-writing-laws.md): parts, the twenty-one
38
+ sentences, modes, dates, exemptions, what a refusal means.
39
+ 5. [Constraints in CI](guides/05-constraints-in-ci.md): the check step.
40
+ 6. [Hooks and agents](guides/06-hooks-and-agents.md): the session hooks,
41
+ the MCP tools, the skills.
42
+ 7. [Providers](guides/07-providers.md): the seam, the Ruby pair, the
43
+ census, the runtime capture.
44
+ 8. [Beyond one repository](guides/08-beyond-one-repo.md): the cluster, the
45
+ seams, the laws across them, the history.
46
+
47
+ ## Skills
48
+
49
+ The skills under [`templates/skills/`](templates/skills) are the ones used
50
+ day to day with enola's MCP in an agent session: `enola` (ask the graph
51
+ before grepping, grade after changing), `enola-laws` (write a law as a
52
+ sentence with a reason, lint it, read what it verdicts),
53
+ `enola-grade-before-push` (the delta is the deliverable, a declined grade
54
+ is not a pass), `enola-intent-first` (read the governing decision before
55
+ the file moves). Copy a directory into your agent's skills folder.
56
+
57
+ ## Templates and recipes
58
+
59
+ [`templates/`](templates): the CI check script and its comment renderer,
60
+ a cluster configuration, a constraints starter. [`recipes/`](recipes/README.md):
61
+ recipes a repository copies into its own `enola/recipes/` and binds in one
62
+ line, starting with the Ember conventions and a house-recipe template;
63
+ 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,32 @@
1
+ # Laws, one directory each
2
+
3
+ Every directory holds a complete declaration you can copy into a repository's
4
+ `enola/` and run. Most are proven by enola's own benchmark suite; the rest are
5
+ generalised from laws in daily use on a multi-repository estate.
6
+
7
+ - [`governed-code`](governed-code/README.md): The knowledge pages compile into the graph as anchors, and a law demands that every file of a part has one.
8
+ - [`rails-forbidden-names`](rails-forbidden-names/README.md): A naming convention phrased as a prohibition.
9
+ - [`rails-mutations-are-authorized`](rails-mutations-are-authorized/README.md): The code behind a mutating route is selected by the route's method through the handled_by edge the Rails binder draws, and must reach a policy.
10
+ - [`rails-request-boundaries`](rails-request-boundaries/README.md): The Rails laws a team actually writes.
11
+ - [`rails-strict-arrangement`](rails-strict-arrangement/README.md): The strict Rails arrangement: the Rails laws, no circle among the parts, and concerns that stay independent of their includers.
12
+ - [`clean-rings`](clean-rings/README.md): Four rings.
13
+ - [`cqrs-queries-never-write`](cqrs-queries-never-write/README.md): A command-query split.
14
+ - [`instantiation`](instantiation/README.md): What Ruby code constructs, as a fact.
15
+ - [`mixins-stay-independent`](mixins-stay-independent/README.md): A module never reaches the classes that include it.
16
+ - [`naming-conventions`](naming-conventions/README.md): Ruby naming conventions: no get_, set_ or is_ prefixes; a predicate ends in a question mark and has_ is fine.
17
+ - [`one-owner-per-table`](one-owner-per-table/README.md): unique_across compares the members of different repositories; with the storage facts of one repository there is nobody to share a table with, so the rule refuses by name rather than reporting every table as unique.
18
+ - [`paired-names`](paired-names/README.md): A naming pair: every with_ method has a without_ twin.
19
+ - [`parts-never-cycle`](parts-never-cycle/README.md): Parts that may not depend on each other in a circle.
20
+ - [`protocol-any-of`](protocol-any-of/README.md): A protocol satisfied by one of several methods.
21
+ - [`public-surface`](public-surface/README.md): A path-defined public surface.
22
+ - [`receiverless-request-api`](receiverless-request-api/README.md): A literal far end narrowed to receiver-less calls.
23
+ - [`resolved-ancestry`](resolved-ancestry/README.md): Ancestry a provider resolved, and a component selected by it.
24
+ - [`routes-have-consumers`](routes-have-consumers/README.md): A law across repositories refuses by name inside one.
25
+ - [`storage-stays-home`](storage-stays-home/README.md): A part keeps to the tables it owns.
26
+ - [`vanilla-rails-stays-empty`](vanilla-rails-stays-empty/README.md): Plain Rails: the extra directories must stay empty, each with a stated reason.
27
+ - [`foreign-keys`](foreign-keys/README.md): A foreign-key law reads the storage facts the schema yields: every table carrying a tenant column must declare the foreign key to the tenant table.
28
+ - [`maintenance-tasks`](maintenance-tasks/README.md): Two laws for a maintenance-tasks directory: a naming convention the runner depends on, and a boundary against controller code that has no request to read.
29
+ - [`public-tool-prefix`](public-tool-prefix/README.md): A naming law on an externally exposed surface: every class in the public tools directory carries the prefix, so the surface stays greppable and a public class can never shadow an internal one.
30
+ - [`one-door`](one-door/README.md): A boundary with one sanctioned crossing: controllers may not reach the GraphQL schema, except the single execute action, named as an exemption with its owner and reason so the carve-out is a decision and not a hole.
31
+ - [`readers-read-the-store-only`](readers-read-the-store-only/README.md): The allow form: a part's calls may land only in the named parts.
32
+ - [`cluster-laws`](cluster-laws/README.md): Laws that only a cluster snapshot can answer: a table owned by two repositories, mutating actions selected through the routes they handle, and routes no loaded client calls.
@@ -0,0 +1,10 @@
1
+ # clean rings
2
+
3
+ Four rings. An entity that calls a use case and a use case that calls an
4
+ adapter both point outward; the controller calling inward is the direction the
5
+ arrangement allows. The shipped clean recipe names the two outward reaches.
6
+
7
+ Declaration: `enola/constraints/clean.yaml`.
8
+
9
+ Verify it on your own tree: copy `enola/` into the repository, run
10
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,8 @@
1
+ use_recipe:
2
+ - recipe: clean
3
+ as: clean
4
+ bind:
5
+ frameworks: { match: ["app/controllers/**"] }
6
+ interface-adapters: { match: ["app/adapters/**"] }
7
+ use-cases: { match: ["app/use_cases/**"] }
8
+ entities: { match: ["app/entities/**"] }
@@ -0,0 +1,9 @@
1
+ # cluster laws
2
+
3
+ Laws that only a cluster snapshot can answer: a table owned by two
4
+ repositories, mutating actions selected through the routes they handle, and
5
+ routes no loaded client calls. Each refuses by name in a single-repository
6
+ snapshot rather than reporting silence as compliance.
7
+
8
+ Declaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,
9
+ then `enola --generate . && enola check .`.
@@ -0,0 +1,24 @@
1
+ Enola.architecture "platform" do
2
+ part :tables, files: "app/models/**", kind: :storage
3
+ part :mutating_actions, service: :backend, files: "app/controllers/**", handles: [:post, :put, :patch, :delete]
4
+ part :policies, service: :backend, files: "app/policies/**"
5
+ part :api, service: :backend, files: "config/**", kind: :route
6
+
7
+ law "one owner per table" do
8
+ tables.must_be_unique_across by: :table
9
+ why "a table two repositories both model is written by two codebases that never see each other's validations or callbacks"
10
+ mode :advisory
11
+ end
12
+
13
+ law "a mutating action is authorized" do
14
+ mutating_actions.must_reach :policies
15
+ why "an action behind a POST, PUT, PATCH or DELETE route changes state, and the policy call is the one place the change is authorized"
16
+ mode :advisory
17
+ end
18
+
19
+ law "every api route has a consumer" do
20
+ api.must_have_consumer
21
+ why "a route nobody calls is a surface nobody maintains"
22
+ mode :advisory
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # cqrs queries never write
2
+
3
+ A command-query split. A query that calls update_all writes; a read model that
4
+ issues a command is a feedback loop; a query that only counts is clean. The
5
+ shipped cqrs recipe names the two breaches by the mutating method and the
6
+ command reached.
7
+
8
+ Declaration: `enola/constraints/cqrs.yaml`.
9
+
10
+ Verify it on your own tree: copy `enola/` into the repository, run
11
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,7 @@
1
+ use_recipe:
2
+ - recipe: cqrs
3
+ as: cqrs
4
+ bind:
5
+ commands: { match: ["app/commands/**"] }
6
+ queries: { match: ["app/queries/**"] }
7
+ read-models: { match: ["app/read_models/**"] }
@@ -0,0 +1,10 @@
1
+ # foreign keys
2
+
3
+ A foreign-key law reads the storage facts the schema yields: every table
4
+ carrying a tenant column must declare the foreign key to the tenant table. The
5
+ require form verdicts what a member fact carries, with a when antecedent
6
+ narrowing to the tables the law is about, so a table without the column is
7
+ never asked.
8
+
9
+ Declaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,
10
+ then `enola --generate . && enola check .`.
@@ -0,0 +1,12 @@
1
+ components:
2
+ - name: tables
3
+ match: ["db/structure.sql", "db/schema.rb", "app/models/**"]
4
+ kind: storage
5
+
6
+ rules:
7
+ - id: account-fk
8
+ require: tables
9
+ when_prop_contains: { prop: columns, value: account_id }
10
+ must_prop_contain: { prop: fk_constraints, value: "account_id->accounts" }
11
+ mode: advisory
12
+ because: "A table carrying account_id references an account, so the database should say so with a foreign key. Advisory while a backfill path that ships the column before the constraint is reconciled with the rule; the verdict names every table that carries the column without the key."
@@ -0,0 +1,11 @@
1
+ # governed code
2
+
3
+ The knowledge pages compile into the graph as anchors, and a law demands that
4
+ every file of a part has one. The charge service is anchored by the decision
5
+ page; the refund service is not, and is named. Without any compiled page the
6
+ rule would refuse rather than call everything ungoverned.
7
+
8
+ Declaration: `enola/constraints/governance.rb`.
9
+
10
+ Verify it on your own tree: copy `enola/` into the repository, run
11
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,8 @@
1
+ Enola.architecture "shop" do
2
+ part :billing, files: "app/billing/**"
3
+
4
+ law "billing is governed" do
5
+ billing.must_be_governed
6
+ why "code under a decision needs the page that decided it"
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ # instantiation
2
+
3
+ What Ruby code constructs, as a fact. `new` on a literal constant is an
4
+ instantiates relation from the calling member to the class, which is the one
5
+ call whose result has a knowable type; `new` on a variable receiver names no
6
+ class and records nothing. A construction whose result is immediately sent a
7
+ message is the one-shot ceremony, named on the calling member as Class.method,
8
+ so a component can select the members that perform it and a rule can forbid
9
+ it. A construction bound to a local first is not the ceremony.
10
+
11
+ Declaration: `enola/constraints/ceremony.yaml`.
12
+
13
+ Verify it on your own tree: copy `enola/` into the repository, run
14
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,11 @@
1
+ components:
2
+ - name: one-shot-builders
3
+ kind: symbol
4
+ match: ["app/services/**"]
5
+ where:
6
+ one_shot_call: "Payments::Charge.call"
7
+
8
+ rules:
9
+ - id: no-one-shot-charge
10
+ forbid_fact: one-shot-builders
11
+ because: "a charge is built and called in one breath, so nobody can hold it to test it"
@@ -0,0 +1,9 @@
1
+ # maintenance tasks
2
+
3
+ Two laws for a maintenance-tasks directory: a naming convention the runner
4
+ depends on, and a boundary against controller code that has no request to
5
+ read. Both were mined from a codebase where they already held, which is the
6
+ right moment to declare a law.
7
+
8
+ Declaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,
9
+ then `enola --generate . && enola check .`.
@@ -0,0 +1,15 @@
1
+ Enola.architecture "app" do
2
+ part :maintenance_tasks, files: "app/tasks/**", kind: :symbol, where: { symbol_kind: "class" }
3
+ part :tasks, files: "app/tasks/**"
4
+ part :controllers, files: "app/controllers/**"
5
+
6
+ law "every maintenance task lives in the Maintenance namespace" do
7
+ maintenance_tasks.names_must_match "Maintenance::*"
8
+ why "the task runner resolves task constants from that namespace, so a task declared outside it never appears in the runner and cannot be run by anyone"
9
+ end
10
+
11
+ law "a maintenance task never invokes controller code" do
12
+ tasks.must_not_call controllers
13
+ why "a task runs from the runner or the console with no request around it, so controller code reached from one has nothing to read"
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # mixins stay independent
2
+
3
+ A module never reaches the classes that include it. Taggable calls Post, which
4
+ includes it; Auditable calls Ledger, which does not. Who includes whom comes
5
+ from the resolved ancestry the rubydex provider emits, so the rule refuses
6
+ with a named cause on a snapshot without it and names Taggable on one with it.
7
+
8
+ Declaration: `enola/constraints/concerns.yaml`.
9
+
10
+ Verify it on your own tree: copy `enola/` into the repository, run
11
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,9 @@
1
+ components:
2
+ - name: concerns
3
+ match: ["app/models/concerns/**"]
4
+ kind: symbol
5
+ where: { symbol_kind: interface }
6
+ rules:
7
+ - id: mixins-stay-independent
8
+ independent: concerns
9
+ because: "a mixin that knows its includer is half a class in hiding"
@@ -0,0 +1,10 @@
1
+ # naming conventions
2
+
3
+ Ruby naming conventions: no get_, set_ or is_ prefixes; a predicate ends in a
4
+ question mark and has_ is fine. Three offenders and two clean names in one
5
+ class.
6
+
7
+ Declaration: `enola/constraints/naming.yaml`.
8
+
9
+ Verify it on your own tree: copy `enola/` into the repository, run
10
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,5 @@
1
+ use_recipe:
2
+ - recipe: ruby-conventions
3
+ as: ruby
4
+ bind:
5
+ code: { match: ["app/**"], kind: symbol, where: { symbol_kind: method } }
@@ -0,0 +1,8 @@
1
+ # one door
2
+
3
+ A boundary with one sanctioned crossing: controllers may not reach the GraphQL
4
+ schema, except the single execute action, named as an exemption with its owner
5
+ and reason so the carve-out is a decision and not a hole.
6
+
7
+ Declaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,
8
+ then `enola --generate . && enola check .`.
@@ -0,0 +1,18 @@
1
+ components:
2
+ - name: controllers
3
+ match: ["app/controllers/**"]
4
+ - name: schema
5
+ match: ["app/graphql/**"]
6
+
7
+ rules:
8
+ - id: controllers-reach-the-schema-through-one-door
9
+ forbid: controllers
10
+ to: schema
11
+ via: calls
12
+ mode: advisory
13
+ exempt:
14
+ - witness: "Api::GraphqlController#execute -> AppSchema.execute via calls"
15
+ owner: platform
16
+ since: "2026-08-22"
17
+ because: "the one sanctioned entry point"
18
+ because: "Controllers reach the schema through exactly one door, the execute action; every other controller stays out of it. A second door is a second place to forget authentication."
@@ -0,0 +1,12 @@
1
+ # one owner per table
2
+
3
+ unique_across compares the members of different repositories; with the storage
4
+ facts of one repository there is nobody to share a table with, so the rule
5
+ refuses by name rather than reporting every table as unique. The verdict that
6
+ names two owners needs the cluster, which the harness measures per repository
7
+ by design.
8
+
9
+ Declaration: `enola/constraints/tables.rb`.
10
+
11
+ Verify it on your own tree: copy `enola/` into the repository, run
12
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,8 @@
1
+ Enola.architecture "shop" do
2
+ part :tables, files: "app/models/**", kind: :storage
3
+
4
+ law "one owner per table" do
5
+ tables.must_be_unique_across by: :table
6
+ why "two writers to one table disagree in the end"
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # paired names
2
+
3
+ A naming pair: every with_ method has a without_ twin. with_history has its
4
+ twin, with_guests does not, archive is outside the pattern and never asked.
5
+
6
+ Declaration: `enola/constraints/pairs.yaml`.
7
+
8
+ Verify it on your own tree: copy `enola/` into the repository, run
9
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,11 @@
1
+ components:
2
+ - name: chat
3
+ match: ["app/chat/**"]
4
+ kind: symbol
5
+ where: { symbol_kind: method }
6
+ rules:
7
+ - id: paired-scopes
8
+ require_name: chat
9
+ pattern: "with_*"
10
+ requires: "without_*"
11
+ because: "a reader who finds with_ expects without_ beside it"
@@ -0,0 +1,12 @@
1
+ # parts never cycle
2
+
3
+ Parts that may not depend on each other in a circle. Jobs reach models and
4
+ models reach jobs, so the two form a cycle at the part level; mailers sit
5
+ downstream of models only. The rule contracts the module graph to one node per
6
+ declared part, admits the symbol-rollup edges Ruby has, and names the circle;
7
+ the downstream part is not in it.
8
+
9
+ Declaration: `enola/constraints/cycles.yaml`.
10
+
11
+ Verify it on your own tree: copy `enola/` into the repository, run
12
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,12 @@
1
+ components:
2
+ - name: jobs
3
+ match: ["app/jobs/**"]
4
+ - name: models
5
+ match: ["app/models/**"]
6
+ - name: mailers
7
+ match: ["app/mailers/**"]
8
+ rules:
9
+ - id: parts-never-cycle
10
+ forbid_cycles: jobs
11
+ among: [models, mailers]
12
+ because: "parts that reach each other in a circle cannot be taken apart"
@@ -0,0 +1,10 @@
1
+ # protocol any of
2
+
3
+ A protocol satisfied by one of several methods. Charge defines call, Refund
4
+ defines run, Reconcile defines neither; any_of accepts either door and names
5
+ the class with none, listing what would have satisfied it.
6
+
7
+ Declaration: `enola/constraints/entry.yaml`.
8
+
9
+ Verify it on your own tree: copy `enola/` into the repository, run
10
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,8 @@
1
+ components:
2
+ - name: services
3
+ match: ["app/services/**"]
4
+ rules:
5
+ - id: entry-point
6
+ require_defines: services
7
+ any_of: [call, run]
8
+ because: "a service answers to one of two doors; a class with neither is one nobody can invoke"
@@ -0,0 +1,11 @@
1
+ # public surface
2
+
3
+ A path-defined public surface. Ruby has no visibility keyword the extractor
4
+ can read at the file level, so a component names the files that are its public
5
+ surface; reaching a public file from outside is fine and reaching an internal
6
+ one is the breach.
7
+
8
+ Declaration: `enola/constraints/surface.yaml`.
9
+
10
+ Verify it on your own tree: copy `enola/` into the repository, run
11
+ `enola constraints lint .`, then `enola --generate . && enola check .`.
@@ -0,0 +1,10 @@
1
+ components:
2
+ - name: billing
3
+ match: ["app/billing/**"]
4
+ public: ["app/billing/public/**"]
5
+ - name: orders
6
+ match: ["app/orders/**"]
7
+ rules:
8
+ - id: billing-surface
9
+ private: billing
10
+ because: "billing is reached through its public files; the rest is implementation"
@@ -0,0 +1,8 @@
1
+ # public tool prefix
2
+
3
+ A naming law on an externally exposed surface: every class in the public tools
4
+ directory carries the prefix, so the surface stays greppable and a public
5
+ class can never shadow an internal one.
6
+
7
+ Declaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,
8
+ then `enola --generate . && enola check .`.
@@ -0,0 +1,8 @@
1
+ Enola.architecture "app" do
2
+ part :public_tools, files: "app/public_tools/**", kind: :symbol, where: { symbol_kind: "class" }
3
+
4
+ law "public tools carry the Public prefix" do
5
+ public_tools.names_must_match "Public*"
6
+ why "an externally exposed tool class is greppable by its prefix and cannot shadow an internal one"
7
+ end
8
+ end