enola-guides 0.8.0 → 0.9.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.
- package/bundle.json +135 -75
- package/package.json +1 -1
package/bundle.json
CHANGED
|
@@ -14,66 +14,66 @@
|
|
|
14
14
|
"examples/go-service/internal/order/pricing.go": "package order\n\nimport \"example.com/shop/internal/store\"\n\n// The second breach: the domain imports the store.\nfunc LastTotal() int {\n\treturn store.LatestTotal()\n}\n",
|
|
15
15
|
"examples/go-service/internal/store/store.go": "package store\n\nimport \"database/sql\"\n\nvar db *sql.DB\n\nfunc SaveOrder(total int) {\n\tdb.Exec(\"INSERT INTO orders (total) VALUES ($1)\", total)\n}\n\nfunc RecentOrders() []int { return nil }\n\nfunc LatestTotal() int { return 0 }\n",
|
|
16
16
|
"examples/go-service/internal/web/routes.go": "package web\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"example.com/shop/internal/checkout\"\n\t\"example.com/shop/internal/store\"\n)\n\nfunc Routes() http.Handler {\n\tr := chi.NewRouter()\n\tr.Post(\"/orders\", createOrder)\n\tr.Get(\"/orders/recent\", listRecent)\n\treturn r\n}\n\nfunc createOrder(w http.ResponseWriter, req *http.Request) {\n\tvar lines []checkout.Line\n\tjson.NewDecoder(req.Body).Decode(&lines)\n\tjson.NewEncoder(w).Encode(checkout.PlaceOrder(lines))\n}\n\n// The breach the example exists to show: a handler reads the store\n// without going through the application layer.\nfunc listRecent(w http.ResponseWriter, req *http.Request) {\n\tjson.NewEncoder(w).Encode(store.RecentOrders())\n}\n",
|
|
17
|
-
"examples/knowledge-base/README.md": "# A knowledge base with enola, minimal\n\nThe smallest knowledge base that gets what [guide 09](../../guides/09-a-knowledge-base-with-enola.md) describes:\ntwo pages anchored to code with `enola_intent`, a cluster config, the\ncommitted receipts, and the finding ledger. The skills under\n[`templates/skills`](../../templates/skills) and the hooks under\n[`templates/hooks`](../../templates/hooks) run against exactly these\nfiles.\n\n- `enola-cluster.yaml`: the members,
|
|
18
|
-
"examples/knowledge-base/enola-cluster.yaml": "
|
|
19
|
-
"examples/knowledge-base/wiki/_state/enola/findings.json": "{\n \"findings\": {\n \"
|
|
20
|
-
"examples/knowledge-base/wiki/_state/enola/receipts.json": "{\n \"repos\": {\n \"
|
|
21
|
-
"examples/knowledge-base/wiki/shop/permanent/architecture.md": "---\ntitle: Shop architecture\nenola_intent:\n governs:\n - app/jobs/**\n - app/models/**\n decisions:\n - jobs enqueue, models decide; a job never calls back into a job\n---\n\n# Shop architecture\n\nOrders are written by `OrderService` only; the two other writers the\ngraph reported in 2026-08 were folded into it. `Order` has 41 dependents\n(receipt shop@3f2a9c1, fingerprint 8d1e…c40a).\n\nThe cycle `app/jobs -> app/models -> app/jobs` through the mailer path is\nreal and accepted in the ledger; it is cut when the mailer moves under\n`app/services`.\n",
|
|
22
|
-
"examples/knowledge-base/wiki/shop/permanent/public-surface.md": "---\ntitle: Public surface\nenola_intent:\n governs:\n - app/controllers/api/**\n decisions:\n - every route under /api has at least one consumer in
|
|
23
|
-
"examples/laws/clean-rings/README.md": "# clean rings\n\nFour rings. An entity that calls a use case and a use case that calls an\nadapter both point outward; the controller calling inward is the direction the\narrangement allows. The shipped clean recipe names the two outward reaches.\n\nDeclaration: `enola/constraints/clean.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
17
|
+
"examples/knowledge-base/README.md": "# A knowledge base with enola, minimal\n\nThe smallest knowledge base that gets what [guide 09](../../guides/09-a-knowledge-base-with-enola.md) describes:\ntwo pages anchored to code with `enola_intent`, a cluster config, the\ncommitted receipts, and the finding ledger. The skills under\n[`templates/skills`](../../templates/skills) and the hooks under\n[`templates/hooks`](../../templates/hooks) run against exactly these\nfiles.\n\n- `enola-cluster.yaml`: the members, the knowledge base among them.\n- `wiki/shop/permanent/architecture.md`: a page whose `enola_intent`\n anchors it to `app/jobs/**` and `app/models/**`, with one receipt\n citation.\n- `wiki/shop/permanent/public-surface.md`: a page anchored to the\n controllers it describes.\n- `wiki/_state/enola/receipts.json`: what `kb enola generate` recorded.\n- `wiki/_state/enola/findings.json`: the ledger, one judged finding.\n\n`governing_intent app/jobs/order_mailer_job.rb` returns the first page;\n`kb enola citations` verifies its citation against the receipts file. The\n`kb` wrapper is [`templates/kb/kb`](../../templates/kb/kb)\n(`enola-guides copy templates/kb` writes it to `bin/kb`); run it from this\ndirectory. enola labels each member by its directory name, so the\nreceipts are keyed `rails-shop` and `ember-app`.\n",
|
|
18
|
+
"examples/knowledge-base/enola-cluster.yaml": "# The members, by path relative to this file; enola labels each by its\n# directory's name. The knowledge base is a member too, so the pages'\n# enola_intent anchors compile over the code beside them.\nrepos:\n - ../rails-shop\n - ../ember-app\n - .\n",
|
|
19
|
+
"examples/knowledge-base/wiki/_state/enola/findings.json": "{\n \"findings\": {\n \"constraints|Constraint background-jobs-never-invoke-controller-code violated: ReceiptJob#perform -> OrdersController via calls\": {\n \"verdict\": \"accepted\",\n \"at\": \"2026-08-22\",\n \"reason\": \"real; the receipt renders through the controller\",\n \"landed\": \"wiki/shop/permanent/architecture.md\"\n }\n }\n}\n",
|
|
20
|
+
"examples/knowledge-base/wiki/_state/enola/receipts.json": "{\n \"repos\": {\n \"ember-app\": {\n \"enola_version\": \"0.4.35\",\n \"fingerprint\": \"2c7e9b1d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e\",\n \"git_commit\": \"9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c\",\n \"providers\": []\n },\n \"rails-shop\": {\n \"enola_version\": \"0.4.35\",\n \"fingerprint\": \"8d1e5a7b93c2f0e4d6a18b7c3e9f0a2d4c6b8e1f3a5c7d9e0b2f4a6c8e0d2c40a\",\n \"git_commit\": \"3f2a9c1d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c\",\n \"providers\": [\n \"prism\",\n \"rubydex\"\n ]\n }\n }\n}\n",
|
|
21
|
+
"examples/knowledge-base/wiki/shop/permanent/architecture.md": "---\ntitle: Shop architecture\nenola_intent:\n governs:\n - app/jobs/**\n - app/models/**\n decisions:\n - jobs enqueue, models decide; a job never calls back into a job\n---\n\n# Shop architecture\n\nOrders are written by `OrderService` only; the two other writers the\ngraph reported in 2026-08 were folded into it. `Order` has 41 dependents\n(receipt rails-shop@3f2a9c1, fingerprint 8d1e…c40a).\n\nThe cycle `app/jobs -> app/models -> app/jobs` through the mailer path is\nreal and accepted in the ledger; it is cut when the mailer moves under\n`app/services`.\n",
|
|
22
|
+
"examples/knowledge-base/wiki/shop/permanent/public-surface.md": "---\ntitle: Public surface\nenola_intent:\n governs:\n - app/controllers/api/**\n decisions:\n - every route under /api has at least one consumer in ember-app\n---\n\n# Public surface\n\nFourteen routes under `/api`, all matched by a consumer in `ember-app`\n(coverage report; receipt rails-shop@3f2a9c1, fingerprint 8d1e…c40a). A route that stops matching is\na finding of `unused-routes`, judged in the ledger before any removal.\n",
|
|
23
|
+
"examples/laws/clean-rings/README.md": "# clean rings\n\nFour rings. An entity that calls a use case and a use case that calls an\nadapter both point outward; the controller calling inward is the direction the\narrangement allows. The shipped clean recipe names the two outward reaches.\n\nDeclaration: `enola/constraints/clean.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
24
24
|
"examples/laws/clean-rings/enola/constraints/clean.yaml": "use_recipe:\n - recipe: clean\n as: clean\n bind:\n frameworks: { match: [\"app/controllers/**\"] }\n interface-adapters: { match: [\"app/adapters/**\"] }\n use-cases: { match: [\"app/use_cases/**\"] }\n entities: { match: [\"app/entities/**\"] }\n",
|
|
25
|
-
"examples/laws/cluster-laws/README.md": "# cluster laws\n\nLaws that only a cluster snapshot can answer: a table owned by two\nrepositories, mutating actions selected through the routes they handle, and\nroutes no loaded client calls. Each refuses by name in a single-repository\nsnapshot rather than reporting silence as compliance.\n\nOne file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola check
|
|
25
|
+
"examples/laws/cluster-laws/README.md": "# cluster laws\n\nLaws that only a cluster snapshot can answer: a table owned by two\nrepositories, mutating actions selected through the routes they handle, and\nroutes no loaded client calls. Each refuses by name in a single-repository\nsnapshot rather than reporting silence as compliance.\n\nOne file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
26
26
|
"examples/laws/cluster-laws/enola/constraints/a-mutating-action-is-authorized.rb": "Enola.architecture \"platform\" do\n part :mutating_actions, service: :backend, files: \"app/controllers/**\", handles: [:post, :put, :patch, :delete]\n part :policies, service: :backend, files: \"app/policies/**\"\n\n law \"a mutating action is authorized\" do\n mutating_actions.must_reach :policies\n 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\"\n mode :advisory\n end\nend\n",
|
|
27
27
|
"examples/laws/cluster-laws/enola/constraints/every-api-route-has-a-consumer.rb": "Enola.architecture \"platform\" do\n part :api, service: :backend, files: \"config/**\", kind: :route\n\n law \"every api route has a consumer\" do\n api.must_have_consumer\n why \"a route nobody calls is a surface nobody maintains\"\n mode :advisory\n end\nend\n",
|
|
28
28
|
"examples/laws/cluster-laws/enola/constraints/one-owner-per-table.rb": "Enola.architecture \"platform\" do\n part :tables, files: \"app/models/**\", kind: :storage\n\n law \"one owner per table\" do\n tables.must_be_unique_across by: :table\n why \"a table two repositories both model is written by two codebases that never see each other's validations or callbacks\"\n mode :advisory\n end\nend\n",
|
|
29
|
-
"examples/laws/cqrs-queries-never-write/README.md": "# cqrs queries never write\n\nA command-query split. A query that calls update_all writes; a read model that\nissues a command is a feedback loop; a query that only counts is clean. The\nshipped cqrs recipe names the two breaches by the mutating method and the\ncommand reached.\n\nDeclaration: `enola/constraints/cqrs.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
29
|
+
"examples/laws/cqrs-queries-never-write/README.md": "# cqrs queries never write\n\nA command-query split. A query that calls update_all writes; a read model that\nissues a command is a feedback loop; a query that only counts is clean. The\nshipped cqrs recipe names the two breaches by the mutating method and the\ncommand reached.\n\nDeclaration: `enola/constraints/cqrs.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
30
30
|
"examples/laws/cqrs-queries-never-write/enola/constraints/cqrs.yaml": "use_recipe:\n - recipe: cqrs\n as: cqrs\n bind:\n commands: { match: [\"app/commands/**\"] }\n queries: { match: [\"app/queries/**\"] }\n read-models: { match: [\"app/read_models/**\"] }\n",
|
|
31
|
-
"examples/laws/foreign-keys/README.md": "# foreign keys\n\nA foreign-key law reads the storage facts the schema yields: every table\ncarrying a tenant column must declare the foreign key to the tenant table. The\nrequire form verdicts what a member fact carries, with a when antecedent\nnarrowing to the tables the law is about, so a table without the column is\nnever asked.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola check
|
|
31
|
+
"examples/laws/foreign-keys/README.md": "# foreign keys\n\nA foreign-key law reads the storage facts the schema yields: every table\ncarrying a tenant column must declare the foreign key to the tenant table. The\nrequire form verdicts what a member fact carries, with a when antecedent\nnarrowing to the tables the law is about, so a table without the column is\nnever asked.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
32
32
|
"examples/laws/foreign-keys/enola/constraints/foreign-keys.yaml": "components:\n - name: tables\n match: [\"db/structure.sql\", \"db/schema.rb\", \"app/models/**\"]\n kind: storage\n\nrules:\n - id: account-fk\n require: tables\n when_prop_contains: { prop: columns, value: account_id }\n must_prop_contain: { prop: fk_constraints, value: \"account_id->accounts\" }\n mode: advisory\n 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.\"\n",
|
|
33
|
-
"examples/laws/governed-code/README.md": "# governed code\n\nThe knowledge pages compile into the graph as anchors, and a law demands that\nevery file of a part has one. The charge service is anchored by the decision\npage; the refund service is not, and is named. Without any compiled page the\nrule would refuse rather than call everything ungoverned.\n\nDeclaration: `enola/constraints/governance.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
33
|
+
"examples/laws/governed-code/README.md": "# governed code\n\nThe knowledge pages compile into the graph as anchors, and a law demands that\nevery file of a part has one. The charge service is anchored by the decision\npage; the refund service is not, and is named. Without any compiled page the\nrule would refuse rather than call everything ungoverned.\n\nDeclaration: `enola/constraints/governance.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
34
34
|
"examples/laws/governed-code/enola/constraints/governance.rb": "Enola.architecture \"shop\" do\n part :billing, files: \"app/billing/**\"\n\n law \"billing is governed\" do\n billing.must_be_governed\n why \"code under a decision needs the page that decided it\"\n end\nend\n",
|
|
35
|
-
"examples/laws/instantiation/README.md": "# instantiation\n\nWhat Ruby code constructs, as a fact. `new` on a literal constant is an\ninstantiates relation from the calling member to the class, which is the one\ncall whose result has a knowable type; `new` on a variable receiver names no\nclass and records nothing. A construction whose result is immediately sent a\nmessage is the one-shot ceremony, named on the calling member as Class.method,\nso a component can select the members that perform it and a rule can forbid\nit. A construction bound to a local first is not the ceremony.\n\nDeclaration: `enola/constraints/ceremony.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
35
|
+
"examples/laws/instantiation/README.md": "# instantiation\n\nWhat Ruby code constructs, as a fact. `new` on a literal constant is an\ninstantiates relation from the calling member to the class, which is the one\ncall whose result has a knowable type; `new` on a variable receiver names no\nclass and records nothing. A construction whose result is immediately sent a\nmessage is the one-shot ceremony, named on the calling member as Class.method,\nso a component can select the members that perform it and a rule can forbid\nit. A construction bound to a local first is not the ceremony.\n\nDeclaration: `enola/constraints/ceremony.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
36
36
|
"examples/laws/instantiation/enola/constraints/ceremony.yaml": "components:\n - name: one-shot-builders\n kind: symbol\n match: [\"app/services/**\"]\n where:\n one_shot_call: \"Payments::Charge.call\"\n\nrules:\n - id: no-one-shot-charge\n forbid_fact: one-shot-builders\n because: \"a charge is built and called in one breath, so nobody can hold it to test it\"\n",
|
|
37
|
-
"examples/laws/maintenance-tasks/README.md": "# maintenance tasks\n\nTwo laws for a maintenance-tasks directory: a naming convention the runner\ndepends on, and a boundary against controller code that has no request to\nread. Both were mined from a codebase where they already held, which is the\nright moment to declare a law.\n\nOne file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola check
|
|
37
|
+
"examples/laws/maintenance-tasks/README.md": "# maintenance tasks\n\nTwo laws for a maintenance-tasks directory: a naming convention the runner\ndepends on, and a boundary against controller code that has no request to\nread. Both were mined from a codebase where they already held, which is the\nright moment to declare a law.\n\nOne file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
38
38
|
"examples/laws/maintenance-tasks/enola/constraints/a-maintenance-task-never-invokes-controller-code.rb": "Enola.architecture \"app\" do\n part :tasks, files: \"app/tasks/**\"\n part :controllers, files: \"app/controllers/**\"\n\n law \"a maintenance task never invokes controller code\" do\n tasks.must_not_call controllers\n 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\"\n end\nend\n",
|
|
39
39
|
"examples/laws/maintenance-tasks/enola/constraints/every-maintenance-task-lives-in-the-maintenance-namespace.rb": "Enola.architecture \"app\" do\n part :maintenance_tasks, files: \"app/tasks/**\", kind: :symbol, where: { symbol_kind: \"class\" }\n\n law \"every maintenance task lives in the Maintenance namespace\" do\n maintenance_tasks.names_must_match \"Maintenance::*\"\n 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\"\n end\nend\n",
|
|
40
|
-
"examples/laws/mixins-stay-independent/README.md": "# mixins stay independent\n\nA module never reaches the classes that include it. Taggable calls Post, which\nincludes it; Auditable calls Ledger, which does not. Who includes whom comes\nfrom the resolved ancestry the rubydex provider emits, so the rule refuses\nwith a named cause on a snapshot without it and names Taggable on one with it.\n\nDeclaration: `enola/constraints/concerns.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
40
|
+
"examples/laws/mixins-stay-independent/README.md": "# mixins stay independent\n\nA module never reaches the classes that include it. Taggable calls Post, which\nincludes it; Auditable calls Ledger, which does not. Who includes whom comes\nfrom the resolved ancestry the rubydex provider emits, so the rule refuses\nwith a named cause on a snapshot without it and names Taggable on one with it.\n\nDeclaration: `enola/constraints/concerns.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
41
41
|
"examples/laws/mixins-stay-independent/enola/constraints/concerns.yaml": "components:\n - name: concerns\n match: [\"app/models/concerns/**\"]\n kind: symbol\n where: { symbol_kind: interface }\nrules:\n - id: mixins-stay-independent\n independent: concerns\n because: \"a mixin that knows its includer is half a class in hiding\"\n",
|
|
42
|
-
"examples/laws/naming-conventions/README.md": "# naming conventions\n\nRuby naming conventions: no get_, set_ or is_ prefixes; a predicate ends in a\nquestion mark and has_ is fine. Three offenders and two clean names in one\nclass.\n\nDeclaration: `enola/constraints/naming.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
42
|
+
"examples/laws/naming-conventions/README.md": "# naming conventions\n\nRuby naming conventions: no get_, set_ or is_ prefixes; a predicate ends in a\nquestion mark and has_ is fine. Three offenders and two clean names in one\nclass.\n\nDeclaration: `enola/constraints/naming.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
43
43
|
"examples/laws/naming-conventions/enola/constraints/naming.yaml": "use_recipe:\n - recipe: ruby-conventions\n as: ruby\n bind:\n code: { match: [\"app/**\"], kind: symbol, where: { symbol_kind: method } }\n",
|
|
44
|
-
"examples/laws/one-door/README.md": "# one door\n\nA boundary with one sanctioned crossing: controllers may not reach the GraphQL\nschema, except the single execute action, named as an exemption with its owner\nand reason so the carve-out is a decision and not a hole.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola check
|
|
44
|
+
"examples/laws/one-door/README.md": "# one door\n\nA boundary with one sanctioned crossing: controllers may not reach the GraphQL\nschema, except the single execute action, named as an exemption with its owner\nand reason so the carve-out is a decision and not a hole.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
45
45
|
"examples/laws/one-door/enola/constraints/one-door.yaml": "components:\n - name: controllers\n match: [\"app/controllers/**\"]\n - name: schema\n match: [\"app/graphql/**\"]\n\nrules:\n - id: controllers-reach-the-schema-through-one-door\n forbid: controllers\n to: schema\n via: calls\n mode: advisory\n exempt:\n - witness: \"Api::GraphqlController#execute -> AppSchema.execute via calls\"\n owner: platform\n since: \"2026-08-22\"\n because: \"the one sanctioned entry point\"\n 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.\"\n",
|
|
46
|
-
"examples/laws/one-owner-per-table/README.md": "# one owner per table\n\nunique_across compares the members of different repositories; with the storage\nfacts of one repository there is nobody to share a table with, so the rule\nrefuses by name rather than reporting every table as unique. The verdict that\nnames two owners needs the cluster, which the harness measures per repository\nby design.\n\nDeclaration: `enola/constraints/tables.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
46
|
+
"examples/laws/one-owner-per-table/README.md": "# one owner per table\n\nunique_across compares the members of different repositories; with the storage\nfacts of one repository there is nobody to share a table with, so the rule\nrefuses by name rather than reporting every table as unique. The verdict that\nnames two owners needs the cluster, which the harness measures per repository\nby design.\n\nDeclaration: `enola/constraints/tables.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
47
47
|
"examples/laws/one-owner-per-table/enola/constraints/tables.rb": "Enola.architecture \"shop\" do\n part :tables, files: \"app/models/**\", kind: :storage\n\n law \"one owner per table\" do\n tables.must_be_unique_across by: :table\n why \"two writers to one table disagree in the end\"\n end\nend\n",
|
|
48
|
-
"examples/laws/paired-names/README.md": "# paired names\n\nA naming pair: every with_ method has a without_ twin. with_history has its\ntwin, with_guests does not, archive is outside the pattern and never asked.\n\nDeclaration: `enola/constraints/pairs.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
48
|
+
"examples/laws/paired-names/README.md": "# paired names\n\nA naming pair: every with_ method has a without_ twin. with_history has its\ntwin, with_guests does not, archive is outside the pattern and never asked.\n\nDeclaration: `enola/constraints/pairs.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
49
49
|
"examples/laws/paired-names/enola/constraints/pairs.yaml": "components:\n - name: chat\n match: [\"app/chat/**\"]\n kind: symbol\n where: { symbol_kind: method }\nrules:\n - id: paired-scopes\n require_name: chat\n pattern: \"with_*\"\n requires: \"without_*\"\n because: \"a reader who finds with_ expects without_ beside it\"\n",
|
|
50
|
-
"examples/laws/parts-never-cycle/README.md": "# parts never cycle\n\nParts that may not depend on each other in a circle. Jobs reach models and\nmodels reach jobs, so the two form a cycle at the part level; mailers sit\ndownstream of models only. The rule contracts the module graph to one node per\ndeclared part, admits the symbol-rollup edges Ruby has, and names the circle;\nthe downstream part is not in it.\n\nDeclaration: `enola/constraints/cycles.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
50
|
+
"examples/laws/parts-never-cycle/README.md": "# parts never cycle\n\nParts that may not depend on each other in a circle. Jobs reach models and\nmodels reach jobs, so the two form a cycle at the part level; mailers sit\ndownstream of models only. The rule contracts the module graph to one node per\ndeclared part, admits the symbol-rollup edges Ruby has, and names the circle;\nthe downstream part is not in it.\n\nDeclaration: `enola/constraints/cycles.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
51
51
|
"examples/laws/parts-never-cycle/enola/constraints/cycles.yaml": "components:\n - name: jobs\n match: [\"app/jobs/**\"]\n - name: models\n match: [\"app/models/**\"]\n - name: mailers\n match: [\"app/mailers/**\"]\nrules:\n - id: parts-never-cycle\n forbid_cycles: jobs\n among: [models, mailers]\n because: \"parts that reach each other in a circle cannot be taken apart\"\n",
|
|
52
|
-
"examples/laws/protocol-any-of/README.md": "# protocol any of\n\nA protocol satisfied by one of several methods. Charge defines call, Refund\ndefines run, Reconcile defines neither; any_of accepts either door and names\nthe class with none, listing what would have satisfied it.\n\nDeclaration: `enola/constraints/entry.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
52
|
+
"examples/laws/protocol-any-of/README.md": "# protocol any of\n\nA protocol satisfied by one of several methods. Charge defines call, Refund\ndefines run, Reconcile defines neither; any_of accepts either door and names\nthe class with none, listing what would have satisfied it.\n\nDeclaration: `enola/constraints/entry.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
53
53
|
"examples/laws/protocol-any-of/enola/constraints/entry.yaml": "components:\n - name: services\n match: [\"app/services/**\"]\nrules:\n - id: entry-point\n require_defines: services\n any_of: [call, run]\n because: \"a service answers to one of two doors; a class with neither is one nobody can invoke\"\n",
|
|
54
|
-
"examples/laws/public-surface/README.md": "# public surface\n\nA path-defined public surface. Ruby has no visibility keyword the extractor\ncan read at the file level, so a component names the files that are its public\nsurface; reaching a public file from outside is fine and reaching an internal\none is the breach.\n\nDeclaration: `enola/constraints/surface.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
54
|
+
"examples/laws/public-surface/README.md": "# public surface\n\nA path-defined public surface. Ruby has no visibility keyword the extractor\ncan read at the file level, so a component names the files that are its public\nsurface; reaching a public file from outside is fine and reaching an internal\none is the breach.\n\nDeclaration: `enola/constraints/surface.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
55
55
|
"examples/laws/public-surface/enola/constraints/surface.yaml": "components:\n - name: billing\n match: [\"app/billing/**\"]\n public: [\"app/billing/public/**\"]\n - name: orders\n match: [\"app/orders/**\"]\nrules:\n - id: billing-surface\n private: billing\n because: \"billing is reached through its public files; the rest is implementation\"\n",
|
|
56
|
-
"examples/laws/public-tool-prefix/README.md": "# public tool prefix\n\nA naming law on an externally exposed surface: every class in the public tools\ndirectory carries the prefix, so the surface stays greppable and a public\nclass can never shadow an internal one.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola check
|
|
56
|
+
"examples/laws/public-tool-prefix/README.md": "# public tool prefix\n\nA naming law on an externally exposed surface: every class in the public tools\ndirectory carries the prefix, so the surface stays greppable and a public\nclass can never shadow an internal one.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
57
57
|
"examples/laws/public-tool-prefix/enola/constraints/public-tools.rb": "Enola.architecture \"app\" do\n part :public_tools, files: \"app/public_tools/**\", kind: :symbol, where: { symbol_kind: \"class\" }\n\n law \"public tools carry the Public prefix\" do\n public_tools.names_must_match \"Public*\"\n why \"an externally exposed tool class is greppable by its prefix and cannot shadow an internal one\"\n end\nend\n",
|
|
58
|
-
"examples/laws/rails-forbidden-names/README.md": "# rails forbidden names\n\nA naming convention phrased as a prohibition. require_name demands a pattern\nand has no negative, so \"no get_ prefixes on a model's public surface\" could\nnot be declared. forbid_name takes the same bounded pattern dialect, tries it\nagainst a method's bare name after its owner, and with surface: exported\njudges the public surface only: the exported get_total is the breach, the\nprivate get_lines is not the convention's surface, and total is outside the\npattern.\n\nDeclaration: `enola/constraints/naming.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
58
|
+
"examples/laws/rails-forbidden-names/README.md": "# rails forbidden names\n\nA naming convention phrased as a prohibition. require_name demands a pattern\nand has no negative, so \"no get_ prefixes on a model's public surface\" could\nnot be declared. forbid_name takes the same bounded pattern dialect, tries it\nagainst a method's bare name after its owner, and with surface: exported\njudges the public surface only: the exported get_total is the breach, the\nprivate get_lines is not the convention's surface, and total is outside the\npattern.\n\nDeclaration: `enola/constraints/naming.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
59
59
|
"examples/laws/rails-forbidden-names/enola/constraints/naming.yaml": "components:\n - name: model-methods\n kind: symbol\n match: [\"app/models/**\"]\n where:\n symbol_kind: method\n\nrules:\n - id: no-getter-prefixes\n forbid_name: model-methods\n pattern: \"get_*\"\n surface: exported\n because: \"a reader is a noun; get_ says the class is a bag of fields\"\n",
|
|
60
|
-
"examples/laws/rails-mutations-are-authorized/README.md": "# rails mutations are authorized\n\nThe code behind a mutating route is selected by the route's method through the\nhandled_by edge the Rails binder draws, and must reach a policy. create does\nand update does not; index handles a GET and is never asked. The handles key\nis what lets the law speak about routes without naming a single action.\n\nDeclaration: `enola/constraints/authorization.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
60
|
+
"examples/laws/rails-mutations-are-authorized/README.md": "# rails mutations are authorized\n\nThe code behind a mutating route is selected by the route's method through the\nhandled_by edge the Rails binder draws, and must reach a policy. create does\nand update does not; index handles a GET and is never asked. The handles key\nis what lets the law speak about routes without naming a single action.\n\nDeclaration: `enola/constraints/authorization.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
61
61
|
"examples/laws/rails-mutations-are-authorized/enola/constraints/authorization.rb": "Enola.architecture \"shop\" do\n part :mutating_actions, files: \"app/controllers/**\", handles: [:post, :put, :patch, :delete]\n part :policies, files: \"app/policies/**\"\n\n law \"a mutating action is authorized\" do\n mutating_actions.must_reach :policies\n why \"every write passes a policy before it touches a record\"\n end\nend\n",
|
|
62
|
-
"examples/laws/rails-request-boundaries/README.md": "# rails request boundaries\n\nThe Rails laws a team actually writes. A model reads session and calls a\nhelper; a service reads params; another service calls only itself. The shipped\nrails-conventions recipe, with its optional helpers and services roles bound,\nnames the model's helper call, the model's session read and the service's\nparams read, and leaves the clean service alone.\n\nDeclaration: `enola/constraints/rails.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
62
|
+
"examples/laws/rails-request-boundaries/README.md": "# rails request boundaries\n\nThe Rails laws a team actually writes. A model reads session and calls a\nhelper; a service reads params; another service calls only itself. The shipped\nrails-conventions recipe, with its optional helpers and services roles bound,\nnames the model's helper call, the model's session read and the service's\nparams read, and leaves the clean service alone.\n\nDeclaration: `enola/constraints/rails.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
63
63
|
"examples/laws/rails-request-boundaries/enola/constraints/rails.yaml": "use_recipe:\n - recipe: rails-conventions\n as: rails\n bind:\n controllers: { match: [\"app/controllers/**\"] }\n jobs: { match: [\"app/jobs/**\"] }\n models: { match: [\"app/models/**\"] }\n mailers: { match: [\"app/mailers/**\"] }\n policies: { match: [\"app/policies/**\"] }\n serializers: { match: [\"app/serializers/**\"] }\n view-components: { match: [\"app/components/**\"] }\n helpers: { match: [\"app/helpers/**\"] }\n services: { match: [\"app/services/**\"] }\n",
|
|
64
|
-
"examples/laws/rails-strict-arrangement/README.md": "# rails strict arrangement\n\nThe strict Rails arrangement: the Rails laws, no circle among the parts, and\nconcerns that stay independent of their includers. Jobs and models reach each\nother, which is the circle; Taggable reaches Invoice, which includes it.\n\nDeclaration: `enola/constraints/strict.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
64
|
+
"examples/laws/rails-strict-arrangement/README.md": "# rails strict arrangement\n\nThe strict Rails arrangement: the Rails laws, no circle among the parts, and\nconcerns that stay independent of their includers. Jobs and models reach each\nother, which is the circle; Taggable reaches Invoice, which includes it.\n\nDeclaration: `enola/constraints/strict.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
65
65
|
"examples/laws/rails-strict-arrangement/enola/constraints/strict.yaml": "use_recipe:\n - recipe: rails-strict\n as: strict\n bind:\n controllers: { match: [\"app/controllers/**\"] }\n jobs: { match: [\"app/jobs/**\"] }\n models: { match: [\"app/models/**\"] }\n mailers: { match: [\"app/mailers/**\"] }\n policies: { match: [\"app/policies/**\"] }\n serializers: { match: [\"app/serializers/**\"] }\n view-components: { match: [\"app/components/**\"] }\n concerns: { match: [\"app/models/concerns/**\"], kind: symbol, where: { symbol_kind: interface } }\n",
|
|
66
|
-
"examples/laws/readers-read-the-store-only/README.md": "# readers read the store only\n\nThe allow form: a part's calls may land only in the named parts. Here readers\nthat must stay replayable may reach the store and nothing else.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola check
|
|
66
|
+
"examples/laws/readers-read-the-store-only/README.md": "# readers read the store only\n\nThe allow form: a part's calls may land only in the named parts. Here readers\nthat must stay replayable may reach the store and nothing else.\n\nDeclaration under `enola/constraints/`. Copy it, run `enola constraints lint .`,\nthen `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
67
67
|
"examples/laws/readers-read-the-store-only/enola/constraints/readers.yaml": "components:\n - name: readers\n match: [\"internal/readers/**\"]\n - name: store\n match: [\"internal/store/**\"]\n\nrules:\n - id: readers-read-the-store-only\n allow: readers\n only: [store]\n via: calls\n mode: advisory\n because: \"A reader derives its answer from the store alone, which is what keeps every reader replayable against a snapshot; a reader that reaches anything else answers differently on a different machine.\"\n",
|
|
68
|
-
"examples/laws/receiverless-request-api/README.md": "# receiverless request api\n\nA literal far end narrowed to receiver-less calls. params alone is the\ncontroller's; request.params is an object the caller handed in. receiver: none\nmatches the first and not the second.\n\nDeclaration: `enola/constraints/bare.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
68
|
+
"examples/laws/receiverless-request-api/README.md": "# receiverless request api\n\nA literal far end narrowed to receiver-less calls. params alone is the\ncontroller's; request.params is an object the caller handed in. receiver: none\nmatches the first and not the second.\n\nDeclaration: `enola/constraints/bare.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
69
69
|
"examples/laws/receiverless-request-api/enola/constraints/bare.yaml": "components:\n - name: models\n match: [\"app/models/**\"]\nrules:\n - id: bare-params\n forbid: models\n to_name: [params]\n via: calls\n receiver: none\n because: \"params without a receiver is the controller's; request.params is an object the caller handed in\"\n",
|
|
70
|
-
"examples/laws/resolved-ancestry/README.md": "# resolved ancestry\n\nAncestry a provider resolved, and a component selected by it. `superclass:`\nreads the parent as the source spelled it, one level, so a class written as\n`Base` inside `module Billing` is never seen as an ApplicationRecord. The\nrubydex provider emits each class's linearised ancestor chain as resolved\nimplements edges, and `ancestor: ApplicationRecord` selects the grandchild\nthrough them, so a rule over the hierarchy judges the hierarchy. Without the\nprovider the selector is refused with a named cause rather than holding\nvacuously, which is what the channels lacking the provider score.\n\nDeclaration: `enola/constraints/records.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
70
|
+
"examples/laws/resolved-ancestry/README.md": "# resolved ancestry\n\nAncestry a provider resolved, and a component selected by it. `superclass:`\nreads the parent as the source spelled it, one level, so a class written as\n`Base` inside `module Billing` is never seen as an ApplicationRecord. The\nrubydex provider emits each class's linearised ancestor chain as resolved\nimplements edges, and `ancestor: ApplicationRecord` selects the grandchild\nthrough them, so a rule over the hierarchy judges the hierarchy. Without the\nprovider the selector is refused with a named cause rather than holding\nvacuously, which is what the channels lacking the provider score.\n\nDeclaration: `enola/constraints/records.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
71
71
|
"examples/laws/resolved-ancestry/enola/constraints/records.yaml": "components:\n - name: records\n ancestor: ApplicationRecord\n - name: jobs\n match: [\"app/jobs/**\"]\nrules:\n - id: jobs-do-not-touch-records\n forbid: jobs\n to: records\n via: calls\n because: \"a job reaches a record through a service, so the record can change shape without every job changing with it\"\n",
|
|
72
|
-
"examples/laws/routes-have-consumers/README.md": "# routes have consumers\n\nA law across repositories refuses by name inside one. require_consumer asks\nwhether a client elsewhere in the cluster calls each route; a single-\nrepository snapshot cannot answer, so the rule emits the named refusal and no\nverdict, which is the whole point: a route with no client in a snapshot that\nloaded no clients must not read as unconsumed.\n\nDeclaration: `enola/constraints/consumers.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
72
|
+
"examples/laws/routes-have-consumers/README.md": "# routes have consumers\n\nA law across repositories refuses by name inside one. require_consumer asks\nwhether a client elsewhere in the cluster calls each route; a single-\nrepository snapshot cannot answer, so the rule emits the named refusal and no\nverdict, which is the whole point: a route with no client in a snapshot that\nloaded no clients must not read as unconsumed.\n\nDeclaration: `enola/constraints/consumers.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
73
73
|
"examples/laws/routes-have-consumers/enola/constraints/consumers.rb": "Enola.architecture \"shop\" do\n part :api, files: \"config/**\", kind: :route\n\n law \"every api route has a consumer\" do\n api.must_have_consumer\n why \"a route nobody calls is a surface nobody maintains\"\n end\nend\n",
|
|
74
|
-
"examples/laws/storage-stays-home/README.md": "# storage stays home\n\nA part keeps to the tables it owns. Billing reaches its own invoices table and\nthe orders table the orders part owns; only the second is a breach, named with\nthe table, and the cut names the orders part's public member that already\nreaches the model. The storage facts come from the Rails model extraction, so\nthe law reads what the graph measured rather than a list someone wrote.\n\nDeclaration: `enola/constraints/ownership.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
74
|
+
"examples/laws/storage-stays-home/README.md": "# storage stays home\n\nA part keeps to the tables it owns. Billing reaches its own invoices table and\nthe orders table the orders part owns; only the second is a breach, named with\nthe table, and the cut names the orders part's public member that already\nreaches the model. The storage facts come from the Rails model extraction, so\nthe law reads what the graph measured rather than a list someone wrote.\n\nDeclaration: `enola/constraints/ownership.rb`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
75
75
|
"examples/laws/storage-stays-home/enola/constraints/ownership.rb": "Enola.architecture \"shop\" do\n part :billing, files: [\"app/billing/**\", \"app/models/invoice.rb\"]\n part :orders, files: [\"app/orders/**\", \"app/models/order.rb\"], public: [\"app/orders/public/**\"]\n\n law \"billing keeps to its own tables\" do\n billing.storage_must_stay_home\n why \"a part that writes another part's table owns a bug it cannot see\"\n end\nend\n",
|
|
76
|
-
"examples/laws/vanilla-rails-stays-empty/README.md": "# vanilla rails stays empty\n\nPlain Rails: the extra directories must stay empty, each with a stated reason.\nOne service object exists; the other directories hold nothing. The recipe\nnames the service and says nothing about the empty ones.\n\nDeclaration: `enola/constraints/vanilla.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola check
|
|
76
|
+
"examples/laws/vanilla-rails-stays-empty/README.md": "# vanilla rails stays empty\n\nPlain Rails: the extra directories must stay empty, each with a stated reason.\nOne service object exists; the other directories hold nothing. The recipe\nnames the service and says nothing about the empty ones.\n\nDeclaration: `enola/constraints/vanilla.yaml`.\n\nVerify it on your own tree: copy `enola/` into the repository, run\n`enola constraints lint .`, then `enola --generate . && enola constraints ledger .` for each law's\ncount of breaches. With a baseline pinned before the copy (`enola baseline pin .`),\n`enola check .` lists each breach under `Newly declared`.\n",
|
|
77
77
|
"examples/laws/vanilla-rails-stays-empty/enola/constraints/vanilla.yaml": "use_recipe:\n - recipe: vanilla-rails\n as: vanilla\n bind:\n models: { match: [\"app/models/**\"] }\n controllers: { match: [\"app/controllers/**\"] }\n services: { match: [\"app/services/**\"] }\n forms: { match: [\"app/forms/**\"] }\n policies: { match: [\"app/policies/**\"] }\n decorators: { match: [\"app/decorators/**\"] }\n presenters: { match: [\"app/presenters/**\"] }\n",
|
|
78
78
|
"examples/python-service/EXPECTED": "# The laws this example breaks on purpose, one rule id per line. CI\n# snapshots the example with the pinned enola and fails when the set of\n# laws reporting a breach differs from this list.\nshop/presentation-reaches-application-only\nthe-domain-imports-no-storage\nsupply/dependencies-are-pinned\n",
|
|
79
79
|
"examples/python-service/README.md": "# A Python service\n\nA FastAPI service in four layers, small enough to read in a minute, that\nbreaks its laws on purpose. [Guide 15](../../guides/15-python.md) walks\nthrough it.\n\n| Layer | Directory |\n|---|---|\n| presentation | `shop/api` |\n| application | `shop/services` |\n| domain | `shop/domain` |\n| infrastructure | `shop/repositories` |\n\nThe laws are in `enola/constraints/`: the `layered` recipe bound to the\nfour directories, the domain's law over `imports` (the recipe's own domain\nrules read `depends_on`, which this language's extractor does not write),\nand the `supply-chain` recipe. What breaks:\n\n- `shop/presentation-reaches-application-only`: `shop/api/orders.py` (`list_recent` reads a repository).\n- `the-domain-imports-no-storage`: `shop/domain/pricing.py` (imports a repository).\n- `supply/dependencies-are-pinned`: `fastapi>=0.110` in `pyproject.toml`, a range with no lockfile.\n\n```bash\ncd examples/python-service\nenola --generate .\nenola constraints lint .\n```\n\nThe breaches are findings in `.enola/insights.json` with source\n`constraints`. `EXPECTED` lists the rule ids that must report one, and\n`script/test-examples` fails when the set changes.\n",
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"examples/rails-shop/enola/constraints/the-request-api-stays-in-controllers.rb": "Enola.architecture \"shop\" do\n rails\n\n law \"the request api stays in controllers\" do\n models.must_not_call \"params\", receiver: :none\n why \"params without a receiver is the controller's; a model reading it only works inside a request\"\n mode :ratchet\n end\nend\n",
|
|
108
108
|
"examples/rails-shop/mcp-arch.yaml": "# Providers run beside enola's own extractors at snapshot time and their\n# facts land in the same graph. Both run inside the binary against the\n# Rubydex library the install script fetches; no Ruby is needed. The binary\n# pins the library, so neither entry carries an expected_version.\nproviders:\n - name: rubydex\n - name: prism\n",
|
|
109
109
|
"examples/rust-service/Cargo.toml": "[package]\nname = \"shop\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\naxum = \"0.7\"\nserde = { version = \"=1.0.215\", features = [\"derive\"] }\n",
|
|
110
|
-
"examples/rust-service/EXPECTED": "# The laws this example breaks on purpose, one rule id per line. CI\n# snapshots the example with the pinned enola and fails when the set of\n# laws reporting a breach differs from this list.\nshop/presentation-reaches-application-only\nthe-domain-imports-no-storage\n# Not supply/dependencies-are-pinned, though axum = \"0.7\" is a caret range:\n# enola v0.4.
|
|
111
|
-
"examples/rust-service/README.md": "# A Rust service\n\nAn Axum service in four layers, small enough to read in a minute, that\nbreaks its laws on purpose. [Guide 18](../../guides/18-rust.md) walks\nthrough it.\n\n| Layer | Directory |\n|---|---|\n| presentation | `src/web` |\n| application | `src/checkout` |\n| domain | `src/order` |\n| infrastructure | `src/store` |\n\nThe laws are in `enola/constraints/`: the `layered` recipe bound to the\nfour directories, the domain's law over `imports` (the recipe's own domain\nrules read `depends_on`, which this language's extractor does not write),\nand the `supply-chain` recipe. What breaks:\n\n- `shop/presentation-reaches-application-only`: `src/web/mod.rs` (`list_recent` reads the store).\n- `the-domain-imports-no-storage`: `src/order/pricing.rs` (uses the store).\n\n`supply-chain` is bound and, in enola v0.4.
|
|
110
|
+
"examples/rust-service/EXPECTED": "# The laws this example breaks on purpose, one rule id per line. CI\n# snapshots the example with the pinned enola and fails when the set of\n# laws reporting a breach differs from this list.\nshop/presentation-reaches-application-only\nthe-domain-imports-no-storage\n# Not supply/dependencies-are-pinned, though axum = \"0.7\" is a caret range:\n# enola v0.4.35 reads a Cargo version with no operator as pinned.\n",
|
|
111
|
+
"examples/rust-service/README.md": "# A Rust service\n\nAn Axum service in four layers, small enough to read in a minute, that\nbreaks its laws on purpose. [Guide 18](../../guides/18-rust.md) walks\nthrough it.\n\n| Layer | Directory |\n|---|---|\n| presentation | `src/web` |\n| application | `src/checkout` |\n| domain | `src/order` |\n| infrastructure | `src/store` |\n\nThe laws are in `enola/constraints/`: the `layered` recipe bound to the\nfour directories, the domain's law over `imports` (the recipe's own domain\nrules read `depends_on`, which this language's extractor does not write),\nand the `supply-chain` recipe. What breaks:\n\n- `shop/presentation-reaches-application-only`: `src/web/mod.rs` (`list_recent` reads the store).\n- `the-domain-imports-no-storage`: `src/order/pricing.rs` (uses the store).\n\n`supply-chain` is bound and, in enola v0.4.35, silent about `axum = \"0.7\"`, which Cargo reads as a caret range; [the guide](../../guides/18-rust.md#where-v0435-reads-less) says why, and `EXPECTED` records it so CI notices when enola reads it.\n\n```bash\ncd examples/rust-service\nenola --generate .\nenola constraints lint .\n```\n\nThe breaches are findings in `.enola/insights.json` with source\n`constraints`. `EXPECTED` lists the rule ids that must report one, and\n`script/test-examples` fails when the set changes.\n",
|
|
112
112
|
"examples/rust-service/enola/constraints/domain.yaml": "# layered's domain rules read depends_on, an edge the Rust extractor does\n# not write; a use is the edge a domain module crosses to reach storage.\ncomponents:\n - name: domain\n match: [\"src/order/**\"]\n - name: storage\n match: [\"src/store/**\"]\nrules:\n - id: the-domain-imports-no-storage\n forbid: domain\n to: storage\n via: imports\n mode: ratchet\n because: \"The domain is the part that outlives the database; a domain module that uses the store cannot be tested or reasoned about without it.\"\n",
|
|
113
113
|
"examples/rust-service/enola/constraints/layers.yaml": "# The four layers of a service, bound to the packages that hold them.\nuse_recipe:\n - recipe: layered\n as: shop\n mode: ratchet\n bind:\n presentation: { match: [\"src/web/**\"] }\n application: { match: [\"src/checkout/**\"] }\n domain: { match: [\"src/order/**\"] }\n infrastructure: { match: [\"src/store/**\"] }\n",
|
|
114
114
|
"examples/rust-service/enola/constraints/supply.yaml": "# Every direct dependency pinned, or resolved by a lockfile enola reads.\nuse_recipe:\n - recipe: supply-chain\n as: supply\n mode: ratchet\n bind:\n unpinned-dependencies: {}\n",
|
|
@@ -132,53 +132,71 @@
|
|
|
132
132
|
"examples/typescript-service/src/server.ts": "import express from \"express\";\nimport { ordersRouter } from \"./routes/orders\";\n\nconst app = express();\napp.use(express.json());\napp.use(\"/orders\", ordersRouter);\napp.listen(3000);\n",
|
|
133
133
|
"examples/typescript-service/src/services/checkout.ts": "import { Order, type Line } from \"../domain/order\";\nimport { saveOrder } from \"../db/orders\";\n\nexport async function placeOrder(lines: Line[]) {\n const order = new Order(lines);\n await saveOrder(order.total());\n return { total: order.total() };\n}\n",
|
|
134
134
|
"examples/typescript-service/tsconfig.json": "{\n \"compilerOptions\": { \"target\": \"ES2022\", \"module\": \"NodeNext\", \"strict\": true, \"outDir\": \"dist\" },\n \"include\": [\"src\"]\n}\n",
|
|
135
|
-
"guides/
|
|
136
|
-
"guides/02-rails.md": "# A Rails application\n\nWhat enola sees in a Rails app, what the two Ruby providers add, and how\nto go from the first snapshot to laws the team wrote.\n\n## What the snapshot holds\n\nFrom the Ruby extractor alone:\n\n- **Symbols** for every class, module and method, with their calls,\n constant references and one-level superclass.\n- **Routes** from `config/routes.rb` and every engine's route file,\n `resources` expanded, each route carrying its method and path and a\n `handled_by` edge to the controller action that serves it.\n- **Storage** facts for every `ApplicationRecord` descendant: the model,\n its table (declared or derived), its columns and foreign keys when the\n schema is present.\n- **Associations** with their macro, target and whether the target was\n declared (`class_name:`) or derived from the name.\n- **Module edges** between directories, weighted by the symbol edges\n behind them, which is what cycle detection reads.\n\nRun `enola --explain .` and then `enola --generate .`; the receipt in\n`.enola/receipt.json` lists what each extractor parsed and skipped.\n\n## The two providers\n\nTwo things the extractor does not settle are settled by providers, which\nenola runs at snapshot time beside its own extraction. Both run inside the\nenola binary against the Rubydex engine library, with no Ruby involved:\n\n- **Rubydex** resolves constants through nesting and inheritance, types the\n receiver of a call, and linearises every class's ancestry with mixins in\n order. It reads your bundle's gem paths when `bundle` is on PATH so\n `ApplicationRecord` and `ViewComponent::Base` resolve.\n- **Prism** adds the call edges a lexical walk can see: a call on the\n enclosing class, and `new` on a written constant. Named with no\n `command`, it reads the index Rubydex built in the same run.\n\n```yaml\nproviders:\n - name: rubydex\n - name: prism\n```\n\nThe install script and `enola upgrade` fetch the library the binary pins;\nafter `pip install enola-cli`, `go install` or a source build, run\n`enola providers fetch rubydex` once. Leave `expected_version` off a\nbuilt-in: the binary already pins the library, and a pin that disagrees\nwith it skips the provider.\n\nThe configuration lives in `mcp-arch.yaml` at the repository root, the\none file name enola reads without being told. Rubydex indexes a tree that\nhas a `Gemfile`. With both providers a law can say \"every class under\n`ApplicationRecord`\" and mean the resolved chain, not the text after `<`.\nThe [providers guide](07-providers.md) covers the census, the named skips,\nthe Prism script for a Ruby of your own, and the runtime capture.\n\n## Name the parts\n\n`enola constraints init .` writes `enola/constraints/recipes.yaml` binding the\nrecipes whose roles your tree has. For a Rails app that is\n`rails-conventions`:\n\n```yaml\nuse_recipe:\n - recipe: rails-conventions\n as: rails\n mode: ratchet\n bind:\n controllers: { match: [\"app/controllers/**\"] }\n jobs: { match: [\"app/jobs/**\"] }\n models: { match: [\"app/models/**\"] }\n mailers: { match: [\"app/mailers/**\"] }\n policies: { match: [\"app/policies/**\"] }\n serializers: { match: [\"app/serializers/**\"] }\n view-components: { match: [\"app/components/**\"] }\n```\n\nTwo roles are optional and off until you bind them: `helpers` and\n`services`. Binding them brings three more laws (services and models\nnever reach helpers; the controller's request API stays out of models and\nservices). `rails-strict` is the same set with the concerns law added and\nnothing advisory. `vanilla-rails` is for an app that keeps Rails plain and\nwants its extra directories to stay empty with a stated reason each.\n\nTwo more recipes ship with enola and suit any Ruby tree. `ruby-conventions`\nforbids the `get_`, `set_` and `is_` prefixes other languages bring along;\n`supply-chain` forbids an unpinned dependency (see\n[writing laws](04-writing-laws.md#laws-over-dependencies)).\n\n`enola constraints lint .` shows every part with what it resolved to and\nevery rule a binding left unexpanded, which is how you check the tree\nbefore the first verdict.\n\n## The laws a Rails team writes\n\nThe recipe's laws, each with the reason it carries:\n\n- jobs do not reach controllers (advisory, because the sanctioned path\n through `ApplicationController.renderer` reads as a crossing);\n- models do not reach controllers;\n- policies only answer, never enqueue;\n- mailers do not enqueue;\n- serializers do not reach controllers;\n- view components do not enqueue and do not reach controllers;\n- maintenance tasks do not reach controllers;\n- with `helpers` bound: services and models never reach helpers;\n- with `services` bound: the request API (`params`, `session`, `render`,\n `redirect_to`, `cookies`, `flash`) stays in controllers, stated as\n receiver-less calls so `request.params` on a typed receiver is not a\n breach.\n\nWrite your own beside them in Ruby, in any file under `enola/constraints/`:\n\n```ruby\nEnola.architecture \"shop\" do\n rails\n part :billing, files: [\"app/billing/**\", \"app/models/invoice.rb\"]\n part :orders, files: [\"app/orders/**\", \"app/models/order.rb\"],\n public: [\"app/orders/public/**\"]\n\n law \"billing keeps to its own tables\" do\n billing.storage_must_stay_home\n why \"a part that writes another part's table owns a bug it cannot see\"\n mode :ratchet\n end\n\n law \"a mutating action is authorized\" do\n part :mutating_actions, files: \"app/controllers/**\",\n handles: [:post, :put, :patch, :delete]\n mutating_actions.must_reach :policies\n why \"every write passes a policy before it touches a record\"\n mode :ratchet\n end\nend\n```\n\nThe first law reads the storage facts: a member of `billing` reaching\n`Order` is a breach that names the table `orders`, and the suggested cut\nnames the `orders` part's public member that already uses the model. The\nsecond selects the code behind POST, PUT, PATCH and DELETE routes through\nthe `handled_by` edges and demands a call into the policies, so the law\nfollows the routes rather than a list of actions.\n\n## Packwerk\n\nA repository that splits into packwerk packages gets the `packwerk`\nexplainer once enola finds the packages. It reads `package_paths` from\n`packwerk.yml` and, when the file sets none, looks only at the root and\n`packages/*`, where packwerk itself searches every directory; with packs\nunder `packs/` or `components/`, name them:\n\n```yaml\n# packwerk.yml\npackage_paths:\n - packs/*\n```\n\nThe snapshot log says how many packages it found. The explainer compares\neach `package.yml`'s declared\ndependencies with what the package's code references, and reports both\nsides. An undeclared reference names its file and line at confidence 0.9,\nbecause a Ruby constant reaches its package by name; one already in\n`package_todo.yml` is counted, not reported. A declared dependency the code\nnever references is reported at 0.7, since a constant reached through\n`constantize`, a YAML class name or only a spec draws no edge. Gate on it\nwith `enola check -fail-on packwerk -min-confidence 0.9` once the todo\nfiles say what you mean.\n\n## Read a verdict\n\n```bash\nenola baseline pin .\n# change something\nenola --generate . && enola check .\n```\n\nA constraint breach names the rule, the member, the edge and the reason,\nleads its suggested actions with the smallest change the graph can see,\nand says whether it is a decided-rule breach (exact membership over a\nmeasured edge) or an advisory. `enola constraints explain app/jobs/x.rb`\nanswers what a file is a member of, by which selector, and which edges it\nmakes, which is the question to ask before arguing with a verdict.\n\n## When the answer is \"cannot evaluate\"\n\nA law over resolved ancestry without the Rubydex provider, a budget\nwithout a runtime capture, a law across repositories in a one-repository\nsnapshot: each is reported as unevaluable with its cause and emits no\nverdict. That is the design. A law nobody could measure must not read as\nkept.\n",
|
|
137
|
-
"guides/
|
|
138
|
-
"guides/
|
|
139
|
-
"guides/05-constraints-in-ci.md": "# Constraints in CI\n\nThe check that grades every pull request against its base branch, reports\nwhat the change added, and never fails the build until the team decides it\nshould. On GitHub that is one step, [enola-action](#on-github-enola-action);\non any other CI, or where you want the comment grouped per rule,\n[`templates/enola-check.sh`](../templates/enola-check.sh) is the script and\nmost of this guide is why it looks the way it does.\n\n## On GitHub: enola-action\n\n```yaml\n# .github/workflows/architecture.yml\non: pull_request\npermissions:\n contents: read\njobs:\n architecture:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v5\n with:\n fetch-depth: 0\n # The action downloads enola but not the Rubydex library; install it\n # with the script, which fetches both, and grade with that binary.\n - run: curl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | ENOLA_INSTALL_DIR=\"$RUNNER_TEMP/enola\" sh\n - uses: enola-labs/enola-action@v2\n with:\n binary: ${{ runner.temp }}/enola/enola\n fail-on: constraints\n warn-only: true\n```\n\nIt resolves the pull request's exact base commit, generates both sides on\nthe runner, annotates the lines that introduced a finding, and writes the\ndelta to the job summary. `pr-comment: true` also posts the report as one\ncomment edited in place (it needs `pull-requests: write`), and\n`sarif: true` writes a SARIF 2.1.0 file for code scanning, exposed as the\n`sarif-file` output. Its `status` output is one of `clean`,\n`partial_clean`, `regression`, `partial_regression`, `usage_error` and\n`incomparable`, and `partial` says whether the verdict graded only the\nproducers both sides share. Without the install step a Ruby repository is\ngraded without its providers, on both sides alike, so the verdict is whole\nand quietly misses what Rubydex would have resolved; a repository with no\nRuby can drop that step and the `binary` input.\n\n## The shape\n\n1. **Pin a release.** The script downloads one version of the binary,\n verifies its checksum, and caches it per version and platform. A\n floating version is how a check that passed yesterday fails today with\n no change of yours.\n2. **Grade against the pull request's base, not against main.** A stacked\n pull request whose parent declares new rules would otherwise report the\n parent's whole backlog as its own. Push-triggered builds often know the\n pull request number but not its base, so the script asks the forge for\n it and falls back to `main` only when there is no pull request at all.\n3. **Two snapshots, one verdict.** Check out the merge-base, generate;\n check out the head, generate; `enola check -baseline previous`. The\n working tree must be clean for this, and the script says so when it is\n not rather than grading a mixed tree.\n4. **Configure the providers the agent has.** In a tree with a `Gemfile`,\n the script fetches the Rubydex library the pinned binary names and\n configures the built-in `rubydex` and `prism` providers, which need no\n Ruby on the agent. A provider of your own (eslint, a runtime capture)\n is one more entry in the same block. Each absence is a named line in\n the log, never a silent drop, and the same configuration generates both\n snapshots and grades them, so they stay comparable.\n5. **Watch a few explainers, warn only.**\n `-fail-on constraints,query-loops,layers,cycles -min-confidence 0.8 -warn-only`\n reports the findings those explainers added and keeps the exit code\n at 0. Removing `-warn-only` is the team's decision, made once the\n comments have been read for a while.\n6. **Render for a reader, grouped per rule.** The verdict's JSON becomes a\n comment with three sections in this order: **Watched** (the explainers\n you track, grouped per rule with a count and three examples, constraints\n first), **Declared** (rules this change declares, reported on code it did\n not touch, the baseline each rule starts from), **Also noticed**\n (everything outside the watched set). One comment per pull request,\n edited in place; a clean run updates an existing comment and posts\n nothing new.\n\n## Not comparable is not a pass\n\nWhen the two snapshots differ in enola version or in the ignore globs,\n`check` declines to grade. The script reports that as \"did not run\" on CI\n(a failed step, so it cannot go quiet) and as a skip locally. Read it as\n\"not asked\", and fix what made the pair incomparable.\n\nWhen only the producers differ (a provider whose tool was missing on one\nside), `check` grades the facts both snapshots share and says the verdict\nis partial; the comment names each producer it left out and the side that\nlacked it, because a regression among those facts is not reported. The\nscript generates both snapshots and grades them with the same\nconfiguration, so a partial verdict on CI means a tool the agent lacks,\nnot a misconfigured step.\n\n## Other shapes of the verdict\n\n`check` writes the same verdict in several shapes, chosen with `-format`:\n`text` (the default), `json` (what the template renders), `sarif` for code\nscanning, `annotations` for the CI's line annotations (`-host github` or\n`-host buildkite`, with `-link` naming the pull request's files view on\nBuildkite), and `summary`, a plain-language account for a reader who does\nnot know the explainer names: whether it passes, what got worse, what got\nbetter, what the run could not see, what to do next. `summary-json` is\nthat account as JSON. Each shape grades nothing of its own, so the exit\ncode is the verdict's whichever one printed it.\n\n`-reviewers` adds who owns the modules the change touched, read from git\nauthorship, and suggests a reviewer when the author is a minor\ncontributor; it never fails the run.\n\n## Reproduce locally\n\n```bash\nbash ci/enola-check.sh # or wherever you keep it\n```\n\nThe script is written to run on a laptop: without the forge's credentials\nit skips the comment and prints the verdict, and it leaves the checkout on\nthe branch it found.\n\n## Adapting the template\n\nThe template targets a generic CI with three seams you fill in: how the\nbinary is fetched (a release asset URL and its checksum), how the pull\nrequest's base branch is read (an environment variable on pull-request\nbuilds, a forge API call on push builds), and how the comment is posted\n(a small script against the forge's comments API). Everything between,\nthe two snapshots and the verdict and the rendering, is the same on every\nCI.\n",
|
|
140
|
-
"guides/06-hooks-and-agents.md": "# Hooks and agents\n\nenola is most useful when nobody has to remember to run it. Two hooks and\none skill do that.\n\n## The hooks\n\n```bash\nenola install --hooks\n```\n\nwrites enola's instructions into the files your coding agents already\nread (Claude Code, Cursor, Copilot, Codex, Pi, opencode) and wires two\nhooks:\n\n- **session start** pins the architecture as the session's baseline, the\n \"before\" every later grade compares against;\n- **stop** grades what the session changed against that baseline when the\n agent finishes a turn, and speaks only when the change introduced a\n structural regression, or when it could not compare at all (then the\n remedy is to re-pin, not to doubt the change).\n\nIn Claude Code the session-start hook also says how to load enola's\ndeferred tools in one ToolSearch call, and a `PreToolUse` hook refuses the\nsession's first two file searches (`Grep`, `Glob`, or `grep`, `rg`, `find`\nin a shell) with a pointer to the enola tool that answers the question;\nthe refusals stop once any enola tool is used.\n\nIt previews every change and asks before writing, never creates a shared\nfile such as `AGENTS.md` that was not already there, and `enola uninstall`\nreverses it byte for byte. `--targets claude,codex` narrows it to the\nagents you name, `--dry-run` shows what it would write, and `--global`\nwrites the user-level files instead.\n\nTwo agents get more than instructions, because they lack what the others\nhave. **Pi** has no MCP client, so `--targets pi` writes\n`.pi/extensions/enola.js`, which starts enola's server at session start\nand registers every tool as `enola_<tool>`; it is written with or without\n`--hooks`, since without it Pi has no enola tools at all. **opencode** has\nno hook configuration of this shape, so `--targets opencode` registers the\nserver in `opencode.json` itself and, with `--hooks`, installs\n`.opencode/plugin/enola.js`, which names the enola tool in the\ndescriptions of `grep`, `glob` and `list` and refuses a session's first\nsearches (`ENOLA_OPENCODE_GATE=off` keeps the rest and drops the\nrefusals).\n\n### The Claude Code plugin\n\nenola's repository is also a Claude Code plugin marketplace:\n\n```text\n/plugin marketplace add enola-labs/enola\n/plugin install enola@enola\n```\n\nThe plugin registers the MCP server, the session-start and stop hooks, and\ntwo skills: one routing an architecture question to the tool that answers\nit, and `/enola:setup`, which checks for the CLI and runs the install\nscript when it is missing. It configures and does not install, so `enola`\nmust be on PATH. It carries neither the search refusal nor the tool-loading\nline `install --hooks` adds; choose one of the two, since both register the\nsame hooks.\n\n```bash\nenola doctor\n```\n\nreports whether the hooks actually fired in this repository (when each\nlast ran and what it concluded), whether the pinned baseline can still be\ngraded against, and whether a newer release is out. `enola providers\nlist` is where the Rubydex library shows. A hook that is configured but never fires looks exactly like one\nthat fires and stays quiet; `doctor` is the only thing that tells them\napart.\n\n## The MCP tools an agent should reach for\n\n| Before a change | |\n|---|---|\n| `explore` | what is in here and what touches it; the one to reach for first |\n| `impact_analysis` | what transitively depends on this, before touching it |\n| `endpoint_impact` | who calls this HTTP endpoint, and which screens sit behind the callers |\n| `traverse`, `find_path`, `show_symbol`, `query_facts` | how something is wired, instead of reconstructing it from files; `find_path` takes sets (`from_nodes`, `to_nodes`) to ask how any of one group reaches any of another in one search |\n| `governing_intent` | which decision pages govern this file or symbol, and which code a page governs |\n| `constraints_for`, `plan_check` | which declared laws bind a path, and what a planned edit would breach before it exists |\n| `set_baseline` | pin the architecture as it is, once, early |\n\n| After a change | |\n|---|---|\n| `generate_snapshot`, `diff_snapshot` | what the change actually did: findings introduced or resolved, coupling added; `generate_snapshot` takes `repo_paths` to index several repositories as one graph |\n| `query_insights` | the explainers' findings, rather than re-deriving them |\n| `snapshot_receipt`, `compare_receipts` | what a graph was generated over and where its limits are, and whether two snapshots are comparable enough to trust a diff between them |\n| `architecture_blame`, `architecture_history` | when a dependency entered the architecture, and how it changed over time |\n\n| Looking for trouble | |\n|---|---|\n| `find_orphans` | what nothing references any more |\n| `package_metrics` | how stable and how abstract a package is, and its distance from the main sequence |\n| `analyze_performance` | where the cost grows with the input |\n| `coverage_report` | which cross-repository edges resolved and which were missed |\n\nIn Claude Code the tools are deferred, listed by name and loaded through\nToolSearch, which is why the session-start hook says how to load them in\none call. Claude Code 2.1.287 and later loads them up front with\n`\"alwaysLoad\": true` on the server's entry in `.mcp.json`, and then the\nsearch refusal rarely fires.\n\n## The same questions from a shell\n\nA hook, a review bot or a person without an agent asks the graph through\nthe CLI. Each command reads the current snapshot, says when it was written\nand whether it matches the working tree, and takes `--json`:\n\n```bash\nenola impact 'kind:symbol OrdersController' . # what depends on it, by hop depth\nenola endpoint 'GET /api/v2/orders' . # the controller, models, tables and callers behind a route\nenola plan --paths app/jobs/receipt_job.rb . # the laws governing a path and its blast radius, before the edit\nenola flows . # the ways into the program, ranked by what each reaches\nenola constraints ledger . # per law, the breaches reported beside the ones excused\nenola dashboard --open . # the snapshot in a read-only local browser view\n```\n\n`plan --patch` evaluates the verdicts a diff would produce over a scratch\ncopy before it lands; `flows --through <symbol>` lists only the flows\nthat reach a symbol. `enola help <command>` prints any command's page,\nand `--help` never runs the command it describes.\n\n## The skill\n\n[`templates/skills/enola/SKILL.md`](../templates/skills/enola/SKILL.md) is\nthe skill to drop into your agent's skills directory. It says when to use\nthe graph (a change whose impact is not obvious, a change to how modules\ndepend on each other, a hook or CI finding), which tool answers which\nquestion, and how to read a finding: it names the structural change and\nthe rule it crossed, and a \"could not compare\" means the baseline is\nstale, not that the change is wrong. Keep it short; the agent's job is to\nask the graph, not to read about it.\n\n## Intent first, optionally\n\nTeams that keep decision records can compile them into the graph: a page\ncarrying an `enola_intent:` block with anchors (repository and path)\nbecomes an intent fact, `governing_intent` answers which pages govern a\nfile, and the `governed_by` component key and `require_governed` form turn\nthat into laws (see [writing laws](04-writing-laws.md)). A pre-edit hook\nthat reads `governing_intent` for the file about to change, and injects\nthe decision trail into the agent's context, is the practice this\nrepository's authors run; it is a hook of a few lines over the MCP tool.\n",
|
|
141
|
-
"guides/07-providers.md": "# Providers\n\nA provider contributes facts to the graph that no extractor measures: the\nresult of running another tool, a capture of the app at run time, a\nlanguage engine's resolution. enola runs the configured providers at\nsnapshot time, validates everything they emit against the fact schema,\nrefuses a provider's whole output on the first invalid line, and records\nin the receipt what each provider contributed and what it skipped.\n\n## Configure\n\n```yaml\n# mcp-arch.yaml\nproviders:\n - name: rubydex\n - name: prism\n - name: eslint\n command: [\"node\", \"/path/to/enola_eslint_provider.mjs\"]\n expected_version: \"0.1.0\"\n```\n\nAn external provider is an executable the seam runs twice: once with\n`--version`, once with the repository path, reading JSONL facts from its\nstdout and a one-line census from its stderr. A provider with no\n`command` is one the binary carries itself: `rubydex` and `prism`. An\n`expected_version` pins what the provider must answer, and a provider that\nanswers anything else is skipped by name; pin a script to its own\n`PROVIDER_VERSION`, and leave a built-in unpinned, since the binary pins\nits library. A configured provider whose tool is missing is a named skip\nin the receipt, never an error: the machine not having a tool must not\nfail the snapshot, it must be visible.\n\n## The Ruby pair\n\n**Prism** adds the call edges a lexical walk can see: a call whose\nreceiver is the enclosing class, and `new` on a written constant. Named\nwith no `command` it runs inside the binary and reads the index Rubydex\nbuilt in the same run, so it needs no Ruby. The reference script,\n`examples/providers/ruby/prism/enola_prism_provider.rb` in the enola tree,\nreads with Ruby's own parser on a Ruby 3.3 or newer; pin it to\n`expected_version: \"0.2.0\"`. The two are close, not identical: the\nbuilt-in also writes the calls a class body makes (`validates`,\n`before_action`), and writes the constant Rubydex resolved where the\nscript writes the one in the source.\n\n**Rubydex** resolves constants through nesting and inheritance, types the\nreceiver of a call when it resolves to a constant, and linearises each\nclass's ancestry with mixins in resolution order. It runs inside the\nbinary against an engine library at the version enola pins: the install\nscript and `enola upgrade` fetch it, and after `pip install enola-cli`,\n`go install` or a source build `enola providers fetch rubydex` does,\nverifying the published checksum and caching it under your user cache\ndirectory. `enola providers list` reports it. It indexes a tree with a\n`Gemfile`; without one the census names the skip. It indexes the workspace and, when `bundle` is on PATH, the\ngem paths the bundle reports, so a base class a gem declares resolves\n(and the 357 view components under a gem's base class become a fact). No\ngem enters your Gemfile.\n\nIts facts carry their own resolution level, `resolved`, so a law can ask\nfor exactly them: the `ancestor:` component key reads the resolved\nchain, and the `independent` form (a mixin never reaches its includers)\nrefuses by name without it.\n\n## Signatures and the autoload map\n\nTwo more reference providers in enola's `examples/providers/ruby/` read\nwhat a Ruby tree declares rather than what it runs. Each is a plain-Ruby\nscript, pinned to its own `PROVIDER_VERSION` (`0.1.0` for both):\n\n```yaml\nproviders:\n - name: rbs\n command: [\"ruby\", \"/path/to/enola_rbs_provider.rb\"]\n expected_version: \"0.1.0\"\n - name: zeitwerk\n command: [\"ruby\", \"/path/to/enola_zeitwerk_provider.rb\"]\n expected_version: \"0.1.0\"\n```\n\n- **rbs** reads RBS files, Sorbet `.rbi` files and inline `sig { }`\n blocks with a hand parser rather than the `rbs` gem, so its facts do not\n depend on which gem a machine has. It writes method contracts\n (`rbs-signature:`) and type declarations (`rbs-decl:`) at level\n `declared`, and the engine marks each extracted method a contract names\n `typed: true` with the files that declared it. A law can then require a\n declared signature on a public API. A declaration is a claim about the\n code, and the level says who made it.\n- **zeitwerk** maps every file under an autoload root to the constant\n Zeitwerk expects it to define, from the path alone, without booting\n the app: `app/models/billing/invoice_item.rb` to\n `Billing::InvoiceItem`, concerns collapsed. Its facts are\n `convention-derived`. A path it cannot camelize with confidence (a\n dash, an uppercase letter) emits nothing and is counted, since without\n boot no inflector override is known.\n\n## Reuse\n\nProvider facts enter the same cache as the extractors' facts, so a\nprovider whose inputs did not move is not run again. A script whose\noutput partitions by file says so with `files: per-file` and the\n`extensions:` it reads; the seam then hands it only the files that\nchanged and keeps the rest. A provider that reads across files must not\ndeclare that, because the cache would serve facts computed against a tree\nthat has since changed. The built-in Rubydex provider keeps one entry for\nthe whole index, keyed by the library version, the Ruby files with their\ndigests and `Gemfile.lock`, and rebuilds it on any change rather than\npatching it. The receipt's `reuse` block says, per provider, what was\nreused and what was computed, and for the index which part of the key did\nnot match.\n\n## The census\n\nEvery provider states what it saw and what it skipped, and enola records\nit per provider in the receipt:\n\n```json\n{\"name\": \"rubydex\", \"version\": \"0.4.0\", \"fact_count\": 666713,\n \"census\": {\"files_seen\": 43297, \"declarations_parsed\": 2698018,\n \"constructs_skipped\": 2244152,\n \"skip_causes\": [{\"cause\": \"receiver resolves to no constant\", \"count\": 1798420},\n {\"cause\": \"unresolved constant reference\", \"count\": 28416}]}}\n```\n\nA skip has a cause; a provider never guesses. Read the census before\nreading a verdict that surprises you.\n\n## The runtime capture\n\nThe runtime provider reads captures an operator produced by running the\napplication: a booted-Rails capture (the final route table, reflected\nassociations, table bindings) and a query-counter capture (database\nqueries per frame under a spec run). Facts carry `runtime-observed` and\nthe channel they came from, and the engine cross-links them to the\nmeasured ones. `examples/providers/ruby/runtime/capture_spec_queries.rb`\nin the enola tree is the runner for the query capture: require it from\n`spec_helper`, run the suite, and `.enola-runtime/queries.json` is written\nwith one frame per example. A `must_keep_budget metric: :queries, max: N`\nlaw then verdicts on it, and refuses by name until a capture exists.\n\n## Writing one\n\nA provider is any program that prints JSONL facts in the store's schema\nwith a `resolution_level` on every fact and a prefix on every name so it\ncannot collide with an extractor's identity, plus the one census line.\nThe Prism and eslint scripts in enola's `examples/providers/` are the\nreference; each is under two hundred lines. Give it a `PROVIDER_VERSION`\nit prints for `--version`, raise it whenever its facts change, and pin\nthat version in the configuration: a pin that no longer matches skips the\nprovider by name rather than mixing two versions' facts in one graph.\n",
|
|
142
|
-
"guides/
|
|
143
|
-
"guides/
|
|
144
|
-
"guides/
|
|
145
|
-
"guides/
|
|
146
|
-
"guides/
|
|
147
|
-
"guides/
|
|
148
|
-
"guides/14-taha.md": "# taha: agents under a mandate\n\n[taha](https://github.com/misabegovic/taha), short for tabula harness, is a\ncommand-line tool that runs coding agents on a [tabula](13-tabula.md)\nproject and keeps them on track. You say what you want; taha routes it\nthrough tabula's intent pages (a pitch, a plan, a decision record, then\nthe build), runs the agent, settles routine questions with a council of\nreviewers, and stops to ask you before anything is published. taha\ndecides nothing tabula can decide: every gate it applies is a tabula\nverdict, and a bug in taha can at worst stop a run, never let a failed\ncheck through.\n\nThis guide is where taha meets enola. taha's own\n[first hour](https://github.com/misabegovic/taha/blob/main/docs/first-hour.md)\nfixes one bug in a sample project in ten stops with no model and no\naccount, and [the menu](https://github.com/misabegovic/taha/blob/main/docs/menu.md)\nlists every command by goal; start there.\n\n## Install\n\ntaha is not on npm yet, and the name `taha` on npm belongs to another\npackage, so `npm install -g taha` installs something else. Install from a\nclone, with Node 22.19 or newer, on Linux or macOS (Windows through WSL):\n\n```bash\ngit clone https://github.com/misabegovic/taha && cd taha\nnpm install\nnpm link\ntaha help\n```\n\ntaha runs the tabula `TABULA_BIN` names, else the newer of the tabula its\nplatform package carries and the one on your PATH. A clone has no platform\npackage, so [install tabula](13-tabula.md#install) first.\n\n## What a run is\n\nOne piece of work, an issue or a PRD slug, is one durable conversation\nkept in SQLite under `state/`, so a crash or a restart never repeats an\nact. The run claims the piece with `tabula claim`, attaches `tabula serve`\nover the project, and journals every committed step into the project's\njournal. Before a piece can finish `done` or open a pull request,\n`tabula evaluate` must read every acceptance bullet met; a PRD bullet\nwritten as `grade: enola check clean` is how enola's verdict becomes one\nof them.\n\nWhat the run may do is written in a mandate, `wiki/mandates/default.md`:\nthe acts it may take (`allow`, `ask`, `never`), its budget, whether it is\n`supervised` or `autonomous`, the repositories in scope, the test command,\nand when it expires. GitHub is reached through one connector whose only\nwrites are a branch, a push, a draft pull request and a comment; each is\njournaled before it runs, a pull request is always a draft, and taha has\nno way to mark one ready or merge it. Anything public waits in the inbox\nfor you:\n\n```bash\ntaha # what runs, what waits on you, what is next\ntaha approve <finding> # a yes to what waits\ntaha steer <run> <words> # change a run in flight\ntaha stop <run> # stop at the next safe point\n```\n\n## Adopting a repository that enola already measures\n\n```bash\ncd ~/src/shop\ntaha adopt --inside # or --beside --repo shop, to write nothing into it\ntaha doctor\n```\n\n`adopt --inside` adds the project's files to the repository, records the\nrepository's own checks so a push waits for them, and, for the code map,\nwrites `mcp-arch.yaml` and runs `cardo --generate` in the repository.\n`adopt --beside` writes nothing into the repository and leaves the\nsnapshot to you. `taha doctor` prints one line each for the layout, the\nmandate, the state folder, the schedule and the code map, and exits 0\nwhen all of them read `ready`.\n\ntaha calls the engine as `cardo`, by that name on your PATH; it does not\nfall back to `enola`. The cardo a [tabula release](13-tabula.md#install)\ncarries is unpacked into tabula's cache, and `tabula doctor` prints where\n(the `engine` line). Until cardo has an install of its own, link that copy\nonto your PATH for taha:\n\n```bash\nln -s \"$HOME/.cache/tabula/engine/<digest>/cardo\" ~/.local/bin/cardo\n```\n\nAn `mcp-arch.yaml` the repository already has is left as it is, so the\nproviders [guide 02](02-rails.md#the-two-providers) configures keep\nrunning.\n\n## Where enola's laws stay\n\ntaha adds no law of its own. The laws are the repository's\n`enola/constraints/`, written by hand or compiled from tabula's\narchitecture pages, and they are graded by the repository's CI with\n`enola check` as [guide 05](05-constraints-in-ci.md) describes, whoever\nor whatever wrote the change. taha's part is to make an agent's run reach\nthat CI only as a draft, after tabula's checks, with you asked first.\n",
|
|
149
|
-
"guides/
|
|
150
|
-
"guides/
|
|
151
|
-
"guides/
|
|
152
|
-
"guides/
|
|
153
|
-
"
|
|
135
|
+
"guides/00-concepts.md": "# What enola is, and the words these guides use\n\nenola builds a graph of a codebase (modules, symbols, routes, tables,\ndependencies, and the edges between them), keeps it as a snapshot, and\ngrades a change by comparing the snapshot before it with the snapshot\nafter it. It reports what the change did: a dependency cycle that now\nspans four files, a job that started calling a controller, a second part\nwriting a table that was one part's own. Those are invisible to a tool\nthat reads one file at a time, and a report of everything already wrong\nwith a repository buries them; a report of what one change added does\nnot.\n\nRead this page once, then [start here](01-start-here.md). Every term\nbelow links to the guide that covers it.\n\n## How the pieces fit\n\n1. **Extractors** read the source, one per language, and write **facts**:\n one line per module, symbol, route, table or dependency, and one per\n edge between them (`imports`, `calls`, `depends_on`, `reads`, ...).\n2. **Providers** add facts no extractor measures: Ruby's own resolution\n of constants and ancestry, a linter's results, a capture of the app at\n run time.\n3. The facts of one run are a **snapshot**, written to `.enola/` with a\n **receipt** saying which extractors and providers ran and what they\n skipped.\n4. **Explainers** read the snapshot and compute **findings**: cycles,\n layer violations, dead code, query loops, coupling hotspots, and the\n breaches of your laws. Each carries a confidence between 0 and 1.\n5. A **baseline** is a snapshot pinned as \"before\". `enola check`\n compares the current snapshot with it and prints a **verdict**: the\n findings the change introduced and resolved, and whether any of them\n fails the build under the policy you set.\n\n## Glossary\n\n| Term | What it means here | Covered in |\n|---|---|---|\n| **fact** | One measured thing (a module, a symbol, a route, a table, a dependency) or one edge between two. A snapshot is a file of them, `.enola/facts.jsonl`. | [Start here](01-start-here.md#3-take-the-first-snapshot) |\n| **extractor** | The part of enola that reads one language's source and writes its facts. Every language in the tree is indexed into one graph; there is nothing to configure. | [Start here](01-start-here.md#2-look-before-you-configure) |\n| **provider** | A program enola runs beside its extractors to add facts they cannot measure. `rubydex` and `prism` are built into the binary; anything else is an executable you configure. | [Providers](07-providers.md) |\n| **census** | The one line a provider prints about what it read and what it skipped, kept in the receipt. | [Providers](07-providers.md#the-census) |\n| **receipt** | `.enola/receipt.json`: which extractors and providers produced a snapshot, at which versions, and what they skipped. Two snapshots with different receipts may not be comparable. | [Start here](01-start-here.md#3-take-the-first-snapshot) |\n| **explainer** | A named analysis over the snapshot (`cycles`, `layers`, `dead-code`, `query-loops`, `constraints`, ...) that produces findings. `enola check --help` lists them; they are what `-fail-on` names. | [Reading a verdict](20-reading-a-verdict.md) |\n| **finding** | One thing an explainer reports, with a confidence. Below 1.00 it is a candidate to verify, not a verdict. | [Reading a verdict](20-reading-a-verdict.md) |\n| **baseline** | The snapshot a change is graded against. `enola baseline pin .` pins the current one; in CI the template grades against the pull request's merge-base. | [Start here](01-start-here.md#4-pin-a-baseline-change-something-grade-it) |\n| **verdict** | What `enola check` prints: PASS or FAIL, what the change introduced, and what it could not see. | [Reading a verdict](20-reading-a-verdict.md) |\n| **law** | A rule you declare about parts of your code, with a reason and a mode. Written in Ruby (`law`) or YAML (`rules:`), kept in `enola/constraints/`, and reported as `Constraint <id> violated`. Law, rule and constraint name the same thing. | [Writing laws](04-writing-laws.md) |\n| **part** | A named set of files or facts a law speaks about: `part` in Ruby, an entry under `components:` in YAML, a role in a recipe. | [Writing laws](04-writing-laws.md#parts) |\n| **mode** | How strictly a law holds: `advisory` reports and never fails, `ratchet` fails only new breaches, `strict` fails every breach. Start in ratchet. | [Writing laws](04-writing-laws.md#modes-dates-and-carve-outs) |\n| **breach** | One place a law is broken, named by its witness: the edge or fact that breaks it. | [Writing laws](04-writing-laws.md) |\n| **recipe** | A named bundle of laws over roles, such as `layered` or `rails-conventions`, which a repository binds to its own directories in one block (`use_recipe:`). enola ships some; [`recipes/`](../recipes/README.md) has more to copy. | [Writing laws](04-writing-laws.md#recipes) |\n| **exemption** | A carve-out written on the law itself, naming one witness with an owner, a reason and a date. | [Writing laws](04-writing-laws.md#modes-dates-and-carve-outs) |\n| **suppression** | A signed, temporary excuse in `.enola/suppressions.yaml`, the only override for a strict breach. | [Writing laws](04-writing-laws.md#modes-dates-and-carve-outs) |\n| **partial verdict** | A grade over only the facts both snapshots share, because a provider or extractor ran on one side only. It names what it left out. | [Reading a verdict](20-reading-a-verdict.md#partial) |\n| **declined** | `check` refused to grade (exit 3) because the two snapshots are not comparable. Not a pass. | [Reading a verdict](20-reading-a-verdict.md#declined) |\n| **unevaluable** | A law the snapshot cannot answer, reported with its cause instead of a verdict. | [Writing laws](04-writing-laws.md#when-a-law-refuses) |\n| **hook** | A command an agent runs at a fixed moment (a session starting, a turn ending). `enola install --hooks` writes enola's. | [Hooks and agents](06-hooks-and-agents.md) |\n| **MCP** | Model Context Protocol, the way a coding agent calls a tool server. `enola` with no arguments is that server; its tools are `explore`, `impact_analysis` and the rest. | [Hooks and agents](06-hooks-and-agents.md#register-the-mcp-server) |\n| **cluster** | Several repositories indexed as one graph, so a call from a client can be matched to the route it reaches. | [Beyond one repository](08-beyond-one-repo.md) |\n| **seam** | A link between two repositories in a cluster: a client call matched to a route, a topic one publishes and another consumes. | [Beyond one repository](08-beyond-one-repo.md) |\n| **intent page** | A Markdown page carrying an `enola_intent:` block, anchored to the code it governs. Optional; the knowledge-base track builds on it. | [A knowledge base](09-a-knowledge-base-with-enola.md) |\n\n## Who makes what\n\n- **enola** is by Dejan Menges, at\n [enola-labs/enola](https://github.com/enola-labs/enola). Its own\n documentation is the reference; these guides are a path through it.\n- **These guides** are by Muhamed Isabegovic and are not part of\n enola-labs. Each release says which enola it was checked against: this\n one is enola-guides 0.9.0, checked against enola v0.4.35.\n- **tabula**, **taha** and **cardo** are separate projects by the guides'\n author: tabula keeps a knowledge base of intent pages, taha runs agents\n on it, and cardo is a build derived from enola that tabula carries.\n None of them is public yet, so [guide 13](13-tabula.md) and\n [guide 14](14-taha.md) are a preview. Nothing else in these guides\n needs them.\n\n## Next to the tools you already run\n\nLinters, type checkers and tests judge the current tree against rules they\nbring, file by file. Keep them; enola does not replace any of them.\n\nTools that check dependency rules (packwerk for Ruby packs, import-linter\nfor Python, dependency-cruiser for JavaScript, ArchUnit for the JVM) are\ncloser, and several keep a list of known violations so only new ones fail.\nIf one of them already enforces what you need in one language, keep it.\nWhat enola adds is one graph across every language and repository in the\nproduct; facts beyond imports (routes, tables, client calls, a runtime\ncapture); findings it computes without being given a rule (cycles, dead\ncode, query loops, hotspots); and the same graph handed to a coding agent\nas tools, so it can ask what a change would touch before making it.\n\n## Where next\n\n[Start here](01-start-here.md): install, a snapshot, a graded change and a\nfirst law, in ten minutes.\n",
|
|
136
|
+
"guides/01-start-here.md": "# Start here: the first useful answer in ten minutes\n\nenola answers one question no linter can: **what did this change do to the\narchitecture?** It builds a graph of your code (modules, symbols, routes,\ntables, and how they depend on each other), keeps that graph as a snapshot,\nand grades a change by comparing the snapshot before it with the snapshot\nafter it. A dependency cycle that spans four files, a job that started\ncalling a controller, a table a second part began writing to: those are\ninvisible to tools that look at one file at a time, and they are what enola\nreports. The words this guide uses (snapshot, baseline, law, verdict) are\ndefined in [what enola is](00-concepts.md).\n\nThis guide takes you from nothing to a graded change and a first law.\nEverything here is checked against enola v0.4.35. Any newer release should\nwork; when you need the exact one, pin it as step 1 shows.\n\n## 1. Install\n\nNo Go toolchain, no compiler, one binary:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh\nenola --version\n```\n\nThe script installs the newest release into `~/.local/bin` and fetches\nthe Rubydex library enola uses to resolve Ruby constants and ancestry\n(more in [providers](07-providers.md)). The same binary is on PyPI as\n`enola-cli` (`enola` was taken); install it as a tool, not into a\nproject's environment, and pin the version there when you need this\nguide's exactly:\n\n```bash\npipx install enola-cli==0.4.35 # or: uv tool install enola-cli==0.4.35\nenola providers fetch rubydex # once; the install script does this for you\n```\n\nenola is not on npm. The npm package called `enola` is an unrelated tool\nthat deletes files; do not `npm install -g enola`. Every release,\nwith a checksum per platform, is on\n[enola's releases page](https://github.com/enola-labs/enola/releases).\n\nSteps 2 to 6 work on any repository. To follow them exactly, with the\noutputs shown here, use the worked Python example, one command away with\n[`enola-guides`](../README.md#these-guides-as-a-command):\n\n```bash\nenola-guides copy examples/python-service && cd python-service && git init -q\n```\n\nIt is a four-package service (`shop/api`, `shop/services`, `shop/domain`,\n`shop/repositories`) that already declares three laws in\n`enola/constraints/` and breaks each on purpose.\n\n## 2. Look before you configure\n\nOne read-only command, nothing written:\n\n```bash\nenola --explain . 2>/dev/null\n```\n\nIt prints what it found (languages, facts by kind, routes, tables,\ndependencies), the architecture it detected (pattern, cycles, layer\nviolations), the coupling hotspots with their blast radius, and the code\nhealth findings. Every command logs its progress on stderr, about a\nhundred `[engine]` lines on a first run; `2>/dev/null` hides them. Nothing\nhere is a setting: enola detects every language in the tree and indexes\nall of them into one graph, and the line `no mcp-arch.yaml ... using\nbuilt-in defaults` (on stderr) means exactly that. `--explain` does not grade your\nlaws; their breaches show in `enola check`, below.\n\n## 3. Take the first snapshot\n\n```bash\nenola --generate .\n```\n\nThis writes `.enola/` in the repository: the facts, the findings the\nexplainers computed, and a receipt saying which extractors and providers\nran and what they skipped. Read the receipt's census when something looks\nmissing: a file enola did not parse is named there, never silently\ndropped. enola also records each snapshot in a history under `~/.enola/`,\nwhich `enola gc` reports on and trims.\n\nKeep `.enola/` out of git, except `.enola/suppressions.yaml` if you add\none later (see [writing laws](04-writing-laws.md#modes-dates-and-carve-outs)):\n\n```gitignore\n.enola/*\n!.enola/suppressions.yaml\n```\n\n## 4. Pin a baseline, change something, grade it\n\nPin the architecture as it is now, the \"before\" every grade compares\nagainst:\n\n```bash\nenola baseline pin .\n```\n\nThen change something. In the example, make the domain call the API, the\nlayer above it:\n\n```bash\ncat > shop/domain/recent.py <<'EOF'\nfrom shop.api.orders import list_recent\n\n\ndef recent():\n return list_recent()\nEOF\nenola check -fail-on cycles .\n```\n\n`check` takes a fresh snapshot of the working tree, compares it with the\npinned one, and prints what the change did:\n\n```text\nFAIL — 1 structural regression introduced.\ncould not see: 1 file and 1 directory excluded by ignore globs; 3 imports targets outside the graph\nlaw: 7 rules · 3 breaches · none excused\n\nRegressions (fail):\n - [cycles] 1.00 — Cyclic dependency detected (3 modules)\n module \"shop/api\" is part of the cycle\n```\n\nThe API already reached the domain through the services, so the domain\ncalling back closed a loop through three packages, which no single file\nshows. The `law:` line counts the example's three laws (seven rules once\ntheir recipes expand), and `could not see` names what the run did not\nmeasure (an ignored directory, imports of libraries outside the\nrepository). Undo the change before going on: `rm shop/domain/recent.py`.\n\n`check` exits 0 unless you set `-fail-on` (here, `cycles`), which names\nthe explainers whose new findings fail the build, or a law in `strict`\nmode is broken. `PASS` or `FAIL` leads the output. Three other words need\nreading before you trust it, and the output names each. **DECLINED**\n(exit 3) means the two snapshots are not comparable (a different enola\nversion, changed ignore globs, another repository), so re-pin the\nbaseline rather than reading it as a pass. A **partial verdict**\n(`PASS (partial verdict)` or `FAIL (partial verdict)`) means a provider\nor extractor ran on one side only: enola grades the facts both sides\nshare and names what it left ungraded. **unevaluable** on a law means the\nsnapshot could not answer it, named with its cause.\n[Reading a verdict](20-reading-a-verdict.md) has every section of the\noutput and every exit code.\n\n## 5. Tell your agents\n\nAn agent uses enola through its MCP server, which is the `enola` binary\nrun with no arguments. Register it once; for Claude Code 2.1.287 or later,\nwith its tools loaded when the session starts:\n\n```bash\nclaude mcp add-json enola '{\"command\":\"enola\",\"alwaysLoad\":true}'\n```\n\n[Hooks and agents](06-hooks-and-agents.md#register-the-mcp-server) has the\nline for Cursor, Codex, Copilot, opencode and Pi. Then the instructions\nand the hooks:\n\n```bash\nenola install --hooks --targets claude --dry-run # what it would write\nenola install --hooks --targets claude\n```\n\n`install` writes enola's instructions into the files your coding agents\nalready read (`.claude/rules/enola.md` for Claude Code) and, with\n`--hooks`, a session-start hook that pins the architecture as the\nsession's baseline and a stop hook that grades what the session changed.\nWithout `--targets` it writes for every agent it knows (Claude Code,\nCursor, Codex, Copilot, Pi, opencode). It always previews and asks before\nwriting; with no terminal it writes nothing and still exits 0, so a\nscript passes `--yes`. `enola uninstall` reverses it byte for byte.\n\nRestart the agent session, then:\n\n```bash\nenola doctor\n```\n\nreports whether the hooks actually fired, what they concluded, and whether\nthe pinned baseline can still be graded against. Until a session has\nstarted in the repository it says `NEITHER HOOK HAS EVER RUN`; that is\nthe expected answer before the restart, and the one to worry about after\nit. The session-start hook leaves a baseline you pinned by hand in\nplace, so after step 4 every session is graded against that pin; run\n`enola baseline clear .` to have each session graded from its own start.\nThe hooks are written with the absolute path of your `enola`, so\nbefore committing `.claude/settings.json` read\n[what to commit](06-hooks-and-agents.md#what-to-commit).\n\n## 6. Declare your first law\n\nA law is a sentence about two parts of your code, its reason, and how\nstrictly it holds. Write it in YAML in `enola/constraints/`; YAML is the\nform every team can write, whatever the language. In the example, the\nAPI should reach storage only through the services:\n\n```yaml\n# enola/constraints/first.yaml\ncomponents:\n - name: api\n match: [\"shop/api/**\"]\n - name: storage\n match: [\"shop/repositories/**\"]\nrules:\n - id: the-api-never-imports-storage\n forbid: api\n to: storage\n via: imports\n mode: ratchet\n because: \"a route goes through a service, so the service can change how orders are stored\"\n```\n\nOn your own repository, change the globs to two directories of yours.\nThen:\n\n```bash\nenola constraints lint . # is it valid, and what did each part match?\nenola check .\n```\n\n`lint` prints each part with the members it matched (`api 5\nmember(s)`); a part that matched nothing is the first thing to fix.\n`check` lists a law the change adds apart from what the change did, with\nevery breach the code already had:\n\n```text\nNewly declared (1) — a rule this change declares, re-forms or un-exempts\nreports these on code the change did not touch. The baseline the rule starts\nfrom, not regressions the change made:\n - [constraints] 1.00 — Constraint the-api-never-imports-storage violated: module-edge: shop/api -> shop/repositories -> shop/repositories via imports\n```\n\nThe breach is named by its witness: the edge between two modules\n(`shop/api -> shop/repositories`), then the module it lands in. A 0.40\nfinding beside it, `reached no verdict on 3 imports target(s)`, says some\nimports name libraries outside the graph; it fails nothing\n([reading a verdict](20-reading-a-verdict.md#outputs-that-surprise-a-first-reader)).\n\nPin again (`enola baseline pin .`) so the law and its one breach are the\nnew \"before\", and add an admin report that reads storage directly:\n\n```bash\nmkdir -p shop/api/admin && touch shop/api/admin/__init__.py\ncat > shop/api/admin/report.py <<'EOF'\nfrom shop.repositories.orders import recent_orders\n\n\ndef report():\n return len(recent_orders())\nEOF\nenola check -fail-on constraints .\n```\n\n```text\nFAIL — 2 structural regressions introduced.\n…\nRegressions (fail):\n - [constraints] 1.00 — Constraint shop/presentation-reaches-application-only violated: shop/api/admin/report.report -> shop/repositories/orders.recent_orders via calls\n - [constraints] 1.00 — Constraint the-api-never-imports-storage violated: module-edge: shop/api/admin -> shop/repositories -> shop/repositories via imports\n```\n\nYour law and one of the example's (`layered`, bound as `shop`) both fail\non the new code and stay quiet about the old breach. That is `ratchet`:\nit reports what already breaks the law and fails only what a change adds,\nwhich is the mode to start every law in. Because a breach is the edge\nbetween two modules, a second import along `shop/api ->\nshop/repositories` would not be new; a new package reaching storage is.\n\n**A shortcut, and when it applies.** `enola constraints init .`, after\na snapshot, writes a declaration binding every shipped recipe all of whose\nroles it finds a directory for. Those are mostly Rails directories, so a\nRails application with controllers, models, jobs, mailers, policies,\nserializers and view components gets `rails-conventions` and its\nneighbours; most other trees get `nothing to bind`, with the directories\neach recipe missed, and nothing is written (`--dry-run` prints the same\nwithout writing, `--recipe NAME` binds one). Bind a recipe to your own\ndirectories as [the Python guide](15-python.md) does, or write the law\nyourself as above.\n\nA Ruby team can write its laws in Ruby, parsed and never run:\n\n```ruby\n# enola/constraints/jobs.rb\nEnola.architecture \"shop\" do\n rails # declares controllers, models, jobs and the other Rails parts\n\n law \"background jobs never invoke controller code\" do\n jobs.must_not_call controllers\n why \"rendering from a job goes through ApplicationController.renderer\"\n mode :ratchet\n end\nend\n```\n\nIn a repository with no Ruby, do not start with a `.rb` law. It is the\nfirst Ruby file the snapshot sees, so the next `check` is partial and\nreports the law's old breaches as new. Use YAML. If you add one anyway,\nlist its breaches first (`enola --generate . && enola constraints ledger .`),\nthen pin again.\n\n[Writing laws](04-writing-laws.md) has the whole vocabulary, and\n[`examples/laws`](../examples/laws/README.md) has twenty-six to copy.\n\n## Where next\n\n- Your stack: [Rails](02-rails.md), [Ember](03-ember.md),\n [Python](15-python.md), [TypeScript](16-typescript.md), [Go](17-go.md),\n [Rust](18-rust.md).\n- [Constraints in CI](05-constraints-in-ci.md): the check on every pull\n request, with a template you copy.\n- [Rolling out](19-rolling-out.md): adopting it on a repository that\n already has years of history, without failing everything on day one.\n- [Hooks and agents](06-hooks-and-agents.md): what an agent does\n differently with enola connected.\n",
|
|
137
|
+
"guides/02-rails.md": "# A Rails application\n\nWhat enola sees in a Rails app, what the two Ruby providers add, and how\nto go from the first snapshot to laws the team wrote. The worked example\nis [`examples/rails-shop`](../examples/rails-shop): thirteen laws in the\nRuby DSL, one file each, with both providers configured.\n\n## What the snapshot holds\n\nFrom the Ruby extractor alone:\n\n- **Symbols** for every class, module and method, with their calls,\n constant references and one-level superclass.\n- **Routes** from `config/routes.rb` and every engine's route file,\n `resources` expanded, each route carrying its method and path and a\n `handled_by` edge to the controller action that serves it.\n- **Storage** facts for every `ApplicationRecord` descendant: the model,\n its table (declared or derived), its columns and foreign keys when the\n schema is present.\n- **Associations** with their macro, target and whether the target was\n declared (`class_name:`) or derived from the name.\n- **Module edges** between directories, weighted by the symbol edges\n behind them, which is what cycle detection reads.\n\nRun `enola --explain .` and then `enola --generate .`; the receipt in\n`.enola/receipt.json` lists what each extractor parsed and skipped.\n\n## The two providers\n\nThe Ruby extractor reads one file at a time, so it cannot tell which\nclass a constant names once nesting and inheritance are involved, or what\na class inherits through its mixins. Two providers settle that at snapshot\ntime, beside enola's own extraction. Both are built into the binary and\nrun against the Rubydex library, with no Ruby on the machine:\n\n- **Rubydex** resolves constants through nesting and inheritance, types the\n receiver of a call, and linearises every class's ancestry with mixins in\n order. It reads your bundle's gem paths when `bundle` is on PATH so\n `ApplicationRecord` and `ViewComponent::Base` resolve.\n- **Prism** adds the call edges a lexical walk can see: a call on the\n enclosing class, and `new` on a written constant. Named with no\n `command`, it reads the index Rubydex built in the same run.\n\n```yaml\nproviders:\n - name: rubydex\n - name: prism\n```\n\nThe install script and `enola upgrade` fetch the library the binary pins;\nafter `pip install enola-cli`, `go install` or a source build, run\n`enola providers fetch rubydex` once. Leave `expected_version` off a\nbuilt-in: the binary already pins the library, and a pin that disagrees\nwith it skips the provider.\n\nThe configuration lives in `mcp-arch.yaml` at the repository root, the\none file name enola reads without being told. Rubydex indexes a tree that\nhas a `Gemfile`. With both providers a law can say \"every class under\n`ApplicationRecord`\" and mean the resolved chain, not the text after `<`.\nThe [providers guide](07-providers.md) covers the census, the named skips,\nthe Prism script for a Ruby of your own, and the runtime capture.\n\n## Name the parts\n\n`enola constraints init .` writes `enola/constraints/recipes.yaml` binding\nevery shipped recipe all of whose roles your tree has a directory for. For\n`rails-conventions` that means `app/controllers`, `app/jobs`, `app/models`,\n`app/mailers`, `app/policies`, `app/serializers` and `app/components`; an\napp missing one gets the recipe named with the directories it lacked and\nnot bound, so write the binding yourself and leave the missing role out:\n\n```yaml\nuse_recipe:\n - recipe: rails-conventions\n as: rails\n mode: ratchet\n bind:\n controllers: { match: [\"app/controllers/**\"] }\n jobs: { match: [\"app/jobs/**\"] }\n models: { match: [\"app/models/**\"] }\n mailers: { match: [\"app/mailers/**\"] }\n policies: { match: [\"app/policies/**\"] }\n serializers: { match: [\"app/serializers/**\"] }\n view-components: { match: [\"app/components/**\"] }\n```\n\nTwo roles are optional and off until you bind them: `helpers` and\n`services`. Binding them brings three more laws (services and models\nnever reach helpers; the controller's request API stays out of models and\nservices). `rails-strict` is the same set with the concerns law added and\nnothing advisory. `vanilla-rails` is for an app that keeps Rails plain and\nwants its extra directories to stay empty with a stated reason each.\n\nTwo more recipes ship with enola and suit any Ruby tree. `ruby-conventions`\nforbids the `get_`, `set_` and `is_` prefixes other languages bring along;\n`supply-chain` forbids an unpinned dependency (see\n[writing laws](04-writing-laws.md#laws-over-dependencies)).\n\n`enola constraints lint .` shows every part with what it resolved to and\nevery rule a binding left unexpanded, which is how you check the tree\nbefore the first verdict.\n\n## The laws a Rails team writes\n\nThe recipe's laws, each with the reason it carries:\n\n- jobs do not reach controllers (advisory, because the sanctioned path\n through `ApplicationController.renderer` reads as a crossing);\n- models do not reach controllers;\n- policies only answer, never enqueue;\n- mailers do not enqueue;\n- serializers do not reach controllers;\n- view components do not enqueue and do not reach controllers;\n- maintenance tasks do not reach controllers;\n- with `helpers` bound: services and models never reach helpers;\n- with `services` bound: the request API (`params`, `session`, `render`,\n `redirect_to`, `cookies`, `flash`) stays in controllers, stated as\n receiver-less calls so `request.params` on a typed receiver is not a\n breach.\n\nWrite your own beside them in Ruby, in any file under `enola/constraints/`:\n\n```ruby\nEnola.architecture \"shop\" do\n rails\n part :billing, files: [\"app/billing/**\", \"app/models/invoice.rb\"]\n part :orders, files: [\"app/orders/**\", \"app/models/order.rb\"],\n public: [\"app/orders/public/**\"]\n\n law \"billing keeps to its own tables\" do\n billing.storage_must_stay_home\n why \"a part that writes another part's table owns a bug it cannot see\"\n mode :ratchet\n end\n\n law \"a mutating action is authorized\" do\n part :mutating_actions, files: \"app/controllers/**\",\n handles: [:post, :put, :patch, :delete]\n mutating_actions.must_reach :policies\n why \"every write passes a policy before it touches a record\"\n mode :ratchet\n end\nend\n```\n\nThe first law reads the storage facts: a member of `billing` reaching\n`Order` is a breach that names the table `orders`, and the suggested cut\nnames the `orders` part's public member that already uses the model. The\nsecond selects the code behind POST, PUT, PATCH and DELETE routes through\nthe `handled_by` edges and demands a call into the policies, so the law\nfollows the routes rather than a list of actions.\n\n## Packwerk\n\nA repository that splits into packwerk packages gets the `packwerk`\nexplainer once enola finds the packages. It reads `package_paths` from\n`packwerk.yml` and, when the file sets none, looks only at the root and\n`packages/*`, where packwerk itself searches every directory; with packs\nunder `packs/` or `components/`, name them:\n\n```yaml\n# packwerk.yml\npackage_paths:\n - packs/*\n```\n\nThe snapshot log says how many packages it found. The explainer compares\neach `package.yml`'s declared\ndependencies with what the package's code references, and reports both\nsides. An undeclared reference names its file and line at confidence 0.9,\nbecause a Ruby constant reaches its package by name; one already in\n`package_todo.yml` is counted, not reported. A declared dependency the code\nnever references is reported at 0.7, since a constant reached through\n`constantize`, a YAML class name or only a spec draws no edge. Gate on it\nwith `enola check -fail-on packwerk -min-confidence 0.9` once the todo\nfiles say what you mean.\n\n## Read a verdict\n\n```bash\nenola baseline pin .\n# change something\nenola check .\n```\n\nA constraint breach names the rule, the member, the edge and the reason,\nleads its suggested actions with the smallest change the graph can see,\nand says whether it is a decided-rule breach (exact membership over a\nmeasured edge) or an advisory. `enola constraints explain app/jobs/x.rb`\nanswers what a file is a member of, by which selector, and which edges it\nmakes, which is the question to ask before arguing with a verdict.\n\n## When the answer is \"cannot evaluate\"\n\nA law over resolved ancestry without the Rubydex provider, a budget\nwithout a runtime capture, a law across repositories in a one-repository\nsnapshot: each is reported as unevaluable with its cause and emits no\nverdict. That is the design. A law nobody could measure must not read as\nkept.\n\n## Where next\n\n- [`examples/laws`](../examples/laws/README.md): the Rails laws among the\n twenty-six (`rails-request-boundaries`, `rails-mutations-are-authorized`,\n `rails-strict-arrangement`, `foreign-keys`, `one-door`), each ready to\n copy.\n- [Writing laws](04-writing-laws.md): every sentence, the modes, and the\n carve-outs.\n- [Providers](07-providers.md): the census, the runtime capture, and a\n Prism script for a Ruby of your own.\n- [Constraints in CI](05-constraints-in-ci.md): the check on every pull\n request.\n",
|
|
138
|
+
"guides/03-ember.md": "# An Ember application\n\nThe same loop for the front end: what enola measures in an Ember app, and\nhow the conventions a team already writes in its style guide become laws.\n\n## What the snapshot holds\n\nThe TypeScript and Ember extractors read `.ts`, `.js`, `.gts` and `.gjs`\nand record, per class member, the things a convention talks about:\n\n- the **decorators** a member carries (`action`, `cached`, `tracked`, `task`),\n- the **fields it writes** (`args`, its own state, or any field at all),\n- whether a member **takes parameters**,\n- the **module a base class was imported from** (`@ember/controller` rather\n than the bare identifier `Controller`, which Stimulus also uses),\n- the **kind** of each member (getter, constructor, method),\n- **imports** by module path, including subpath imports from packages,\n- and for test files, the calls a test makes, as `test_ref` facts a\n component can name by kind.\n\nThat is why the conventions below are laws and not lint rules: a component\nis selected by what its facts carry, and a rule is set membership over\nmeasured edges.\n\n## The conventions, as a recipe\n\n[`recipes/ember-conventions.yaml`](../recipes/ember-conventions.yaml) in\nthis repository carries nine conventions an Ember team recognises, with\nthe reason for each: no `@action` decorator, no framework controllers, no\nrender-modifier imports, constructors do not fetch, component tests carry\nno fixtures, no legacy property accessor, arguments are not mutated, a\nmodifier takes its element, derivations do not write. Every role carries\nits selector as a default, so the binding is short. Copy the recipe into\nyour repository with [`enola-guides`](../README.md#these-guides-as-a-command),\nor download the file from this repository, and bind it:\n\n```bash\nenola-guides copy recipes/ember-conventions # writes enola/recipes/ember-conventions.yaml\n```\n\n```yaml\n# enola/constraints/ember.yaml\nuse_recipe:\n - recipe: ember-conventions\n as: app\n mode: ratchet\n```\n\nTwo roles are optional and yours to name, because their far end is a\nhelper of your own: `task-factories` (the functions that build tracked\ntasks in your app) and `fixture-setup` (the entry points of your fixture\nlibrary). Until you bind them the two laws that need them are left\nunexpanded, and `enola constraints lint .` says so:\n\n```yaml\n bind:\n task-factories: { match: [\"app/utils/**\"], name_pattern: \"*.trackedFunction\" }\n fixture-setup: { match: [\"tests/helpers/**\"] }\n```\n\nCopying the recipe, rather than pointing at a packaged copy, is the point:\nthe team reads the reasons, deletes a law it disagrees with, and reviews\nthe file on a pull request. A convention that appears from a tool is not\na convention.\n\n## An Ember app inside a larger repository\n\nWhen the app lives under a directory such as `frontend/`, override the\npath-rooted roles key by key; the roles selected by a decorator or a\nwritten field need nothing:\n\n```yaml\nuse_recipe:\n - recipe: ember-conventions\n as: app\n mode: ratchet\n bind:\n components: { match: [\"frontend/app/components/**\"] }\n integration-tests: { match: [\"frontend/tests/integration/**\"] }\n constructors: { match: [\"frontend/app/**\"], name_pattern: \"*.constructor\" }\n fetchers: { match: [\"frontend/app/services/**\"] }\n legacy-accessor: { match: [\"frontend/app/utils/**\"], name_pattern: \"*.get\" }\n element-modifiers: { kind: symbol, match: [\"frontend/app/components/**\", \"frontend/app/modifiers/**\"] }\n```\n\nOr keep a house recipe in `enola/recipes/` with your paths as the\ndefaults, which is what a team with several conventions of its own ends\nup doing anyway. A repository's recipe replaces a shipped one of the same\nname, and lint notes it.\n\n## Conventions the recipe cannot carry for you\n\nSome laws hinge on a helper only your app has: a getter that unwraps a\npromise through your helper must carry `@cached`, a reader of an async\nrelationship must pass it through your resolving helper. Write those in\nyour own recipe or inline, with your names:\n\n```yaml\ncomponents:\n - name: promise-getters\n kind: symbol\n match: [\"app/**\"]\n where: { symbol_kind: getter }\nrules:\n - id: promise-getters-are-cached\n require: promise-getters\n when_edge_to: [\"*.unwrapPromise\"]\n via: calls\n must_prop_contain: { prop: decorators, value: cached }\n mode: ratchet\n because: \"A getter that unwraps a promise recomputes on every read unless it memoizes, and the renderer re-renders on each new value.\"\n```\n\nAnd a guide rule for what no fact can decide, delivered where the edit\nhappens:\n\n```yaml\n - id: reach-for-an-existing-helper\n guide: components\n message: \"Before writing a helper here, look in app/helpers and app/utils first.\"\n mode: notify\n because: \"Duplication is a judgement about what two implementations mean, which no fact answers.\"\n```\n\n## What a breach looks like\n\n`no-action-decorator violated: Dashboard.toggle is measured in actions`,\nwith the file and line, the reason from the recipe, and the suggested\naction. A pull request that adds four breaches and fixes none reports\nfour findings; the 1,700 the codebase already carries stay out of the\nway under `ratchet` and stop growing.\n\n## Where next\n\n- [`examples/ember-app`](../examples/ember-app): an Ember application\n bound to this recipe in one block.\n- [Writing laws](04-writing-laws.md): every sentence and mode, and the\n carve-outs for a convention with one sanctioned exception.\n- [Constraints in CI](05-constraints-in-ci.md): the check on every pull\n request.\n- [Beyond one repository](08-beyond-one-repo.md): the app and the backend\n it calls as one graph, each client call matched to its route.\n",
|
|
139
|
+
"guides/04-writing-laws.md": "# Writing laws\n\nA law is a sentence about parts of your application, its reason, and how\nstrictly it holds. Laws live in `enola/constraints/`, in Ruby (`.rb`,\nparsed and never executed) or in YAML, and compile to the same\ndeclaration. This guide is the Ruby surface; every sentence names a rule\nform the YAML also accepts, and enola's\n[constraints reference](https://github.com/enola-labs/enola/blob/main/docs/CONSTRAINTS.md)\ngives each form in YAML. [`examples/laws`](../examples/laws/README.md)\nhas twenty-six complete laws, one directory each, to copy.\n\nRuby or YAML is a choice of reading, not of power: both compile to the\nsame thing, a repository may hold both, and a `.rb` law needs no Ruby on\nthe machine. In a repository with no Ruby, though, the first `.rb` file is\nthe first Ruby the snapshot sees and makes the next verdict partial\n([start here](01-start-here.md#6-declare-your-first-law) shows what that\ndoes), so write YAML there.\n\n## Parts\n\n```ruby\nEnola.architecture \"shop\" do\n rails\n part :billing, files: [\"app/billing/**\"], public: [\"app/billing/public/**\"]\n part :records, ancestor: \"ApplicationRecord\"\n part :getters, files: \"app/**\", kind: :symbol, where: { symbol_kind: \"getter\" }\n part :mutating_actions, files: \"app/controllers/**\", handles: [:post, :put, :patch, :delete]\n part :decided, governed_by: \"docs/decisions/*.md\"\n part :api, files: \"config/**\", kind: :route\n part :tables, files: \"app/models/**\", kind: :storage\nend\n```\n\n`rails` declares the conventional parts (controllers, models, jobs,\nmailers, policies, serializers, view components, helpers, services,\nconcerns) from the directories Rails keeps them in. A `part` takes\n`files` (bounded globs), `kind` (symbol, module, route, storage,\ndependency, and the reference kinds test_ref, file_ref, lint), `named` (one prefix, one\nsuffix or an exact name), `where` (a conjunction over measured props),\n`owns: :methods` (a member's methods count as the member's when an edge\nis walked), `ancestor` (every class under it, over resolved ancestry),\n`public` (the files that are the part's surface), `handles` (the code a\nroute of those methods reaches), and `governed_by` (the files a knowledge\npage anchors, with `status:` or `supersedes:` qualifiers). A part name\nwritten in snake_case is the component `snake-case`.\n\nQuote every value in a `where:` hash, booleans and numbers included:\n`where: { pinned: \"false\" }`. In v0.4.35 the Ruby surface reads only\nstrings and symbols there and drops any other value without a word, so\n`pinned: false` selects every package, pinned or not. `constraints lint`\nshows each part's selector as it resolved (`where pinned=false`), which is\nhow to see that a condition arrived.\n\n## Sentences\n\n| Sentence | What it states |\n|---|---|\n| `a.must_not_call :b` | no `calls` edge from a into b (`via :imports` on its own line in the law for another edge kind) |\n| `a.must_not_call \"params\", receiver: :none` | no receiver-less call to a literal |\n| `a.must_not_reach :b` | no path from a to b over any edge kind |\n| `a.may_only_call :b, :c` | a's edges land only in b and c |\n| `a.is_reached_only_by :b` | only b reaches a |\n| `a.stays_inside except: :b` | a's non-public members are reached only from inside a, or from b |\n| `a.must_reach :b` / `a.must_be_reached_by :b` | an edge exists, outbound or inbound |\n| `a.must_follow :b, :c` | a's members that reach b also reach c |\n| `a.must_define :call` / `a.must_define_one_of :call, :run` | every class member defines the method, or one of them |\n| `a.names_must_match \"*Job\"` / `a.names_must_match \"with_*\", requires: \"without_*\"` | a naming convention, or a pair |\n| `a.names_must_not_match \"get_*\"` | a forbidden prefix, suffix or name |\n| `a.must_be_empty` | the part matches nothing |\n| `a.at_most 12` | a cap on the membership (`growth 2` lets it exceed the baseline's count by two) |\n| `a.must_carry prop: \"decorators\", value: \"cached\"` | every member carries a prop (narrow with `when_calling` or `when_carrying`) |\n| `a.must_not_cycle_with :b, :c` | no dependency cycle among the parts |\n| `a.must_not_reach_includers` | a mixin never reaches a class that includes it |\n| `a.storage_must_stay_home` | every table a member reaches is the part's own |\n| `a.must_keep_budget metric: :queries, max: 20` | no frame in a exceeds the budget a runtime capture measured |\n| `a.must_have_consumer` | every route in a has a client somewhere in the cluster |\n| `a.must_be_unique_across by: :table` | no table is owned by two repositories |\n| `a.must_be_governed` | every file in a is anchored by a knowledge page |\n| `a.advises \"look in app/utils first\"` | guidance delivered where the edit happens, never a verdict |\n\n`via` is a word of the law, not of the sentence: write `via :imports` on\nits own line inside `law ... do`. In v0.4.35 a `via:` keyword written on\nthe sentence (`a.must_not_call b, via: :imports`) is dropped without a\nword and the law reads `calls`; the breach's witness ends in `via calls`\nor `via imports`, which is how to see which one arrived.\n\nA breach is named by its witness, the edge or fact that breaks the law.\nAn edge law's witness is the edge between two modules\n(`module-edge: shop/domain -> shop/api -> shop/api via imports`), so a second import\nalong an edge that already breaks the law is the same breach, not a new\none, and a ratchet law does not fail it.\n\n## Modes, dates and carve-outs\n\nEvery law carries `why` (`because:` in YAML): the reason is what a reader\nof a breach needs to judge it. `mode` decides how strictly it holds:\n\n| Mode | A breach is reported as | What fails `check` |\n|---|---|---|\n| `ratchet` (the default) | `Constraint <id> violated`, confidence 1.00 | only a breach the change added |\n| `advisory` | `Advisory constraint <id> violated`, confidence 0.90 | nothing, under the default confidence floor of 1.00 |\n| `strict` | `Strict constraint <id> violated` | every breach, including those the baseline already had, even with no `-fail-on` |\n\nStart every law in ratchet. A law that fails only what a change adds can\nbe adopted on a repository that breaks it in a hundred places; strict is\nfor a law decided to hold now, after the backlog is gone. Advisory stays\nsilent in the gate only while the gate's confidence floor is above 0.90:\n`-min-confidence 0.8` lets an advisory breach fail the build.\n[Constraints in CI](05-constraints-in-ci.md) says how the template sets\nthe floor.\n\n`since \"2026-08-01\"` dates a law against the architecture history: a\nbreach the revision at that date already carried is reported, one\nintroduced after it is graded.\n\n- Where the history holds no revision that old, git dates the breach\n instead, by the author date of the line that witnesses it (cached in\n `.enola/blame_cache.json`).\n- A breach git cannot date (no git, an uncommitted line, a shallow clone\n whose boundary is newer than the date) grades as if the law had no\n date, and the run says so once. A CI checkout that runs dated laws\n fetches enough history.\n\nWhen a breach should not fail the build, there are four answers, and the\nearliest one that is true is the right one:\n\n1. **Fix it.** The code is wrong and the law stands.\n2. **Exempt it.** The witness is decided to be outside the law, for good\n and with a reason. The exemption is written on the law, names the\n exact witness string the breach is titled with, and carries an owner,\n a reason and a date:\n\n ```ruby\n law \"controllers reach the schema through one door\" do\n controllers.must_not_call schema\n why \"a second door is a second place to forget authentication\"\n exempt \"Api::GraphqlController#execute -> AppSchema.execute via calls\",\n because: \"the one sanctioned entry point\", owner: \"platform\", since: \"2026-08-22\"\n end\n ```\n\n ```yaml\n exempt:\n - witness: \"Api::GraphqlController#execute -> AppSchema.execute via calls\"\n owner: platform\n because: \"the one sanctioned entry point\"\n since: \"2026-08-22\"\n ```\n\n An exempted witness is reported in its own `Exempted by declaration`\n section on every run and never fails. An exemption that matches\n nothing is itself a finding: the breach is gone, or the witness was\n mistyped. [`examples/laws/one-door`](../examples/laws/one-door) carries\n one.\n3. **Suppress it.** The breach is real, but a strict law must not block\n while it is worked off. A suppression is a signed, temporary entry in\n `.enola/suppressions.yaml`, committed (keep it out of `.gitignore`)\n and never written by enola:\n\n ```yaml\n entries:\n - rule: company-fk # or finding_title_prefix: \"…\"; exactly one\n owner: alice\n reason: \"legacy tables migrate in Q4\"\n date: \"2026-08-10\"\n ```\n\n A suppressed breach is reported under `Suppressed` and never fails;\n an invalid file is rejected whole.\n4. **Leave it in the baseline.** A ratchet law already ignores a breach\n the baseline had. Nobody decided anything; that is the reason to use\n one of the forms above for anything somebody did decide.\n\n`enola constraints ledger .` (below) shows, per law, how much is excused\nby each of these and by whom.\n\n## Recipes\n\nA recipe is a named bundle of laws over roles, bound to your directories\nin one block. enola ships `layered`, `clean`, `ports-and-adapters`,\n`cqrs`, `event-driven`, `modular-monolith`, `rails-conventions`,\n`rails-strict`, `vanilla-rails`, `ruby-conventions` and `supply-chain`;\n[`recipes/`](../recipes/README.md) has more to copy into your own\n`enola/recipes/`.\n\n```yaml\n# enola/constraints/layers.yaml\nuse_recipe:\n - recipe: layered\n as: shop\n mode: ratchet\n bind:\n presentation: { match: [\"shop/api/**\"] }\n application: { match: [\"shop/services/**\"] }\n domain: { match: [\"shop/domain/**\"] }\n infrastructure: { match: [\"shop/repositories/**\"] }\n```\n\nEach rule the recipe expands is reported under the binding's name\n(`shop/presentation-reaches-application-only`), and `constraints lint`\nsays how many rules each binding expanded. `layered`, `clean` and\n`ports-and-adapters` are alternative arrangements: bind the one your code\nfollows. `enola constraints init` binds every recipe whose roles it\nfinds, so on a large Rails application it can bind several at once, and\n`rails-conventions` beside `rails-strict` states some laws twice, in two\nmodes; `init --recipe NAME` binds one.\n\nA recipe's rules name one edge kind each, and `via:` matches that kind\nexactly. `layered`'s two domain rules\n(`domain-knows-no-infrastructure`, `domain-knows-no-presentation`) read\n`depends_on`, which the Python, TypeScript, Go and Rust extractors do not\nwrite; they write `imports`. On those trees the two rules are silent\nwhatever the domain imports, and the language guides state the domain's\nlaw over `imports` beside the recipe. The three rules over `calls` hold\nas written.\n\n## Laws over dependencies\n\nEvery direct dependency a manifest declares (`Gemfile`, `package.json`,\n`go.mod`, `Cargo.toml`, `pyproject.toml`, `requirements.txt`,\n`pubspec.yaml`) is a `kind: dependency` fact named by its Package URL\n(`pkg:gem/rails`), carrying the constraint as written, the version the\nlockfile resolved and `pinned`. `pinned` is absent, not false, when a\nlockfile sits beside the manifest that enola cannot read, so a law over it\nstays silent rather than wrong. The shipped `supply-chain` recipe is the\nlaw that every dependency is pinned:\n\n```yaml\nuse_recipe:\n - recipe: supply-chain\n as: supply\n bind:\n unpinned-dependencies: {}\n```\n\nor the same in Ruby:\n\n```ruby\npart :unpinned, kind: :dependency, where: { type: \"package\", pinned: \"false\" }\n\nlaw \"dependencies are pinned\" do\n unpinned.must_be_empty\n why \"a range is a different program on every build\"\n mode :ratchet\nend\n```\n\nA repository with no manifest enola reads gets a finding that the\nselector cannot be evaluated, never a silent pass. What a repository means\nto depend on, with a purpose for each, goes under `dependencies:` in its\n`enola-intent.yaml`, and the intent check reports the difference between\nthat list and what the manifests declare.\n\n## Reading what a law did\n\n```bash\nenola constraints lint .\nenola constraints explain app/billing/charge.rb\nenola check .\n```\n\n`lint` validates every declaration and shows what each part resolved to;\n`explain` answers, for one file, which parts admit it, by which selector,\nand which edges it makes; `check` grades the working tree against the\npinned baseline (`enola baseline pin .` first, or it exits 2 with `no\npinned baseline`). A law added since the baseline is listed under `Newly\ndeclared` with every breach it already has; that list is how to see what\na new law will say before it can fail anything. A breach leads its\nsuggested actions with the smallest cut the graph can see: the far\npart's public member with the same name, its public surface, the part\nthe offender's other edges mostly reach, the lightest edge of a cycle,\nthe owner's member that already reaches a table. When the facts support\nno cut, the action says so.\n\n## When a law refuses\n\nA law that the snapshot cannot answer is reported as unevaluable with its\ncause and emits no verdict: `no_resolved_ancestry` (no Rubydex provider\nran), `no_runtime_capture` (no capture for a budget), `no_counterparty`\n(one repository where the law needs two), `no_frame_in_members` (a\ncapture whose frames all lie outside the part, as a capture keyed by spec\nfiles does), `no_compiled_pages` (no knowledge pages in the snapshot), `unmeasured_property` (a `where` over a\nprop no extractor emits, with the nearest measured names suggested). Fix\nthe snapshot, not the law.\n\n## How much of the law is excused\n\n```bash\nenola constraints ledger .\n```\n\nreports each law's breaches beside the ones its exemptions and\nsuppressions excuse, each excuse with its owner, reason and age, and\nnames the excuses that now match nothing. A law that holds only because\neverything is exempt shows as exactly that. `--json` writes the same as\none document.\n\n## Mining laws you already keep\n\n```bash\nenola constraints mine .\n```\n\nproposes candidate laws from what the snapshot shows holds today (a\nnamespace every task already lives in, a prefix every public tool already\ncarries), each with the count it was mined at, so a team can adopt what\nit practises before it writes what it wants.\n\n## Where next\n\n- [Constraints in CI](05-constraints-in-ci.md): the laws graded on every\n pull request.\n- [Rolling out](19-rolling-out.md): which laws to start with on a\n repository that already breaks them, and when to enforce.\n- [Reading a verdict](20-reading-a-verdict.md): every section a breach can\n land in.\n",
|
|
140
|
+
"guides/05-constraints-in-ci.md": "# Constraints in CI\n\nThe check that grades every pull request against its base branch, reports\nwhat the change added, and never fails the build until the team decides it\nshould. On GitHub that is one step, [enola-action](#on-github-enola-action);\non any other CI, or where you want the comment grouped per rule,\n[`templates/enola-check.sh`](../templates/enola-check.sh) is the script and\nmost of this guide is why it looks the way it does.\n\n## On GitHub: enola-action\n\n```yaml\n# .github/workflows/architecture.yml\non: pull_request\npermissions:\n contents: read\njobs:\n architecture:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n with:\n fetch-depth: 0\n - uses: enola-labs/enola-action@v2\n with:\n version: 0.4.35\n fail-on: constraints\n warn-only: true\n```\n\nIt resolves the pull request's exact base commit, generates both sides on\nthe runner, annotates the lines that introduced a finding, and writes the\ndelta to the job summary. `version` pins the release; left out, it is\n`latest`, and a check that passed yesterday can fail today with no change\nof yours. `pr-comment: true` also posts the report as one comment edited\nin place (it needs `pull-requests: write`), and `sarif: true` writes a\nSARIF 2.1.0 file for code scanning, exposed as the `sarif-file` output.\nIts `status` output is one of `clean`, `partial_clean`, `regression`,\n`partial_regression`, `usage_error` and `incomparable`, and `partial`\nsays whether the verdict graded only the producers both sides share.\n\nThe action downloads enola but not the Rubydex library. A repository with\nRuby installs both, pinned, and hands the action that binary; without it\nthe Ruby is graded without its providers on both sides alike, so the\nverdict looks whole and quietly misses everything Rubydex resolves:\n\n```yaml\n - name: Install enola 0.4.35 and the Rubydex library it pins\n run: |\n v=0.4.35; a=enola-$v-linux-amd64\n curl -fsSLO \"https://github.com/enola-labs/enola/releases/download/v$v/$a.tar.gz\"\n curl -fsSLO \"https://github.com/enola-labs/enola/releases/download/v$v/$a.sha256\"\n sha256sum -c \"$a.sha256\"\n tar -xzf \"$a.tar.gz\"\n mkdir -p \"$RUNNER_TEMP/enola\" && mv \"$a\" \"$RUNNER_TEMP/enola/enola\"\n \"$RUNNER_TEMP/enola/enola\" providers fetch rubydex\n - uses: enola-labs/enola-action@v2\n with:\n binary: ${{ runner.temp }}/enola/enola\n fail-on: constraints\n warn-only: true\n```\n\nRemoving `warn-only` is the team's decision, made once the reports have\nbeen read for a while; [rolling out](19-rolling-out.md) says when.\n\n## Any other CI: the template\n\n[`templates/enola-check.sh`](../templates/enola-check.sh) and its renderer\n[`templates/enola-findings.rb`](../templates/enola-findings.rb) are the\nsame check as a script. `enola-guides copy templates/enola-check` writes\nboth to `ci/`, side by side; run `ci/enola-check.sh` as a CI step.\n\nWhat it needs from the CI:\n\n- **The full history** of the base branch and the change: on GitHub\n `fetch-depth: 0`, on GitLab `GIT_DEPTH: 0`. A shallow clone has no\n merge-base, and on CI the script fails the step saying so rather than\n skipping.\n- **A clean working tree**, since it checks out the merge-base to snapshot\n it.\n- `curl`, `tar` and `sha256sum`. Optional: `ruby`, to group the comment\n per rule (without it the comment is enola's own `check --summary`), and\n `gh`, to post the comment (without it the comment is printed).\n\nAnd what it reads:\n\n| Variable | Default | What it does |\n|---|---|---|\n| `ENOLA_VERSION` | the first line of a committed `.enola-version`, else `0.4.35` | the release it downloads, checks against its checksum and caches |\n| `ENOLA_CHECK_ENFORCE` | unset: report only | `1` makes it a gate that exits with `check`'s own code |\n| `ENOLA_CHECK_WATCHED` | `constraints,query-loops,layers,cycles` | the explainers whose new findings it watches (`-fail-on`) |\n| `ENOLA_CHECK_MIN_CONFIDENCE` | unset: enola's floor of 1.00 | lowers the floor; at 0.90 or below, advisory laws fail too |\n| `PR_NUMBER` | unset | the pull request to read the base from and comment on |\n\n## The shape\n\n1. **Pin a release.** The script downloads one version of the binary,\n verifies its checksum, and caches it per version and platform. A\n floating version is how a check that passed yesterday fails today with\n no change of yours. A committed `.enola-version` pins it in one place.\n2. **Grade against the pull request's base, not against main.** A stacked\n pull request whose parent declares new rules would otherwise report the\n parent's whole backlog as its own. Push-triggered builds often know the\n pull request number but not its base, so the script asks the forge for\n it and falls back to `main` only when there is no pull request at all.\n3. **Two snapshots, one verdict.** Check out the merge-base, generate;\n check out the head, generate; `enola check -baseline previous`.\n4. **Configure the providers the runner has.** In a tree with a `Gemfile`,\n the script fetches the Rubydex library the pinned binary names and\n configures the built-in `rubydex` and `prism` providers, which need no\n Ruby on the runner. A provider of your own (eslint, a runtime capture)\n is one more entry in the same block. Each absence is a named line in\n the log, never a silent drop, and the same configuration generates both\n snapshots and grades them, so they stay comparable.\n5. **Watch a few explainers, report only, then enforce.** By default the\n step runs `check -fail-on constraints,query-loops,layers,cycles\n -warn-only`: it reports what those explainers added and exits 0.\n Setting `ENOLA_CHECK_ENFORCE=1` is the team's decision, made once the\n comments have been read for a while; [rolling out](19-rolling-out.md)\n says when. The floor stays at enola's 1.00, so an advisory law (0.90)\n and an estimate (an inferred layer, a query reached through an\n interface) are reported and never fail. A strict law fails on every\n breach, new or not.\n6. **Render for a reader, grouped per rule.** The verdict's JSON becomes a\n comment with four sections in this order: **Watched** (the watched\n explainers' new findings at or above the floor, grouped per rule with a\n count and three examples, constraints first; under enforcement the\n headline is \"This blocks the merge.\"), **Below the floor** (the watched\n explainers' findings under it, which never fail), **Declared** (rules\n this change declares, reported on code it did not touch, the baseline\n each rule starts from) and **Also noticed** (everything outside the\n watched set). One comment per pull request, found by a marker and\n edited in place; a clean run updates an existing comment and posts\n nothing new.\n\n## Exit codes, and why \"did not run\" fails\n\n| `check` exits | The step | The comment |\n|---|---|---|\n| 0, clean | passes | posted, or an existing one updated |\n| 1, regression | passes while reporting only; fails under `ENOLA_CHECK_ENFORCE=1` | posted, naming what fails |\n| 2, error (no baseline, a bad flag) | fails on CI as \"did not run\" | none |\n| 3, declined (not comparable) | fails on CI as \"did not run\" | none |\n\nOn a laptop, 2 and 3 skip with the reason instead. \"Did not run\" means no\ngrade was produced, never a pass: fix what kept it from running, such as\na different enola version or different ignore globs on the two sides.\n\nWhen only the producers differ (a provider whose tool was missing on one\nside, or a language or the first Markdown file appearing in the change),\n`check` grades the facts both snapshots share and says the verdict is\npartial; the comment names each producer it left out and the side that\nlacked it, because a regression among those facts is not reported. The\nscript generates both snapshots with the same configuration, so a partial\nverdict on CI means the change added a kind of file the base did not\nhave, or a tool the runner lacks, not a misconfigured step.\n\nUnder `-warn-only` the JSON verdict's `status` reads `clean` even when\nregressions were reported, and enola-action's `status` output follows it;\nread the findings, not `status`, to know whether anything was found while\nthe check only reports.\n\n## Other shapes of the verdict\n\n`check` writes the same verdict in several shapes, chosen with `-format`:\n`text` (the default), `json` (what the template renders), `sarif` for code\nscanning, `annotations` for the CI's line annotations (`-host github` or\n`-host buildkite`, with `-link` naming the pull request's files view on\nBuildkite), and `summary`, a plain-language account for a reader who does\nnot know the explainer names. `summary-json` is that account as JSON. Each\nshape grades nothing of its own, so every shape exits with the same\ncode. [Reading a verdict](20-reading-a-verdict.md)\nhas every section.\n\n`-reviewers` adds who owns the modules the change touched, read from git\nauthorship, and suggests a reviewer when the author is a minor\ncontributor; it never fails the run.\n\n## Reproduce locally\n\n```bash\nbash ci/enola-check.sh # or wherever you keep it\n```\n\nThe script is written to run on a laptop: without the forge's credentials\nit skips the comment and prints the verdict, and it leaves the checkout on\nthe branch it found.\n\n## Adapting the template\n\nThe template targets a generic CI with three places you fill in, each\nmarked `FILL IN`: how the binary is fetched (a release asset URL and its\nchecksum), how the pull request's base branch is read (an environment\nvariable on pull-request builds, a forge API call on push builds), and how\nthe comment is posted (a small script against the forge's comments API).\nEverything between, the two snapshots and the verdict and the rendering,\nis the same on every CI.\n\n## Where next\n\n- [Rolling out](19-rolling-out.md): which laws to start with, and when to\n enforce.\n- [Reading a verdict](20-reading-a-verdict.md): every section of the\n output.\n",
|
|
141
|
+
"guides/06-hooks-and-agents.md": "# Hooks and agents\n\nenola is most useful when nobody has to remember to run it. An agent\ngets three things: the MCP server, so it can ask the graph; the\ninstructions and hooks `enola install` writes, so it asks before\nsearching and is graded when it stops; and, optionally, a skill. This\nguide sets up each and says what changes in the agent's work.\n\n## What the agent does differently\n\n| When | Without enola | With the server and `install --hooks` |\n|---|---|---|\n| The session starts | nothing | the architecture is pinned as the session's baseline, and the agent is told enola's tools are there |\n| It needs to know what depends on X | greps for the name, and misses a caller reached through an interface or a package in between | asks `impact_analysis` or `explore`; in Claude Code its first two tree searches are refused with a pointer to the tool that answers |\n| It is about to edit a file | reads the file | can ask `constraints_for` and `plan_check` which laws bind it and what the edit would breach |\n| The turn ends | reports it is done | the stop hook grades the change and speaks only when it introduced a regression, a finding no policy enforces, or could not be compared |\n\n## Register the MCP server\n\nThe server is the `enola` binary run with no arguments; the agent\nstarts it. Register it once per machine:\n\n| Agent | Register with |\n|---|---|\n| Claude Code 2.1.287 or later | `claude mcp add-json enola '{\"command\":\"enola\",\"alwaysLoad\":true}'` |\n| an older Claude Code | `claude mcp add enola enola` |\n| Codex | `codex mcp add enola -- enola` |\n| Copilot in VS Code | `code --add-mcp '{\"name\":\"enola\",\"command\":\"enola\"}'` |\n| Cursor | `{ \"mcpServers\": { \"enola\": { \"command\": \"enola\" } } }` in `.cursor/mcp.json`, or `~/.cursor/mcp.json` for every project |\n| opencode | nothing: `enola install --targets opencode` registers it in `.opencode/opencode.json` |\n| Pi | nothing: Pi has no MCP client, so `enola install --targets pi` writes `.pi/extensions/enola.js`, which serves every tool as `enola_<tool>` |\n\n`alwaysLoad` loads enola's tools when the session starts. Without it,\nClaude Code lists them by name only (deferred) and the agent spends a turn\nloading them through ToolSearch while a file search is already in reach;\nthe session-start hook tells it how, in one call. With a config file,\npass it in `args`\n(`'{\"command\":\"enola\",\"args\":[\"mcp-arch.yaml\"],\"alwaysLoad\":true}'`).\nAfter `enola upgrade`, restart the session so the server process is the\nnew binary.\n\n## The hooks\n\n```bash\nenola install --hooks --targets claude --dry-run # preview, write nothing\nenola install --hooks --targets claude\n```\n\nwrites enola's instructions into the file your agent already reads\n(`.claude/rules/enola.md`, `.cursor/rules/enola.mdc`, `AGENTS.md`,\n`.github/instructions/enola.instructions.md`, ...) and wires the hooks:\n\n- **session start** pins the architecture as the session's baseline, the\n \"before\" every later grade compares against. A baseline you pinned by\n hand is left in place; `enola baseline clear .` hands the choice back\n to the hook;\n- **stop** grades what the session changed against that baseline when the\n agent finishes a turn, and speaks only when the change introduced a\n regression or a finding no policy enforces, or when it could not compare\n at all (then the remedy is to re-pin, not to doubt the change). It says\n each distinct report once per session.\n\nIn Claude Code two more: a `PreToolUse` hook refuses the session's first\ntwo file searches over a tree (the `Grep` and `Glob` tools, or a `find`,\na recursive `grep` or an `rg` over a directory in the shell) with a\npointer to the enola tool that answers the question, and a `PostToolUse`\nhook records that an enola tool was used, which ends the refusals. A\n`grep` of files the agent names, or of piped output, is a read and goes\nthrough; a search run again after a refusal goes through. With\n`alwaysLoad` set, the refusals rarely fire.\n\n`install` without `--targets` writes for every agent it knows (Claude\nCode, Cursor, Codex, Copilot, Pi, opencode). It always previews every\nchange and asks before writing; with no terminal it writes nothing and\nstill exits 0, so a script passes `--yes`. It never creates a\nshared file such as `AGENTS.md` that was not already there, and\n`enola uninstall` reverses it byte for byte. `--global` writes the\nuser-level files instead (`~/.claude/settings.json` and\n`~/.claude/rules/enola.md` for Claude Code), for every repository you\nopen.\n\nIn a **folder of repositories** (a directory whose subfolders are git\nrepositories, such as a backend beside its clients), `--hooks` installs\nonly Claude Code's steering hooks: the session-start note, the search\nrefusal and the use recorder. A baseline needs one repository, so there\nis no stop hook there; run `install --hooks` inside each repository whose\nchanges should be graded.\n\nTwo agents get more than instructions, because they lack what the others\nhave. **Pi** gets the extension above, with or without `--hooks`, since\nwithout it Pi has no enola tools at all. **opencode** has no hook\nconfiguration of this shape, so `--targets opencode` registers the server\nin `.opencode/opencode.json` and, with `--hooks`, installs\n`.opencode/plugin/enola.js`, which names the enola tool in the\ndescriptions of `grep`, `glob` and `list` and refuses a session's first\nsearches (`ENOLA_OPENCODE_GATE=off` keeps the rest and drops the\nrefusals).\n\n### Did they fire?\n\nRestart the agent session after installing, then:\n\n```bash\nenola doctor\n```\n\nreports whether the hooks actually fired in this repository (when each\nlast ran and what it concluded), whether the pinned baseline can still be\ngraded against, whether the Rubydex library is in place, and whether a\nnewer release is out. Before any session has started it says `NEITHER\nHOOK HAS EVER RUN`; after a restart, that line means the agent is not\nreading the configuration. A hook that is configured but never fires\nlooks exactly like one that fires and stays quiet, and `doctor` is the\nonly thing that tells them apart. Each hook has a 60-second timeout; on a\nrepository where a snapshot takes longer (see\n[rolling out](19-rolling-out.md#1-measure-it-on-your-repository)), the stop hook can time\nout.\n\n### The Claude Code plugin\n\nenola's repository is also a Claude Code plugin marketplace:\n\n```text\n/plugin marketplace add enola-labs/enola\n/plugin install enola@enola\n```\n\nThe plugin registers the MCP server, the session-start and stop hooks, and\ntwo skills: one routing an architecture question to the tool that answers\nit, and `/enola:setup`, which checks for the CLI and runs the install\nscript when it is missing. It configures and does not install, so `enola`\nmust be on PATH. It carries neither the search refusal nor the\ntool-loading line `install --hooks` adds. Choose the plugin or\n`install --hooks`, not both: both register the same hooks, and the\nsession would be pinned and graded twice.\n\n## What to commit\n\n`install` writes the absolute path of the `enola` that ran it into the\nhook commands (`/home/you/.local/bin/enola hook stop`). Committed, that\npath is wrong on every other machine, and a hook that cannot start never\nbreaks a session, so nobody notices. Commit what the team shares and let\neach developer install the rest:\n\n| Commit | Do not commit |\n|---|---|\n| `enola/constraints/`, `enola/recipes/` | `.enola/`, except `.enola/suppressions.yaml` |\n| `mcp-arch.yaml`, `enola-intent.yaml` | the hook entries in `.claude/settings.json`, `.codex/hooks.json` |\n| `.enola/suppressions.yaml` | |\n| `.claude/rules/enola.md` and the other instruction files (they carry no paths) | |\n\nEach developer then runs `enola install --hooks --global` once, which\nputs the hooks in their own user settings with their own path, for every\nrepository they open.\n\n## Turning it off\n\n- One agent, or all: `enola uninstall` (`--targets` to choose, `--global`\n for the user-level files) reverses what `install` wrote.\n- The search refusal alone: in opencode, `ENOLA_OPENCODE_GATE=off`; in\n Claude Code it has no switch, so register the server with `alwaysLoad`\n (it then rarely fires) or remove the `PreToolUse` entry.\n- The update check: `ENOLA_NO_UPDATE_CHECK=1`.\n- The snapshot history under `~/.enola/`: `history: { enabled: false }`\n in `mcp-arch.yaml` stops recording it, and `enola gc` reports what it\n holds and trims it.\n\n## The MCP tools an agent should reach for\n\n| Before a change | |\n|---|---|\n| `explore` | what is in here and what touches it; the one to reach for first |\n| `impact_analysis` | what transitively depends on this, before touching it |\n| `endpoint_impact` | who calls this HTTP endpoint, and which screens sit behind the callers |\n| `traverse`, `find_path`, `show_symbol`, `query_facts` | how something is wired, instead of reconstructing it from files; `find_path` takes sets (`from_nodes`, `to_nodes`) to ask how any of one group reaches any of another in one search, and `show_symbol` takes several names in one call |\n| `governing_intent` | which decision pages govern this file or symbol, and which code a page governs |\n| `constraints_for`, `plan_check` | which declared laws bind a path, and what a planned edit would breach before it exists |\n| `set_baseline` | pin the architecture as it is, once, early |\n\n| After a change | |\n|---|---|\n| `generate_snapshot`, `diff_snapshot` | what the change actually did: findings introduced or resolved, coupling added; `generate_snapshot` takes `repo_paths` to index several repositories as one graph |\n| `query_insights` | the explainers' findings, rather than re-deriving them |\n| `snapshot_receipt`, `compare_receipts` | what a graph was generated over and where its limits are, and whether two snapshots are comparable enough to trust a diff between them |\n| `architecture_blame`, `architecture_history` | when a dependency entered the architecture, and how it changed over time |\n\n| Looking for trouble | |\n|---|---|\n| `find_orphans` | what nothing references any more |\n| `package_metrics` | how stable and how abstract a package is, and its distance from the main sequence |\n| `analyze_performance` | where the cost grows with the input |\n| `coverage_report` | which cross-repository edges resolved and which were missed |\n\n## The same questions from a shell\n\nA hook, a review bot or a person without an agent asks the graph through\nthe CLI. Each command reads the current snapshot, says when it was written\nand whether it matches the working tree, and takes `--json`:\n\n```bash\nenola impact 'kind:symbol OrdersController' . # what depends on it, by hop depth\nenola endpoint 'GET /api/v2/orders' . # the controller, models, tables and callers behind a route\nenola plan --paths app/jobs/receipt_job.rb . # the laws governing a path and its blast radius, before the edit\nenola flows . # the ways into the program, ranked by what each reaches\nenola constraints ledger . # per law, the breaches reported beside the ones excused\nenola dashboard --open . # the snapshot in a read-only local browser view\n```\n\n`plan --patch` evaluates the verdicts a diff would produce over a scratch\ncopy before it lands; `flows --through <symbol>` lists only the flows\nthat reach a symbol. Every flag takes one dash or two (`-paths` and\n`--paths` are the same). `enola help <command>` prints any command's\npage, and `--help` never runs the command it describes.\n\n## The skill\n\n[`templates/skills/enola/SKILL.md`](../templates/skills/enola/SKILL.md) is\nthe skill to drop into your agent's skills directory\n(`enola-guides copy skills/enola`). It says when to use the graph (a\nchange whose impact is not obvious, a change to how modules depend on\neach other, a hook or CI finding), which tool answers which question, and\nhow to read a finding: it names the structural change and the rule it\ncrossed, and a \"could not compare\" means the baseline is stale, not that\nthe change is wrong. `install` already writes most of that into the\nagent's instructions (`.claude/rules/enola.md`); add the skill when you\nwant it loaded only for architecture work, rather than in every session.\nThree more skills for any repository sit beside it: `enola-laws`,\n`enola-grade-before-push` and `enola-intent-first`.\n\n## Intent first, optionally\n\nTeams that keep decision records can compile them into the graph: a page\ncarrying an `enola_intent:` block with anchors (repository and path)\nbecomes an intent fact, `governing_intent` answers which pages govern a\nfile, and the `governed_by` component key and `require_governed` form turn\nthat into laws (see [writing laws](04-writing-laws.md)). A pre-edit hook\nthat reads `governing_intent` for the file about to change and puts the\ndecision trail in front of the agent is\n[the intent-first gate](10-hooks.md#the-intent-first-gate-pretooluse-on-edit-and-write);\nthe [knowledge-base track](09-a-knowledge-base-with-enola.md) builds the\nrest. Nothing above needs it.\n\n## Where next\n\n- [Rolling out](19-rolling-out.md): the hooks, the CI check and the laws\n adopted on a team, and what each costs.\n- [Knowledge-base hooks](10-hooks.md): the intent-first gate and the\n stop-time debt, for a team that keeps decision records.\n",
|
|
142
|
+
"guides/07-providers.md": "# Providers\n\nA provider contributes facts to the graph that no extractor measures: the\nresult of running another tool, a capture of the app at run time, a\nlanguage engine's resolution. enola runs the configured providers at\nsnapshot time, validates everything they emit against the fact schema,\nrefuses a provider's whole output on the first invalid line, and records\nin the receipt what each provider contributed and what it skipped.\n\n## Configure\n\n```yaml\n# mcp-arch.yaml\nproviders:\n - name: rubydex\n - name: prism\n - name: eslint\n command: [\"node\", \"/path/to/enola_eslint_provider.mjs\"]\n expected_version: \"0.1.0\"\n```\n\nenola runs an external provider twice: once with `--version`, to check\nthe pin, and once with the repository path. The provider prints its facts\nas JSONL on stdout and one line on stderr saying what it read and what it\nskipped, its census. A provider with no\n`command` is one the binary carries itself: `rubydex` and `prism`. An\n`expected_version` pins what the provider must answer, and a provider that\nanswers anything else is skipped by name; pin a script to its own\n`PROVIDER_VERSION`, and leave a built-in unpinned, since the binary pins\nits library. A configured provider whose tool is missing is a named skip\nin the receipt, never an error: the machine not having a tool must not\nfail the snapshot, it must be visible.\n\n## The Ruby pair\n\n**Prism** adds the call edges a lexical walk can see: a call whose\nreceiver is the enclosing class, and `new` on a written constant. Named\nwith no `command` it runs inside the binary and reads the index Rubydex\nbuilt in the same run, so it needs no Ruby. The reference script,\n[`enola_prism_provider.rb`](https://github.com/enola-labs/enola/tree/main/examples/providers/ruby/prism) in enola's repository,\nreads with Ruby's own parser on a Ruby 3.3 or newer; pin it to\n`expected_version: \"0.2.0\"`. The two are close, not identical: the\nbuilt-in also writes the calls a class body makes (`validates`,\n`before_action`), and writes the constant Rubydex resolved where the\nscript writes the one in the source.\n\n**Rubydex** resolves constants through nesting and inheritance, types the\nreceiver of a call when it resolves to a constant, and linearises each\nclass's ancestry with mixins in resolution order. It runs inside the\nbinary against an engine library at the version enola pins: the install\nscript and `enola upgrade` fetch it, and after `pip install enola-cli`,\n`go install` or a source build `enola providers fetch rubydex` does,\nverifying the published checksum and caching it under your user cache\ndirectory. `enola providers list` reports it. It indexes a tree with a\n`Gemfile`; without one the census names the skip. It indexes the workspace and, when `bundle` is on PATH, the\ngem paths the bundle reports, so a base class a gem declares resolves\n(and the 357 view components under a gem's base class become a fact). No\ngem enters your Gemfile.\n\nIts facts carry their own resolution level, `resolved`, so a law can ask\nfor exactly them: the `ancestor:` component key reads the resolved\nchain, and the `independent` form (a mixin never reaches its includers)\nrefuses by name without it.\n\n## Signatures and the autoload map\n\nTwo more reference providers in enola's\n[`examples/providers/ruby/`](https://github.com/enola-labs/enola/tree/main/examples/providers/ruby) read\nwhat a Ruby tree declares rather than what it runs. Each is a plain-Ruby\nscript, pinned to its own `PROVIDER_VERSION` (`0.1.0` for both):\n\n```yaml\nproviders:\n - name: rbs\n command: [\"ruby\", \"/path/to/enola_rbs_provider.rb\"]\n expected_version: \"0.1.0\"\n - name: zeitwerk\n command: [\"ruby\", \"/path/to/enola_zeitwerk_provider.rb\"]\n expected_version: \"0.1.0\"\n```\n\n- **rbs** reads RBS files, Sorbet `.rbi` files and inline `sig { }`\n blocks with a hand parser rather than the `rbs` gem, so its facts do not\n depend on which gem a machine has. It writes method contracts\n (`rbs-signature:`) and type declarations (`rbs-decl:`) at level\n `declared`, and the engine marks each extracted method a contract names\n `typed: true` with the files that declared it. A law can then require a\n declared signature on a public API. A declaration is a claim about the\n code, and the level says who made it.\n- **zeitwerk** maps every file under an autoload root to the constant\n Zeitwerk expects it to define, from the path alone, without booting\n the app: `app/models/billing/invoice_item.rb` to\n `Billing::InvoiceItem`, concerns collapsed. Its facts are\n `convention-derived`. A path it cannot camelize with confidence (a\n dash, an uppercase letter) emits nothing and is counted, since without\n boot no inflector override is known.\n\n## Reuse\n\nProvider facts enter the same cache as the extractors' facts, so a\nprovider whose inputs did not move is not run again. A script whose\noutput partitions by file says so with `files: per-file` and the\n`extensions:` it reads; enola then hands it only the files that\nchanged and keeps the rest. A provider that reads across files must not\ndeclare that, because the cache would serve facts computed against a tree\nthat has since changed. The built-in Rubydex provider keeps one entry for\nthe whole index, keyed by the library version, the Ruby files with their\ndigests and `Gemfile.lock`, and rebuilds it on any change rather than\npatching it. The receipt's `reuse` block says, per provider, what was\nreused and what was computed, and for the index which part of the key did\nnot match.\n\n## The census\n\nEvery provider states what it saw and what it skipped, and enola records\nit per provider in the receipt:\n\n```json\n{\"name\": \"rubydex\", \"version\": \"0.4.0\", \"fact_count\": 666713,\n \"census\": {\"files_seen\": 43297, \"declarations_parsed\": 2698018,\n \"constructs_skipped\": 2244152,\n \"skip_causes\": [{\"cause\": \"receiver resolves to no constant\", \"count\": 1798420},\n {\"cause\": \"unresolved constant reference\", \"count\": 28416}]}}\n```\n\nA skip has a cause; a provider never guesses. Read the census before\nreading a verdict that surprises you.\n\n## The runtime capture\n\nThe runtime provider reads captures an operator produced by running the\napplication: a booted-Rails capture (the final route table, reflected\nassociations, table bindings) and a query-counter capture (database\nqueries per frame under a spec run). Facts carry `runtime-observed` and\nthe channel they came from, and the engine cross-links them to the\nmeasured ones. [`capture_spec_queries.rb`](https://github.com/enola-labs/enola/tree/main/examples/providers/ruby/runtime) in enola's\nrepository is the runner for the query capture: require it from\n`spec_helper`, run the suite, and `.enola-runtime/queries.json` is written\nwith one frame per example. A `must_keep_budget metric: :queries, max: N`\nlaw then verdicts on it, and refuses by name until a capture exists.\n\n## Writing one\n\nA provider is any program that prints JSONL facts in the store's schema\nwith a `resolution_level` on every fact and a prefix on every name so it\ncannot collide with an extractor's identity, plus the one census line.\nThe Prism and eslint scripts in enola's\n[`examples/providers/`](https://github.com/enola-labs/enola/tree/main/examples/providers) are the reference; each is under two hundred lines. Give it a `PROVIDER_VERSION`\nit prints for `--version`, raise it whenever its facts change, and pin\nthat version in the configuration: a pin that no longer matches skips the\nprovider by name rather than mixing two versions' facts in one graph.\n\n## Where next\n\n- [A Rails application](02-rails.md): the Ruby pair in context, and the\n laws that need resolved ancestry.\n- [Writing laws](04-writing-laws.md#when-a-law-refuses): what a law says\n when the provider it needs did not run.\n- [Reading a verdict](20-reading-a-verdict.md#partial): why a provider\n present on one side only makes the verdict partial.\n",
|
|
143
|
+
"guides/08-beyond-one-repo.md": "# Beyond one repository\n\nA product is rarely one repository. enola indexes a cluster, several\nrepositories, into one union graph, links the seams between them (a\nclient's call matched to the route it reaches, a topic one publishes and\nanother consumes), and lets a law speak about them.\n\n## The cluster\n\nFor a folder whose subfolders are the repositories, one command writes the\nconfiguration:\n\n```bash\nenola cluster init ~/src/shop # writes ~/src/shop/cluster.yaml\nenola --generate ~/src/shop/cluster.yaml\n```\n\nA folder that is not itself a git repository and holds two or more of\nthem is indexed as a cluster without being asked: `--generate` and the\n`generate_snapshot` tool say they detected the repositories, write\n`cluster.yaml` there, and link them (`--no-cluster` indexes the folder as\none repository instead). Paths in the file resolve relative to it. When\nthe members do not share a folder, or the cluster carries settings of its\nown, write the file yourself. Any name works, since you pass its path;\nthese guides call it `enola-cluster.yaml`, and\n[`templates/enola-cluster.yaml`](../templates/enola-cluster.yaml) is one\nto copy:\n\n```yaml\n# enola-cluster.yaml\nrepos:\n - ../backend\n - ../mobile\n - ../admin\n - .\nignore:\n - \"**/node_modules/**\"\n - \"**/vendor/**\"\n - \"**/dist/**\"\noutput:\n dir: .enola-cluster\nproviders:\n - name: rubydex\n - name: prism\n```\n\n```bash\nenola --generate enola-cluster.yaml\n```\n\nEvery repository's facts land in one store, labelled by repository, and\nevery member receives the whole union. Two things to know: **regenerate\nthe whole cluster, never one member**, since a snapshot scoped to one\nrepository replaces the union rather than updating its slice; and name\nan output directory distinct from `.enola/`. The directory is resolved\ninside each member (`backend/.enola-cluster`, `mobile/.enola-cluster`),\nso with the default name the union overwrites each member's own\nsnapshot and the baseline pinned beside it.\n\n## What the linker draws\n\n- **Cross-repository dependencies**: a client's HTTP calls matched to a\n server's routes, GraphQL operations matched to a schema, shared code by\n import, each as a `repo -> repo` edge carrying the endpoints or imports\n that justify it.\n- **Route coverage**: every server route marked as matched or unmatched by\n the loaded clients, and every client call marked as matched or unmatched\n by the loaded servers. A client call names the server routes it resolved\n to (`matched_routes`) and the symbol whose body makes it (`caller`), so\n \"who calls this endpoint\" is a query, and `enola endpoint 'GET\n /api/v2/orders' enola-cluster.yaml` asks it directly. In a Next.js or\n React client a call the app never sends is marked so, and a route whose\n every call is like that carries `unreached_by_clients`: called in the\n source, never sent.\n- **What it missed**: `enola coverage` reports per service how many\n outbound calls it detected, resolved and left unresolved, and\n `--unresolved` lists each unresolved call with its file and line. An\n unresolved call is a repository you have not loaded, a third-party\n endpoint, or a blind spot; a service whose calls all go through an\n in-house wrapper reads as isolated until the wrapper is declared.\n- **Declared seams**: a repository may declare what it consumes from\n another on a knowledge page, and the intent check verdicts the\n declaration against what was measured, refusing when the counterparty is\n absent.\n\n## Your own HTTP client\n\nWhen every call goes through something like\n`http.sendRequest(serviceName, path, options)`, nothing says HTTP and the\ncalls are invisible. Declare the client in the configuration and the\nTypeScript extractor reads its calls as client routes like any other:\n\n```yaml\nclients:\n - name: sdk-http\n language: typescript\n receiver_types: [IHttpRequestService]\n methods:\n - name: sendRequest\n service_arg: 0\n path_arg: 1\n options_arg: 2\n verb_option: method\n default_verb: GET\n\nservice_aliases:\n resource-api: gateway\n```\n\nA declaration teaches the extractor to read a call site; it never draws an\nedge. The linker still links only to a loaded repository that serves the\npath, and `service_aliases` only chooses among repositories that already\ndo. `linking.match_literal_against_params: true` lets a server path\nparameter absorb a literal client segment, under conditions strict enough\nthat two candidate patterns match nothing. `service_aliases` and the\nlinking setting change the snapshot ID, so a baseline taken before either\nis not comparable.\n\n## Laws across the seam\n\n```ruby\nEnola.architecture \"platform\" do\n part :tables, files: \"app/models/**\", kind: :storage\n part :api, service: :backend, files: \"config/**\", kind: :route\n\n law \"one owner per table\" do\n tables.must_be_unique_across by: :table\n why \"two writers to one table disagree in the end\"\n mode :advisory\n end\n\n law \"every api route has a consumer\" do\n api.must_have_consumer\n why \"a route nobody calls is a surface nobody maintains\"\n mode :advisory\n end\nend\n```\n\n`service:` scopes a part to one repository by its label. Both laws refuse\nby name in a single-repository snapshot (`no_counterparty`): a table\nnobody else could share is not unique, and a route with no client in a\nsnapshot that loaded no clients is not unconsumed. `forbid` between parts\nin two services works with nothing new once the linker has drawn the edge.\n\n## Grading the cluster\n\n```bash\nenola baseline pin enola-cluster.yaml\n# change a member\nenola --generate enola-cluster.yaml && enola check enola-cluster.yaml\n```\n\nA cluster baseline is comparable only when every member ran with the\nsame enola, the same providers and the same ignore globs; a member that\ndropped out makes the pair incomparable and `check` declines, which is\nthe right answer when a whole repository's verdicts went quiet at once.\n\nIn CI that means checking out every member side by side, at the commits\nunder test, before generating; the [CI template](05-constraints-in-ci.md)\ngrades one repository, and these guides have no tested recipe for a\ncluster yet. For an agent, `enola install --hooks` in the cluster's folder\ninstalls only the steering hooks, since a baseline needs one repository\n([hooks and agents](06-hooks-and-agents.md#the-hooks)); install inside\neach member whose changes should be graded.\n\n## The history\n\nEvery snapshot appends a revision to the architecture history under\n`~/.enola/graphs`, so `enola log`, `enola blame` and `enola diff` answer\nwhen a dependency entered the architecture and what the cluster looked\nlike at a date, and a law with `since \"2026-08-01\"` grades only the\nbreaches introduced after it (git dates a breach the history is too young\nto know, as [writing laws](04-writing-laws.md) says). `enola history push` shares the record\nthrough a directory store and `enola history pull` imports what other\nmachines pushed, so a team's machines agree on the past; with\n`history.shared_dir` in the configuration the store can be left off both,\nand `blame` and `diff` answer over the shared record. `history gc\n--keep-last N` prints what it would prune from the shared store and\nprunes only with `--apply`; `enola gc` does the same for the local one.\n`history.dir` moves the local record, for instance into a directory you\ncommit.\n\n## Where next\n\n- [`examples/laws/cluster-laws`](../examples/laws/cluster-laws) and\n [`routes-have-consumers`](../examples/laws/routes-have-consumers): the\n laws above, ready to copy.\n- [A knowledge base with enola](09-a-knowledge-base-with-enola.md): a\n twenty-repository cluster with decision pages anchored to its code.\n",
|
|
144
|
+
"guides/09-a-knowledge-base-with-enola.md": "# A knowledge base with enola\n\nA team that keeps decisions beside its code (a wiki, ADRs, a design log)\ncan compile them into the graph. Then a file answers which decision\ngoverns it, a decision answers which code it covers, a structural claim\nin prose can be re-checked mechanically, and a finding judged once stays\njudged. This guide is that practice, as it runs day to day on a\ntwenty-repository knowledge base.\n\n> **An optional track.** Guides 09 to 12 are for a team that keeps\n> decision records and wants each anchored to the code it governs. They\n> assume enola set up as in [start here](01-start-here.md) and, for more\n> than one repository, a [cluster](08-beyond-one-repo.md). Nothing in\n> guides 01 to 08 needs them.\n>\n> They build the practice by hand, small enough to read and run without\n> any tool of its own; [`examples/knowledge-base`](../examples/knowledge-base/README.md)\n> is the smallest one the skills and hooks run against.\n> [tabula](13-tabula.md), in preview and not yet public, is the same\n> practice as one program.\n\n## Pages compile into the graph\n\nA markdown page carrying an `enola_intent:` block is an intent fact when\nthe snapshot is taken, and every anchor on it is an edge to a file:\n\n```yaml\n---\ntitle: Charges run through one service\nenola_intent:\n page:\n type: decision\n status: accepted\n scope:\n - shop\n relations:\n - rel: supersedes\n to: wiki/adrs/charges-in-controllers.md\n anchors:\n - repo: shop\n path: app/billing/charge.rb\n - repo: shop\n path: app/billing/refund.rb\n---\n```\n\nDerive the block rather than hand-maintain it: if your pages already cite\nthe code they describe (`sources:` lines naming repository paths), the\nanchors are those citations, and a small script stamps the block from\nthem and checks the derivation in CI. enola validates the block; keeping\nit truthful to your conventions is yours.\n\nIndex the knowledge base as a member of the cluster, last, so its pages\ncompile over the code they anchor:\n\n```yaml\nrepos:\n - ../shop\n - ../mobile\n - .\n```\n\n## Ask the graph what governs a file\n\n```bash\nenola plan -paths app/billing/charge.rb .\n```\n\nand over MCP, `governing_intent` for a file or a symbol: the pages whose\nanchors cover it, with their relation trails (what supersedes what, what\ndepends on what), and for a page, which code its anchors cover. A file no\npage governs is an answer too, and the difference between \"no page\ngoverns this\" and \"the graph was not asked\" must stay visible: a missing\nsnapshot is a named skip, never silence.\n\nThis is the call behind the [intent-first gate](10-hooks.md): before an\nagent's first edit into a repository, the governing pages are read.\n\n## Laws over the pages\n\nBecause anchors are facts, laws can read them (see [writing\nlaws](04-writing-laws.md)):\n\n```ruby\npart :billing, files: \"app/billing/**\"\npart :old_way, governed_by: \"wiki/adrs/*.md status:superseded\"\npart :new_way, governed_by: \"wiki/adrs/charges-in-one-service.md\"\n\nlaw \"billing is documented\" do\n billing.must_be_governed\n why \"code under a decision needs the page that decided it\"\nend\n\nlaw \"the new way never reaches the old\" do\n new_way.must_not_call :old_way\n why \"a superseded decision's code is on its way out, not a dependency\"\nend\n```\n\n## Receipts are the committed state\n\nGraph artifacts (`.enola/`, the cluster's output directory) are\nmachine-local and ignored. What enters version control is the receipt:\none entry per repository with the snapshot's fingerprint, the commit it\nwas taken at, the enola version and the providers that ran. Keep them in\none file the knowledge base owns:\n\n```json\n{\"repos\": {\"shop\": {\"fingerprint\": \"…\", \"git_commit\": \"…\", \"enola_version\": \"0.4.35\", \"providers\": [\"prism\", \"rubydex\"]}}}\n```\n\nTwo rules from [beyond one repository](08-beyond-one-repo.md) matter\nmore here, because a page cites what the union said: regenerate the\nwhole cluster, never one member, and read a declined grade as \"not\nasked\", never as clean.\n\n## Citations with a grammar\n\nA structural claim in prose (\"`internal/facts` has 157 dependents\")\ncites the receipt it was read from, in one fixed shape, so a script can\nre-check it against the committed receipts and flag the ones whose\ndigest moved. The shape is yours to choose; what matters is that there\nis one, that one extractor parses it, and that a periodic sweep lists\nevery citation with a verdict (verified, stale, malformed). A claim\nnobody can re-check is a sentence, not a fact.\n\n## Findings are judged once\n\nThe explainers report candidates, not verdicts: a cycle at 0.7\nconfidence, a god class, an unused route. Before a finding grounds a\npage, confirm it against the code and record the judgment in a ledger\nbeside the receipts, one entry per finding signature: accepted (and\nwhere it landed), rejected, or permanent noise. The next session reads\nthe ledger instead of re-deciding, and the periodic sweep reports a\nledger entry whose finding no longer exists, which is the code moving\npast a recorded judgment.\n\n## Pulled, with one push\n\nSkills fetch the graph while already reasoning about something: a\ndeepdive asks `impact_analysis` and `governing_intent`, an ingest checks\na claim with `query_facts` while the source is open, a drift sweep\nregenerates and diffs receipts. Nothing lands on a board because the\ngraph said so. The one exception is the session-end hook, which speaks\nonly when the session's change introduced a structural regression, or\nwhen it could not grade at all; a gate nobody remembers to run is no gate.\n\n## The wrapper\n\nEvery skill calls a thin command the knowledge base owns rather than\nthe binary: `kb enola generate | receipt | diff | findings | judge | impact\n| govern | citations | baseline | check | coverage | doctor`. It resolves\nthe binary, the cluster config and the receipts file, and when any is\nabsent it prints one line saying so and exits 0. That is what keeps a\nskill from failing on a machine without enola, and what keeps \"the graph\nwas not asked\" distinguishable from \"the graph agreed\". The [wrapper\nguide](12-the-wrapper.md) has its shape.\n\n## Where next\n\n[Knowledge-base hooks](10-hooks.md), then [the skills](11-the-skills-as-they-run.md)\nand [the wrapper](12-the-wrapper.md) they call.\n",
|
|
145
|
+
"guides/10-hooks.md": "# Knowledge-base hooks: the graph at the moments that matter\n\nA hook fires at a moment nobody has to remember: a session starting, the\nfirst edit into a repository, a turn ending, a push. Each one here reads\nthe graph at that moment, blocks at most once, and forces a reading,\nnever a verdict. The scripts are in [`templates/hooks/`](../templates/hooks);\nthey are written for Claude Code's hook protocol (JSON on stdin, exit 2\nto block with the message on stderr) and translate to any agent with\nhooks.\n\nPart of the [optional knowledge-base track](09-a-knowledge-base-with-enola.md):\nthese hooks hold an agent to a team's decision records. enola's own\nsession hooks, which pin a baseline and grade each turn in any\nrepository, come from `enola install --hooks` and are in\n[hooks and agents](06-hooks-and-agents.md#the-hooks); install those first,\nthen these beside them.\n\nWhat the four need:\n\n- **A governed repository.** A repository is governed when one of\n `ENOLA_DECISION_DIRS` exists at its root (space-separated; default\n `wiki docs/decisions docs/adrs`). A Markdown file under one of them is a\n decision record; anything else in that repository is code. In a\n repository with none of those directories the gate, the record and the\n code-moved debt do nothing, so installing them globally is safe.\n- `jq` on PATH for all four, and `python3` for the session refresh.\n- For the session refresh, the knowledge base's receipts and cluster\n config ([guide 09](09-a-knowledge-base-with-enola.md)): `RECEIPTS`\n (default `wiki/_state/enola/receipts.json`) and `CLUSTER` (default\n `enola-cluster.yaml`).\n\nCopy them into `.claude/hooks/`:\n\n```bash\nenola-guides copy hooks/intent-first-gate\nenola-guides copy hooks/record\nenola-guides copy hooks/stop-debt\nenola-guides copy hooks/session-refresh\n```\n\n> With [tabula](13-tabula.md) (in preview, not yet public),\n> `tabula install --harness claude` writes the gate, the record, the debt\n> and the session hooks this guide wires by hand, and `--uninstall`\n> removes exactly what it wrote. The hand-wired scripts below are the same\n> ideas, short enough to read.\n\n## The intent-first gate (PreToolUse on Edit and Write)\n\nThe first write into each repository per piece of work is blocked once.\nThe block carries two things: the pages that govern the file\n(`governing_intent`, or `enola plan -paths <file>` when the MCP is not\nin reach) and the pre-edit contract (the declared laws binding the path\nand the blast radius over the current snapshot). The agent reads, then\nretries, and the retry proceeds. The gate re-arms on every user message,\nso each ask gets its own forced stop and no piece of work gets two.\n\nWhat it refuses to do: decide. If the governing page contradicts the\nintended change, the contract says amend the page first; the hook only\nmakes the page impossible to miss. When no page governs the file, it\nsays so and asks whether the work should be shaped. When the graph is\nabsent, it says so and lets the write through; a missing graph is a\nnamed skip, never a wall.\n\n`templates/hooks/intent-first-gate.sh` gates Edit and Write. The gate the\nguides' author runs also classifies shell commands as writes (a `sed -i`, a\nredirect, a `cp` into the repository); the template marks where that\nclassifier goes and leaves it out, because the rules for what counts as a\nwrite depend on your tree.\n\n## The stop-time debt (Stop)\n\nWhen the turn ends, two debts can block it once each:\n\n- **Code moved, no spec moved.** Files changed in a governed repository\n this turn and no decision record in it did. The block asks for either the\n amendment or an explicit sentence, in the reply, saying why the pages\n as written already cover the change.\n- **A push went out with no grade.** A `git push` happened this turn and\n no `enola check` ran. This one applies in every repository. The block asks for the\n grade, read, before finishing.\n\nBoth read a small per-session record the PostToolUse hook writes\n(`templates/hooks/record.sh`): which files moved, whether a push\nhappened, whether a check ran. The debt hook is the mechanical half of\ntwo rules a team can otherwise only ask people to remember.\n\n## The union refresh (SessionStart)\n\nWhen the session starts, compare the paired repository's HEAD with the\ncommit the committed receipt records. When they differ, regenerate that\nmember's slice of the union in the background and say so in one line;\nwhen they agree, say that. The agent's first structural question then\nreads a graph that matches the checkout it is about to edit.\n\n## Wiring\n\nClaude Code, in `.claude/settings.json`, beside the entries\n`enola install --hooks` wrote (it already registers `enola hook\nsession-start` and `enola hook stop`; do not add them a second time):\n\n```json\n{\n \"hooks\": {\n \"SessionStart\": [{\"matcher\": \"startup|resume\", \"hooks\": [\n {\"type\": \"command\", \"command\": \"\\\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/session-refresh.sh\\\"\"}]}],\n \"PreToolUse\": [{\"matcher\": \"Edit|Write|MultiEdit\", \"hooks\": [\n {\"type\": \"command\", \"command\": \"\\\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/intent-first-gate.sh\\\"\"}]}],\n \"PostToolUse\": [{\"matcher\": \"Edit|Write|MultiEdit|Bash\", \"hooks\": [\n {\"type\": \"command\", \"command\": \"\\\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/record.sh\\\"\"}]}],\n \"UserPromptSubmit\": [{\"hooks\": [\n {\"type\": \"command\", \"command\": \"\\\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/record.sh\\\" reset\"}]}],\n \"Stop\": [{\"hooks\": [\n {\"type\": \"command\", \"command\": \"\\\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/stop-debt.sh\\\"\"}]}]\n }\n}\n```\n\n## What it feels like\n\nAn agent asked to change a billing service edits nothing until it has\nread the decision that put charges through one service; it grades the\nchange before the push and writes the grade into the pull request; and\nwhen it ends the turn having changed code under a decision without\ntouching the decision, it is asked to account for that once. None of\nthis needs the agent to know enola exists.\n\n## Where next\n\n[The knowledge-base skills](11-the-skills-as-they-run.md), which these\nhooks enforce, and [the wrapper](12-the-wrapper.md) they call.\n",
|
|
146
|
+
"guides/11-the-skills-as-they-run.md": "# The knowledge-base skills, as they run\n\nA skill is a named piece of practice an agent follows. Each of these asks\nthe graph one specific question at one specific moment; the value is the\nmoment, not the prompt. They are described here as they run on a\ntwenty-repository knowledge base, and shipped under\n[`templates/skills/`](../templates/skills) to copy. They call the\nknowledge base's [wrapper](12-the-wrapper.md), not the binary, so install\nit first. Part of the [optional knowledge-base track](09-a-knowledge-base-with-enola.md);\nthe four skills for any repository are in\n[hooks and agents](06-hooks-and-agents.md#the-skill).\n\n> With [tabula](13-tabula.md) (in preview, not yet public), one `tabula`\n> skill and the loop it runs (search, claim, govern, align, impact,\n> baseline, grade, report, release) take the place of the five skills\n> below; they stay here as the practice written out.\n\n## Before shaping work: the deepdive (`enola-deepdive`)\n\nShaping a change starts with three graph questions, answered before a\nline of the plan is written:\n\n- `impact_analysis` on every symbol the work would change: fan-in,\n fan-out, named callers. A symbol with 157 dependents is a different\n plan from one with 3.\n- `governing_intent` on the paths: which decisions already cover the\n area, so the plan amends a page rather than contradicting it.\n- `query_insights` for the area: what the explainers already computed (a\n cycle someone must have introduced, a layer crossed, a dead route), so\n the plan inherits judged findings instead of rediscovering them.\n\nThe plan cites the receipt it read from, so a later reader can re-check\nthe numbers.\n\n## Before building: the grade (`enola-grade-before-push`)\n\n`set_baseline` once, early; after the change, `generate_snapshot` and\n`diff_snapshot`; the delta (introduced, resolved, edges added) goes into\nthe pull request in one sentence. A declined comparison is not a pass.\nThis is also the [stop-time debt](10-hooks.md) hook's demand.\n\n## While ingesting a source: the claim check (`enola-ingest-check`)\n\nA page that states a structural fact (a stack, a public surface, a\nroute, a table) checks it while the source is open: `query_facts` for\nthe fact, `impact_analysis` for the count. A wrong claim ingested today\nis cited by a page tomorrow and reasoned from by an agent next week, so\nverification is cheapest now. For an incremental ingest, each changed\npath runs through `governing_intent`: the pages whose anchors cover a\nchanged file are exactly the synthesis that may have drifted, and a\nchanged file no page governs is a gap the ingest may be about to fill.\n\n## The periodic sweep: drift (`enola-drift`)\n\nOn a schedule, or before publishing: regenerate the cluster, diff the\ncommitted receipts (fingerprint and commit per member), and read\n`coverage_report` for which cross-repository edges resolved and which did\nnot. Receipts that moved name the members whose pages want a re-read.\nNot a comparison of findings between two runs; that is the grade's job.\n\n## Knowledge hygiene: citations and the ledger (`enola-citation-hygiene`)\n\nEvery receipt citation in prose gets a verdict against the committed\nreceipts: verified, stale (the digest moved), malformed. Stale ones are\nre-read, not rewritten. The finding ledger is swept the same way: an\naccepted finding whose evidence no longer exists in the graph is the\ncode moving past a judgment, and the page that cites it is re-read.\n\n## Stepping back: the blast radius (`enola-zoom-out`)\n\nWhen deep in a change, one call answers how it sits in the whole:\n`impact_analysis` for the symbols in hand, `find_path` between the\npart being changed and the parts that consume it, `governing_intent`\nfor the decisions around it. The answer is one paragraph of technical\nfit the agent can stand on, beside the product fit it reasons out.\n\n## Writing and explaining laws (`enola-laws`)\n\nCovered in [writing laws](04-writing-laws.md): lint before verdict,\n`constraints explain` before arguing with a verdict, ratchet before\nstrict.\n\n## What makes these skills and not prompts\n\nEach names the call, the moment, and the refusal: what the agent does\nwhen the graph is absent (say so, proceed, never pretend), when the\ncomparison declines (re-pin, never read as clean), when a finding is a\ncandidate (confirm against the code, judge once, record). The\n[hooks](10-hooks.md) enforce these rules; the skills explain them.\n\n## Where next\n\n[The wrapper](12-the-wrapper.md) every one of these skills calls.\n",
|
|
147
|
+
"guides/12-the-wrapper.md": "# The wrapper\n\nSkills call a command the knowledge base owns, not the binary. The\nwrapper is thin: it knows where the binary, the cluster config and the\nreceipts file live, it records receipts after a generate, and when\nanything is absent it prints one line saying so and exits 0. That is the\nwhole design, and it is what lets every skill run on a machine without\nenola without failing, while keeping \"the graph was not asked\" a\ndifferent sentence from \"the graph agreed\".\n\n[`templates/kb/kb`](../templates/kb/kb) is one, in Python 3 with the\nstandard library only. From the knowledge base's root:\n\n```bash\nenola-guides copy templates/kb # writes bin/kb; put bin/ on PATH\nkb enola doctor\n```\n\nIt reads three settings from the environment: `ENOLA_BIN` (the binary,\ndefault `enola` on PATH), `KB_CLUSTER` (the cluster config, default\n`enola-cluster.yaml`, else `cluster.yaml`) and `KB_KNOWLEDGE` (the pages\ndirectory, default `wiki`; receipts go in\n`wiki/_state/enola/receipts.json` and the ledger in `findings.json`\nbeside them). [`examples/knowledge-base`](../examples/knowledge-base/README.md)\nis a knowledge base it runs against. Part of the\n[optional knowledge-base track](09-a-knowledge-base-with-enola.md).\n\n> With [tabula](13-tabula.md) there is no wrapper to write: `govern`,\n> `impact` and `evaluate` ask the engine themselves and say \"not asked\"\n> rather than pass, which is the named skip this guide builds.\n\n## The operations\n\n| Op | Answers |\n|---|---|\n| `generate` | snapshot the cluster, then record every member's receipt |\n| `receipt [member]` | what was recorded |\n| `diff` | which members' live receipts drifted from the recorded ones |\n| `findings [--repo] [--all]` | the explainers' findings across the cluster, attributed to the member their evidence names, with judged ones hidden |\n| `judge <signature> accepted\\|rejected\\|noise [--reason TEXT] [--landed PAGE]` | record a verdict in the ledger |\n| `impact <symbol>` | fan-in, fan-out, named callers, read from the on-disk facts |\n| `govern <path or page>` | the pages governing a file, or the code a page anchors |\n| `citations` | every receipt citation in prose with its verdict |\n| `baseline pin\\|show\\|clear [member]` | the \"before\" a change is graded against |\n| `check [member]` | the grade, reported, never gating |\n| `coverage` | which cross-repository edges resolved |\n| `doctor` | whether the hooks fire |\n\n## The shape, in any language\n\nThe template is this, and a wrapper of your own in another language keeps\nthe same branches:\n\n```\nresolve binary -> absent: print \"enola: not installed, the graph was not asked\"; exit 0\nresolve config -> absent: print the path looked for; exit 0\nresolve receipts -> absent on a read op: print; exit 0\nrun the op -> on generate, read each member's .enola/receipt.json\n and write fingerprint, commit, version, providers\n into the receipts file\nexit codes -> the binary's, except check: report, exit 0\n```\n\nThree things to keep:\n\n- **Receipts are the only committed state.** `generate` writes them;\n nothing else does. `diff` compares them to the live ones; it is not a\n comparison of findings.\n- **Findings read from disk, not the MCP server.** `impact` and\n `findings` parse the fact and insight files, so scheduled and headless\n runs keep working when no server is up.\n- **`check` reports; it never gates.** The binary exits 1 on a\n regression and the wrapper does not propagate it: no graph-dependent\n step may stop work, so the verdict is surfaced for a person to weigh.\n Exit 3 from the binary means the snapshots were not comparable; the\n wrapper prints that as \"not asked\", never as a pass.\n\nA citation in prose has one grammar, `receipt <member>@<commit>,\nfingerprint <4 hex>…<4 hex>` (`receipt rails-shop@3f2a9c1, fingerprint\n8d1e…c40a`), and `citations` reports anything else that starts\n`receipt X@` as malformed. Members are labelled by their directory name.\n\n## The ledger beside the receipts\n\n```json\n{\"findings\": {\"constraints|Constraint background-jobs-never-invoke-controller-code violated: ReceiptJob#perform -> OrdersController via calls\": {\"verdict\": \"accepted\", \"at\": \"2026-08-22\", \"reason\": \"real; the receipt renders through the controller\", \"landed\": \"wiki/shop/permanent/architecture.md\"}}}\n```\n\nOne entry per finding signature, `<explainer>|<title>`, written on\njudgment. There is no\npending state: unjudged findings are observations, not a to-do list, and\nnothing enumerates them as work.\n\n## Why not call the binary directly\n\nEvery skill would then carry the absent-binary branch, the config\nresolution and the receipt bookkeeping, and they would drift. The\nwrapper is where those live once, which is why the skills say\n`kb enola impact` and never a path to the binary.\n\n## Where next\n\n[tabula](13-tabula.md), in preview: the same practice as one program.\n",
|
|
148
|
+
"guides/13-tabula.md": "# tabula: the knowledge base, kept by a tool\n\n> **Preview.** tabula is not public yet: the links to its repository and\n> releases will not open until it is, and nothing else in these guides\n> needs it. This guide is here so the practice it describes can be read\n> ahead of the release.\n\nGuides 09 to 12 build a knowledge base by hand: pages anchored to code, a\nwrapper, four hooks, five skills, receipts and a ledger. [tabula](https://github.com/misabegovic/tabula)\nis that practice as one program. It keeps the pages, checks them against\na contract, compiles their laws into enola constraints, grades a plan\nagainst the code with enola, and holds every agent that works on the\ncode to the page that governs it. This guide is what an enola user needs\nto know to start; tabula's own documentation under `docs/wiki/` goes\nfurther, and every verb prints the command to run next and why.\n\n## What it adds to enola\n\nenola measures what the code is and grades what a change did to it.\ntabula holds what the code should be, in pages a person writes:\n\n- **Architecture pages** (`.tabula`, `kind: architecture`) draw parts and\n arrows and state laws as sentences; `tabula architecture --write`\n compiles them into `enola/constraints/` in the repository, so the\n repository's own `enola check` enforces them with no tabula in CI.\n- **PRDs** (`kind: initiative`) carry acceptance bullets, each settled by\n an observation: a test to run, a count over the facts, or\n `grade: enola check clean`. `tabula evaluate` settles every bullet as\n met, not met, not checked or does not apply, with no model involved,\n and writes the verdict into the decision record beside the PRD with a\n receipt.\n- **The loop**: search before reading, claim before working, find the\n governing page before editing, grade before finishing, report what\n happened. Hooks refuse an edit until the agent has recorded the page it\n is working from.\n\n## Install\n\nA tabula release carries cardo inside the binary: a build derived from\nenola by tabula's author, the engine tabula is developed against. Download the archive for your platform from\nthe [releases](https://github.com/misabegovic/tabula/releases), check it\nagainst `SHA256SUMS`, and put `tabula` on your PATH:\n\n```bash\nsha256sum -c SHA256SUMS\ntar -xzf tabula-0.0.1-linux-amd64.tar.gz\ninstall -m 755 tabula-0.0.1-linux-amd64/tabula ~/.local/bin/tabula\ntabula doctor\n```\n\nv0.0.1 is built for linux-amd64 only. On any other platform, build from a\nclone with `cargo build --release` (the repository pins its Rust\ntoolchain); a plain build carries no engine and uses the one on your PATH.\n\ntabula looks for an engine in this order: `engine.binary` in\n`tabula.config.yml`, then `cardo`, `cardo-current` and `enola` on your\nPATH, and only then the cardo it carries, which it unpacks into its cache\nthe first time a verb needs it and checks against its digest. So an\n`enola` you installed for the other guides is the engine tabula runs,\nwith a warning on every line that carries its result. `tabula doctor` says which engine answered and how it\nwas found (`engine` and `carried` lines); set `engine.binary` to choose\none, or `engine.carried: never` to leave the carried copy out. tabula\nnever installs or upgrades an engine of its own accord.\n\n## The first run\n\n```bash\nmkdir shop-brain && cd shop-brain\ntabula init --tour\ntabula tour\n```\n\n`init --tour` writes an empty project (a `wiki/`, an `AGENTS.md` stating\nthe contract, `tabula.config.yml`) and a small inherited shop with the\nfacts an engine already read from it, so the tour runs without one.\n`tabula tour` names the one command to run next, read off the project\neach time: write a page, find it with `search`, `explain` the shop from\nits facts, `picture` its architecture page, then `evaluate` a PRD the\nshop does not meet yet, fix one line, and evaluate it again.\n\nFor your own code, name the repositories in `tabula.config.yml`\n(`projects_root`, `active_repos`) and let the engine draft a first page:\n\n```bash\ntabula explain shop --generate\n```\n\nwrites a page a newcomer can read, every sentence a claim with its file\nand line, and, when the repository has no architecture page yet, a draft\nat `wiki/shop/architecture/explained.tabula` that forbids nothing.\n\n## Laws, from a page to `enola check`\n\n```bash\ntabula say shop/architecture/shop.tabula \"orders never calls gateway.\" \\\n --because \"billing alone talks to the payment gateway.\"\ntabula architecture shop/architecture/shop.tabula --write\n```\n\n`say` writes the law under the page's `***laws***` and reads it back with\nwhat the code shows today, a count and never a verdict. `--write` puts\nthe compiled rules in the repository:\n\n```yaml\n# enola/constraints/shop.yaml\n# Compiled by `tabula architecture` from the page `shop/architecture/shop.tabula`; edit the page, not this file.\ncomponents:\n - name: orders\n match: [\"shop/orders.py\"]\n - name: gateway\n match: [\"shop/gateway.py\"]\nrules:\n # orders never calls gateway.\n - id: orders-never-calls-gateway\n forbid: orders\n to: gateway\n via: calls\n mode: ratchet\n since: 2026-10-25\n because: >-\n billing alone talks to the payment gateway.\n```\n\nThat file is plain enola YAML, and upstream enola v0.4.35 lints it clean.\nCommit it in the repository and grade it there exactly as\n[constraints in CI](05-constraints-in-ci.md) describes. A sentence with no\nstrength word is `ratchet`; `, as a warning.` is `advisory` and\n`, without exception.` is `strict`. A rule no sentence can say is written\nin enola's own form under `laws/` at the project root, and `tabula\nexport` copies it into the repository with the compiled pages.\n\n## A plan graded against the code\n\n```text\n***specs***\n\n- An order's total counts every line, discounts included.\n\n ***acceptance***\n\n - A discount line lowers the total.\n test: ~/projects/shop/tests/test_totals.py -- python3 -B -m unittest tests.test_totals\n - The change crosses no law.\n grade: enola check clean\n```\n\n`tabula evaluate shop/prds/totals-add-up.tabula` runs the test and asks the\nengine for `check --json --fail-on=constraints` on every repository the\nPRD anchors. A regression, whole or partial, is not met; no baseline,\nincomparable snapshots or no engine is \"not checked\", never a pass. A\nbullet not met exits 5, and the fix is in the code: tabula's rule is that\nthe bullet, its observation and the checked block are never edited to\nmake a verdict pass.\n\n## Agents\n\n```bash\ntabula install --harness claude # or agents, pi, codex\n```\n\nopens tabula's doors to one harness: for Claude Code, hooks in\n`.claude/settings.json`, a `tabula` MCP server in `.mcp.json` and a\nskill, each file recorded in `wiki/_state/install.json` so `--uninstall`\nremoves exactly what it wrote. The `agents` harness covers anything that\nreads `AGENTS.md` (opencode, goose, Cline, OpenHands, Amp). tabula's\nrepository is also a Claude Code plugin marketplace, under\n`editors/claude-code`, carrying the same server, hooks and skill plus the\n`.tabula` language server.\n\nThe skill's rule is one line: search before reading, claim before\nworking, evaluate before finishing. The loop it runs is\n`tabula run loop --flow`: search, claim, find the governing page with\n`govern`, record it with `align` (or write the intent first), check the\nblast radius with `impact`, pin a baseline, edit from the aligned\nbullets, grade, report, release. The edit gate refuses a write until\n`align` has recorded the page the change is generated from, and a write\nfrom the shell is caught by git's status, not by reading the command.\n\ntabula's hooks and enola's (`enola install --hooks`) do different jobs\nand sit side by side in `.claude/settings.json`: enola's pin a baseline,\ngrade the session and turn the first searches toward the graph; tabula's\nhold the agent to the page. Install enola's first. In v0.4.34, running\n`enola install --hooks` again after tabula's were in place added enola's\nstop hook a second time, inside tabula's stop entry, so the session was\ngraded twice per turn. After re-running it, check `.claude/settings.json`\nholds one `enola hook stop`, and remove a copy if it holds two.\n\n## Which hand-built piece tabula replaces\n\n| Guides 09 to 12 | With tabula |\n|---|---|\n| a script stamping `enola_intent:` onto a page | intent lives under `intent:` on the page; `tabula export` writes the frontmatter mirror the engine's `mdintent` extractor reads |\n| laws in the Ruby DSL beside the page | `***laws***` sentences on the architecture page, compiled by `tabula architecture --write` (the Ruby DSL still works beside them) |\n| a receipts file and a citation sweep | receipts under `wiki/_state/receipts/`, re-checked by `tabula verify`; `tabula check` for citations, links and scope |\n| a findings ledger | the inbox, where only a person confirms or dismisses a finding, beside `enola constraints ledger` |\n| the wrapper (`kb enola …`) and its named skip | the verbs themselves: `govern`, `impact`, `evaluate`, each saying \"not asked\" rather than passing |\n| the intent-first gate | `tabula hook gate`, which refuses until `align` records the governing page |\n| stop-time debt | a stop hook that blocks until the change is aligned or reverted |\n| hand-wired hook settings | `tabula install --harness claude`, or the plugin |\n| the five knowledge-base skills | the one `tabula` skill and the loop's govern, impact, baseline and grade steps |\n\nThe hand-built path stays documented because it is what tabula does,\nwritten small enough to read and run without it.\n\n## Next\n\n- tabula's [tutorials and how-tos](https://github.com/misabegovic/tabula/tree/main/docs/wiki):\n explain your own repository, add a constraint to a repository, run\n tabula in CI, connect a harness over MCP.\n- [taha](14-taha.md): agents that run on a tabula project under a\n mandate, stopping to ask before anything is published.\n\n## Where next\n\n[taha](14-taha.md), in preview: agents run on a tabula project.\n",
|
|
149
|
+
"guides/14-taha.md": "# taha: agents under a mandate\n\n> **Preview.** taha is not public yet: the links to its repository and\n> releases will not open until it is, and nothing else in these guides\n> needs it. This guide is here so the practice it describes can be read\n> ahead of the release.\n\n[taha](https://github.com/misabegovic/taha), short for tabula harness, is a\ncommand-line tool that runs coding agents on a [tabula](13-tabula.md)\nproject and keeps them on track. You say what you want; taha routes it\nthrough tabula's intent pages (a pitch, a plan, a decision record, then\nthe build), runs the agent, settles routine questions with a council of\nreviewers, and stops to ask you before anything is published. taha\ndecides nothing tabula can decide: every gate it applies is a tabula\nverdict, and a bug in taha can at worst stop a run, never let a failed\ncheck through.\n\nThis guide is where taha meets enola. taha's own\n[first hour](https://github.com/misabegovic/taha/blob/main/docs/first-hour.md)\nfixes one bug in a sample project in ten stops with no model and no\naccount, and [the menu](https://github.com/misabegovic/taha/blob/main/docs/menu.md)\nlists every command by goal; start there.\n\n## Install\n\ntaha is not on npm yet, and the name `taha` on npm belongs to another\npackage, so `npm install -g taha` installs something else. Install from a\nclone, with Node 22.19 or newer, on Linux or macOS (Windows through WSL):\n\n```bash\ngit clone https://github.com/misabegovic/taha && cd taha\nnpm install\nnpm link\ntaha help\n```\n\ntaha runs the tabula `TABULA_BIN` names, else the newer of the tabula its\nplatform package carries and the one on your PATH. A clone has no platform\npackage, so [install tabula](13-tabula.md#install) first.\n\n## What a run is\n\nOne piece of work, an issue or a PRD slug, is one durable conversation\nkept in SQLite under `state/`, so a crash or a restart never repeats an\nact. The run claims the piece with `tabula claim`, attaches `tabula serve`\nover the project, and journals every committed step into the project's\njournal. Before a piece can finish `done` or open a pull request,\n`tabula evaluate` must read every acceptance bullet met; a PRD bullet\nwritten as `grade: enola check clean` is how enola's verdict becomes one\nof them.\n\nWhat the run may do is written in a mandate, `wiki/mandates/default.md`:\nthe acts it may take (`allow`, `ask`, `never`), its budget, whether it is\n`supervised` or `autonomous`, the repositories in scope, the test command,\nand when it expires. GitHub is reached through one connector whose only\nwrites are a branch, a push, a draft pull request and a comment; each is\njournaled before it runs, a pull request is always a draft, and taha has\nno way to mark one ready or merge it. Anything public waits in the inbox\nfor you:\n\n```bash\ntaha # what runs, what waits on you, what is next\ntaha approve <finding> # a yes to what waits\ntaha steer <run> <words> # change a run in flight\ntaha stop <run> # stop at the next safe point\n```\n\n## Adopting a repository that enola already measures\n\n```bash\ncd ~/src/shop\ntaha adopt --inside # or --beside --repo shop, to write nothing into it\ntaha doctor\n```\n\n`adopt --inside` adds the project's files to the repository, records the\nrepository's own checks so a push waits for them, and, for the code map,\nwrites `mcp-arch.yaml` and runs `cardo --generate` in the repository.\n`adopt --beside` writes nothing into the repository and leaves the\nsnapshot to you. `taha doctor` prints one line each for the layout, the\nmandate, the state folder, the schedule and the code map, and exits 0\nwhen all of them read `ready`.\n\ntaha calls the engine as `cardo`, by that name on your PATH; it does not\nfall back to `enola`. The cardo a [tabula release](13-tabula.md#install)\ncarries is unpacked into tabula's cache, and `tabula doctor` prints where\n(the `engine` line). Until cardo has an install of its own, link that copy\nonto your PATH for taha:\n\n```bash\nln -s \"$HOME/.cache/tabula/engine/<digest>/cardo\" ~/.local/bin/cardo\n```\n\nAn `mcp-arch.yaml` the repository already has is left as it is, so the\nproviders [guide 02](02-rails.md#the-two-providers) configures keep\nrunning.\n\n## Where enola's laws stay\n\ntaha adds no law of its own. The laws are the repository's\n`enola/constraints/`, written by hand or compiled from tabula's\narchitecture pages, and they are graded by the repository's CI with\n`enola check` as [guide 05](05-constraints-in-ci.md) describes, whoever\nor whatever wrote the change. taha's part is to make an agent's run reach\nthat CI only as a draft, after tabula's checks, with you asked first.\n\n## Where next\n\n[Constraints in CI](05-constraints-in-ci.md): the check every change\nreaches, whoever wrote it.\n",
|
|
150
|
+
"guides/15-python.md": "# A Python service\n\nWhat enola measures in a Python codebase, the laws a Python team states,\nand the one place the shipped recipes need a hand. The worked example is\n[`examples/python-service`](../examples/python-service): a FastAPI\nservice in four packages that breaks three laws on purpose, snapshotted\nby CI on every change to this repository.\n\n## Install\n\n```bash\npipx install enola-cli # or: uv tool install enola-cli; the command is enola\nenola --version\n```\n\n`enola` was taken on PyPI, so the project is `enola-cli`; it is the same\nbinary as the install script and the GitHub release. Install it as a\ntool, outside your project's environment; a bare `pip install` into the\nsystem Python is refused on current Debian and Ubuntu. Pin it with\n`enola-cli==0.4.35` when you need the release these guides were checked\nagainst. Then the loop from [start here](01-start-here.md):\n\n```bash\nenola --explain .\nenola --generate .\nenola baseline pin .\n```\n\n## What enola measures\n\nA tree is Python when it carries `pyproject.toml`, `setup.py`,\n`requirements.txt`, `Pipfile` or one of the tool configs, at the root or\nup to three levels down. Parsed with tree-sitter, never imported:\n\n- **Modules are directories.** `shop/api` and `shop/domain` are modules;\n a law's `match:` globs select files, and the module edges between\n directories are what cycles and layers read.\n- **Imports** are `imports` edges, each tagged `internal`, `external` or\n `stdlib`. Calls are `calls` edges from a function to what it calls,\n with the calls made inside a loop recorded on the caller for N+1\n hunting.\n- **Routes** from FastAPI and Flask, at their composed runtime path:\n `include_router(r, prefix=\"/p\")` folds `/p` onto every route `r`\n declares, across files.\n- **Storage** from SQLAlchemy models, each with its table name.\n- **Module-level code** (what runs at import time) is kept on a\n `file_ref`, an edge carrier rather than architecture.\n- **Tests** (`tests/`, `test_*.py`, `conftest.py`) are reference-only.\n\nWhat it leaves out on purpose: a route whose path is not a literal, a\nmount with a computed prefix (its routes keep their bare paths), routes a\nplugin registers at import time, and dispatch through `getattr`.\n\n## The laws\n\nThe `layered` recipe names four roles. Bind them to your packages:\n\n```yaml\nuse_recipe:\n - recipe: layered\n as: shop\n mode: ratchet\n bind:\n presentation: { match: [\"shop/api/**\"] }\n application: { match: [\"shop/services/**\"] }\n domain: { match: [\"shop/domain/**\"] }\n infrastructure: { match: [\"shop/repositories/**\"] }\n```\n\nIts three rules over `calls` hold on Python as written: the API reaches\nthe services only, services never call back into the API, repositories\nnever drive the services (`infrastructure-does-not-drive-the-domain`,\nwhich despite its name forbids infrastructure calling the application\nlayer). In the example, a route that reads a\nrepository directly breaks the first one:\n\n```text\nConstraint shop/presentation-reaches-application-only violated: shop/api/orders.list_recent -> shop/repositories/orders.recent_orders via calls\n```\n\n**Its two domain rules read `depends_on`, and Python's extractor writes\n`imports`,** so on a Python tree they are silent whatever the domain\nimports. `via:` names one edge kind exactly ([writing laws](04-writing-laws.md#recipes)\nsays why). State the domain's law over the edge Python has:\n\n```yaml\ncomponents:\n - name: domain\n match: [\"shop/domain/**\"]\n - name: storage\n match: [\"shop/repositories/**\"]\nrules:\n - id: the-domain-imports-no-storage\n forbid: domain\n to: storage\n via: imports\n mode: ratchet\n because: \"The domain is the part that outlives the database; a domain that imports a table cannot be tested or reasoned about without it.\"\n```\n\n```text\nConstraint the-domain-imports-no-storage violated: module-edge: shop/domain -> shop/repositories -> shop/repositories via imports\n```\n\n`ports-and-adapters`, `clean` and `modular-monolith` are the other\narrangements enola ships; read which `via:` each rule names before\nbinding one, and run `enola constraints lint .` after a snapshot to see\nwhat every part resolved to.\n\n## Dependencies\n\nEach direct dependency in `pyproject.toml` or `requirements.txt` is a\nfact (`pkg:pypi/fastapi`), pinned when the constraint is `==` or a\nlockfile resolves it. The `supply-chain` recipe is the law that every one\nis pinned:\n\n```yaml\nuse_recipe:\n - recipe: supply-chain\n as: supply\n mode: ratchet\n bind:\n unpinned-dependencies: {}\n```\n\n```text\nConstraint supply/dependencies-are-pinned violated: pkg:pypi/fastapi is measured in supply/unpinned-dependencies\n```\n\n## Explainers worth gating on\n\n- **`import-closure`** answers what `import shop` actually executes,\n including the `__init__.py` files no import names, and names the\n initialisations responsible for most of it: a barrel written for\n convenience that makes importing the public API load a scraping\n library. The barrels it names are gateable with\n `-fail-on import-closure`.\n- **`query-loops`** reports a call that reaches storage inside a loop\n over input, the N+1 candidates.\n- **`cycles`** between packages, which Python allows and an import at\n function scope hides from a reader.\n\nFor CI, [constraints in CI](05-constraints-in-ci.md) and enola-action\napply unchanged; the Rubydex step is for Ruby and can be dropped.\n\n## Where next\n\n- [Writing laws](04-writing-laws.md): every sentence, the modes, and the\n carve-outs.\n- [Constraints in CI](05-constraints-in-ci.md): the check on every pull\n request.\n- [Rolling out](19-rolling-out.md): adopting it on a codebase that already\n breaks the laws you want.\n- [Reading a verdict](20-reading-a-verdict.md): every section of the\n output.\n",
|
|
151
|
+
"guides/16-typescript.md": "# A TypeScript or JavaScript service\n\nWhat enola measures in a TypeScript or JavaScript codebase, the laws a\nNode team states, and the two places the code's shape decides what a law\ncan see. The worked example is\n[`examples/typescript-service`](../examples/typescript-service): an\nExpress service in four directories that breaks three laws on purpose,\nsnapshotted by CI on every change to this repository. Front ends have a\nguide of their own for Ember ([guide 03](03-ember.md)); Angular, Vue,\nNuxt, Svelte and Next.js are read by the same extractor.\n\n## Install\n\nenola is not on npm, and the npm package named `enola` is an unrelated\ntool that deletes files: do not `npm install -g enola`. Install the\nbinary once per machine:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh\nenola --version\n```\n\nor `pipx install enola-cli`, or a binary from the\n[releases](https://github.com/enola-labs/enola/releases) for Linux, macOS\nand Windows. Then the loop from [start here](01-start-here.md).\n\n## What enola measures\n\n- **Modules are directories**; `import … from` is an `imports` edge,\n tagged `internal` or `external`, and a call is a `calls` edge from the\n function that makes it.\n- **Server routes** from Express (mounts composed across files), NestJS\n (controller prefixes composed), InversifyJS, and file-based routing in\n Next.js, Nuxt and SvelteKit.\n- **Client calls** (`fetch`, axios, Angular's `HttpClient`, a wrapper\n another file exports) as client routes, which the cluster linker\n matches to a server's routes ([guide 08](08-beyond-one-repo.md)); an\n in-house client is declared under `clients:`.\n- **Storage** from Prisma, TypeORM and Drizzle, each with its physical\n table name; a Drizzle query flags the function doing I/O.\n- **GraphQL** operations and schema-first servers, **Kafka** and AsyncAPI\n contracts.\n\n## The laws\n\nBind the `layered` recipe to your directories:\n\n```yaml\nuse_recipe:\n - recipe: layered\n as: shop\n mode: ratchet\n bind:\n presentation: { match: [\"src/routes/**\"] }\n application: { match: [\"src/services/**\"] }\n domain: { match: [\"src/domain/**\"] }\n infrastructure: { match: [\"src/db/**\"] }\n```\n\n```text\nConstraint shop/presentation-reaches-application-only violated: src/routes.listRecent -> src/db.recentOrders via calls\n```\n\n**A handler's calls belong to the handler only when it has a name.** An\ninline arrow, `router.get(\"/recent\", async (req, res) => …)`, puts its\ncalls on the file's `file_ref`, which the edge rules do not walk, so the\nsame breach written inline reports nothing. Name the handler (an\nexported function the router registers) and the route also carries a\n`handled_by` edge to it, which is what `enola endpoint` and the\nroute-scoped laws follow.\n\n**The recipe's two domain rules read `depends_on`, and the TypeScript\nextractor writes `imports`,** so they are silent on a TypeScript tree\n([writing laws](04-writing-laws.md#recipes) says why). State the domain's\nlaw over imports:\n\n```yaml\ncomponents:\n - name: domain\n match: [\"src/domain/**\"]\n - name: storage\n match: [\"src/db/**\"]\nrules:\n - id: the-domain-imports-no-storage\n forbid: domain\n to: storage\n via: imports\n mode: ratchet\n because: \"The domain is the part that outlives the database; a domain that imports a schema cannot be tested or reasoned about without it.\"\n```\n\n```text\nConstraint the-domain-imports-no-storage violated: src/domain -> src/db/schema -> src/db/schema via imports\n```\n\nIn v0.4.35 an Express `router.post(\"/\", …)` mounted at `/orders`\nproduces no route, while `router.post(\"/create\", …)` comes out as\n`POST /orders/create`; a law or a coverage count over a router's root\npath does not see it yet.\n\n## Dependencies\n\nEach direct dependency in `package.json` is a fact (`pkg:npm/express`),\npinned when its constraint is one exact version or `package-lock.json` or\n`yarn.lock` resolves it. A `pnpm-lock.yaml` or `bun.lockb` beside the\nmanifest leaves `pinned` absent, since enola cannot read it, and a law\nover `pinned` then says nothing rather than something false. The example\nhas no lockfile, so its `^4.19.2` is a breach of `supply-chain`:\n\n```text\nConstraint supply/dependencies-are-pinned violated: pkg:npm/express is measured in supply/unpinned-dependencies\n```\n\n## A provider of your own\n\neslint's findings join the graph through the reference provider in\nenola's `examples/providers/js/eslint/`, a Node script under two hundred\nlines that reads the results file your lint step writes; configure it as\n[providers](07-providers.md) shows.\n\n## Where next\n\n- [Writing laws](04-writing-laws.md): every sentence, the modes, and the\n carve-outs.\n- [Constraints in CI](05-constraints-in-ci.md): the check on every pull\n request.\n- [Rolling out](19-rolling-out.md): adopting it on a codebase that already\n breaks the laws you want.\n- [Reading a verdict](20-reading-a-verdict.md): every section of the\n output.\n",
|
|
152
|
+
"guides/17-go.md": "# A Go service\n\nWhat enola measures in a Go codebase, and the laws worth writing where\nthe compiler already forbids an import cycle. The worked example is\n[`examples/go-service`](../examples/go-service): a chi service in four\npackages that breaks two laws on purpose, snapshotted by CI on every\nchange to this repository.\n\n## Install\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh\nenola --version\n```\n\nenola is written in Go, and a source build is `go install ./cmd/enola`\nfrom a clone of [enola-labs/enola](https://github.com/enola-labs/enola),\nwith Go 1.26 or newer and a C compiler for the tree-sitter bindings. The\nprebuilt binary needs neither. Then the loop from\n[start here](01-start-here.md).\n\n## What enola measures\n\n- **Packages are modules**, imports are `imports` edges between them, and\n calls are `calls` edges from the function that makes them, including a\n function used as a value (a dispatch-table entry, a callback).\n- **Routes** from `net/http`, gin, chi and the other routers, with prefixes\n composed across function boundaries; outbound HTTP calls as client\n routes; gRPC and Kafka on both sides.\n- **Loops**: a call made inside a loop over input is recorded on the\n caller, which is what `query-loops` reads for N+1 candidates.\n- **Multi-module repositories**: imports resolve through every `go.mod`\n in the repository, so a monorepo with one module per service, or a\n module nested in another, gets internal edges between them rather than\n external, unresolved ones.\n\n## The laws\n\nGo's compiler refuses an import cycle between packages, so gating on\n`cycles` enforces what the toolchain enforces first. The laws a Go team\nneeds are the ones no compiler states: which package may reach which.\n\n```yaml\nuse_recipe:\n - recipe: layered\n as: shop\n mode: ratchet\n bind:\n presentation: { match: [\"internal/web/**\"] }\n application: { match: [\"internal/checkout/**\"] }\n domain: { match: [\"internal/order/**\"] }\n infrastructure: { match: [\"internal/store/**\"] }\n```\n\n```text\nConstraint shop/presentation-reaches-application-only violated: internal/web.listRecent -> internal/store.RecentOrders via calls\n```\n\n**The recipe's two domain rules read `depends_on`, and Go's extractor\nwrites `imports`,** so they are silent on a Go tree\n([writing laws](04-writing-laws.md#recipes) says why). State the domain's\nlaw over imports:\n\n```yaml\ncomponents:\n - name: domain\n match: [\"internal/order/**\"]\n - name: storage\n match: [\"internal/store/**\"]\nrules:\n - id: the-domain-imports-no-storage\n forbid: domain\n to: storage\n via: imports\n mode: ratchet\n because: \"The domain is the part that outlives the database; a domain package that imports the store cannot be tested or reasoned about without it.\"\n```\n\n```text\nConstraint the-domain-imports-no-storage violated: internal/order -> example.com/shop/internal/store -> internal/store via imports\n```\n\nGo's own `internal/` rule stops another module importing your internals;\na `forbid` between two of your own `internal/` packages is the rule it\nleaves to you.\n\n## Dependencies\n\nA `require` in `go.mod` names an exact version, so every direct\ndependency is pinned and `supply-chain` holds; the example binds it and\nit reports that its part matches nothing. `// indirect` lines are the\nclosure and are not read.\n\n## Where next\n\n- [Writing laws](04-writing-laws.md): every sentence, the modes, and the\n carve-outs.\n- [Constraints in CI](05-constraints-in-ci.md): the check on every pull\n request.\n- [Rolling out](19-rolling-out.md): adopting it on a codebase that already\n breaks the laws you want.\n- [Reading a verdict](20-reading-a-verdict.md): every section of the\n output.\n",
|
|
153
|
+
"guides/18-rust.md": "# A Rust service\n\nWhat enola measures in a Rust crate, the laws a Rust team states, and two\nplaces where v0.4.35 reads less than the code says. The worked example is\n[`examples/rust-service`](../examples/rust-service): an Axum service in\nfour modules that breaks two laws on purpose, snapshotted by CI on every\nchange to this repository.\n\n## Install\n\nenola is not on crates.io. Install the binary once per machine:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/enola-labs/enola/main/install.sh | sh\nenola --version\n```\n\nor `pipx install enola-cli`, or a binary from the\n[releases](https://github.com/enola-labs/enola/releases). Then the loop\nfrom [start here](01-start-here.md).\n\n## What enola measures\n\n- **Crate and module directories are modules**; `use crate::store::…` is\n an `imports` edge, a call is a `calls` edge, a constructor call an\n `instantiates` edge, and a method in an `impl` a symbol with its\n receiver.\n- **Routes** from Axum, `.nest(\"/p\", other::router())` folded onto every\n route the nested router declares across files, and from\n `#[utoipa::path]` attributes, one route per declared verb.\n- **Complexity and loops** per function.\n\n## The laws\n\n```yaml\nuse_recipe:\n - recipe: layered\n as: shop\n mode: ratchet\n bind:\n presentation: { match: [\"src/web/**\"] }\n application: { match: [\"src/checkout/**\"] }\n domain: { match: [\"src/order/**\"] }\n infrastructure: { match: [\"src/store/**\"] }\n```\n\n```text\nConstraint shop/presentation-reaches-application-only violated: src/web.list_recent -> src/store.recent_orders via calls\n```\n\n**The recipe's two domain rules read `depends_on`, and Rust's extractor\nwrites `imports`,** so they are silent on a Rust crate\n([writing laws](04-writing-laws.md#recipes) says why). State the domain's\nlaw over the `use` edge:\n\n```yaml\ncomponents:\n - name: domain\n match: [\"src/order/**\"]\n - name: storage\n match: [\"src/store/**\"]\nrules:\n - id: the-domain-imports-no-storage\n forbid: domain\n to: storage\n via: imports\n mode: ratchet\n because: \"The domain is the part that outlives the database; a domain module that uses the store cannot be tested or reasoned about without it.\"\n```\n\n```text\nConstraint the-domain-imports-no-storage violated: src/order -> crate::store::latest_total -> src/store via imports\n```\n\n## Where v0.4.35 reads less\n\n- **A nested router built inline keeps its bare paths.**\n `.nest(\"/orders\", Router::new().route(\"/recent\", …))` records\n `GET /recent`, while the same router returned from a function,\n `.nest(\"/orders\", orders())`, records `GET /orders/recent`. The example\n uses the function form.\n- **A Cargo version with no operator counts as pinned.** `axum = \"0.7\"` is\n a caret range in Cargo, but without a `Cargo.lock` enola marks it\n `pinned: true`, so `supply-chain` does not report it; `\"^0.7\"`, `\"~0.7\"`\n and `\">=0.7\"` are read as ranges. Commit `Cargo.lock` for a binary\n crate, which pins every dependency whatever its constraint says. The\n example keeps the bare `\"0.7\"`, and its `EXPECTED` file records that\n the law is silent, so CI notices the day enola reads it.\n\n## Where next\n\n- [Writing laws](04-writing-laws.md): every sentence, the modes, and the\n carve-outs.\n- [Constraints in CI](05-constraints-in-ci.md): the check on every pull\n request.\n- [Rolling out](19-rolling-out.md): adopting it on a codebase that already\n breaks the laws you want.\n- [Reading a verdict](20-reading-a-verdict.md): every section of the\n output.\n",
|
|
154
|
+
"guides/19-rolling-out.md": "# Rolling out\n\nHow a team adopts enola on a repository that already has years of\nhistory and already breaks most of the laws it would like to state,\nwithout failing everything on day one and without a check nobody reads.\nThe order below is the one that works: measure, choose a few laws, report\nfor a while, then enforce.\n\n## 1. Measure it on your repository\n\n```bash\nenola --generate .\nenola baseline pin .\ntime enola check .\n```\n\nThe first snapshot says what enola sees (the receipt names what it\nskipped), and `check` with no change tells you what every later check\ncosts. On two repositories, with enola v0.4.35 on one Linux machine:\n\n| Repository | Files | Snapshot, cold | Peak memory | `check`, no change | `.enola/` |\n|---|---|---|---|---|---|\n| Mastodon (Ruby and TypeScript), shallow clone | 10,185 | 4.5 s | 315 MB | 3.4 s | 42 MB |\n| the same, with the `rubydex` and `prism` providers | | 7.6 s | 600 MB | | 82 MB |\n| a private Rails monolith | 20,102 | 33 s | 1.3 GB | 27 s | 189 MB |\n| the same, with the `rubydex` and `prism` providers | | 45 s | 2.0 GB | 33 s | 318 MB |\n\nA warm snapshot reuses what did not change (2.8 s on Mastodon). The CI\ntemplate generates two snapshots per run, the base's and the head's.\nCache `~/.cache/enola` between CI runs so the Rubydex library is fetched\nonce per release. enola's session hooks have a 60-second timeout; a\nrepository near that should leave the stop hook out and grade in CI.\n\n## 2. Choose a few laws, in ratchet\n\n`enola constraints init` binds every shipped recipe whose roles your\ndirectories have. On the monolith above that was five at once (`clean`,\n`layered`, `rails-conventions`, `rails-strict`, `ruby-conventions`): 38\nrules and 5,029 breaches, several of them the same law stated twice in\ntwo modes. Binding one recipe instead gave 12 rules and 146 breaches:\n\n```bash\nenola constraints init --recipe rails-conventions .\nenola --generate . && enola constraints ledger .\n```\n\n```text\nlaw: 12 rules (5 ratchet, 7 advisory) · 146 breaches · none excused\n```\n\nStart with one recipe, or with two or three laws the team already\nbelieves, each with its `because:`. Keep every one in `ratchet`: it\nreports what already breaks the law and fails only what a change adds,\nso 146 breaches, or 5,029, are the starting line rather than a red build.\n`constraints ledger` is the number to watch shrink.\n\n## 3. Report on every pull request\n\nAdd the check in report-only mode, as [constraints in\nCI](05-constraints-in-ci.md) shows (`warn-only: true` on the action,\n`-warn-only` in the template). Grading is against each pull request's\nbase, so a pull request reports only what it added. A pull request that\nadds a law shows that law's existing breaches under `Newly declared`,\nwhich is where a team argues about a law before it can fail anything.\n\nRun it like that for a few weeks and read the comments. A law whose\nreports nobody acts on is a law to delete or rewrite; a finding that is\nwrong is worth an issue upstream with the witness it named.\n\n## 4. Enforce, one step at a time\n\n- Remove `warn-only` with `fail-on: constraints` alone: the laws you\n chose fail only what a change adds.\n- Leave `-min-confidence` at its default of 1.00. At 0.90 or below, an\n `advisory` law fails too; [writing laws](04-writing-laws.md#modes-dates-and-carve-outs)\n has the table.\n- Add another explainer (`cycles`, `layers`) to `fail-on` only after its\n reports have been read the same way.\n- Make a law `strict` only when its backlog is zero and it should stay\n zero; until then a strict law fails every pull request for breaches it\n did not make. Date a law with `since` to grade only what was introduced\n after a day.\n- When a breach is decided rather than fixed, write an exemption on the\n law, with an owner and a reason. A suppression is for a strict law\n being worked off, and it expires in practice only when someone deletes\n it.\n\n## 5. Give the laws owners\n\nLaws are code the whole team is held to. Put\n`enola/constraints/`, `enola/recipes/` and `.enola/suppressions.yaml`\nunder `CODEOWNERS`, so a new law, a new exemption and a new suppression\neach get a reviewer who owns the rule. Every exemption and suppression\ncarries an owner and a date already; `enola constraints ledger .` lists\nthem with their age, and an exemption whose witness no longer matches\nanything is reported, so excuses do not outlive their reason.\n\n## 6. Agents\n\nEach developer registers the MCP server and runs\n`enola install --hooks --global` once ([hooks and\nagents](06-hooks-and-agents.md)). Commit the laws, the recipes, the\nsuppression ledger and the instruction files; do not commit hook entries,\nwhich carry a path to one machine's binary\n([what to commit](06-hooks-and-agents.md#what-to-commit)).\n\n## 7. Keep enola current\n\nPin the version in CI and move it on purpose: set the new version, run\nthe check on a branch, and read what changed before merging. A new\nrelease can read more of your code, so a law can start reporting breaches\nit could not see before; ratchet keeps those out of every pull request's\ngrade, since the baseline and the head are graded by the same binary. A\nbaseline pinned by another version is not comparable and `check`\ndeclines, which is the signal to re-pin.\n\n## Turning it off\n\nEvery piece comes out on its own and leaves nothing behind:\n\n- CI: delete the step.\n- Agents: `enola uninstall` (`--global` for the user-level install)\n reverses `install` byte for byte.\n- Laws: delete `enola/constraints/`; nothing else reads it.\n- Local state: delete `.enola/` and `~/.enola/`.\n\n## Where next\n\n- [Reading a verdict](20-reading-a-verdict.md): the output a team reads\n on every pull request.\n- [Writing laws](04-writing-laws.md): the laws after the first few.\n",
|
|
155
|
+
"guides/20-reading-a-verdict.md": "# Reading a verdict\n\n`enola check` prints one verdict: what a change did to the architecture,\ngraded against a baseline. This guide is every part of it, every exit\ncode, and the outputs that surprise a first reader. The examples are from\n[guide 01](01-start-here.md#6-declare-your-first-law)'s walkthrough on\n[`examples/python-service`](../examples/python-service), at its last\nstep (a new `shop/api/admin` package reading storage directly), on enola\nv0.4.35.\n\n## In plain words first\n\n```bash\nenola check --summary -fail-on constraints .\n```\n\nwrites the same verdict for a reader who does not know the explainer\nnames, in fixed sections: the result, what you changed, what got worse,\nwhat got better, what moved without getting better or worse, what the run\ncould not see, and what to run next.\n\n```text\nResult: fails\n Fails the build (constraints).\n\nWhat you changed\n Added 1 folder of code and 1 function or type (shop/api/admin).\n shop/api/admin/report now uses shop/repositories/orders.\n\nGot worse\n Fails the build. Breaks the project's rule\n \"shop/presentation-reaches-application-only\".\n at shop/api/admin/report.py:4\n Fails the build. Breaks the project's rule \"the-api-never-imports-storage\".\n at shop/api/admin\n```\n\nEvery form exits with the same code, so `--summary` is safe in a\nscript. The rest of this guide is the full form, which is\nwhat a CI log and the template's comment carry.\n\n## Exit codes\n\n| Code | Word | Means | Do |\n|---|---|---|---|\n| 0 | clean | nothing the policy enforces was introduced | nothing; with no `-fail-on` every run is 0 unless a `strict` law is broken |\n| 1 | regression | the change introduced a finding the policy fails on | read `Regressions (fail)` |\n| 2 | error | the gate could not run: no pinned baseline, a bad flag, an inverted pair | fix the command; it graded nothing |\n| 3 | declined | the baseline is not comparable with the current snapshot | re-pin, or fix what made them differ; it graded nothing |\n\n`-warn-only` turns a 1 into a 0 and keeps everything else: a declined run\nis still 3 and an error still 2, so a check that could not run never\nreads as a pass.\n\n## The header\n\n```text\nFAIL — 2 structural regressions introduced.\ncould not see: 1 file and 1 directory excluded by ignore globs; 3 imports targets outside the graph\nlaw: 8 rules · 6 breaches · none excused\n```\n\n- **`PASS` or `FAIL`** is the verdict under the policy you set with\n `-fail-on` and `-min-confidence`. With no `-fail-on`, it says\n `nothing enforced: no policy set` and passes, unless a `strict` law is\n broken: strict fails every run it is broken in, new breach or old.\n `-warn-only` reports it as `PASS (--warn-only)`.\n- **`could not see`** is what this run did not measure: files your ignore\n globs leave out, import targets outside the repository (a library, the\n standard library). It is a limit of the run, not a finding.\n- **`law:`** counts the laws in force, their breaches anywhere in the\n tree, and how many are excused by exemptions or suppressions.\n `enola constraints ledger .` has the same per law.\n\n## The sections\n\n| Section | What is in it | Fails the build? |\n|---|---|---|\n| `Regressions (fail)` | findings the change introduced, from an explainer in `-fail-on`, at or above `-min-confidence` (default 1.00) | yes, unless `-warn-only` |\n| `New findings (advisory — below the failure policy)` | findings the change introduced that the policy does not cover: another explainer, or a confidence under the floor | no |\n| `New findings (reported — no failure policy set)` | the same, when there is no policy at all | no |\n| `Newly declared (N)` | breaches of a law this change adds, re-forms or un-exempts, on code the change did not touch: where the law starts from | no |\n| `Exempted by declaration` | breaches a law's `exempt:` carves out, on every run, with owner and reason | no |\n| `Suppressed` | breaches `.enola/suppressions.yaml` excuses | no |\n| `Resolved by this change (N)` | findings the baseline had and the change removed | no |\n| `What changed`, `Added`, `New coupling` | the structural delta: symbols, dependencies and edges added or removed | no; new coupling is reported, never failed, because an added call is what ordinary work looks like |\n\nA finding reads `[explainer] confidence — title`, then where it is:\n\n```text\n - [constraints] 1.00 — Constraint the-api-never-imports-storage violated: module-edge: shop/api/admin -> shop/repositories -> shop/repositories via imports\n forbidden imports edge\n```\n\nThe title names the law and the witness: for an edge law, the edge\nbetween two modules, then the module it lands in.\n\nUnder 1.00 a finding is a candidate to verify, not a verdict; the output\nsays so under every list that holds one.\n\n## Partial\n\n```text\nFAIL (partial verdict) — 1 structural regression introduced.\n\nPartial verdict — the two snapshots were produced by different producer sets, so only\nfacts from producers present in BOTH snapshots were graded. This is NOT a full verdict.\n Graded over the shared producer set (3 families: manifests, mdintent, python).\n Excluded from grading: ruby (baseline lacks it) — 4 facts on the current side not graded.\n A regression among an excluded producer's facts cannot be graded here and is NOT reported.\n```\n\nA producer is an extractor or a provider. When one ran on one side only\n(a provider whose tool was missing on the CI runner, a language that\nappeared in the change, the first Markdown file in a repository that had\nnone), enola grades what both sides share and names what it left out. Two consequences:\n\n- A regression among the excluded facts is not reported.\n- A law's breach can be counted against the shared producers differently\n on each side, so a breach the baseline already had can appear as\n introduced. The output above came from adding a first `.rb` law to a\n Python repository: the `.rb` file is the first Ruby the snapshot sees,\n and the old breach of the new law was graded as a regression. Re-pin\n after a change that adds a language or a provider, then grade.\n\n## Declined\n\n```text\nDECLINED — refusing to grade: the baseline is not comparable to the current snapshot.\nThis is NOT a statement about your change.\n\nBaseline comparability:\n - ignore globs differ between baseline and current — the set of files parsed changed, so some deltas may be exclusion changes rather than code changes\n```\n\nExit 3. The two snapshots were made under different enola versions,\nignore globs or repositories, so their difference would describe how they\nwere made rather than what you edited. Re-pin the baseline, or, in CI,\ngenerate both sides with the same binary and configuration. Never read a\ndeclined run as a pass; the template and the action both report it as\n\"did not run\".\n\n## Outputs that surprise a first reader\n\n- **`reached no verdict on N imports target(s) naming nothing measured`**,\n at 0.40, beside a law over `imports`. Some imports name something\n outside the graph (a library, the standard library), and the law cannot\n say whether those cross it. It is advisory and fails nothing.\n- **A second breach along the same edge is not new.** An edge law's\n witness is the edge between two modules, so a ratchet law already\n broken by `shop/domain -> shop/repositories` does not fail another\n import along it.\n- **`Advisory constraint … violated` failed the build.** Advisory breaches\n report at 0.90; a `-min-confidence` under that, such as `0.8`, lets them\n fail. Keep constraints at the default floor, or run a second `check` for\n the explainers you want at a lower one.\n- **`no pinned baseline`, exit 2.** `check` grades against the pinned\n baseline; run `enola baseline pin .` first, or pass `--baseline\n previous` to compare with the snapshot taken before this one.\n- **`no mcp-arch.yaml … using built-in defaults`** on every command. No\n configuration file is needed; the line says none was found.\n- **`--explain` shows no breaches.** It describes the repository; laws are\n graded by `check`.\n- **A hundred `[engine]` lines.** Progress goes to stderr;\n `2>/dev/null` leaves the verdict.\n- **A law that matched nothing.** `enola constraints lint .` lists each\n part with its member count; a part with none makes every law over it\n hold trivially, and lint's count is where to see it.\n\n## Other shapes\n\n`-format` writes the same verdict as `text` (the default), `json` (what the\nCI template renders), `sarif` for code scanning, `annotations` for a CI's\nline annotations (`-host github` or `-host buildkite`), `summary` and\n`summary-json`. `-reviewers` adds who owns the modules the change\ntouched, read from git authorship; it never fails the run.\n\n## Where next\n\n- [Writing laws](04-writing-laws.md#modes-dates-and-carve-outs): modes,\n exemptions and suppressions, the answers to a breach.\n- [Constraints in CI](05-constraints-in-ci.md): the same verdict on every\n pull request.\n",
|
|
156
|
+
"recipes/README.md": "# Recipes\n\nA recipe is a named bundle of architecture laws over roles. A repository\nbinds it in its `enola/constraints/` declaration and the laws apply to the\ncode the roles select. The recipes here are starting points: copy one into\nyour repository's `enola/recipes/`, read every `because:`, change what your\nteam decides differently, and review it like any other file. A convention\nnobody on the team wrote is not a convention.\n\n## Binding a recipe\n\nRoles carry selector defaults (`match`, `kind`, `name_pattern`, `where`), so\na binding is the recipe's name and the mode:\n\n```yaml\nuse_recipe:\n - recipe: ember-conventions\n as: app\n mode: ratchet\n```\n\nOverride a role's selector when your tree differs, key by key:\n\n```yaml\nuse_recipe:\n - recipe: ember-conventions\n as: app\n mode: ratchet\n bind:\n components: { match: [\"frontend/app/components/**\"] }\n```\n\nA role marked `optional: true` carries no default because its selector is\nyours to name: the law that references it expands away until you bind it.\n`enola constraints lint` lists the rules left unexpanded for that reason.\n\nModes: `ratchet` reports what already breaks the law and fails only what a\nchange adds; `advisory` reports and never fails; `strict` fails on every\nbreach. Start with ratchet.\n\n## The recipes\n\nenola carries its own built-in recipes (`rails-conventions`,\n`rails-strict`, `vanilla-rails`, `ruby-conventions`, `supply-chain` and\nthe six arrangements: `layered`, `clean`, `ports-and-adapters`, `cqrs`,\n`event-driven`, `modular-monolith`), and a binding names them with nothing\nto copy. The others below are not built in: copy one with\n`enola-guides copy recipes/<name>`, which writes\n`enola/recipes/<name>.yaml`, or download the file. Each law is an existing\nenola form; each recipe is here because its laws held on a real Rails\nmonolith at a number a team can act on, given beside it.\n\n| Recipe | States | Held at |\n|---|---|---|\n| `rails-conventions` (upstream) | jobs, models, serializers and components never render; policies and mailers never enqueue; request API stays in controllers | five of seven laws with zero exceptions, three advisory at 134 crossings |\n| `rails-strict` (upstream) | the above plus no cycles among the Rails roles and independent concerns | 0 of 127 concerns breach |\n| `vanilla-rails` (upstream) | the directories the vanilla arrangement leaves empty stay empty | an arrangement a team chooses |\n| `ember-conventions.yaml` | nine Ember conventions selected by what the code carries | the neutral eight hold; the house ones stay out |\n| `data-ownership.yaml` | readers read the store only | advisory; bind your query objects |\n| `api-boundaries.yaml` | the public API is reached only by controllers; a mutating action reaches a policy | 736 unauthorized mutations reported, advisory |\n| `background-work.yaml` | maintenance tasks live in their namespace and never reach controllers | 0 of 3,250 task call edges reach a controller |\n| `tenant-foreign-key.yaml` | a table carrying the tenant column carries the foreign key | 3,720 declared, 0 new |\n\nTwo data-ownership laws, one owner per table and storage stays home, are\ndeclarations rather than recipe rules for now: they live as worked examples\nunder [`examples/laws/`](../examples/laws/README.md) because recipe expansion\nin the current release does not rebind the part a graph law names. They join\n`data-ownership.yaml` when it does.\n\n- `ember-conventions.yaml`: nine Ember conventions a team can adopt as laws,\n selected by what the code carries (a decorator, a written field, a base\n class's module) rather than by where it sits. Two optional roles are\n yours to bind: `task-factories` (the functions that make tracked tasks or\n tracked functions in your app) and `fixture-setup` (the entry points of\n your fixture library in tests).\n\n- `data-ownership.yaml`: readers (your query objects, bound by you) reach\n the store and nothing else.\n- `api-boundaries.yaml`: the API layer is entered through controllers, and a\n mutating action reaches a policy; bind `mutating-actions` to the part your\n routes reach with POST, PUT, PATCH or DELETE.\n- `background-work.yaml`: maintenance tasks live in the `Maintenance`\n namespace the runner resolves, and never reach controllers.\n- `tenant-foreign-key.yaml`: a template; replace `TENANT_COLUMN` and\n `TENANT_TABLE` and every table carrying the\n column must carry the key.\n\nLaws whose far end is a helper of your own (a promise-unwrapping getter\nhelper, an async-relationship reader) belong in your repository's recipe\nbeside these, with your names in them.\n",
|
|
154
157
|
"recipes/api-boundaries.yaml": "recipe: api-boundaries\nroles:\n - name: controllers\n match: [\"app/controllers/**\"]\n - name: policies\n match: [\"app/policies/**\"]\n - name: public-api\n match: [\"app/controllers/api/**\"]\n - name: mutating-actions\n optional: true\nrules:\n - id: public-api-reached-only-by-controllers\n protect: public-api\n owners: [controllers]\n via: calls\n mode: advisory\n because: \"The API layer is entered through a request so that authentication, rate limits and serialization run once, at the edge; an internal caller that reaches an API class directly skips all three and nobody reading the API can see it.\"\n\n - id: mutations-are-authorized\n require_edge: mutating-actions\n to: policies\n direction: outbound\n via: calls\n mode: advisory\n because: \"Every write passes a policy before it touches a record. Bind the actions that routes reach with POST, PUT, PATCH or DELETE (a part declared with handles: on those methods) and the law names each mutating action that reaches no policy.\"\n",
|
|
155
158
|
"recipes/background-work.yaml": "recipe: background-work\nroles:\n - name: tasks\n match: [\"app/tasks/**\"]\n - name: task-classes\n match: [\"app/tasks/**\"]\n kind: symbol\n where: { symbol_kind: \"class\" }\n - name: controllers\n match: [\"app/controllers/**\"]\nrules:\n - id: maintenance-tasks-live-in-their-namespace\n require_name: task-classes\n pattern: \"Maintenance::*\"\n because: \"The task runner resolves task constants from that namespace, which is the one its generator creates, so a task declared outside it never appears in the runner and cannot be run by anyone.\"\n\n - id: maintenance-tasks-never-reach-controllers\n forbid: tasks\n to: controllers\n via: calls\n because: \"A task runs from the runner or the console with no request around it, so controller code reached from one has nothing to read.\"\n",
|
|
156
159
|
"recipes/data-ownership.yaml": "recipe: data-ownership\nroles:\n - name: store\n match: [\"app/models/**\"]\n - name: readers\n optional: true\nrules:\n - id: readers-read-the-store-only\n allow: readers\n only: [store]\n via: calls\n mode: advisory\n because: \"A reader derives its answer from the store alone, which is what keeps it replayable against a snapshot; a reader that reaches a service or a client answers differently on a different machine. Bind your query objects or report classes as the readers.\"\n",
|
|
157
160
|
"recipes/ember-conventions.yaml": "recipe: ember-conventions\nroles:\n - name: components\n match: [\"app/components/**\"]\n - name: integration-tests\n kind: test_ref\n match: [\"tests/integration/**\"]\n - name: actions\n kind: symbol\n where: { decorators: action }\n - name: framework-controllers\n kind: symbol\n where: { superclass_module: \"@ember/controller\" }\n - name: constructors\n match: [\"app/**\"]\n name_pattern: \"*.constructor\"\n - name: fetchers\n match: [\"app/services/**\"]\n - name: legacy-accessor\n match: [\"app/utils/**\"]\n name_pattern: \"*.get\"\n - name: argument-mutators\n kind: symbol\n where: { fields_written: args }\n - name: element-modifiers\n kind: symbol\n match: [\"app/components/**\", \"app/modifiers/**\"]\n - name: state-writing-members\n kind: symbol\n where: { fields_written: \"<any>\" }\n - name: task-factories\n optional: true\n - name: fixture-setup\n optional: true\nrules:\n - id: no-action-decorator\n forbid_fact: actions\n because: \"An arrow function binds lexically and needs no decorator; the decorator predates it and the convention says to stop writing it.\"\n\n - id: no-framework-controllers\n forbid_fact: framework-controllers\n 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.\"\n\n - id: components-import-no-render-modifiers\n forbid: components\n to_name: [\"@ember/render-modifiers*\", \"ember-render-helpers*\"]\n via: imports\n 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.\"\n\n - id: constructors-do-not-fetch\n forbid: constructors\n to: fetchers\n via: calls\n 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.\"\n\n - id: no-legacy-property-accessor\n forbid: components\n to: legacy-accessor\n via: calls\n 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.\"\n\n - id: arguments-are-not-mutated\n forbid_fact: argument-mutators\n 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.\"\n\n - id: a-modifier-takes-its-element\n require: element-modifiers\n when_edge_to: [\"*.modifier\"]\n via: calls\n must_prop_contain: { prop: takes_parameters, value: \"yes\" }\n 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.\"\n\n - id: derivations-do-not-write\n forbid: state-writing-members\n to: task-factories\n via: calls\n 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.\"\n\n - id: component-tests-carry-no-fixtures\n forbid: integration-tests\n to: fixture-setup\n via: calls\n 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.\"\n",
|
|
158
161
|
"recipes/house-recipe-template.yaml": "# A house recipe: the conventions your team writes, with the selectors your\n# tree needs as role defaults, so enola/constraints/ binds it in one line.\n# Copy to enola/recipes/<name>.yaml, rename, and replace every law with one\n# of yours. A recipe with the same name as a shipped one replaces it.\nrecipe: house-conventions\nroles:\n - name: services\n match: [\"app/services/**\"]\n - name: entry-points\n kind: symbol\n match: [\"app/services/**\"]\n name_pattern: \"*#call\"\n - name: helpers\n optional: true\nrules:\n - id: services-answer-to-call\n require_defines: services\n method: call\n mode: ratchet\n because: \"A service has one door, so a caller never has to read it to find the verb.\"\n\n - id: services-keep-off-helpers\n forbid: services\n to: helpers\n via: calls\n mode: ratchet\n because: \"A helper is view code; a service that reaches one can no longer run off the request.\"\n",
|
|
159
162
|
"recipes/tenant-foreign-key.yaml": "recipe: tenant-foreign-key\nroles:\n - name: tables\n match: [\"db/schema.rb\", \"db/structure.sql\", \"app/models/**\"]\n kind: storage\nrules:\n - id: tenant-foreign-key\n require: tables\n when_prop_contains: { prop: columns, value: TENANT_COLUMN }\n must_prop_contain: { prop: fk_constraints, value: \"TENANT_COLUMN->TENANT_TABLE\" }\n mode: advisory\n because: \"A table carrying the tenant column references the tenant, so the database should say so with a foreign key; a row that outlives its tenant is the leak nobody notices until an export. 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.\"\n",
|
|
160
|
-
"templates/constraints-starter/background-jobs-never-invoke-controller-code.rb": "
|
|
161
|
-
"templates/constraints-starter/models-do-not-reach-controllers.rb": "
|
|
162
|
-
"templates/constraints-starter/policies-only-answer.rb": "
|
|
163
|
-
"templates/constraints-starter/the-request-api-stays-in-controllers.rb": "
|
|
164
|
-
"templates/enola-check.sh": "#!/bin/bash\n# Grades this pull request's architectural delta against its base branch and\n# reports what the change added: declared constraints, queries in loops, layer\n# violations, dependency cycles. Findings are reported, never blocking:\n# -warn-only keeps the exit code at 0. Removing it makes this a gate, which\n# is a team decision.\n#\n# Three seams are yours to fill, marked FILL IN below: how the pinned binary\n# is fetched, how the pull request's base branch is read on a push build,\n# and how the comment is posted. Everything between them runs the same on\n# any CI and on a laptop.\nset -euo pipefail\n\nENOLA_VERSION=\"${ENOLA_VERSION:-0.4.34}\"\nWATCHED_EXPLAINERS=\"constraints,query-loops,layers,cycles\"\nWATCHED_FLAGS=(-fail-on \"$WATCHED_EXPLAINERS\" -min-confidence 0.8 -warn-only)\n\nsay() { echo \"enola check: $1\"; }\n\nskip() {\n say \"skipped: $1. The architectural delta was not graded this build.\"\n exit 0\n}\n\n# On CI a missing prerequisite fails the step so the check cannot go quiet;\n# locally it skips.\nnotrun() {\n if [ -n \"${CI:-}\" ]; then\n say \"did not run: $1. The architectural delta was not graded this build.\"\n exit 1\n fi\n skip \"$1\"\n}\n\nwork=$(mktemp -d)\ncurrent_ref=\"\"\ncleanup() {\n if [ -n \"$current_ref\" ]; then\n git checkout -q \"$current_ref\" 2>/dev/null || true\n fi\n rm -rf \"$work\"\n}\ntrap cleanup EXIT\n\n# FILL IN 1: fetch the pinned binary. Cached per version and platform so a\n# laptop downloads once. Verify the checksum the release publishes.\nos=$(uname -s | tr '[:upper:]' '[:lower:]')\narch=$(uname -m)\ncase \"$arch\" in x86_64 | amd64) arch=amd64 ;; arm64 | aarch64) arch=arm64 ;; *) skip \"unsupported arch $arch\" ;; esac\nENOLA=\"${HOME:-$work}/.cache/enola/${ENOLA_VERSION}/enola\"\nif [ ! -x \"$ENOLA\" ]; then\n mkdir -p \"$(dirname \"$ENOLA\")\"\n asset=\"enola-${ENOLA_VERSION}-${os}-${arch}.tar.gz\"\n curl -fsSL \"https://github.com/enola-labs/enola/releases/download/v${ENOLA_VERSION}/${asset}\" -o \"$work/$asset\" || notrun \"could not download enola ${ENOLA_VERSION}\"\n curl -fsSL \"https://github.com/enola-labs/enola/releases/download/v${ENOLA_VERSION}/${asset%.tar.gz}.sha256\" -o \"$work/${asset%.tar.gz}.sha256\" || notrun \"could not download the checksum for enola ${ENOLA_VERSION}\"\n (cd \"$work\" && sha256sum -c \"${asset%.tar.gz}.sha256\" >/dev/null) || notrun \"checksum mismatch for ${asset}\"\n tar -xzf \"$work/$asset\" -C \"$work\"\n mv \"$work/${asset%.tar.gz}\" \"$ENOLA\"\n chmod +x \"$ENOLA\"\nfi\nsay \"enola ${ENOLA_VERSION} at ${ENOLA}\"\n\n# Providers the agent can run. Each absence is a named line, never silence,\n# and the same providers feed both snapshots so they stay comparable. Both\n# Ruby providers run inside the binary once the Rubydex library is fetched;\n# no Ruby is needed. The binary pins the library, so neither entry carries an\n# expected_version.\nprovider_entries=\"\"\nif [ -f Gemfile ]; then\n rubydex_fetch=\"$(\"$ENOLA\" providers fetch rubydex 2>&1 | tail -n 1 || true)\"\n if \"$ENOLA\" providers list 2>/dev/null | grep -q '^ rubydex 0\\.'; then\n provider_entries=\" - name: rubydex\n - name: prism\n\"\n else\n say \"rubydex and prism providers not configured (${rubydex_fetch:-providers fetch printed nothing})\"\n fi\nfi\n\nGENERATE_TARGET=\"$PWD\"\nif [ -n \"$provider_entries\" ]; then\n GENERATE_TARGET=\"$work/enola-generate.yaml\"\n printf 'repos:\\n - %s\\nproviders:\\n%s' \"$PWD\" \"$provider_entries\" > \"$GENERATE_TARGET\"\nfi\n\n# FILL IN 2: the pull request's base branch. Pull-request builds usually\n# carry it in an environment variable; push builds know the pull request\n# number and must ask the forge. main only when there is no pull request.\nbase_branch=\"${PR_BASE_BRANCH:-}\"\nif [ -z \"$base_branch\" ] && [ -n \"${PR_NUMBER:-}\" ] && command -v gh >/dev/null 2>&1; then\n base_branch=$(gh pr view \"$PR_NUMBER\" --json baseRefName -q .baseRefName 2>/dev/null || true)\nfi\nbase_branch=\"${base_branch:-main}\"\ngit fetch origin \"$base_branch\" >/dev/null 2>&1 || true\nbase_sha=$(git merge-base HEAD \"origin/${base_branch}\" 2>/dev/null || true)\n[ -n \"$base_sha\" ] || skip \"no merge-base with origin/${base_branch}\"\n\ndirty=$(git status --porcelain -uno)\n[ -z \"$dirty\" ] || notrun \"working tree not clean, cannot generate the baseline in place\"\n\nhead_ref=$(git symbolic-ref -q --short HEAD || git rev-parse HEAD)\ncurrent_ref=\"$head_ref\"\ngit -c advice.detachedHead=false checkout -q \"$base_sha\" || notrun \"could not check out the merge-base\"\n\"$ENOLA\" --generate \"$GENERATE_TARGET\" >/dev/null 2>&1 || notrun \"baseline snapshot failed at ${base_sha:0:11}\"\ngit checkout -q \"$head_ref\"\ncurrent_ref=\"\"\n\"$ENOLA\" --generate \"$GENERATE_TARGET\" >/dev/null 2>&1 || notrun \"snapshot generation failed on this checkout\"\n\nset +e\nverdict=$(\"$ENOLA\" check -baseline previous \"${WATCHED_FLAGS[@]}\" -json \"$GENERATE_TARGET\" 2>/dev/null)\ncheck_exit=$?\nset -e\n[ \"$check_exit\" -eq 0 ] || notrun \"verdict unavailable (check exited ${check_exit}: not comparable or gate error)\"\n\ncommand -v ruby >/dev/null 2>&1 || notrun \"ruby is not on this agent, so the verdict could not be rendered\"\nsummary=$(printf '%s' \"$verdict\" | ruby \"$(dirname \"$0\")/enola-findings.rb\")\nstatus=$(printf '%s' \"$summary\" | head -n 1)\ndetails=$(printf '%s' \"$summary\" | tail -n +3)\n\ncase \"$status\" in\n CLEAN)\n body=\"No new architectural findings against \\`${base_sha:0:11}\\`.\"\n ;;\n PARTIAL)\n body=\"enola check: no new findings against \\`${base_sha:0:11}\\` among the facts both snapshots share. **This is a partial verdict.**\n\n$details\"\n ;;\n DECLARED)\n body=\"enola check: this change declares rules that existing code already breaks, and introduces no new findings on the code it touched (against \\`${base_sha:0:11}\\`). **Nothing is blocked.**\n\n$details\"\n ;;\n *)\n body=\"enola check found new architectural findings against \\`${base_sha:0:11}\\`. **Nothing is blocked** while the team gets used to the check.\n\n$details\"\n ;;\nesac\n\n# FILL IN 3: post one comment per pull request, edited in place. Without a\n# forge credential, print it.\nif [ -n \"${PR_NUMBER:-}\" ] && command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then\n marker=\"<!-- enola-check -->\"\n existing=$(gh pr view \"$PR_NUMBER\" --json comments -q \".comments[] | select(.body | startswith(\\\"$marker\\\")) | .id\" 2>/dev/null | head -n 1)\n if [ -n \"$existing\" ]; then\n gh api -X PATCH \"repos/{owner}/{repo}/issues/comments/$existing\" -f body=\"$marker\n$body\" >/dev/null && say \"updated the existing enola comment\"\n elif [ \"$status\" != \"CLEAN\" ]; then\n gh pr comment \"$PR_NUMBER\" --body \"$marker\n$body\" >/dev/null && say \"posted a new enola comment\"\n fi\nelse\n printf '%s\\n' \"$body\"\nfi\n",
|
|
163
|
+
"templates/constraints-starter/background-jobs-never-invoke-controller-code.rb": "# A starter law for a Rails application: `rails` binds jobs, models,\n# controllers and policies to their app/ directories. In a repository with\n# no Ruby, write laws in YAML instead (guide 04): a first .rb file is new\n# Ruby to the extractor, and the next grade against an older baseline is\n# partial.\n\nEnola.architecture \"app\" do\n rails\n\n law \"background jobs never invoke controller code\" do\n jobs.must_not_call controllers\n why \"rendering from a job goes through ApplicationController.renderer\"\n mode :ratchet\n end\nend\n",
|
|
164
|
+
"templates/constraints-starter/models-do-not-reach-controllers.rb": "# A starter law for a Rails application: `rails` binds jobs, models,\n# controllers and policies to their app/ directories. In a repository with\n# no Ruby, write laws in YAML instead (guide 04): a first .rb file is new\n# Ruby to the extractor, and the next grade against an older baseline is\n# partial.\n\nEnola.architecture \"app\" do\n rails\n\n law \"models do not reach controllers\" do\n models.must_not_call controllers\n why \"a model that knows the request cannot be used off the request\"\n mode :ratchet\n end\nend\n",
|
|
165
|
+
"templates/constraints-starter/policies-only-answer.rb": "# A starter law for a Rails application: `rails` binds jobs, models,\n# controllers and policies to their app/ directories. In a repository with\n# no Ruby, write laws in YAML instead (guide 04): a first .rb file is new\n# Ruby to the extractor, and the next grade against an older baseline is\n# partial.\n\nEnola.architecture \"app\" do\n rails\n\n law \"policies only answer\" do\n policies.must_not_call jobs\n why \"authorization is asked many times per request and sometimes speculatively\"\n mode :ratchet\n end\nend\n",
|
|
166
|
+
"templates/constraints-starter/the-request-api-stays-in-controllers.rb": "# A starter law for a Rails application: `rails` binds jobs, models,\n# controllers and policies to their app/ directories. In a repository with\n# no Ruby, write laws in YAML instead (guide 04): a first .rb file is new\n# Ruby to the extractor, and the next grade against an older baseline is\n# partial.\n\nEnola.architecture \"app\" do\n rails\n\n law \"the request api stays in controllers\" do\n models.must_not_call \"params\", receiver: :none\n why \"params without a receiver is the controller's; a model reading it only works inside a request\"\n mode :ratchet\n end\nend\n",
|
|
167
|
+
"templates/enola-check.sh": "#!/bin/bash\n# Grades this pull request's architectural delta against its base branch and\n# reports what the change added: declared constraints, queries in loops, layer\n# violations, dependency cycles. By default findings are reported, never\n# blocking (-warn-only). ENOLA_CHECK_ENFORCE=1 makes this a gate: the step\n# then exits with enola check's own code, 1 when the change introduced a\n# finding the policy fails. Turning it on is a team decision.\n#\n# What fails at all is enola's policy, unchanged here: a new finding from a\n# watched explainer at confidence 1.00, and a strict law's breach whether new\n# or not. An advisory law reports at 0.90 and never fails; neither does a\n# finding an explainer only estimates (an inferred layer, a query reached\n# through an interface). ENOLA_CHECK_MIN_CONFIDENCE lowers that floor, and\n# advisory laws then fail with the rest.\n#\n# Requirements: the full history of the base branch and the change (on\n# GitHub `fetch-depth: 0`, on GitLab `GIT_DEPTH: 0`), a clean working tree,\n# curl, tar and sha256sum. Optional: ruby, to render the findings grouped by\n# rule (without it the comment is enola's own `check --summary`), and gh,\n# to post the comment (without it the comment is printed).\n#\n# On CI (CI set) anything that keeps the delta from being graded fails the\n# step, so the check cannot go quiet; on a laptop it skips with the reason.\n# The one skip on CI is a build with nothing to grade: this commit is the\n# base itself.\n#\n# Three places are yours to fill, marked FILL IN below: how the pinned binary\n# is fetched, how the pull request's base branch is read on a push build,\n# and how the comment is posted. Everything between them runs the same on\n# any CI and on a laptop.\nset -euo pipefail\n\n# The enola release this check runs: ENOLA_VERSION, else the first line of a\n# committed .enola-version, so a repository pins it in one place.\nENOLA_VERSION=\"${ENOLA_VERSION:-$(head -n 1 .enola-version 2>/dev/null || true)}\"\nENOLA_VERSION=\"${ENOLA_VERSION:-0.4.35}\"\nWATCHED_EXPLAINERS=\"${ENOLA_CHECK_WATCHED:-constraints,query-loops,layers,cycles}\"\nWATCHED_FLAGS=(-fail-on \"$WATCHED_EXPLAINERS\")\n[ -z \"${ENOLA_CHECK_MIN_CONFIDENCE:-}\" ] || WATCHED_FLAGS+=(-min-confidence \"$ENOLA_CHECK_MIN_CONFIDENCE\")\n[ \"${ENOLA_CHECK_ENFORCE:-0}\" = 1 ] || WATCHED_FLAGS+=(-warn-only)\n\nsay() { echo \"enola check: $1\"; }\n\nskip() {\n say \"skipped: $1. The architectural delta was not graded this build.\"\n exit 0\n}\n\n# On CI a missing prerequisite fails the step so the check cannot go quiet;\n# locally it skips.\nnotrun() {\n if [ -n \"${CI:-}\" ]; then\n say \"did not run: $1. The architectural delta was not graded this build.\"\n exit 1\n fi\n skip \"$1\"\n}\n\nwork=$(mktemp -d)\ncurrent_ref=\"\"\ncleanup() {\n if [ -n \"$current_ref\" ]; then\n git checkout -q \"$current_ref\" 2>/dev/null || true\n fi\n rm -rf \"$work\"\n}\ntrap cleanup EXIT\n\n# FILL IN 1: fetch the pinned binary. Cached per version and platform so a\n# laptop downloads once. Verify the checksum the release publishes.\nos=$(uname -s | tr '[:upper:]' '[:lower:]')\narch=$(uname -m)\ncase \"$arch\" in x86_64 | amd64) arch=amd64 ;; arm64 | aarch64) arch=arm64 ;; *) notrun \"no enola build for $arch\" ;; esac\nENOLA=\"${HOME:-$work}/.cache/enola/${ENOLA_VERSION}/enola\"\nif [ ! -x \"$ENOLA\" ]; then\n mkdir -p \"$(dirname \"$ENOLA\")\"\n asset=\"enola-${ENOLA_VERSION}-${os}-${arch}.tar.gz\"\n curl -fsSL \"https://github.com/enola-labs/enola/releases/download/v${ENOLA_VERSION}/${asset}\" -o \"$work/$asset\" || notrun \"could not download enola ${ENOLA_VERSION}\"\n curl -fsSL \"https://github.com/enola-labs/enola/releases/download/v${ENOLA_VERSION}/${asset%.tar.gz}.sha256\" -o \"$work/${asset%.tar.gz}.sha256\" || notrun \"could not download the checksum for enola ${ENOLA_VERSION}\"\n (cd \"$work\" && sha256sum -c \"${asset%.tar.gz}.sha256\" >/dev/null) || notrun \"checksum mismatch for ${asset}\"\n tar -xzf \"$work/$asset\" -C \"$work\"\n mv \"$work/${asset%.tar.gz}\" \"$ENOLA\"\n chmod +x \"$ENOLA\"\nfi\nsay \"enola ${ENOLA_VERSION} at ${ENOLA}\"\n\n# Providers the agent can run. Each absence is a named line, never silence,\n# and the same providers feed both snapshots so they stay comparable. Both\n# Ruby providers run inside the binary once the Rubydex library is fetched;\n# no Ruby is needed. The binary pins the library, so neither entry carries an\n# expected_version.\nprovider_entries=\"\"\nif [ -f Gemfile ]; then\n rubydex_fetch=\"$(\"$ENOLA\" providers fetch rubydex 2>&1 | tail -n 1 || true)\"\n if \"$ENOLA\" providers list 2>/dev/null | grep -q '^ rubydex 0\\.'; then\n provider_entries=\" - name: rubydex\n - name: prism\n\"\n else\n say \"rubydex and prism providers not configured (${rubydex_fetch:-providers fetch printed nothing})\"\n fi\nfi\n\nGENERATE_TARGET=\"$PWD\"\nif [ -n \"$provider_entries\" ]; then\n GENERATE_TARGET=\"$work/enola-generate.yaml\"\n printf 'repos:\\n - %s\\nproviders:\\n%s' \"$PWD\" \"$provider_entries\" > \"$GENERATE_TARGET\"\nfi\n\n# A shallow clone has no merge-base to grade against; say how to get one\n# rather than grading nothing.\n[ \"$(git rev-parse --is-shallow-repository 2>/dev/null)\" != true ] ||\n notrun \"this is a shallow clone; fetch the full history (fetch-depth: 0 on GitHub, GIT_DEPTH: 0 on GitLab)\"\n\n# FILL IN 2: the pull request's base branch. Pull-request builds usually\n# carry it in an environment variable; push builds know the pull request\n# number and must ask the forge. main only when there is no pull request.\nbase_branch=\"${PR_BASE_BRANCH:-}\"\nif [ -z \"$base_branch\" ] && [ -n \"${PR_NUMBER:-}\" ] && command -v gh >/dev/null 2>&1; then\n base_branch=$(gh pr view \"$PR_NUMBER\" --json baseRefName -q .baseRefName 2>/dev/null || true)\nfi\nbase_branch=\"${base_branch:-main}\"\ngit fetch origin \"$base_branch\" >/dev/null 2>&1 || true\nbase_sha=$(git merge-base HEAD \"origin/${base_branch}\" 2>/dev/null || true)\n[ -n \"$base_sha\" ] || notrun \"no merge-base with origin/${base_branch}\"\n[ \"$base_sha\" != \"$(git rev-parse HEAD)\" ] || skip \"this commit is origin/${base_branch} itself, so there is no change to grade\"\n\ndirty=$(git status --porcelain -uno)\n[ -z \"$dirty\" ] || notrun \"working tree not clean, cannot generate the baseline in place\"\n\nhead_ref=$(git symbolic-ref -q --short HEAD || git rev-parse HEAD)\ncurrent_ref=\"$head_ref\"\ngit -c advice.detachedHead=false checkout -q \"$base_sha\" || notrun \"could not check out the merge-base\"\n\"$ENOLA\" --generate \"$GENERATE_TARGET\" >/dev/null 2>&1 || notrun \"baseline snapshot failed at ${base_sha:0:11}\"\ngit checkout -q \"$head_ref\"\ncurrent_ref=\"\"\n\"$ENOLA\" --generate \"$GENERATE_TARGET\" >/dev/null 2>&1 || notrun \"snapshot generation failed on this checkout\"\n\n# Exit 0 is clean and 1 a regression the policy fails: both are verdicts,\n# rendered and posted. 3 is a declined comparison and 2 a gate that could\n# not run: neither graded anything.\nset +e\nverdict=$(\"$ENOLA\" check -baseline previous \"${WATCHED_FLAGS[@]}\" -json \"$GENERATE_TARGET\" 2>/dev/null)\ncheck_exit=$?\nset -e\ncase \"$check_exit\" in\n 0 | 1) ;;\n 3) notrun \"enola declined to grade: the two snapshots are not comparable (enola check exited 3)\" ;;\n *) notrun \"enola check could not run (exited ${check_exit})\" ;;\nesac\n\nif command -v ruby >/dev/null 2>&1; then\n summary=$(printf '%s' \"$verdict\" | ruby \"$(dirname \"$0\")/enola-findings.rb\")\n status=$(printf '%s' \"$summary\" | head -n 1)\n details=$(printf '%s' \"$summary\" | tail -n +3)\nelse\n # The verdict's own status reads \"clean\" under -warn-only whatever it\n # found, so the summary's sections decide: no \"Got worse\", nothing new.\n say \"ruby is not on this agent; the comment is enola's own summary\"\n plain=$(\"$ENOLA\" check -baseline previous \"${WATCHED_FLAGS[@]}\" -format summary \"$GENERATE_TARGET\" 2>/dev/null || true)\n status=FINDINGS\n grep -q '^Got worse' <<<\"$plain\" || status=CLEAN\n details=\"\\`\\`\\`\n$plain\n\\`\\`\\`\"\nfi\n\nif [ \"$check_exit\" -eq 1 ]; then\n blocking=\"**This blocks the merge.**\"\nelif [ \"${ENOLA_CHECK_ENFORCE:-0}\" = 1 ]; then\n blocking=\"**Nothing here fails the check.**\"\nelse\n blocking=\"**Nothing is blocked** while the team gets used to the check.\"\nfi\n\ncase \"$status\" in\n CLEAN)\n body=\"No new architectural findings against \\`${base_sha:0:11}\\`.\"\n ;;\n PARTIAL)\n body=\"enola check: no new findings against \\`${base_sha:0:11}\\` among the facts both snapshots share. **This is a partial verdict.**\n\n$details\"\n ;;\n DECLARED)\n body=\"enola check: this change declares rules that existing code already breaks, and introduces no new findings on the code it touched (against \\`${base_sha:0:11}\\`). $blocking\n\n$details\"\n ;;\n *)\n body=\"enola check found new architectural findings against \\`${base_sha:0:11}\\`. $blocking\n\n$details\"\n ;;\nesac\n\n# FILL IN 3: post one comment per pull request, edited in place. Without a\n# forge credential, print it. The REST API edits a comment by its numeric\n# id, so the lookup goes through the same API.\nif [ -n \"${PR_NUMBER:-}\" ] && command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then\n marker=\"<!-- enola-check -->\"\n existing=$(gh api \"repos/{owner}/{repo}/issues/${PR_NUMBER}/comments\" --paginate \\\n --jq \".[] | select(.body | startswith(\\\"$marker\\\")) | .id\" 2>/dev/null | head -n 1 || true)\n if [ -n \"$existing\" ]; then\n if gh api -X PATCH \"repos/{owner}/{repo}/issues/comments/$existing\" -f body=\"$marker\n$body\" >/dev/null; then\n say \"updated the existing enola comment\"\n else\n say \"could not update the enola comment $existing\"\n fi\n elif [ \"$status\" != \"CLEAN\" ]; then\n gh pr comment \"$PR_NUMBER\" --body \"$marker\n$body\" >/dev/null && say \"posted a new enola comment\"\n fi\nelse\n printf '%s\\n' \"$body\"\nfi\n\nexit \"$check_exit\"\n",
|
|
165
168
|
"templates/enola-cluster.yaml": "# A cluster: every repository's facts land in one union graph, labelled by\n# repository, and the linker draws the seams between them. Regenerate the\n# whole cluster, never one member; keep the output directory distinct from\n# each member's own .enola/.\nrepos:\n - ../backend\n - ../mobile\n - ../admin\n - .\n\nignore:\n - \"**/node_modules/**\"\n - \"**/vendor/**\"\n - \"**/dist/**\"\n - \"**/build/**\"\n - \"**/tmp/**\"\n - \"**/log/**\"\n - \"**/coverage/**\"\n\noutput:\n dir: .enola-cluster\n\nproviders:\n - name: rubydex\n - name: prism\n",
|
|
166
|
-
"templates/enola-findings.rb": "# frozen_string_literal: true\n\nrequire \"json\"\n\nLISTED = 10\nEXAMPLES_PER_RULE = 3\nRULES_LISTED = 12\n\ndef rule_of(finding)\n finding[\"title\"][/constraint (\\S+) violated/i, 1] || finding[\"source\"] || \"?\"\nend\n\ndef constraint?(finding)\n finding[\"source\"] == \"constraints\"\nend\n\ndef breach_of(finding)\n finding[\"title\"].sub(/\\A.*violated: /, \"\")\nend\n\ndef render_flat(findings)\n findings.first(LISTED).each do |finding|\n puts \"- **#{finding[\"source\"] || \"?\"}** — #{finding[\"title\"]}\"\n fact = finding.dig(\"evidence\", 0, \"fact\")\n puts \" `#{fact}`\" if fact\n end\n puts \"- _...and #{findings.size - LISTED} more_\" if findings.size > LISTED\nend\n\ndef render_grouped(findings, phrase)\n groups = findings.group_by { |finding| rule_of(finding) }\n ordered = groups.sort_by { |rule, breaches| [constraint?(breaches.first) ? 0 : 1, -breaches.size, rule] }\n ordered.first(RULES_LISTED).each do |rule, breaches|\n puts \"- **#{rule}** — #{breaches.size} #{phrase.call(breaches.size, constraint?(breaches.first))}\"\n breaches.first(EXAMPLES_PER_RULE).each do |finding|\n puts \" - #{breach_of(finding)}\"\n end\n puts \" - _...and #{breaches.size - EXAMPLES_PER_RULE} more_\" if breaches.size > EXAMPLES_PER_RULE\n end\n puts \"- _...and #{ordered.size - RULES_LISTED} more rules_\" if ordered.size > RULES_LISTED\nend\n\nNEW_PHRASE = lambda do |count, constraint|\n if constraint\n (count == 1) ? \"new place breaks it\" : \"new places break it\"\n else\n (count == 1) ? \"new finding\" : \"new findings\"\n end\nend\nEXISTING_PHRASE = ->(count, _constraint) { (count == 1) ? \"existing place already breaks it\" : \"existing places already break it\" }\n\nverdict = JSON.parse($stdin.read)\nwatched = verdict[\"failures\"] || []\
|
|
167
|
-
"templates/hooks/intent-first-gate.sh": "#!/bin/bash\n# PreToolUse on Edit|Write|MultiEdit: block the first write into each\n# repository per piece of work, once, with the governing pages and
|
|
168
|
-
"templates/hooks/record.sh": "#!/bin/bash\n# PostToolUse on Edit|Write|MultiEdit|Bash: record what this turn did, so\n# the stop-time debt hook can account for it. With \"reset\" as the first\n# argument (UserPromptSubmit): a new user message begins a new piece of\n# work, so clear the record and the gate's acknowledgements.\nset -u\nstate=\"${TMPDIR:-/tmp}/intent-first-$(id -u)\"\nmkdir -p \"$state\"\ninput=$(cat 2>/dev/null || true)\nsid=$(jq -r '.session_id // \"nosession\"' <<<\"$input\" 2>/dev/null)\nturn=\"$state/${sid}.turn\"\n\nif [ \"${1:-}\" = \"reset\" ]; then\n rm -f \"$state/${sid}\"-*.ack \"$turn\" \"$state/${sid}.debt-blocked\" \"$state/${sid}.push-blocked\"\n exit 0\nfi\n\nrecord() { printf '%s\\n' \"$1\" >> \"$turn\"; }\n\ntool=$(jq -r '.tool_name // empty' <<<\"$input\" 2>/dev/null)\nif [ \"$tool\" = \"Bash\" ]; then\n cmd=$(jq -r '.tool_input.command // empty' <<<\"$input\" 2>/dev/null)\n [ -n \"$cmd\" ] || exit 0\n printf '%s' \"$cmd\" | grep -qE '(^|[[:space:];&|])git[[:space:]]+push' && record \"push\"\n printf '%s' \"$cmd\" | grep -qE 'enola[[:space:]]+check' && record \"check\"\n exit 0\nfi\n\nfile=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty' <<<\"$input\" 2>/dev/null)\n[ -n \"$file\" ] || exit 0\ntop=$(git -C \"$(dirname \"$file\")\" rev-parse --show-toplevel 2>/dev/null) || exit 0\nrel=\"${file#\"$top\"/}\"\
|
|
169
|
+
"templates/enola-findings.rb": "# frozen_string_literal: true\n\nrequire \"json\"\n\nLISTED = 10\nEXAMPLES_PER_RULE = 3\nRULES_LISTED = 12\n\ndef rule_of(finding)\n finding[\"title\"][/constraint (\\S+) violated/i, 1] || finding[\"source\"] || \"?\"\nend\n\ndef constraint?(finding)\n finding[\"source\"] == \"constraints\"\nend\n\ndef breach_of(finding)\n finding[\"title\"].sub(/\\A.*violated: /, \"\")\nend\n\ndef render_flat(findings)\n findings.first(LISTED).each do |finding|\n puts \"- **#{finding[\"source\"] || \"?\"}** — #{finding[\"title\"]}\"\n fact = finding.dig(\"evidence\", 0, \"fact\")\n puts \" `#{fact}`\" if fact\n end\n puts \"- _...and #{findings.size - LISTED} more_\" if findings.size > LISTED\nend\n\ndef render_grouped(findings, phrase)\n groups = findings.group_by { |finding| rule_of(finding) }\n ordered = groups.sort_by { |rule, breaches| [constraint?(breaches.first) ? 0 : 1, -breaches.size, rule] }\n ordered.first(RULES_LISTED).each do |rule, breaches|\n puts \"- **#{rule}** — #{breaches.size} #{phrase.call(breaches.size, constraint?(breaches.first), strict?(breaches.first))}\"\n breaches.first(EXAMPLES_PER_RULE).each do |finding|\n puts \" - #{breach_of(finding)}\"\n end\n puts \" - _...and #{breaches.size - EXAMPLES_PER_RULE} more_\" if breaches.size > EXAMPLES_PER_RULE\n end\n puts \"- _...and #{ordered.size - RULES_LISTED} more rules_\" if ordered.size > RULES_LISTED\nend\n\ndef strict?(finding)\n finding[\"title\"].start_with?(\"Strict constraint\")\nend\n\nNEW_PHRASE = lambda do |count, constraint, strict = false|\n if strict\n (count == 1) ? \"place breaks it; a strict law fails on every breach, new or not\" : \"places break it; a strict law fails on every breach, new or not\"\n elsif constraint\n (count == 1) ? \"new place breaks it\" : \"new places break it\"\n else\n (count == 1) ? \"new finding\" : \"new findings\"\n end\nend\nEXISTING_PHRASE = ->(count, _constraint, _strict = false) { (count == 1) ? \"existing place already breaks it\" : \"existing places already break it\" }\n\nverdict = JSON.parse($stdin.read)\npolicy = verdict[\"policy\"] || {}\ngated = policy[\"fail_explainers\"] || []\nfloor = policy[\"min_confidence\"] || 1.0\nwarn_only = policy[\"warn_only\"]\nwatched = verdict[\"failures\"] || []\n# Findings that fail nothing: a watched explainer's below the confidence\n# floor (an advisory law, an estimate), and every other explainer's.\nbelow, other = (verdict[\"advisories\"] || []).partition { |finding| gated.include?(finding[\"source\"]) }\ndeclared = verdict[\"declared\"] || []\n\nexcluded = verdict.dig(\"intersection_grading\", \"excluded\") || []\n\ndef render_partial(excluded)\n puts \"**Partial verdict** — graded only the facts both snapshots share. A regression among these producers' facts is not reported:\"\n puts\n excluded.each do |producer|\n puts \"- **#{producer[\"name\"]}** (#{producer[\"kind\"]}), missing from the #{producer[\"lacked_by\"]} snapshot\"\n end\nend\n\nif watched.empty? && below.empty? && other.empty? && declared.empty?\n if excluded.empty?\n puts \"CLEAN\"\n else\n puts \"PARTIAL\"\n puts\n render_partial(excluded)\n end\n exit 0\nend\n\nputs((watched.empty? && below.empty? && other.empty?) ? \"DECLARED\" : \"FINDINGS\")\nputs\n\nif excluded.any?\n render_partial(excluded)\n puts\nend\n\nif watched.any?\n verdict_line = warn_only ? \"They would fail the check once it enforces; nothing blocks today.\" : \"They fail the check.\"\n puts \"**Watched** — new findings from the explainers this check gates on (#{gated.join(\", \")}), at confidence #{format(\"%.2f\", floor)} or above. #{verdict_line}\"\n puts\n render_grouped(watched, NEW_PHRASE)\nend\n\nif below.any?\n puts if watched.any?\n puts \"**Below the floor** — new findings from the same explainers under confidence #{format(\"%.2f\", floor)}: an advisory law (0.90) or a finding the explainer estimates. Reported, never failing.\"\n puts\n render_grouped(below, NEW_PHRASE)\nend\n\nif declared.any?\n puts if watched.any? || below.any?\n puts \"**Declared** — rules this change declares, re-forms or un-exempts, reported on code the change did not touch. The baseline each rule starts from, not new findings.\"\n puts\n render_grouped(declared, EXISTING_PHRASE)\nend\n\nif other.any?\n puts if watched.any? || below.any? || declared.any?\n puts \"**Also noticed** — outside the watched explainers.\"\n puts\n render_flat(other)\nend\n",
|
|
170
|
+
"templates/hooks/intent-first-gate.sh": "#!/bin/bash\n# PreToolUse on Edit|Write|MultiEdit: block the first write into each\n# governed repository per piece of work, once, with the governing pages and\n# the pre-edit contract on stderr. The retry proceeds; the gate forces the\n# reading, never the verdict. Re-armed by record.sh on every user message.\n#\n# Governed means what it means to record.sh: one of ENOLA_DECISION_DIRS\n# (default \"wiki docs/decisions docs/adrs\") exists at the repository's\n# root. A repository with none is not gated. Needs jq, and enola on PATH\n# for the contract (without it the block says so and still lets the retry\n# through).\n#\n# Where a shell-command classifier would go: a Bash tool call whose text\n# carries a write shape (sed -i, a redirect, tee, cp or mv into the tree,\n# an interpreter fed a heredoc) is a write too. What counts depends on your\n# tree; gate it the same way once you have decided.\nset -u\ninput=$(cat 2>/dev/null || true)\nsid=$(jq -r '.session_id // \"nosession\"' <<<\"$input\" 2>/dev/null)\nfile=$(jq -r '.tool_input.file_path // empty' <<<\"$input\" 2>/dev/null)\n[ -n \"$file\" ] || exit 0\n\ntop=$(git -C \"$(dirname \"$file\")\" rev-parse --show-toplevel 2>/dev/null) || exit 0\ngoverned=\"\"\nfor dir in ${ENOLA_DECISION_DIRS:-wiki docs/decisions docs/adrs}; do\n [ -d \"$top/$dir\" ] && governed=1\ndone\n[ -n \"$governed\" ] || exit 0\ncase \"$file\" in\n \"$top\"/.enola/*|\"$top\"/wiki/_views/*|\"$top\"/wiki/_state/*) exit 0 ;;\nesac\nrel=\"${file#\"$top\"/}\"\nrepo=$(basename \"$top\")\n\nstate=\"${TMPDIR:-/tmp}/intent-first-$(id -u)\"\nmkdir -p \"$state\"\nack=\"$state/${sid}-${repo}.ack\"\n[ -e \"$ack\" ] && exit 0\ntouch \"$ack\"\n\ngovern=\"\"\nif command -v enola >/dev/null 2>&1; then\n govern=$(cd \"$top\" && timeout 30 enola plan -paths \"$rel\" . 2>/dev/null | sed -n '/^Plan report/,$p' | head -40)\nfi\n[ -n \"$govern\" ] || govern=\"(enola plan unavailable: no binary or no snapshot for this checkout; named skip, consult the decision records by hand)\"\n\ncat >&2 <<EOF\nintent-first gate (${repo}): first write for this piece of work.\n\nBefore ${rel} moves, read what governs it and check the intended change\nagainst it. If the change deviates from what the governing page records,\namend the page first; if nothing governs this work, say so and ask\nwhether it should be shaped.\n\n${govern}\n\nRetrying proceeds. This gate re-arms on every user message; within one\npiece of work it will not fire again for ${repo}.\nEOF\nexit 2\n",
|
|
171
|
+
"templates/hooks/record.sh": "#!/bin/bash\n# PostToolUse on Edit|Write|MultiEdit|Bash: record what this turn did, so\n# the stop-time debt hook can account for it. With \"reset\" as the first\n# argument (UserPromptSubmit): a new user message begins a new piece of\n# work, so clear the record and the gate's acknowledgements.\n#\n# A repository is governed when one of ENOLA_DECISION_DIRS (space-separated,\n# relative to its root; default \"wiki docs/decisions docs/adrs\") exists in\n# it. A markdown file under one of them is a decision record; any other file\n# in a governed repository is code. A file in a repository with none of\n# those directories is not recorded, so the debt hook never asks it for a\n# record it has nowhere to keep.\nset -u\nstate=\"${TMPDIR:-/tmp}/intent-first-$(id -u)\"\nmkdir -p \"$state\"\ninput=$(cat 2>/dev/null || true)\nsid=$(jq -r '.session_id // \"nosession\"' <<<\"$input\" 2>/dev/null)\nturn=\"$state/${sid}.turn\"\ndirs=\"${ENOLA_DECISION_DIRS:-wiki docs/decisions docs/adrs}\"\n\nif [ \"${1:-}\" = \"reset\" ]; then\n rm -f \"$state/${sid}\"-*.ack \"$turn\" \"$state/${sid}.debt-blocked\" \"$state/${sid}.push-blocked\"\n exit 0\nfi\n\nrecord() { printf '%s\\n' \"$1\" >> \"$turn\"; }\n\ntool=$(jq -r '.tool_name // empty' <<<\"$input\" 2>/dev/null)\nif [ \"$tool\" = \"Bash\" ]; then\n cmd=$(jq -r '.tool_input.command // empty' <<<\"$input\" 2>/dev/null)\n [ -n \"$cmd\" ] || exit 0\n printf '%s' \"$cmd\" | grep -qE '(^|[[:space:];&|])git[[:space:]]+push' && record \"push\"\n printf '%s' \"$cmd\" | grep -qE 'enola[[:space:]]+check' && record \"check\"\n exit 0\nfi\n\nfile=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty' <<<\"$input\" 2>/dev/null)\n[ -n \"$file\" ] || exit 0\ntop=$(git -C \"$(dirname \"$file\")\" rev-parse --show-toplevel 2>/dev/null) || exit 0\nrel=\"${file#\"$top\"/}\"\ngoverned=\"\"\nfor dir in $dirs; do\n [ -d \"$top/$dir\" ] || continue\n governed=1\n case \"$rel\" in \"$dir\"/*.md) record \"spec\t$rel\"; exit 0 ;; esac\ndone\n[ -z \"$governed\" ] || record \"code\t$rel\"\nexit 0\n",
|
|
169
172
|
"templates/hooks/session-refresh.sh": "#!/bin/bash\n# SessionStart: when a cluster member's HEAD moved past the commit the\n# committed receipts record, regenerate the cluster in the background and\n# say so in one line; when they agree, say that. The agent's first\n# structural question then reads a graph that matches the checkout.\n#\n# RECEIPTS is the knowledge base's committed receipts file; CLUSTER the\n# cluster config; MEMBER the repository to compare (default: this one).\nset -u\nRECEIPTS=\"${RECEIPTS:-wiki/_state/enola/receipts.json}\"\nCLUSTER=\"${CLUSTER:-enola-cluster.yaml}\"\nMEMBER=\"${MEMBER:-.}\"\ncommand -v enola >/dev/null 2>&1 || { echo \"enola hooks: no binary on PATH; the graph was not asked (named skip)\"; exit 0; }\n[ -f \"$RECEIPTS\" ] || { echo \"enola hooks: no receipts at $RECEIPTS; the graph was not asked (named skip)\"; exit 0; }\n\nlabel=$(basename \"$(cd \"$MEMBER\" && pwd -P)\")\nhead=$(git -C \"$MEMBER\" rev-parse HEAD 2>/dev/null || true)\nrecorded=$(python3 - \"$RECEIPTS\" \"$label\" <<'PY' 2>/dev/null\nimport json, sys\nr = json.load(open(sys.argv[1]))\nprint(((r.get(\"repos\") or r).get(sys.argv[2]) or {}).get(\"git_commit\") or \"\")\nPY\n)\nif [ -n \"$head\" ] && [ \"$head\" != \"$recorded\" ]; then\n log=\"${TMPDIR:-/tmp}/enola-refresh-$(id -u).log\"\n (nohup enola --generate \"$CLUSTER\" > \"$log\" 2>&1 &)\n echo \"enola hooks: $label is at ${head:0:11}, the recorded receipt at ${recorded:0:11}; regenerating the cluster in the background (log $log).\"\nelse\n echo \"enola hooks: the recorded receipt matches $label at ${head:0:11}.\"\nfi\nexit 0\n",
|
|
170
|
-
"templates/hooks/stop-debt.sh": "#!/bin/bash\n# Stop: block the end of the turn once when this turn pushed without a\n# grade, and once when code moved
|
|
171
|
-
"templates/skills/enola-citation-hygiene/SKILL.md": "---\nname: enola-citation-hygiene\ndescription: Use during grooming; gives every receipt citation in prose a verdict against the committed receipts and sweeps the finding ledger for judgments the code has moved past\n---\n\n# enola-citation-hygiene: every citation gets a verdict\n\n1. `kb enola citations`: each receipt citation in prose is verified, stale\n (the digest moved) or malformed.\n2. Stale: re-read the cited page against the current facts and amend the\n sentence; never update the citation alone, the sentence is what may be\n wrong.\n3. Malformed: fix the citation to the recorded form.\n4. `kb enola findings --all`: for each accepted finding in the ledger,\n confirm its evidence still exists in the graph. Evidence gone means\n the code moved past the judgment; re-read the page that landed it and\n record the new state.\n5. Rejected and noise verdicts stay; they keep the finding hidden on\n purpose.\n\nA citation is a promise that a number was read, not remembered; this\nskill keeps the promise current.\n",
|
|
172
|
-
"templates/skills/enola-
|
|
173
|
-
"templates/skills/enola-
|
|
174
|
-
"templates/skills/enola-
|
|
175
|
-
"templates/skills/enola-
|
|
173
|
+
"templates/hooks/stop-debt.sh": "#!/bin/bash\n# Stop: block the end of the turn once when this turn pushed without a\n# grade, and once when code moved in a governed repository and no decision\n# record moved. Reads the per-session record written by record.sh, which\n# also decides what governed means (ENOLA_DECISION_DIRS); a repository with\n# no decision records is never asked for one.\nset -u\nstate=\"${TMPDIR:-/tmp}/intent-first-$(id -u)\"\ninput=$(cat 2>/dev/null || true)\nsid=$(jq -r '.session_id // \"nosession\"' <<<\"$input\" 2>/dev/null)\nturn=\"$state/${sid}.turn\"\n[ -f \"$turn\" ] || exit 0\n\nif grep -q '^push$' \"$turn\" && ! grep -q '^check$' \"$turn\" && [ ! -e \"$state/${sid}.push-blocked\" ]; then\n touch \"$state/${sid}.push-blocked\"\n cat >&2 <<'EOF'\npush without a grade: this turn pushed and ran no enola check.\n\nBefore finishing: run `enola check <repo>` against the pinned baseline\nand read the grade. A declined grade is not a pass; re-pin and grade\nagain. This check blocks once per turn.\nEOF\n exit 2\nfi\n\nif grep -q '^code' \"$turn\" && ! grep -q '^spec' \"$turn\" && [ ! -e \"$state/${sid}.debt-blocked\" ]; then\n touch \"$state/${sid}.debt-blocked\"\n summary=$(awk -F'\\t' '$1 == \"code\" { print \" \" $2 }' \"$turn\" | sort -u | head -15)\n cat >&2 <<EOF\nintent debt: code moved this turn, and no decision record did.\n\nChanged without a record moving:\n${summary}\n\nBefore finishing: either amend the governing record to match what was\nbuilt, or state explicitly, in your reply, why the records as written\nalready cover the change. This check blocks once per turn.\nEOF\n exit 2\nfi\nexit 0\n",
|
|
174
|
+
"templates/kb/kb": "#!/usr/bin/env python3\n\"\"\"kb enola <op>: the knowledge base's one door to enola, as guide 12 shapes it.\n\nSkills call this, not the binary. It knows where the binary, the cluster config\nand the receipts file live, records receipts after a generate, and when one of\nthem is absent prints one line saying so and exits 0, so \"the graph was not\nasked\" stays a different sentence from \"the graph agreed\".\n\n generate snapshot the cluster, then record every member's receipt\n receipt [member] what was recorded\n diff which members moved since their receipts were recorded\n findings [--repo R] [--all] the explainers' findings, by member, judged ones hidden\n judge <signature> accepted|rejected|noise [--reason TEXT] [--landed PAGE]\n impact <symbol> the blast radius, from the on-disk snapshot\n govern <path|page> what governs a file, or the code a page anchors\n citations every receipt citation in prose, with a verdict\n baseline pin|show|clear [member]\n check [member] the grade, reported, never gating\n coverage which cross-repository edges resolved\n doctor [member] whether enola's hooks fire\n\nRun from the knowledge base's root. Where things are, each overridable:\n ENOLA_BIN the binary (default: enola on PATH)\n KB_CLUSTER the cluster config (default: enola-cluster.yaml, else cluster.yaml)\n KB_KNOWLEDGE the pages (default: wiki); receipts and the ledger live in\n <pages>/_state/enola/receipts.json and findings.json\n\nA citation in prose has one shape, the one `citations` parses; anything else\nthat starts \"receipt <member>@\" is reported malformed:\n receipt <member>@<commit, 7+ hex>, fingerprint <first 4 hex>…<last 4 hex>\nA finding's signature is \"<explainer>|<title>\", as `findings` prints it.\n\"\"\"\nimport datetime\nimport fnmatch\nimport json\nimport os\nimport pathlib\nimport re\nimport shutil\nimport subprocess\nimport sys\n\nCITATION = re.compile(r\"receipt ([\\w.-]+)@([0-9a-f]{7,40}), fingerprint ([0-9a-f]{4})…([0-9a-f]{4})\")\nLOOSE_CITATION = re.compile(r\"receipt [\\w.-]+@[^\\s,)]*(?:, fingerprint [^\\s)]*)?\")\nVERDICTS = (\"accepted\", \"rejected\", \"noise\")\n\n\ndef say(line):\n print(f\"kb enola: {line}\")\n\n\ndef not_asked(why):\n say(f\"{why}; the graph was not asked\")\n sys.exit(0)\n\n\ndef binary():\n found = os.environ.get(\"ENOLA_BIN\") or shutil.which(\"enola\")\n if not found or not os.access(found, os.X_OK):\n not_asked(\"enola is not installed\")\n return found\n\n\ndef cluster_config():\n named = os.environ.get(\"KB_CLUSTER\")\n candidates = [named] if named else [\"enola-cluster.yaml\", \"cluster.yaml\"]\n for candidate in candidates:\n if pathlib.Path(candidate).is_file():\n return pathlib.Path(candidate).resolve()\n not_asked(f\"no cluster config at {' or '.join(candidates)}\")\n\n\ndef read_config(path):\n \"\"\"The three keys this wrapper reads from a cluster config, without a YAML\n library: the repos list, output.dir and the providers' names.\"\"\"\n repos, providers, output = [], [], None\n section = None\n for raw in path.read_text().splitlines():\n line = raw.split(\" #\", 1)[0].rstrip()\n if not line or line.lstrip().startswith(\"#\"):\n continue\n if not line.startswith(\" \") and not line.startswith(\"-\"):\n section = line.split(\":\", 1)[0].strip()\n continue\n item = line.strip()\n if section == \"repos\" and item.startswith(\"- \"):\n repos.append(item[2:].strip().strip(\"\\\"'\"))\n elif section == \"providers\" and item.startswith(\"- name:\"):\n providers.append(item.split(\":\", 1)[1].strip().strip(\"\\\"'\"))\n elif section == \"output\" and item.startswith(\"dir:\"):\n output = item.split(\":\", 1)[1].strip().strip(\"\\\"'\")\n members = {}\n for repo in repos:\n member = (path.parent / repo).resolve()\n members[member.name] = member\n return members, providers, (path.parent / output).resolve() if output else None\n\n\ndef state_dir():\n return pathlib.Path(os.environ.get(\"KB_KNOWLEDGE\", \"wiki\")) / \"_state\" / \"enola\"\n\n\ndef load(path, empty):\n return json.loads(path.read_text()) if path.is_file() else empty\n\n\ndef save(path, data):\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(json.dumps(data, indent=2, sort_keys=True) + \"\\n\")\n\n\ndef snapshot_dir(member, output):\n \"\"\"Where a member's snapshot is: its own .enola, else the cluster's output.\"\"\"\n own = member / \".enola\"\n if (own / \"receipt.json\").is_file():\n return own\n if output and (output / \"receipt.json\").is_file():\n return output\n return None\n\n\ndef head(member):\n out = subprocess.run([\"git\", \"-C\", str(member), \"rev-parse\", \"HEAD\"], capture_output=True, text=True)\n return out.stdout.strip() if out.returncode == 0 else \"\"\n\n\ndef live_receipt(member, output, providers):\n snap = snapshot_dir(member, output)\n if not snap:\n return None\n receipt = json.loads((snap / \"receipt.json\").read_text())\n return {\n \"fingerprint\": receipt.get(\"snapshot_id\", \"\").removeprefix(\"sha256:\"),\n \"git_commit\": head(member),\n \"enola_version\": receipt.get(\"enola_version\", \"\"),\n \"providers\": sorted(providers),\n }\n\n\ndef run(argv, check_exit=True):\n \"\"\"Runs the binary with its output passed through; returns its exit code.\"\"\"\n sys.stdout.flush()\n code = subprocess.run(argv).returncode\n if check_exit and code != 0:\n sys.exit(code)\n return code\n\n\ndef op_generate(enola, config, args):\n members, providers, output = read_config(config)\n run([enola, \"--generate\", \"--no-dashboard\", str(config)])\n receipts_path = state_dir() / \"receipts.json\"\n receipts = load(receipts_path, {\"repos\": {}})\n for label, member in members.items():\n receipt = live_receipt(member, output, providers)\n if receipt is None:\n say(f\"{label}: no receipt after the generate, not recorded\")\n continue\n receipts.setdefault(\"repos\", {})[label] = receipt\n say(f\"{label}: recorded {receipt['fingerprint'][:4]}…{receipt['fingerprint'][-4:]} at {receipt['git_commit'][:7] or 'no commit'}\")\n save(receipts_path, receipts)\n\n\ndef recorded():\n path = state_dir() / \"receipts.json\"\n if not path.is_file():\n not_asked(f\"no receipts at {path}\")\n return json.loads(path.read_text()).get(\"repos\", {})\n\n\ndef op_receipt(enola, config, args):\n repos = recorded()\n if args:\n if args[0] not in repos:\n not_asked(f\"no receipt recorded for {args[0]}\")\n repos = {args[0]: repos[args[0]]}\n print(json.dumps(repos, indent=2, sort_keys=True))\n\n\ndef op_diff(enola, config, args):\n repos = recorded()\n members, providers, output = read_config(config)\n for label in sorted(set(members) | set(repos)):\n was = repos.get(label)\n now = live_receipt(members[label], output, providers) if label in members else None\n if was is None:\n print(f\"{label}: no receipt recorded\")\n elif label not in members:\n print(f\"{label}: recorded, but not a member of {config.name}\")\n elif now is None:\n print(f\"{label}: no live snapshot to compare; run kb enola generate\")\n else:\n moved = [key for key in (\"fingerprint\", \"git_commit\", \"enola_version\", \"providers\") if was.get(key) != now.get(key)]\n print(f\"{label}: moved ({', '.join(moved)})\" if moved else f\"{label}: unchanged\")\n\n\ndef snapshot_files(config):\n members, _, output = read_config(config)\n for member in members.values():\n snap = snapshot_dir(member, output)\n if snap and (snap / \"insights.json\").is_file():\n return snap\n not_asked(\"no snapshot on disk; run kb enola generate\")\n\n\ndef op_findings(enola, config, args):\n only = args[args.index(\"--repo\") + 1] if \"--repo\" in args else None\n show_all = \"--all\" in args\n snap = snapshot_files(config)\n repo_of = {}\n with open(snap / \"facts.jsonl\") as facts:\n for line in facts:\n fact = json.loads(line)\n repo_of.setdefault(fact.get(\"name\"), fact.get(\"repo\"))\n ledger = load(state_dir() / \"findings.json\", {\"findings\": {}}).get(\"findings\", {})\n for finding in json.loads((snap / \"insights.json\").read_text()):\n signature = f\"{finding.get('source')}|{finding.get('title')}\"\n repos = sorted({repo_of.get(e.get(\"fact\")) for e in finding.get(\"evidence\") or [] if repo_of.get(e.get(\"fact\"))}) or [\"cluster\"]\n if only and only not in repos:\n continue\n judged = ledger.get(signature)\n if judged and not show_all:\n continue\n mark = f\" [{judged['verdict']}]\" if judged else \"\"\n print(f\"{finding.get('confidence', 0):.2f} {','.join(repos)} {signature}{mark}\")\n\n\ndef op_judge(enola, config, args):\n if len(args) < 2 or args[1] not in VERDICTS:\n say(f\"usage: kb enola judge <signature> {'|'.join(VERDICTS)} [--reason TEXT] [--landed PAGE]\")\n sys.exit(2)\n entry = {\"verdict\": args[1], \"at\": datetime.date.today().isoformat()}\n for flag, key in ((\"--reason\", \"reason\"), (\"--landed\", \"landed\")):\n if flag in args:\n entry[key] = args[args.index(flag) + 1]\n path = state_dir() / \"findings.json\"\n ledger = load(path, {\"findings\": {}})\n ledger.setdefault(\"findings\", {})[args[0]] = entry\n save(path, ledger)\n say(f\"{args[1]}: {args[0]}\")\n\n\ndef op_impact(enola, config, args):\n if not args:\n say(\"usage: kb enola impact <symbol>\")\n sys.exit(2)\n run([enola, \"impact\", *args, str(config)])\n\n\ndef frontmatter(page):\n text = page.read_text()\n if not text.startswith(\"---\\n\"):\n return \"\"\n return text[4:text.find(\"\\n---\", 4)]\n\n\ndef op_govern(enola, config, args):\n if not args:\n say(\"usage: kb enola govern <path or page>\")\n sys.exit(2)\n target = pathlib.Path(args[0])\n if target.suffix == \".md\" and target.is_file():\n globs = re.findall(r\"^\\s+- (\\S+)$\", frontmatter(target).split(\"governs:\", 1)[1], re.M) if \"governs:\" in frontmatter(target) else []\n if not globs:\n say(f\"{target} anchors no code (no enola_intent governs)\")\n return\n print(f\"{target} governs:\")\n for glob in globs:\n print(f\" {glob}\")\n return\n pages = []\n for page in sorted(pathlib.Path(os.environ.get(\"KB_KNOWLEDGE\", \"wiki\")).rglob(\"*.md\")):\n block = frontmatter(page)\n if \"governs:\" in block:\n globs = re.findall(r\"^\\s+- (\\S+)$\", block.split(\"governs:\", 1)[1].split(\"\\n decisions:\", 1)[0], re.M)\n if any(fnmatch.fnmatch(args[0], g) or fnmatch.fnmatch(args[0], g.removesuffix(\"/**\") + \"/*\") for g in globs):\n pages.append(page)\n if pages:\n print(f\"{args[0]} is governed by:\")\n for page in pages:\n print(f\" {page}\")\n else:\n print(f\"{args[0]}: no page governs it\")\n print()\n run([enola, \"plan\", \"-paths\", args[0], str(config)], check_exit=False)\n\n\ndef op_citations(enola, config, args):\n repos = recorded()\n counts = {\"verified\": 0, \"stale\": 0, \"malformed\": 0}\n for page in sorted(pathlib.Path(os.environ.get(\"KB_KNOWLEDGE\", \"wiki\")).rglob(\"*.md\")):\n if \"_state\" in page.parts:\n continue\n for number, line in enumerate(page.read_text().splitlines(), 1):\n for loose in LOOSE_CITATION.finditer(line):\n match = CITATION.fullmatch(loose.group(0))\n if not match:\n verdict = \"malformed\"\n else:\n label, commit, first, last = match.groups()\n receipt = repos.get(label) or {}\n fingerprint = receipt.get(\"fingerprint\", \"\")\n current = receipt.get(\"git_commit\", \"\").startswith(commit) and fingerprint[:4] == first and fingerprint[-4:] == last\n verdict = \"verified\" if current else \"stale\"\n counts[verdict] += 1\n print(f\"{verdict:9} {page}:{number} {loose.group(0)}\")\n print(\", \".join(f\"{n} {v}\" for v, n in counts.items()))\n\n\ndef op_baseline(enola, config, args):\n if not args or args[0] not in (\"pin\", \"show\", \"clear\"):\n say(\"usage: kb enola baseline pin|show|clear [member]\")\n sys.exit(2)\n run([enola, \"baseline\", args[0], member_or_config(config, args[1:])])\n\n\ndef member_or_config(config, args):\n if not args:\n return str(config)\n members, _, _ = read_config(config)\n if args[0] not in members:\n not_asked(f\"no member {args[0]} in {config.name}\")\n return str(members[args[0]])\n\n\ndef op_check(enola, config, args):\n code = run([enola, \"check\", member_or_config(config, args)], check_exit=False)\n if code == 1:\n say(\"the change introduced a regression; reported, not gating\")\n elif code == 3:\n say(\"not asked: the snapshots are not comparable (a different enola or provider set); re-pin and regenerate\")\n elif code != 0:\n say(f\"not asked: enola check could not run (exit {code}); with no pinned baseline, pin one with kb enola baseline pin\")\n sys.exit(0)\n\n\ndef op_coverage(enola, config, args):\n run([enola, \"coverage\", str(config)])\n\n\ndef op_doctor(enola, config, args):\n run([enola, \"doctor\", *([member_or_config(config, args)] if args else [])])\n\n\nOPS = {\n \"generate\": op_generate, \"receipt\": op_receipt, \"diff\": op_diff, \"findings\": op_findings,\n \"judge\": op_judge, \"impact\": op_impact, \"govern\": op_govern, \"citations\": op_citations,\n \"baseline\": op_baseline, \"check\": op_check, \"coverage\": op_coverage, \"doctor\": op_doctor,\n}\n\n\ndef main(argv):\n if len(argv) < 2 or argv[0] != \"enola\" or argv[1] not in OPS:\n print(__doc__.strip())\n return 0 if argv[1:2] in ([\"help\"], [\"--help\"]) or argv in ([], [\"enola\"]) else 2\n op = argv[1]\n if op == \"judge\":\n OPS[op](None, None, argv[2:])\n return 0\n config = cluster_config()\n enola = None if op in (\"receipt\", \"citations\", \"findings\", \"diff\") else binary()\n OPS[op](enola, config, argv[2:])\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv[1:]))\n",
|
|
175
|
+
"templates/skills/enola-citation-hygiene/SKILL.md": "---\nname: enola-citation-hygiene\ndescription: Use during grooming; gives every receipt citation in prose a verdict against the committed receipts and sweeps the finding ledger for judgments the code has moved past; knowledge-base track, needs the kb wrapper\n---\n\n# enola-citation-hygiene: every citation gets a verdict\n\nNeeds the knowledge base's `kb` wrapper on PATH: `enola-guides copy\ntemplates/kb` writes it to `bin/kb`, and guide 12 describes it. Run from\nthe knowledge base's root, beside its `enola-cluster.yaml`. Where `kb` is\nabsent, say the graph was not asked; never fill its answers in from\nmemory.\n\n1. `kb enola citations`: each receipt citation in prose is verified, stale\n (the digest moved) or malformed.\n2. Stale: re-read the cited page against the current facts and amend the\n sentence; never update the citation alone, the sentence is what may be\n wrong.\n3. Malformed: fix the citation to the recorded form.\n4. `kb enola findings --all`: for each accepted finding in the ledger,\n confirm its evidence still exists in the graph. Evidence gone means\n the code moved past the judgment; re-read the page that landed it and\n record the new state.\n5. Rejected and noise verdicts stay; they keep the finding hidden on\n purpose.\n\nA citation is a promise that a number was read, not remembered; this\nskill keeps the promise current.\n",
|
|
176
|
+
"templates/skills/enola-deepdive/SKILL.md": "---\nname: enola-deepdive\ndescription: Use before shaping or planning a change; asks the graph three questions (impact, governing decisions, existing findings) and writes the answers into the plan with the receipt they came from; knowledge-base track, needs the kb wrapper\n---\n\n# enola-deepdive: ask the graph before writing the plan\n\nNeeds the knowledge base's `kb` wrapper on PATH: `enola-guides copy\ntemplates/kb` writes it to `bin/kb`, and guide 12 describes it. Run from\nthe knowledge base's root, beside its `enola-cluster.yaml`. Where `kb` is\nabsent, say the graph was not asked; never fill its answers in from\nmemory.\n\n1. Name the symbols the change would touch. For each:\n `kb enola impact <Symbol>` (or `impact_analysis` over MCP). Record fan-in,\n fan-out and the named callers. A symbol with 157 dependents is a\n different plan from one with 3.\n2. For the paths: `kb enola govern <path>` (`governing_intent`). The pages\n returned are the decisions already covering the area; the plan amends\n one of them, it does not contradict it in silence.\n3. For the area: `query_insights` filtered to it. Cycles, layer crossings,\n dead routes and hotspots are already computed and some are already\n judged in the ledger; inherit them.\n4. Write the three answers into the plan, each with the receipt\n (`kb enola receipt <member>`) it was read from.\n5. If the graph is absent or the receipts are stale (`kb enola diff`), say\n so in the plan and proceed; never fill the numbers from memory.\n",
|
|
177
|
+
"templates/skills/enola-drift/SKILL.md": "---\nname: enola-drift\ndescription: Use on a schedule or before publishing; regenerates the cluster, compares committed receipts to live ones, and names the members whose pages want a re-read; knowledge-base track, needs the kb wrapper\n---\n\n# enola-drift: which members moved\n\nNeeds the knowledge base's `kb` wrapper on PATH: `enola-guides copy\ntemplates/kb` writes it to `bin/kb`, and guide 12 describes it. Run from\nthe knowledge base's root, beside its `enola-cluster.yaml`. Where `kb` is\nabsent, say the graph was not asked; never fill its answers in from\nmemory.\n\n1. `kb enola generate` to regenerate the cluster and record receipts.\n2. `kb enola diff`: each member whose fingerprint or commit moved is\n listed. Those are the members whose pages may describe a structure that\n no longer exists.\n3. `kb enola coverage` (`coverage_report`): which cross-repository edges\n resolved and which did not. An edge that stopped resolving is a\n consumer a page may still claim.\n4. For each moved member: `kb enola govern <member path>` and re-read the\n pages returned against the new facts. Amend what changed; leave what\n did not.\n5. Commit the receipts with the page amendments, in one commit.\n\nThis is a comparison of receipts, not of findings. Findings between two\nruns are the grade's job (`enola-grade-before-push`).\n",
|
|
178
|
+
"templates/skills/enola-grade-before-push/SKILL.md": "---\nname: enola-grade-before-push\ndescription: Use before pushing or presenting a change that touched how modules depend on each other, and whenever the session-end hook reports a finding or says it could not compare\n---\n\n# enola-grade-before-push: the delta is the deliverable\n\n1. Pin once, early: `enola baseline pin .` (or `set_baseline` over MCP) before\n editing. A baseline pinned after the edit grades nothing.\n2. After the change: `enola check .` (it snapshots the tree itself), or\n `generate_snapshot` then `diff_snapshot`. Exit 0 is clean, 1 a\n regression the policy fails, 2 a gate that could not run (no pinned\n baseline, a bad flag), 3 declined.\n3. Read the verdict in this order: findings introduced (fix or justify before\n pushing), findings resolved (say so in the description), edges added.\n \"Newly declared\" lists what a law this change added reports on code it\n did not touch: the law's starting point, not a regression.\n4. A DECLINED verdict (exit 3) is not a pass. The snapshots are not\n comparable: a different enola version or different ignore globs. Re-pin\n the baseline and grade again.\n5. A partial verdict is not a full one. A producer ran on one side only (a\n provider, or an extractor for a language the baseline had none of, such\n as a first `.rb` law file in a Python repository): only the facts both\n sides share were graded, and the verdict names what it left out. Re-pin\n with the same producers, or say in the description what was not graded.\n6. Put the grade in the pull request description in one sentence: what the\n change did to the architecture, and what it did not.\n\nNever push a change whose grade you have not read, and never call a\ndeclined or partial grade clean.\n",
|
|
179
|
+
"templates/skills/enola-ingest-check/SKILL.md": "---\nname: enola-ingest-check\ndescription: Use while ingesting a source into the knowledge base; checks every structural claim against the graph while the source is open, and routes changed paths to the pages that govern them; knowledge-base track, needs the kb wrapper\n---\n\n# enola-ingest-check: verify the claim while the source is open\n\nNeeds the knowledge base's `kb` wrapper on PATH: `enola-guides copy\ntemplates/kb` writes it to `bin/kb`, and guide 12 describes it. Run from\nthe knowledge base's root, beside its `enola-cluster.yaml`. Where `kb` is\nabsent, say the graph was not asked; never fill its answers in from\nmemory.\n\n1. When a page is about to state a structural fact (a stack, a public\n surface, a route, a table, a count): `query_facts` for the fact,\n `kb enola impact <Symbol>` for the count. Write what the graph says,\n and cite the receipt.\n2. For an incremental ingest, for each changed path:\n `kb enola govern <path>`. The pages returned are the synthesis that may\n have drifted; re-read them against the change. A changed path no page\n governs is a gap the ingest is about to fill, so anchor the new page to\n it with `enola_intent`.\n3. A claim the graph cannot answer (product intent, history, a reason) is\n stated as the source's claim, not as a fact of the code.\n4. If the graph is absent, the page says the claim is unverified; it does\n not silently become a fact.\n\nA wrong claim ingested today is cited tomorrow and reasoned from next\nweek; verification is cheapest now.\n",
|
|
176
180
|
"templates/skills/enola-intent-first/SKILL.md": "---\nname: enola-intent-first\ndescription: Use before editing code in a repository whose team keeps decision records compiled into enola, so the decision governing the file is read before the file moves\n---\n\n# enola-intent-first: read the decision before touching the code\n\nWhen a repository compiles its knowledge pages into the graph (pages with\nan `enola_intent:` block and anchors), the graph answers which decisions\ngovern a file.\n\nBefore the first edit to a file:\n\n1. `governing_intent` (MCP) or `enola plan -paths <file> .` for the page\n trail and the declared constraints that bind the path, with the blast\n radius over the current snapshot.\n2. Read the governing page. If the intended change contradicts it, amend\n the page first, then edit the code; a deviation narrated afterwards is\n drift.\n3. If nothing governs the file and the area is one the team documents,\n say so and ask whether it should be shaped before building.\n\nA hook can enforce the reading: on the first edit into a repository per\npiece of work, block once, inject the trail, and let the retry proceed.\nThe enforcement is the reading, never the verdict.\n",
|
|
177
|
-
"templates/skills/enola-laws/SKILL.md": "---\nname: enola-laws\ndescription: Use when asked to add, change or explain an architecture law, when a constraints finding names a rule, or when a new directory or convention should be declared\n---\n\n# enola-laws: laws are sentences with a reason\n\nLaws live in `enola/constraints/`, in Ruby (parsed, never executed) or YAML.\nA law is a part, a sentence, a reason and a mode. Before writing one, read\nthe existing declaration and `enola constraints lint .` so the new part\ndoes not shadow an existing one.\n\nWriting a law:\n\n1. Name the part by what it is (`part :billing, files: \"app/billing/**\"`),\n or by what its facts carry (`kind: :symbol, where: { symbol_kind: \"getter\" }`,\n `ancestor: \"ApplicationRecord\"`, `handles: [:post, :put, :patch, :delete]`).\n2. Pick the sentence from the vocabulary (`must_not_call`, `must_not_reach`,\n `is_reached_only_by`, `stays_inside`, `must_define`, `names_must_match`,\n `must_not_cycle_with`, `must_not_reach_includers`, `storage_must_stay_home`,\n `must_keep_budget`, `must_have_consumer`, `must_be_unique_across`,\n `must_be_governed`, `at_most`, `must_carry`, `advises`).\n3. Write `why` in one sentence a reviewer outside the team understands.\n4. Start in `mode :ratchet`. `strict` is a team decision, never a default.\n5. Run `enola constraints lint .`; every part must resolve to something and\n every rule must be in force, or the lint says which role is unbound.\n6.
|
|
178
|
-
"templates/skills/enola-zoom-out/SKILL.md": "---\nname: enola-zoom-out\ndescription: Use when deep in a change and the question is how it sits in the whole; answers the technical blast radius in one paragraph from the graph, beside the product fit reasoned out\n---\n\n# enola-zoom-out: the blast radius in one paragraph\n\n1. `kb enola impact <Symbol>` for the symbols in hand: fan-in, named callers.\n2. `find_path` from the part being changed to the parts that consume it:\n the route the change travels.\n3. `kb enola govern <path>`: the decisions around it.\n4. Write one paragraph of technical fit: what depends on this, through\n what, under which decisions. Put it beside the product fit, which the\n graph cannot answer.\n5. If the paragraph names a dependent the plan did not, the plan is\n incomplete; return to `enola-deepdive`.\n",
|
|
179
|
-
"templates/skills/enola/SKILL.md": "---\nname: enola\ndescription: Use before a change whose impact across the codebase is not obvious, after a change to how modules depend on each other, or when the enola hook or CI check reports a finding\n---\n\n# enola: the architecture before and after a change\n\nThis project has enola, which serves a map of the codebase's structure over\nMCP: modules, symbols, routes, storage, and how they depend on each other.\nThe map is built from a snapshot of the code at a commit, and a change is\ngraded by comparing the snapshot before it with the snapshot after it.\n\nBefore changing code whose impact is not obvious:\n\n- `impact_analysis`: what transitively depends on this, before you touch it.\n- `explore` / `traverse` / `find_path`: how something is wired, instead of\n reconstructing it by reading files.\n- `constraints_for` / `plan_check`: which declared laws bind the path, and\n what the planned edit would breach.\n- `governing_intent`: which decision pages govern this file, when the team\n keeps them.\n- `set_baseline`: record the architecture as it is BEFORE you start editing,\n so the change can be graded afterwards. Do this once, early.\n\nAfter a change to how modules depend on each other, run `generate_snapshot`\nand then `diff_snapshot` to see what the change did: findings introduced or\nresolved, coupling added, symbols added or removed. A dependency cycle or\ncoupling nobody asked for is a reason to fix the change before presenting\nit, not something to mention afterwards.\n\nPrefer these over re-deriving structure by grepping. They are exact, and\nthey cost a fraction of the file reading they replace.\n\nIf the session-end hook or the CI check reports a finding, read it the same\nway: it names the structural change and the rule it crossed, and its first\nsuggested action is the smallest cut the graph can see. If it reports\ninstead that it
|
|
181
|
+
"templates/skills/enola-laws/SKILL.md": "---\nname: enola-laws\ndescription: Use when asked to add, change or explain an architecture law, when a constraints finding names a rule, or when a new directory or convention should be declared\n---\n\n# enola-laws: laws are sentences with a reason\n\nLaws live in `enola/constraints/`, in Ruby (parsed, never executed) or YAML.\nA law is a part, a sentence, a reason and a mode. Before writing one, read\nthe existing declaration and `enola constraints lint .` so the new part\ndoes not shadow an existing one.\n\nWrite the law in the language the repository's laws already use. In a\nrepository with no Ruby, write YAML: the first `.rb` file is a new Ruby\nsource to the extractor, the baseline has no Ruby facts, and the grade\nturns partial, reporting breaches the law finds on existing code as\nregressions.\n\nWriting a law:\n\n1. Name the part by what it is (`part :billing, files: \"app/billing/**\"`),\n or by what its facts carry (`kind: :symbol, where: { symbol_kind: \"getter\" }`,\n `ancestor: \"ApplicationRecord\"`, `handles: [:post, :put, :patch, :delete]`).\n2. Pick the sentence from the vocabulary (`must_not_call`, `must_not_reach`,\n `is_reached_only_by`, `stays_inside`, `must_define`, `names_must_match`,\n `must_not_cycle_with`, `must_not_reach_includers`, `storage_must_stay_home`,\n `must_keep_budget`, `must_have_consumer`, `must_be_unique_across`,\n `must_be_governed`, `at_most`, `must_carry`, `advises`).\n3. Write `why` in one sentence a reviewer outside the team understands.\n4. Start in `mode :ratchet`. `strict` is a team decision, never a default.\n5. Run `enola constraints lint .`; every part must resolve to something and\n every rule must be in force, or the lint says which role is unbound.\n6. Grade it against a baseline pinned before the law existed: `enola check .`\n (pin first with `enola baseline pin .` if `check` says there is none, then\n add the law). The breaches it finds on existing code are listed under\n \"Newly declared\": the law's starting point, not regressions. Read them\n before presenting it. With no baseline at all, `enola --generate .` then\n `enola constraints ledger .` lists each rule's breaches. A law that\n reports hundreds of existing breaches is fine under ratchet; a law that\n reports none may be selecting nothing, which\n `enola constraints explain <a file it should select>` settles.\n7. `mode :advisory` reports at confidence 0.90, below the 1.00 a check\n fails at, so it never fails a build unless the check lowers its floor\n with `-min-confidence`. `mode :strict` fails on every breach, old or new;\n its only override is a signed entry in `.enola/suppressions.yaml`.\n\nReading a finding: the title names the rule, the member and the edge; the\nfirst suggested action is the smallest cut the graph can see; \"cannot be\nevaluated\" names the cause (no resolved ancestry, no runtime capture, no\ncounterparty, no compiled pages) and is never a pass.\n",
|
|
182
|
+
"templates/skills/enola-zoom-out/SKILL.md": "---\nname: enola-zoom-out\ndescription: Use when deep in a change and the question is how it sits in the whole; answers the technical blast radius in one paragraph from the graph, beside the product fit reasoned out; knowledge-base track, needs the kb wrapper\n---\n\n# enola-zoom-out: the blast radius in one paragraph\n\nNeeds the knowledge base's `kb` wrapper on PATH: `enola-guides copy\ntemplates/kb` writes it to `bin/kb`, and guide 12 describes it. Run from\nthe knowledge base's root, beside its `enola-cluster.yaml`. Where `kb` is\nabsent, say the graph was not asked; never fill its answers in from\nmemory.\n\n1. `kb enola impact <Symbol>` for the symbols in hand: fan-in, named callers.\n2. `find_path` from the part being changed to the parts that consume it:\n the route the change travels.\n3. `kb enola govern <path>`: the decisions around it.\n4. Write one paragraph of technical fit: what depends on this, through\n what, under which decisions. Put it beside the product fit, which the\n graph cannot answer.\n5. If the paragraph names a dependent the plan did not, the plan is\n incomplete; return to `enola-deepdive`.\n",
|
|
183
|
+
"templates/skills/enola/SKILL.md": "---\nname: enola\ndescription: Use before a change whose impact across the codebase is not obvious, after a change to how modules depend on each other, or when the enola hook or CI check reports a finding\n---\n\n# enola: the architecture before and after a change\n\nThis project has enola, which serves a map of the codebase's structure over\nMCP: modules, symbols, routes, storage, and how they depend on each other.\nThe map is built from a snapshot of the code at a commit, and a change is\ngraded by comparing the snapshot before it with the snapshot after it.\n\nThe tools below are enola's MCP tools. If none of them is in reach, the\nserver is not registered; in Claude Code that is\n`claude mcp add-json enola '{\"command\":\"enola\",\"alwaysLoad\":true}'` (Claude\nCode 2.1.287 or later; on an older one `claude mcp add enola enola`), then a\nnew session. If they are listed by name only (deferred), load them with\nToolSearch before the first one. The CLI answers the same questions\nwithout the server: `enola impact`, `enola plan`, `enola check`.\n\nBefore changing code whose impact is not obvious:\n\n- `impact_analysis`: what transitively depends on this, before you touch it.\n- `explore` / `traverse` / `find_path`: how something is wired, instead of\n reconstructing it by reading files.\n- `constraints_for` / `plan_check`: which declared laws bind the path, and\n what the planned edit would breach.\n- `governing_intent`: which decision pages govern this file, when the team\n keeps them.\n- `set_baseline`: record the architecture as it is BEFORE you start editing,\n so the change can be graded afterwards. Do this once, early.\n\nAfter a change to how modules depend on each other, run `generate_snapshot`\nand then `diff_snapshot` to see what the change did: findings introduced or\nresolved, coupling added, symbols added or removed. A dependency cycle or\ncoupling nobody asked for is a reason to fix the change before presenting\nit, not something to mention afterwards.\n\nPrefer these over re-deriving structure by grepping. They are exact, and\nthey cost a fraction of the file reading they replace.\n\nIf the session-end hook or the CI check reports a finding, read it the same\nway: it names the structural change and the rule it crossed, and its first\nsuggested action is the smallest cut the graph can see. If it reports\ninstead that it declined to grade, the two snapshots were not produced the\nsame way (a different enola version, different ignore globs); nothing is\nwrong with your change, run `set_baseline` again and re-check. A partial\nverdict graded only what both snapshots share; it names what it left out.\n\n`enola install` writes `.claude/rules/enola.md`, which tells the agent much\nof this on every session. Keep that rule or this skill, not both.\n"
|
|
180
184
|
},
|
|
181
185
|
"items": [
|
|
186
|
+
{
|
|
187
|
+
"default_dir": ".",
|
|
188
|
+
"files": [
|
|
189
|
+
{
|
|
190
|
+
"mode": 420,
|
|
191
|
+
"path": "guides/00-concepts.md",
|
|
192
|
+
"to": "00-concepts.md"
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"id": "guides/00-concepts",
|
|
196
|
+
"kind": "guide",
|
|
197
|
+
"show": "guides/00-concepts.md",
|
|
198
|
+
"title": "What enola is, and the words these guides use"
|
|
199
|
+
},
|
|
182
200
|
{
|
|
183
201
|
"default_dir": ".",
|
|
184
202
|
"files": [
|
|
@@ -317,7 +335,7 @@
|
|
|
317
335
|
"id": "guides/10-hooks",
|
|
318
336
|
"kind": "guide",
|
|
319
337
|
"show": "guides/10-hooks.md",
|
|
320
|
-
"title": "
|
|
338
|
+
"title": "Knowledge-base hooks: the graph at the moments that matter"
|
|
321
339
|
},
|
|
322
340
|
{
|
|
323
341
|
"default_dir": ".",
|
|
@@ -331,7 +349,7 @@
|
|
|
331
349
|
"id": "guides/11-the-skills-as-they-run",
|
|
332
350
|
"kind": "guide",
|
|
333
351
|
"show": "guides/11-the-skills-as-they-run.md",
|
|
334
|
-
"title": "The skills, as they run"
|
|
352
|
+
"title": "The knowledge-base skills, as they run"
|
|
335
353
|
},
|
|
336
354
|
{
|
|
337
355
|
"default_dir": ".",
|
|
@@ -431,6 +449,34 @@
|
|
|
431
449
|
"show": "guides/18-rust.md",
|
|
432
450
|
"title": "A Rust service"
|
|
433
451
|
},
|
|
452
|
+
{
|
|
453
|
+
"default_dir": ".",
|
|
454
|
+
"files": [
|
|
455
|
+
{
|
|
456
|
+
"mode": 420,
|
|
457
|
+
"path": "guides/19-rolling-out.md",
|
|
458
|
+
"to": "19-rolling-out.md"
|
|
459
|
+
}
|
|
460
|
+
],
|
|
461
|
+
"id": "guides/19-rolling-out",
|
|
462
|
+
"kind": "guide",
|
|
463
|
+
"show": "guides/19-rolling-out.md",
|
|
464
|
+
"title": "Rolling out"
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"default_dir": ".",
|
|
468
|
+
"files": [
|
|
469
|
+
{
|
|
470
|
+
"mode": 420,
|
|
471
|
+
"path": "guides/20-reading-a-verdict.md",
|
|
472
|
+
"to": "20-reading-a-verdict.md"
|
|
473
|
+
}
|
|
474
|
+
],
|
|
475
|
+
"id": "guides/20-reading-a-verdict",
|
|
476
|
+
"kind": "guide",
|
|
477
|
+
"show": "guides/20-reading-a-verdict.md",
|
|
478
|
+
"title": "Reading a verdict"
|
|
479
|
+
},
|
|
434
480
|
{
|
|
435
481
|
"default_dir": ".",
|
|
436
482
|
"files": [
|
|
@@ -1534,7 +1580,7 @@
|
|
|
1534
1580
|
}
|
|
1535
1581
|
],
|
|
1536
1582
|
"id": "recipes/README",
|
|
1537
|
-
"kind": "
|
|
1583
|
+
"kind": "guide",
|
|
1538
1584
|
"show": "recipes/README.md",
|
|
1539
1585
|
"title": "Recipes"
|
|
1540
1586
|
},
|
|
@@ -1586,6 +1632,20 @@
|
|
|
1586
1632
|
"show": "templates/enola-check.sh",
|
|
1587
1633
|
"title": "the CI check that grades a pull request and renders the comment"
|
|
1588
1634
|
},
|
|
1635
|
+
{
|
|
1636
|
+
"default_dir": "bin",
|
|
1637
|
+
"files": [
|
|
1638
|
+
{
|
|
1639
|
+
"mode": 493,
|
|
1640
|
+
"path": "templates/kb/kb",
|
|
1641
|
+
"to": "kb"
|
|
1642
|
+
}
|
|
1643
|
+
],
|
|
1644
|
+
"id": "templates/kb",
|
|
1645
|
+
"kind": "template",
|
|
1646
|
+
"show": "templates/kb/kb",
|
|
1647
|
+
"title": "kb enola: the knowledge base's wrapper the five knowledge-base skills call"
|
|
1648
|
+
},
|
|
1589
1649
|
{
|
|
1590
1650
|
"default_dir": ".",
|
|
1591
1651
|
"files": [
|
|
@@ -1601,6 +1661,6 @@
|
|
|
1601
1661
|
"title": "A cluster: every repository's facts land in one union graph, labelled by"
|
|
1602
1662
|
}
|
|
1603
1663
|
],
|
|
1604
|
-
"usage": "enola-guides 0.
|
|
1605
|
-
"version": "0.
|
|
1664
|
+
"usage": "enola-guides 0.9.0: the enola guides, examples, laws, skills, hooks, recipes and templates\n\n enola-guides list [kind] every item, or those of one kind: guide, example, law, skill, hook, recipe, template\n enola-guides show <id> print an item: a guide, or the README of an example or a law\n enola-guides copy <id> [dir] write an item's files under dir, refusing to overwrite; each kind has a default dir\n enola-guides version print the version\n",
|
|
1665
|
+
"version": "0.9.0"
|
|
1606
1666
|
}
|