Repository rules that are all held at once — unlike an agent, whose rules fall out of context in a long session.
A question is asked before the action. The answer is three-valued, and it comes with the circle of consideration: which rules bore on the case, and what happened to each.
Status: proof of concept. The approach is not new — start with docs/prior-art.md, which lists honestly who did this first and how this assembly differs.
Two numbers before anything else. Of 21 norms in a real Go style guide —
Thanos's, minus what its own authors file under "enforced by linters" — one
is expressible in this fact vocabulary. Of 52 norms in prometheus/prometheus's
governing documents, the process-and-authority domain this was actually built
for, also one.
The engine is correct. The extractor is pointed at the wrong object: every fact here describes a file at rest, and every norm worth enforcing is about a change, an actor, or process state. See docs/approach.md.
Read docs/known-defects.md before trusting anything here. An agent was told to break this repository and did: the hook approved every write for as long as it existed, and every provenance quotation was unverifiable. Both are fixed; several defects are not. The file records all of them, with reproductions.
./setup.sh # installs clingo and s(CASP) (needs Homebrew)
./demo.sh # the whole path from code to verdict, explained step by stepdemo.sh is where to start: seven steps from a source directory to a hook
decision.
Three sources, and they never mix. This is the thing worth understanding.
example-repo/ rules/project.pl facts/session.pl
real source code the team's decisions the current session
│ │ │
│ ./extract.sh │ written by hand │ human instructions,
│ (mechanical) │ (layers, contexts, │ proposed actions
│ │ allowlists) │
▼ │ │
facts/extracted.pl │ │
facts/map.txt │ │
│ │ │
└──────────────┬───────────────┴─────────────────────────┘
▼
rules/codebase.pl + rules/general.pl ← the rules themselves
engine/core.pl ← semantics, 30 lines
│
┌────────────┴────────────┐
▼ ▼
./check.sh ./why.sh 'action'
every verdict at once circle of consideration,
(clingo, ~3 ms) what happened to each rule,
│ quotation from the source document
▼
hooks/pretooluse.sh → Claude Code: allow / deny / ask
The key split: extract.sh decides nothing. It sees paths, packages,
imports, the generated-file marker, and whether a test sits alongside. What the
code cannot show — which package is domain and which is infrastructure, which
boundary crossings are allowed — the team declares in rules/project.pl.
| Command | What it does | What computes it |
|---|---|---|
./setup.sh |
installs both solvers, builds the scasp command |
brew, swipl |
./extract.sh <dir> |
code → facts/extracted.pl + facts/map.txt |
sh + extract.py |
./check.sh |
verdicts for every action, plus notes on the corpus | clingo |
./why.sh '<action>' |
circle of consideration, per-rule outcome, quotations | clingo |
./why.sh '<action>' --tree |
plus a justification tree in words | s(CASP) |
./verdict.sh '<action>' |
one word: allowed / forbidden / undetermined / unknown | clingo |
./verify-sources.sh |
step 1 of the review: is every quotation verbatim | grep |
./test.sh |
15 expected outcomes plus provenance | clingo |
./reach.sh <dir> |
what the corpus can say about a repository at all | clingo |
./demo.sh |
the whole path, explained | all of it |
hooks/pretooluse.sh |
asks the corpus before Write and Edit (not Bash — see D4) | verdict.sh, why.sh |
./extract.sh example-repo # or ./extract.sh ../my-service
./check.sh
./why.sh "write(internal_billing_domain_go)" # forbidden: domain pulls in the database
./why.sh "write(internal_billing_ledger_go)" # allowed: the pair is on the allowlist
./why.sh "write(internal_api_api_pb_go)" # allowed: an instruction applied
./why.sh "write(config_prod_env)" # strictly forbidden; an instruction does not help
./why.sh "add_dep(fancylog)" # UNDETERMINED - a human decides
./why.sh "write(internal_billing_ship_go)" --tree # plus the s(CASP) treeAn action names an identifier from facts/map.txt (paths become atoms, because
clingo does not accept quoted ones). To see the mapping:
column -t -s"$(printf '\t')" facts/map.txt$ ./why.sh "write(internal_billing_domain_go)"
ACTION: write(internal_billing_domain_go)
CIRCLE OF CONSIDERATION - rules that bore on this action:
for r_cb_default docs/approach.md
«permits by default: writing code is normal, and the»
against r_cb_layer example-repo/docs/architecture.md
«Domain code must not depend on transport, database or serialization types»
against r_cb_boundary example-repo/docs/architecture.md
«organized first by bounded context rather than by technical layer»
VERDICT: FORBIDDEN
WHAT HAPPENED TO EACH RULE IN THE CIRCLE:
r_cb_default overruled by: r_cb_layer r_cb_boundary
r_cb_layer APPLIED
r_cb_boundary APPLIED
The circle is the point. A rule the agent never read still shows up: the engine holds every declared rule at once, whatever is in the context window.
The three outcomes map onto three hook decisions: forbidden → deny,
undetermined → ask (a question for the human), otherwise silence. A fourth
answer, unknown, means the action is not in the declared domain — a typo, or a
file the corpus has never seen. It is deliberately not the same as undetermined:
conflating them makes a typo look like a normative gap.
demo.sh check.sh why.sh verdict.sh extract.sh verify-sources.sh test.sh setup.sh
engine/core.pl defeasible-rule semantics - 30 lines, the whole "engine"
rules/codebase.pl the codebase corpus: layers, boundaries, packages, deps
rules/general.pl the process corpus: authority, branch, statuses
rules/project.pl the team's decisions: layers, contexts, allowlists ← by hand
rules/sources.pl provenance: rule → document → verbatim quotation
rules/vocabulary.pl closedness declarations (needed by s(CASP) only)
rules/helpers.pl derived predicates, the domain of actions
facts/extracted.pl ← extract.sh, do not edit
facts/map.txt ← extract.sh: identifier → path
facts/session.pl human instructions and proposed actions
example-repo/ real Go code that breaks exactly the right rules
hooks/ the PreToolUse hook and a settings example
docs/ approach, review procedure, prior art
- clingo 5.8 (Potassco) — an ASP grounder and
solver. Computes the whole corpus in one pass: ~3 ms for the solver itself,
~100 ms for
./check.shend to end on a 256-file repository. The two numbers are three orders apart and the README used to quote only the first. Its diagnosticatom does not occur in any rule headis a free check on the corpus itself; - s(CASP) 1.1 on
SWI-Prolog 10 — a goal-directed solver without
grounding, producing justification trees in words; with
--unknown=errorit refuses to derive anything from an undefined predicate; - Defeasible Logic (Nute; Governatori's line) — strict rules, defeasible rules, defeaters, a superiority relation. Translation into logic programming: arXiv:cs/0511055;
- Claude Code hooks — the point at which consulting the corpus stops being voluntary.
There is no engine of our own, and none is planned.
Why two solvers rather than one: clingo computes everything in milliseconds but produces no proof tree. s(CASP) produces the tree and proves a conclusion in ~1 s, but does not finish proving that no conclusion exists. So the verdict is always asked of clingo, and s(CASP) is asked only about the side already known to hold — and on a slice of the corpus, which is what turns 106 s of not finishing into 0.05 s. Details in docs/approach.md.
Run ./reach.sh <your-repo> first. It prints which rules said anything about
your code and the whole fact census, so you can see in one screen whether the
corpus can express what you care about.
Then, honestly:
- Do not gate anything that matters on this yet. The hook is not a security
boundary: it fires on
WriteandEdit, a shell command walks past it, and it judges the file as it stands rather than the content being written; - The two numbers at the top of this file are the ceiling, not a starting point. If your rules are about who may merge, what needs review, or what must change together, this cannot express them — the extractor reads a working tree, and those norms are about a change and an actor;
- What does work: rules about where code may live, which package may import which, what is generated or vendored, and who may edit the rules themselves. Those are file-shaped, and file-shaped is what the vocabulary covers;
./verify-sources.shbelongs in your CI. A rule whose quotation no longer matches its document suspends the whole corpus, which is the intended behaviour and useless if nothing runs it.
A rule a transformation can enforce does not go into the corpus. "No comments in code", "hoist repeated strings into constants", "run the formatter" — that is a linter pass, cheaper than any check, and the agent need not be involved at all. The corpus is for rules where no transformation exists: rules with exceptions, rules about authority, rules that may turn out undetermined.
The corpus has not been reviewed by a human yet. It was extracted from documents and, until read through, is a retelling rather than a source. The five-step procedure is in docs/verification.md; two of the five steps are not machine-checkable and do not pretend to be.
The hook was broken until an agent tested it live. It compared the absolute
path Claude Code sends against the relative paths in facts/map.txt, never
matched, and approved everything. Fixed, with the normalisation spelled out in
hooks/pretooluse.sh. The lesson is in the repository rather than in a claim:
a hook exercised only with hand-written input is untested. To check for
yourself:
./extract.sh example-repo # a clone has no facts: they are generated
mkdir -p .claude && cp hooks/settings.example.json .claude/settings.json
claude # then ask it to edit example-repo/internal/billing/domain.goThe first line is not optional. facts/ is generated and gitignored; without it
the hook knows no files and approves everything — which is exactly how it
shipped once.
Built in Claude Code (Claude Opus 5) in a single session, together with the repository's author. Verified on macOS 15 / arm64, clingo 5.8.2, SWI-Prolog 10.0.2, s(CASP) 1.1.4.