enola-guides 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +20 -3
- data/examples/knowledge-base/README.md +20 -0
- data/examples/knowledge-base/enola-cluster.yaml +7 -0
- data/examples/knowledge-base/wiki/_state/enola/findings.json +10 -0
- data/examples/knowledge-base/wiki/_state/enola/receipts.json +16 -0
- data/examples/knowledge-base/wiki/shop/permanent/architecture.md +19 -0
- data/examples/knowledge-base/wiki/shop/permanent/public-surface.md +14 -0
- data/examples/laws/cluster-laws/README.md +1 -1
- data/examples/laws/cluster-laws/enola/constraints/a-mutating-action-is-authorized.rb +10 -0
- data/examples/laws/cluster-laws/enola/constraints/every-api-route-has-a-consumer.rb +9 -0
- data/examples/laws/cluster-laws/enola/constraints/one-owner-per-table.rb +9 -0
- data/examples/laws/maintenance-tasks/README.md +1 -1
- data/examples/laws/maintenance-tasks/enola/constraints/a-maintenance-task-never-invokes-controller-code.rb +9 -0
- data/examples/laws/maintenance-tasks/enola/constraints/{maintenance-tasks.rb → every-maintenance-task-lives-in-the-maintenance-namespace.rb} +0 -7
- data/examples/rails-shop/enola/constraints/a-concern-never-reaches-the-class-that-includes-it.rb +9 -0
- data/examples/rails-shop/enola/constraints/a-mutating-action-is-authorized.rb +10 -0
- data/examples/rails-shop/enola/constraints/a-service-answers-to-call.rb +9 -0
- data/examples/rails-shop/enola/constraints/background-jobs-never-invoke-controller-code.rb +9 -0
- data/examples/rails-shop/enola/constraints/billing-is-documented.rb +10 -0
- data/examples/rails-shop/enola/constraints/billing-keeps-to-its-own-tables.rb +9 -0
- data/examples/rails-shop/enola/constraints/frames-keep-their-query-budget.rb +9 -0
- data/examples/rails-shop/enola/constraints/jobs-models-and-mailers-never-depend-on-each-other-in-a-circle.rb +9 -0
- data/examples/rails-shop/enola/constraints/orders-are-reached-through-their-public-surface.rb +9 -0
- data/examples/rails-shop/enola/constraints/reach-for-an-existing-service.rb +8 -0
- data/examples/rails-shop/enola/constraints/scopes-come-in-pairs.rb +9 -0
- data/examples/rails-shop/enola/constraints/the-public-api-stays-small.rb +10 -0
- data/examples/rails-shop/enola/constraints/the-request-api-stays-in-controllers.rb +9 -0
- data/guides/09-a-knowledge-base-with-enola.md +148 -0
- data/guides/10-hooks.md +101 -0
- data/guides/11-the-skills-as-they-run.md +81 -0
- data/guides/12-the-wrapper.md +69 -0
- data/lib/enola-guides.rb +1 -1
- data/templates/constraints-starter/background-jobs-never-invoke-controller-code.rb +9 -0
- data/templates/constraints-starter/models-do-not-reach-controllers.rb +9 -0
- data/templates/constraints-starter/policies-only-answer.rb +9 -0
- data/templates/constraints-starter/the-request-api-stays-in-controllers.rb +9 -0
- data/templates/hooks/intent-first-gate.sh +49 -0
- data/templates/hooks/record.sh +37 -0
- data/templates/hooks/session-refresh.sh +31 -0
- data/templates/hooks/stop-debt.sh +39 -0
- data/templates/skills/enola-citation-hygiene/SKILL.md +22 -0
- data/templates/skills/enola-deepdive/SKILL.md +21 -0
- data/templates/skills/enola-drift/SKILL.md +21 -0
- data/templates/skills/enola-ingest-check/SKILL.md +23 -0
- data/templates/skills/enola-zoom-out/SKILL.md +16 -0
- metadata +42 -5
- data/examples/laws/cluster-laws/enola/constraints/cluster.rb +0 -24
- data/examples/rails-shop/enola/constraints/laws.rb +0 -93
- data/templates/constraints-starter.rb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8d331c92a1d9c3343d76d079967a3d6c96ec41227c35ca99c2977c8980764b6
|
|
4
|
+
data.tar.gz: 578aebe657940e45dcab775af903b1e8f6ddd4eb47ab4880d77b81a8ae67fcf1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57c7b7963c5d75e2750338d4f287c8be1115a808d28004f3cefae76b2823de297b3b44e94a2150638b499bb32994f4df32449494b0f2a5daa7cee5e80d3072d6
|
|
7
|
+
data.tar.gz: 7571fc1b51b77801f6d8f0c032b59f1c550a442ac1e73fb686ba5aded8efc000b2ba4c6c83aa1579c587b8b073abb095fc035c89bb88fc367d7eedaf8e4e8b65
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-08-22)
|
|
4
|
+
|
|
5
|
+
What a knowledge base gets. Four guides (a knowledge base with enola,
|
|
6
|
+
hooks, the skills as they run, the wrapper), four hook templates
|
|
7
|
+
(intent-first gate, turn record, stop-time debt, session refresh), five
|
|
8
|
+
skills as a knowledge base runs them (deepdive, ingest check, drift,
|
|
9
|
+
citation hygiene, zoom out) and a worked example knowledge base with two
|
|
10
|
+
anchored pages, receipts and a finding ledger.
|
|
11
|
+
|
|
12
|
+
## 0.1.2 (2026-08-22)
|
|
13
|
+
|
|
14
|
+
One law per file: the Rails shop, the cluster laws, the maintenance-task laws
|
|
15
|
+
and the starter each put every law in a file of its own that declares the parts it
|
|
16
|
+
speaks about, so a reader finds a law by its name and a reviewer sees one
|
|
17
|
+
law per diff.
|
|
18
|
+
|
|
3
19
|
## 0.1.1 (2026-08-22)
|
|
4
20
|
|
|
5
21
|
The laws catalogue: `examples/laws/`, twenty-six complete declarations one
|
data/README.md
CHANGED
|
@@ -18,11 +18,13 @@ gem contents enola-guides
|
|
|
18
18
|
## Start with an example
|
|
19
19
|
|
|
20
20
|
[`examples/rails-shop`](examples/rails-shop): a Rails application declaring
|
|
21
|
-
thirteen laws in the Ruby DSL, from "jobs never invoke controller code" to
|
|
21
|
+
thirteen laws in the Ruby DSL, one file each, from "jobs never invoke controller code" to
|
|
22
22
|
"billing keeps to its own tables" and "a mutating action is authorized",
|
|
23
23
|
with Prism and Rubydex configured as providers beside the extractors.
|
|
24
24
|
[`examples/ember-app`](examples/ember-app): an Ember application bound to
|
|
25
|
-
the conventions recipe in one block. [`examples/
|
|
25
|
+
the conventions recipe in one block. [`examples/knowledge-base`](examples/knowledge-base/README.md):
|
|
26
|
+
two pages anchored to code, a cluster config, receipts and a finding ledger,
|
|
27
|
+
the smallest knowledge base the skills and hooks run against. [`examples/laws`](examples/laws/README.md):
|
|
26
28
|
twenty-six laws, one directory each, from foreign keys on a tenant column
|
|
27
29
|
to parts that may not cycle, each a complete declaration you copy and run.
|
|
28
30
|
|
|
@@ -43,6 +45,16 @@ to parts that may not cycle, each a complete declaration you copy and run.
|
|
|
43
45
|
census, the runtime capture.
|
|
44
46
|
8. [Beyond one repository](guides/08-beyond-one-repo.md): the cluster, the
|
|
45
47
|
seams, the laws across them, the history.
|
|
48
|
+
9. [A knowledge base with enola](guides/09-a-knowledge-base-with-enola.md):
|
|
49
|
+
what a wiki of decisions gets when its pages anchor to code and every
|
|
50
|
+
number it states was read from a receipt.
|
|
51
|
+
10. [Hooks](guides/10-hooks.md): the four agent hooks a knowledge base runs
|
|
52
|
+
(intent-first gate, turn record, stop-time debt, session refresh).
|
|
53
|
+
11. [The skills, as they run](guides/11-the-skills-as-they-run.md): deepdive
|
|
54
|
+
before shaping, claim check while ingesting, drift on a schedule,
|
|
55
|
+
citation hygiene, the blast radius.
|
|
56
|
+
12. [The wrapper](guides/12-the-wrapper.md): the thin command the skills
|
|
57
|
+
call, the named skip, the receipts and the ledger.
|
|
46
58
|
|
|
47
59
|
## Skills
|
|
48
60
|
|
|
@@ -52,7 +64,12 @@ before grepping, grade after changing), `enola-laws` (write a law as a
|
|
|
52
64
|
sentence with a reason, lint it, read what it verdicts),
|
|
53
65
|
`enola-grade-before-push` (the delta is the deliverable, a declined grade
|
|
54
66
|
is not a pass), `enola-intent-first` (read the governing decision before
|
|
55
|
-
the file moves)
|
|
67
|
+
the file moves), and the five a knowledge base runs: `enola-deepdive`,
|
|
68
|
+
`enola-ingest-check`, `enola-drift`, `enola-citation-hygiene`,
|
|
69
|
+
`enola-zoom-out`, each described as it runs in
|
|
70
|
+
[guide 11](guides/11-the-skills-as-they-run.md). Copy a directory into
|
|
71
|
+
your agent's skills folder; the hooks they pair with are under
|
|
72
|
+
[`templates/hooks/`](templates/hooks).
|
|
56
73
|
|
|
57
74
|
## Templates and recipes
|
|
58
75
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# A knowledge base with enola, minimal
|
|
2
|
+
|
|
3
|
+
The smallest knowledge base that gets what [guide 09](../../guides/09-a-knowledge-base-with-enola.md) describes:
|
|
4
|
+
two pages anchored to code with `enola_intent`, a cluster config, the
|
|
5
|
+
committed receipts, and the finding ledger. The skills under
|
|
6
|
+
[`templates/skills`](../../templates/skills) and the hooks under
|
|
7
|
+
[`templates/hooks`](../../templates/hooks) run against exactly these
|
|
8
|
+
files.
|
|
9
|
+
|
|
10
|
+
- `enola-cluster.yaml`: the members, with `knowledge` pointing at the wiki.
|
|
11
|
+
- `wiki/shop/permanent/architecture.md`: a page whose `enola_intent`
|
|
12
|
+
anchors it to `app/jobs/**` and `app/models/**`, with one receipt
|
|
13
|
+
citation.
|
|
14
|
+
- `wiki/shop/permanent/public-surface.md`: a page anchored to the
|
|
15
|
+
controllers it describes.
|
|
16
|
+
- `wiki/_state/enola/receipts.json`: what `kb enola generate` recorded.
|
|
17
|
+
- `wiki/_state/enola/findings.json`: the ledger, one judged finding.
|
|
18
|
+
|
|
19
|
+
`governing_intent app/jobs/order_mailer_job.rb` returns the first page;
|
|
20
|
+
`kb enola citations` verifies its citation against the receipts file.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repos": {
|
|
3
|
+
"shop": {
|
|
4
|
+
"fingerprint": "8d1e5a7b93c2f0e4d6a18b7c3e9f0a2d4c6b8e1f3a5c7d9e0b2f4a6c8e0d2c40a",
|
|
5
|
+
"git_commit": "3f2a9c1d4e5b6a7c8d9e0f1a2b3c4d5e6f7a8b9c",
|
|
6
|
+
"enola_version": "0.1.22",
|
|
7
|
+
"providers": ["prism", "rubydex"]
|
|
8
|
+
},
|
|
9
|
+
"shop-ui": {
|
|
10
|
+
"fingerprint": "2c7e9b1d4f6a8c0e2b4d6f8a0c2e4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e",
|
|
11
|
+
"git_commit": "9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c",
|
|
12
|
+
"enola_version": "0.1.22",
|
|
13
|
+
"providers": []
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shop architecture
|
|
3
|
+
enola_intent:
|
|
4
|
+
governs:
|
|
5
|
+
- app/jobs/**
|
|
6
|
+
- app/models/**
|
|
7
|
+
decisions:
|
|
8
|
+
- jobs enqueue, models decide; a job never calls back into a job
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Shop architecture
|
|
12
|
+
|
|
13
|
+
Orders are written by `OrderService` only; the two other writers the
|
|
14
|
+
graph reported in 2026-08 were folded into it. `Order` has 41 dependents
|
|
15
|
+
(receipt shop@3f2a9c1, fingerprint 8d1e…c40a).
|
|
16
|
+
|
|
17
|
+
The cycle `app/jobs -> app/models -> app/jobs` through the mailer path is
|
|
18
|
+
real and accepted in the ledger; it is cut when the mailer moves under
|
|
19
|
+
`app/services`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Public surface
|
|
3
|
+
enola_intent:
|
|
4
|
+
governs:
|
|
5
|
+
- app/controllers/api/**
|
|
6
|
+
decisions:
|
|
7
|
+
- every route under /api has at least one consumer in shop-ui
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Public surface
|
|
11
|
+
|
|
12
|
+
Fourteen routes under `/api`, all matched by a consumer in `shop-ui`
|
|
13
|
+
(coverage report, receipt shop@3f2a9c1). A route that stops matching is
|
|
14
|
+
a finding of `unused-routes`, judged in the ledger before any removal.
|
|
@@ -5,5 +5,5 @@ repositories, mutating actions selected through the routes they handle, and
|
|
|
5
5
|
routes no loaded client calls. Each refuses by name in a single-repository
|
|
6
6
|
snapshot rather than reporting silence as compliance.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
One file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,
|
|
9
9
|
then `enola --generate . && enola check .`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Enola.architecture "platform" do
|
|
2
|
+
part :mutating_actions, service: :backend, files: "app/controllers/**", handles: [:post, :put, :patch, :delete]
|
|
3
|
+
part :policies, service: :backend, files: "app/policies/**"
|
|
4
|
+
|
|
5
|
+
law "a mutating action is authorized" do
|
|
6
|
+
mutating_actions.must_reach :policies
|
|
7
|
+
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"
|
|
8
|
+
mode :advisory
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "platform" do
|
|
2
|
+
part :tables, files: "app/models/**", kind: :storage
|
|
3
|
+
|
|
4
|
+
law "one owner per table" do
|
|
5
|
+
tables.must_be_unique_across by: :table
|
|
6
|
+
why "a table two repositories both model is written by two codebases that never see each other's validations or callbacks"
|
|
7
|
+
mode :advisory
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -5,5 +5,5 @@ depends on, and a boundary against controller code that has no request to
|
|
|
5
5
|
read. Both were mined from a codebase where they already held, which is the
|
|
6
6
|
right moment to declare a law.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
One file per law under `enola/constraints/`, each file declaring the parts it speaks about. Copy it, run `enola constraints lint .`,
|
|
9
9
|
then `enola --generate . && enola check .`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "app" do
|
|
2
|
+
part :tasks, files: "app/tasks/**"
|
|
3
|
+
part :controllers, files: "app/controllers/**"
|
|
4
|
+
|
|
5
|
+
law "a maintenance task never invokes controller code" do
|
|
6
|
+
tasks.must_not_call controllers
|
|
7
|
+
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"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
Enola.architecture "app" do
|
|
2
2
|
part :maintenance_tasks, files: "app/tasks/**", kind: :symbol, where: { symbol_kind: "class" }
|
|
3
|
-
part :tasks, files: "app/tasks/**"
|
|
4
|
-
part :controllers, files: "app/controllers/**"
|
|
5
3
|
|
|
6
4
|
law "every maintenance task lives in the Maintenance namespace" do
|
|
7
5
|
maintenance_tasks.names_must_match "Maintenance::*"
|
|
8
6
|
why "the task runner resolves task constants from that namespace, so a task declared outside it never appears in the runner and cannot be run by anyone"
|
|
9
7
|
end
|
|
10
|
-
|
|
11
|
-
law "a maintenance task never invokes controller code" do
|
|
12
|
-
tasks.must_not_call controllers
|
|
13
|
-
why "a task runs from the runner or the console with no request around it, so controller code reached from one has nothing to read"
|
|
14
|
-
end
|
|
15
8
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
rails
|
|
3
|
+
part :mutating_actions, files: "app/controllers/**", handles: [:post, :put, :patch, :delete]
|
|
4
|
+
|
|
5
|
+
law "a mutating action is authorized" do
|
|
6
|
+
mutating_actions.must_reach :policies
|
|
7
|
+
why "every write passes a policy before it touches a record"
|
|
8
|
+
mode :ratchet
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
part :service_objects, files: "app/services/**", kind: :symbol, where: { symbol_kind: "class" }, owns: :methods
|
|
3
|
+
|
|
4
|
+
law "a service answers to call" do
|
|
5
|
+
service_objects.must_define :call
|
|
6
|
+
why "one door per service, so a caller never reads it to find the verb"
|
|
7
|
+
mode :ratchet
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
part :billing, files: ["app/billing/**", "app/models/invoice.rb"]
|
|
3
|
+
|
|
4
|
+
law "billing is documented" do
|
|
5
|
+
billing.must_be_governed
|
|
6
|
+
since "2026-09-01"
|
|
7
|
+
why "code under a decision needs the page that decided it"
|
|
8
|
+
mode :ratchet
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
part :billing, files: ["app/billing/**", "app/models/invoice.rb"]
|
|
3
|
+
|
|
4
|
+
law "billing keeps to its own tables" do
|
|
5
|
+
billing.storage_must_stay_home
|
|
6
|
+
why "a part that writes another part's table owns a bug it cannot see"
|
|
7
|
+
mode :ratchet
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
rails
|
|
3
|
+
|
|
4
|
+
law "frames keep their query budget" do
|
|
5
|
+
controllers.must_keep_budget metric: :queries, max: 20
|
|
6
|
+
why "a frame past twenty queries is a page that will not scale; refused by name until a capture exists"
|
|
7
|
+
mode :advisory
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
part :orders, files: ["app/orders/**", "app/models/order.rb"], public: ["app/orders/public/**"]
|
|
3
|
+
|
|
4
|
+
law "orders are reached through their public surface" do
|
|
5
|
+
orders.stays_inside
|
|
6
|
+
why "the public directory is the contract; everything else may change"
|
|
7
|
+
mode :ratchet
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
part :service_objects, files: "app/services/**", kind: :symbol, where: { symbol_kind: "class" }, owns: :methods
|
|
3
|
+
|
|
4
|
+
law "reach for an existing service" do
|
|
5
|
+
service_objects.advises "look in app/services before writing a new one"
|
|
6
|
+
why "duplication is a judgement no fact answers; advice where the edit happens is what can be offered honestly"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
part :records, ancestor: "ApplicationRecord"
|
|
3
|
+
|
|
4
|
+
law "scopes come in pairs" do
|
|
5
|
+
records.names_must_match "with_*", requires: "without_*"
|
|
6
|
+
why "every with_ scope has a without_ sibling, or the query reads half a thought"
|
|
7
|
+
mode :advisory
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Enola.architecture "shop" do
|
|
2
|
+
rails
|
|
3
|
+
|
|
4
|
+
law "the request api stays in controllers" do
|
|
5
|
+
models.must_not_call "params", receiver: :none
|
|
6
|
+
why "params without a receiver is the controller's; a model reading it only works inside a request"
|
|
7
|
+
mode :ratchet
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# A knowledge base with enola
|
|
2
|
+
|
|
3
|
+
A team that keeps decisions beside its code (a wiki, ADRs, a design log)
|
|
4
|
+
can compile them into the graph. Then a file answers which decision
|
|
5
|
+
governs it, a decision answers which code it covers, a structural claim
|
|
6
|
+
in prose can be re-checked mechanically, and a finding judged once stays
|
|
7
|
+
judged. This guide is that practice, as it runs day to day on a
|
|
8
|
+
twenty-repository knowledge base.
|
|
9
|
+
|
|
10
|
+
## Pages compile into the graph
|
|
11
|
+
|
|
12
|
+
A markdown page carrying an `enola_intent:` block is an intent fact when
|
|
13
|
+
the snapshot is taken, and every anchor on it is an edge to a file:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
---
|
|
17
|
+
title: Charges run through one service
|
|
18
|
+
enola_intent:
|
|
19
|
+
page:
|
|
20
|
+
type: decision
|
|
21
|
+
status: accepted
|
|
22
|
+
scope:
|
|
23
|
+
- shop
|
|
24
|
+
relations:
|
|
25
|
+
- rel: supersedes
|
|
26
|
+
to: wiki/adrs/charges-in-controllers.md
|
|
27
|
+
anchors:
|
|
28
|
+
- repo: shop
|
|
29
|
+
path: app/billing/charge.rb
|
|
30
|
+
- repo: shop
|
|
31
|
+
path: app/billing/refund.rb
|
|
32
|
+
---
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Derive the block rather than hand-maintain it: if your pages already cite
|
|
36
|
+
the code they describe (`sources:` lines naming repository paths), the
|
|
37
|
+
anchors are those citations, and a small script stamps the block from
|
|
38
|
+
them and checks the derivation in CI. enola validates the block; keeping
|
|
39
|
+
it truthful to your conventions is yours.
|
|
40
|
+
|
|
41
|
+
Index the knowledge base as a member of the cluster, last, so its pages
|
|
42
|
+
compile over the code they anchor:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
repos:
|
|
46
|
+
- ../shop
|
|
47
|
+
- ../mobile
|
|
48
|
+
- .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Ask the graph what governs a file
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
enola plan -paths app/billing/charge.rb .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
and over MCP, `governing_intent` for a file or a symbol: the pages whose
|
|
58
|
+
anchors cover it, with their relation trails (what supersedes what, what
|
|
59
|
+
depends on what), and for a page, which code its anchors cover. A file no
|
|
60
|
+
page governs is an answer too, and the difference between "no page
|
|
61
|
+
governs this" and "the graph was not asked" must stay visible: a missing
|
|
62
|
+
snapshot is a named skip, never silence.
|
|
63
|
+
|
|
64
|
+
This is the call behind the [intent-first gate](10-hooks.md): before an
|
|
65
|
+
agent's first edit into a repository, the governing pages are read.
|
|
66
|
+
|
|
67
|
+
## Laws over the pages
|
|
68
|
+
|
|
69
|
+
Because anchors are facts, laws can read them (see [writing
|
|
70
|
+
laws](04-writing-laws.md)):
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
part :billing, files: "app/billing/**"
|
|
74
|
+
part :old_way, governed_by: "wiki/adrs/*.md status:superseded"
|
|
75
|
+
part :new_way, governed_by: "wiki/adrs/charges-in-one-service.md"
|
|
76
|
+
|
|
77
|
+
law "billing is documented" do
|
|
78
|
+
billing.must_be_governed
|
|
79
|
+
why "code under a decision needs the page that decided it"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
law "the new way never reaches the old" do
|
|
83
|
+
new_way.must_not_call :old_way
|
|
84
|
+
why "a superseded decision's code is on its way out, not a dependency"
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Receipts are the committed state
|
|
89
|
+
|
|
90
|
+
Graph artifacts (`.enola/`, the cluster's output directory) are
|
|
91
|
+
machine-local and ignored. What enters version control is the receipt:
|
|
92
|
+
one entry per repository with the snapshot's fingerprint, the commit it
|
|
93
|
+
was taken at, the enola version and the providers that ran. Keep them in
|
|
94
|
+
one file the knowledge base owns:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{"repos": {"shop": {"git_commit": "…", "content_digest": "sha256:…", "enola_version": "0.5.0"}}}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Two rules follow. **Regenerate the whole cluster, never one member**: a
|
|
101
|
+
snapshot scoped to one repository replaces the union rather than
|
|
102
|
+
updating its slice, and everything that reads the union then answers
|
|
103
|
+
from a narrower world without saying so. **Not comparable is not a
|
|
104
|
+
pass**: when a member's snapshot changed enola version or provider set,
|
|
105
|
+
the grade declines; re-pin and regenerate rather than reading silence as
|
|
106
|
+
clean.
|
|
107
|
+
|
|
108
|
+
## Citations with a grammar
|
|
109
|
+
|
|
110
|
+
A structural claim in prose ("`internal/facts` has 157 dependents")
|
|
111
|
+
cites the receipt it was read from, in one fixed shape, so a script can
|
|
112
|
+
re-check it against the committed receipts and flag the ones whose
|
|
113
|
+
digest moved. The shape is yours to choose; what matters is that there
|
|
114
|
+
is one, that one extractor parses it, and that a periodic sweep lists
|
|
115
|
+
every citation with a verdict (verified, stale, malformed). A claim
|
|
116
|
+
nobody can re-check is a sentence, not a fact.
|
|
117
|
+
|
|
118
|
+
## Findings are judged once
|
|
119
|
+
|
|
120
|
+
The explainers report candidates, not verdicts: a cycle at 0.7
|
|
121
|
+
confidence, a god class, an unused route. Before a finding grounds a
|
|
122
|
+
page, confirm it against the code and record the judgment in a ledger
|
|
123
|
+
beside the receipts, one entry per finding signature: accepted (and
|
|
124
|
+
where it landed), rejected, or permanent noise. The next session reads
|
|
125
|
+
the ledger instead of re-deciding, and the periodic sweep reports a
|
|
126
|
+
ledger entry whose finding no longer exists, which is the code moving
|
|
127
|
+
past a recorded judgment.
|
|
128
|
+
|
|
129
|
+
## Pulled, with one push
|
|
130
|
+
|
|
131
|
+
Skills fetch the graph while already reasoning about something: a
|
|
132
|
+
deepdive asks `impact_analysis` and `governing_intent`, an ingest checks
|
|
133
|
+
a claim with `query_facts` while the source is open, a drift sweep
|
|
134
|
+
regenerates and diffs receipts. Nothing lands on a board because the
|
|
135
|
+
graph said so. The one exception is the session-end hook, which speaks
|
|
136
|
+
only when the session's change introduced a structural regression, or
|
|
137
|
+
when it could not grade at all; a gate nobody remembers to run is no gate.
|
|
138
|
+
|
|
139
|
+
## The wrapper
|
|
140
|
+
|
|
141
|
+
Every skill calls a thin command the knowledge base owns rather than
|
|
142
|
+
the binary: `kb enola generate | receipt | diff | findings | judge | impact
|
|
143
|
+
| govern | citations | baseline | check | coverage | doctor`. It resolves
|
|
144
|
+
the binary, the cluster config and the receipts file, and when any is
|
|
145
|
+
absent it prints one line saying so and exits 0. That is what keeps a
|
|
146
|
+
skill from failing on a machine without enola, and what keeps "the graph
|
|
147
|
+
was not asked" distinguishable from "the graph agreed". The [wrapper
|
|
148
|
+
guide](12-the-wrapper.md) has its shape.
|
data/guides/10-hooks.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Hooks: the graph at the moments that matter
|
|
2
|
+
|
|
3
|
+
A hook fires at a moment nobody has to remember: a session starting, the
|
|
4
|
+
first edit into a repository, a turn ending, a push. Each one here reads
|
|
5
|
+
the graph at that moment, blocks at most once, and forces a reading,
|
|
6
|
+
never a verdict. The scripts are in [`templates/hooks/`](../templates/hooks);
|
|
7
|
+
they are written for Claude Code's hook protocol (JSON on stdin, exit 2
|
|
8
|
+
to block with the message on stderr) and translate to any agent with
|
|
9
|
+
hooks.
|
|
10
|
+
|
|
11
|
+
## enola's own pair
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
enola install --hooks
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
wires two: **session start** restores the snapshot and serves it over
|
|
18
|
+
MCP; **stop** grades the session's delta against the pinned baseline and
|
|
19
|
+
speaks only when the change introduced a regression or could not be
|
|
20
|
+
compared. `enola doctor` reports when each last fired and what it
|
|
21
|
+
concluded; a hook that never fires is the failure to look for.
|
|
22
|
+
|
|
23
|
+
## The intent-first gate (PreToolUse on Edit and Write)
|
|
24
|
+
|
|
25
|
+
The first write into each repository per piece of work is blocked once.
|
|
26
|
+
The block carries two things: the pages that govern the file
|
|
27
|
+
(`governing_intent`, or `enola plan -paths <file>` when the MCP is not
|
|
28
|
+
in reach) and the pre-edit contract (the declared laws binding the path
|
|
29
|
+
and the blast radius over the current snapshot). The agent reads, then
|
|
30
|
+
retries, and the retry proceeds. The gate re-arms on every user message,
|
|
31
|
+
so each ask gets its own forced stop and no piece of work gets two.
|
|
32
|
+
|
|
33
|
+
What it refuses to do: decide. If the governing page contradicts the
|
|
34
|
+
intended change, the contract says amend the page first; the hook only
|
|
35
|
+
makes the page impossible to miss. When no page governs the file, it
|
|
36
|
+
says so and asks whether the work should be shaped. When the graph is
|
|
37
|
+
absent, it says so and lets the write through; a missing graph is a
|
|
38
|
+
named skip, never a wall.
|
|
39
|
+
|
|
40
|
+
`templates/hooks/intent-first-gate.sh` gates Edit and Write. The brain's
|
|
41
|
+
own gate also classifies shell commands as writes (a `sed -i`, a
|
|
42
|
+
redirect, a `cp` into the repository); the template marks where that
|
|
43
|
+
classifier goes and leaves it out, because the rules for what counts as a
|
|
44
|
+
write depend on your tree.
|
|
45
|
+
|
|
46
|
+
## The stop-time debt (Stop)
|
|
47
|
+
|
|
48
|
+
When the turn ends, two debts can block it once each:
|
|
49
|
+
|
|
50
|
+
- **Code moved, no spec moved.** Files changed in a governed repository
|
|
51
|
+
this turn and no governing page did. The block asks for either the
|
|
52
|
+
amendment or an explicit sentence, in the reply, saying why the pages
|
|
53
|
+
as written already cover the change.
|
|
54
|
+
- **A push went out with no grade.** A `git push` to a repository
|
|
55
|
+
happened this turn and no `enola check` ran. The block asks for the
|
|
56
|
+
grade, read, before finishing.
|
|
57
|
+
|
|
58
|
+
Both read a small per-session record the PostToolUse hook writes
|
|
59
|
+
(`templates/hooks/record.sh`): which files moved, whether a push
|
|
60
|
+
happened, whether a check ran. The debt hook is the mechanical half of
|
|
61
|
+
two rules a team can otherwise only ask people to remember.
|
|
62
|
+
|
|
63
|
+
## The union refresh (SessionStart)
|
|
64
|
+
|
|
65
|
+
When the session starts, compare the paired repository's HEAD with the
|
|
66
|
+
commit the committed receipt records. When they differ, regenerate that
|
|
67
|
+
member's slice of the union in the background and say so in one line;
|
|
68
|
+
when they agree, say that. The agent's first structural question then
|
|
69
|
+
reads a graph that matches the checkout it is about to edit.
|
|
70
|
+
|
|
71
|
+
## Wiring
|
|
72
|
+
|
|
73
|
+
Claude Code, in `.claude/settings.json`:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"hooks": {
|
|
78
|
+
"SessionStart": [{"matcher": "startup|resume", "hooks": [
|
|
79
|
+
{"type": "command", "command": "enola hook session-start"},
|
|
80
|
+
{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/session-refresh.sh\""}]}],
|
|
81
|
+
"PreToolUse": [{"matcher": "Edit|Write|MultiEdit", "hooks": [
|
|
82
|
+
{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/intent-first-gate.sh\""}]}],
|
|
83
|
+
"PostToolUse": [{"matcher": "Edit|Write|MultiEdit|Bash", "hooks": [
|
|
84
|
+
{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/record.sh\""}]}],
|
|
85
|
+
"UserPromptSubmit": [{"hooks": [
|
|
86
|
+
{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/record.sh\" reset"}]}],
|
|
87
|
+
"Stop": [{"hooks": [
|
|
88
|
+
{"type": "command", "command": "enola hook stop"},
|
|
89
|
+
{"type": "command", "command": "\"${CLAUDE_PROJECT_DIR:-.}/hooks/stop-debt.sh\""}]}]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## What it feels like
|
|
95
|
+
|
|
96
|
+
An agent asked to change a billing service edits nothing until it has
|
|
97
|
+
read the decision that put charges through one service; it grades the
|
|
98
|
+
change before the push and writes the grade into the pull request; and
|
|
99
|
+
when it ends the turn having changed code under a decision without
|
|
100
|
+
touching the decision, it is asked to account for that once. None of
|
|
101
|
+
this needs the agent to know enola exists.
|