.glia/overlay.toml: declared edges, walk skips and test paths, the id vocabulary, wrappers, cell sidecars, rule invariants, the reflexion model, and the gaps-to-overlay loop.
One checked-in, reviewable file that feeds a glia build what static extraction cannot see.
The build stays deterministic: glia never calls a model; it applies this file the same way on every build.
Schema and loader: code-domain/src/glia_config.rs.
Only <repo root>/.glia/overlay.toml is read. glia never walks .glia/, so a nested
.glia/overlay.toml in a sub-project is ignored. Address sub-projects from the root file
with scope / path (a project label or a repo-relative path; . or "" means the whole repo).
Check the file in. It is an input, not a cache.
| sections | kind | applied | --no-overlay |
|---|---|---|---|
[walk], [[project]], [entrypoints] |
user config | always | still applied |
[ids] |
user config, read at query time (no graph change) | always | still applied |
[[constraint]], [[decision]], [[note]], [[component]], [[layer]] |
declared knowledge -> CONSTRAINT / DECISION / CONV cells | always | still applied |
[constants], [[route_prefix]], [[wrapper]], [[edge]] |
overlay: inference written by a model or a person | by default | skipped |
Provenance: every node, edge or cell the overlay creates carries an ORIGIN cell. An origin = "llm"
stanza (the default) gets overlay:llm and Weak confidence; an origin = "human" stanza gets
overlay:human and Medium. Nothing from the overlay is ever Strong. Primitives can hide
overlay results, and --no-overlay builds without them.
version = 1 # required: 1, or 2 to use rule / transitive / via / min_tier; any other value ignores the whole file
[walk]
skip = ["legacy/", "*.generated.ts"] # gitignore syntax, anchored at the repo root; only adds skips
tests = ["playwright/", "qa/**/*.py"] # test / fixture code the built-in test paths miss (ORIGIN test_fixture); only adds
[[project]]
path = "tools/migrator" # a sub-project root that has no manifest
label = "migrator" # optional; defaults to the directory name
[entrypoints]
qnames = ["app::jobs::nightly_rollup", "app::cli::*"] # exact qname, or <prefix>::* for its descendants
[constants]
GATEWAY = "/orders-svc" # pins `${GATEWAY}` to this literal (secret-shaped names are refused)
"api.base_url" = "/api" # quote keys that contain a dot
[[route_prefix]]
scope = "orders" # every ROUTE in this project is also mounted at prefix + path
prefix = "/orders-svc"
[[wrapper]] # request('GET', '/users') -> ENDPOINT GET /users
call = "request"
kind = "http"
method_arg = 0 # or a fixed `method = "GET"`: exactly one of the two
path_arg = 1
[[wrapper]] # api.get('/orders') -> ENDPOINT GET /orders
call = "api"
kind = "http"
receiver = true # the member verb is the method; path_arg defaults to 0
[[wrapper]]
call = "publish"
kind = "queue_producer" # or queue_consumer
topic_arg = 0
broker = "nats" # optional framework tag
languages = ["typescript"] # optional: engine language names; empty = all
[[wrapper]] # NewCollection[Room](c, db, "rooms") -> DATA_ENTITY data_entity:nosql:rooms
call = "NewCollection"
kind = "data_entity"
flavor = "nosql" # sql | nosql | graph; default nosql
name_arg = 2 # required: the argument holding the table / collection name
[[edge]]
from = "web::src::report::loadReport"
to = "api::report::build_report"
category = "CALLS"
note = "called through a runtime-built URL"
origin = "human"
[[constraint]]
id = "web-no-db"
kind = "forbid_edge" # forbid_edge (needs from + to) | no_cycle | invariant (needs text) | allow
from = "web"
to = "services/api"
categories = ["CALLS"] # optional; edge category names
[[component]] # the reflexion model: see "Reflexion model" below
name = "web"
paths = ["web"] # repo-relative paths or project labels; one path, one component
[[component]]
name = "api"
paths = ["services/api"]
text = "orders and payments" # optional
[[layer]] # layers rank in file order, top first
name = "ui"
components = ["web"]
strict = false # true: may use the NEXT layer only
[[constraint]]
id = "web-uses-api"
kind = "allow" # from / to are [[component]] names
from = "web"
to = "api"
[[decision]]
id = "adr-7"
scope = "services/api" # or anchor = "<qname>"
title = "Charges are idempotent" # title or text is required
status = "accepted"
[[note]]
anchor = "services::api::app::charge"
text = "retries are safe" # 1..=4096 chars; id defaults to note#<n>
by = "james"
Anchors. A [[constraint]], [[decision]] or [[note]] with anchor = "<qname>" hangs on that
node, and an anchor that names no node orphans the stanza (it never falls back to a scope). A
[[constraint]] without anchor (an allow aside: see "Reflexion model") hangs by its scope, the
from of a forbid_edge and the scope of a no_cycle / invariant (none is the repo root): on the
PROJECT at that path, else on the deepest PROJECT above it (from = "backend/internal/handlers"
hangs on project:backend; a root PROJECT encloses every path), else, in a repo with no root
manifest, on the smallest-id MODULE under it. Where a rule hangs only stores it: glia check
evaluates the rule's own scopes, so a misspelt directory inside a project is reported there (from
scope ... matches no located node), not dropped. A [[decision]] without anchor hangs on the
PROJECT at its scope only. A stanza that hangs nowhere is orphaned: the build prints
[declared] orphaned <section> id=<id> <decl> ... on stderr and writes no cell.
glia check (and PyGraph.check()) reads each repo's overlay again and binds its
[[constraint]], [[component]] and [[layer]] stanzas through the build's own binder, so a
declared rule that checks nothing is never silent. A stanza the build anchored nowhere (orphaned:
anchor=<qname> binds no node, orphaned: scope=<raw> -> <path> (no PROJECT at or above it, no
MODULE under it), orphaned: component=<name> (...)), one it rejected (rejected: <why>, such as
a path two components claim), one this graph does not hold (absent: ..., a layout older than
the overlay: rebuild), and a rule whose scope or via facade no located node sits in, are listed
under "Rules that check nothing", each with its reason and a suggested fix (the nearest qname or
path that would anchor it, or what to declare). So are the stanzas the loader itself drops (see
"Validation"): a [[constraint]], [[component]] or [[layer]] dropped at load (a version-2
key in a version-1 file, an exact path two components claim, a layer or allow naming a dropped
component, a bad kind) is a rejected row at its line with the loader's message (rejected:
`transitive` need `version = 2` (this file says version = 1) (stanza dropped)) and what to
change, listed before the other rows of its repo. A whole file the loader ignores (a TOML or
schema error, an unsupported or missing version) while it holds a rule stanza is one row, id
.glia/overlay.toml, section overlay, at the error's line: rejected: <message> (rule stanzas
not read: constraint=<c> component=<m> layer=<l>); such a file is never "no rules declared". A
file with no rule stanza (only [[edge]] or [[wrapper]] stanzas) is not listed: its [overlay]
error: line and glia gaps report it. stderr carries [check] overlay rejected root=<root>
whole_file=<true|false> constraint=<c> component=<m> layer=<l> once per such repo. None of these
rows changes the exit code: glia check still exits 0 or 1 by its violations. glia check
--strict (PyGraph.check(strict=True)) makes each a rule error, so CI that wants every declared
rule to run exits 2 on one. Decisions and notes
are records, not rules, and are not listed (glia gaps reports a stale-anchor one as
orphaned_rule).
origin is accepted on [[route_prefix]], [[wrapper]], [[edge]], [[constraint]] and [[component]].
Stanza ids are 1-128 characters with no control characters, unique within their section.
Component and layer names are 1-118 characters (their entry ids are component:<name> /
layer:<name>, so a [[constraint]] id may not start with either prefix).
Every *_arg is a 0-based positional index, at most 8.
A [[project]] path that a manifest already roots keeps the detected root (shadowed= on the
[roots] declared= line), unless that manifest names no project (a requirements.txt or Pipfile
root, or one named after its directory): then the root takes the declared label and keeps its
ecosystem and manifest (relabelled=, DG.12). The PROJECT's ORIGIN cell records where its label
came from as label_source: manifest, dir, root or declared (DD.4).
[walk]: skips and declared test pathsBoth lists are gitignore syntax, anchored at the repo root: legacy/ matches that directory at
any depth, /legacy/ only at the root, qa/**/*.py the Python files below qa/, and a
!pattern re-includes a path against the earlier patterns of the same list. Both only add to
the built-in rules, and both are user config: applied with or without --no-overlay.
skip: a matching path is never parsed. A skipped directory becomes a REGION node with
ORIGIN provenance excluded.tests: test / fixture code the built-in test paths miss (they name directories such as
test, tests, __tests__, e2e, cypress, fixtures and suffixes such as _test.go,
.spec.ts; playwright/, qa/, acceptance/, smoke/, k6/ and perf/ are not on that
list). The code is still parsed, and every node whose POSITION file matches carries ORIGIN
provenance test_fixture, like a built-in test path: tests-for lists it as a test case,
gaps leaves it out of dead_symbol, patterns excludes it, test-report ingest treats it
as a fixture, and engram-export / Engram drop it by default. A pattern applies to its own
repo's nodes only, never un-tags a path, and leaves a node that already carries an ORIGIN
(a contract operation, a region anchor) as it is. A node with no POSITION (a package
dependency, a queue tag) is never matched.A module under a declared path is also a test module for the TESTS pass: it pairs with the
app module of the same stem (qa/flows/cart.ts -> src/cart.ts; a test affix is stripped
first, qa/cart.spec.ts -> src/cart.ts), EVIDENCE rule declared_name, and its root
functions get function-level TESTS edges to what they call there. It never pairs another
test or declared file of its stem, and no test pairs it as the code under test. A layout
merge (glia merge) does not read the overlay, so it pairs declared tests by name only, like
any other module. The build's [tests] module TESTS edges: <n> (snake=<s> camel=<c>
declared=<d>) line counts these edges as declared.
A build whose overlay declares tests prints, per repo,
[provenance] declared tests repo=<label> patterns=<n> tagged=<d>, where tagged counts the
node entries only a declared pattern tagged (modules included); the
[provenance] test_fixture=<n> (path=<p> qname=<q>) ... total counts them too.
[ids]: the id vocabularyThe ids a repo's PR titles, commit subjects and task docs name: ticket ids, spec-kit
requirement and task ids, a plan's packet ids. review and find --files read them to route a
text's ids to the files that define them. [ids] is user config like [walk]: legal in versions 1
and 2, applied with or without --no-overlay, never an overlay-loop candidate, and it changes no
graph (nothing is built from it; it is read at query time by glia_engine::intent::ids::IdVocab).
| key | what |
|---|---|
patterns |
id shapes, at most 16. Set, they replace the defaults; patterns = [] turns id routing off |
local_words |
the findings words (lowercase ASCII a-z), replacing the defaults when set |
markdown |
markdown tracker globs (gitignore syntax, repo-root anchored): a matching file defines ids like an ingested doc, even outside the doc include rules (a TODO.md in a source dir, a plan under dev-notes/) |
[[ids.tracker]] |
a JSON task tracker: path (a glob), list (the key of the task array; absent = the top-level array), id (the id field, default id), files (the field listing a task's files: a string or an array of strings), state / done (the state field and the values that mean done) |
The shape grammar (one implementation, code-domain/src/glia_config.rs IdPattern):
| token | matches |
|---|---|
{U} |
one ASCII uppercase letter, then zero or more ASCII uppercase letters / digits (greedy, backtracking) |
{N} |
one or more ASCII digits |
{N<k>} (k 1..=9) |
exactly k ASCII digits |
{l?} |
an optional single ASCII lowercase letter |
| any other char | itself, case-sensitively |
A shape is 1..=64 chars, holds at least one {N} / {N<k>} and names no other {..}. A match
is word-bounded: the byte before it and the byte after it are not [A-Za-z0-9_], and a following
. or - is a boundary only when no digit follows it (DT.5h is found in DT.5h:; {U}-{N}
finds nothing in UTF-8x or LANG-12.3). An id's family is the text before the digit group of
its shape's first {N}: LANG-, T, DT., E0.. Where shapes overlap, the longest match
wins, then the earliest. A shape ending in {l?} also reads letter shorthands: X.5a/b names
X.5a and X.5b; X.5a-c, X.5a–c and X.5a-X.5c name every letter from a to c (at most 20
past the start).
Defaults, used when patterns is not set: {U}-{N} (tickets, spec-kit requirement and success
ids: LANG-1234, FR-001, SC-002) and T{N3} (spec-kit task ids: T012); findings words
analysis, finding, remediation, review, audit. {U}-{N} also matches UTF-8,
SHA-256 or ISO-8601: such a mention has no defining row, so it routes nowhere; narrow
patterns if the count bothers you.
A ticket repo:
version = 1
[ids]
patterns = ["{U}-{N}"] # LANG-1234, HTTPCLIENT-77
spec-kit, with a task list kept outside specs/:
version = 1
[ids]
patterns = ["{U}-{N}", "T{N3}"] # FR-001, SC-002, T012
markdown = ["TODO.md", "planning/*.md"]
local_words = ["analysis", "audit"]
A JSON task tracker, shaped like a leap packet file ({"packets": [{"id": "DT.5h",
"files_touched": [..]}, ..]}):
version = 1
[ids]
patterns = ["{U}.{N}{l?}"] # DT.5h, E0.1, LA.25a/b, DT.5h-DT.5l
[[ids.tracker]]
path = "dev-notes/leap-*-packets.json"
list = "packets"
files = "files_touched"
Whatever reads the vocabulary prints [ids] vocab repo=<label> surface=<s> patterns=<n>
source=<config|default|off> trackers=<t> rejected=<r> markdown=<m> on stderr, and the
[overlay] loaded line counts the section as ids=<n> (patterns, words, globs and trackers).
[[wrapper]]: call sites of a project's own helpersA wrapper stanza names a callee (call: a bare or dotted name such as request,
api.request or NewCollection). Every call site of it mints the node a direct call
to the framework would have minted, with an edge from the function that holds the site
(the file's module when no function does).
kind |
identity from | mints |
|---|---|---|
http |
method_arg or a fixed method, plus path_arg; or receiver = true (api.get('/x'): the member verb is the method, the path is path_arg, default 0) |
the client ENDPOINT, owner -CALLS-> endpoint |
queue_producer / queue_consumer |
topic_arg, optional broker |
queue_producer:<topic> / queue_consumer:<topic>, owner -USES-> producer / consumer -HANDLED_BY-> owner |
data_entity |
name_arg (required), flavor = sql / nosql / graph (default nosql) |
DATA_ENTITY data_entity:<flavor>:<name>, owner -ACCESSES_DATA-> entity |
data_entity is for a project-local constructor that every repository goes through,
where the table or collection name is an argument and the database driver call inside
the constructor sees only a variable:
func NewCollection[T any](client *mongo.Client, database string, name string) *Collection[T] {
return &Collection[T]{inner: client.Database(database).Collection(name)}
}
func NewChatPreviewRepository(client *mongo.Client, database string) *ChatPreviewRepository {
return &ChatPreviewRepository{collection: NewCollection[ChatPreview](client, database, "chat_previews")}
}
With call = "NewCollection", kind = "data_entity", name_arg = 2, the call site mints
data_entity:nosql:chat_previews with an ACCESSES_DATA edge from
NewChatPreviewRepository. The qname is the one the data-entity extractor gives a direct
.Collection("chat_previews") call, so a wrapper site and a driver call collapse onto one
node, and the cross-service DB resolver pairs it like any other entity. A sql wrapper
works the same way (Table("orders") -> data_entity:sql:orders).
In Go you do not need the stanza for NewCollection: a function that hands its own
parameter to a driver collection call (.Collection(name), .GetCollection(name), ...) is
inferred as a data_entity wrapper with no overlay file, at that parameter's index, and so
is a function that hands its own parameter on to an inferred wrapper
(NewNamedCollection(c, db, name) calling NewCollection[T](c, db, name), up to three
hops). Its call sites mint exactly what the stanza above would, except for provenance:
the node's ORIGIN is
{"provenance":"inferred:wrapper","rule":"inferred:NewCollection","def":"<file>:<line>"}
(the wrapper's func line), the node and edge are medium confidence, and the edge's
evidence emitter is pass:inferred_wrapper, which glia why ranks as derived. A wrapper
name shorter than 3 characters, or one that two Go functions or methods share, is not
inferred. Inference is read from the code, so it runs under --no-overlay too. A stanza
that names the same call shadows the inferred wrapper, so a repo that already declares
it builds exactly as before. Every build that finds a candidate prints one line:
[wrappers] inferred repo=<label> wrappers=<w> (direct=<d> forwarding=<f>) sites=<n> minted=<m> duplicate=<u> skipped_nonliteral=<x> skipped_comment=<c> skipped_invalid=<i> shadowed_by_overlay=<s> skipped_ambiguous=<a> skipped_unparsed=<p>.
Wrappers in other languages still need the stanza.
Rules shared by every kind:
NewCollection[ChatPreview](, create<User>(, api.get<Order[]>(. It must follow the
name directly (no space), stay on one line and close within 256 bytes, so a < b && c > (d)
is never a call.func NewCollection[T any](...),
function request(...), a typed parameter list that opens a body).skipped_comment and mints nothing.:symbol
also works for a method, a topic or an entity name). Anything else (a variable,
a concatenation, a ${x} placeholder in a topic or entity name) is skipped_nonliteral.data_entity name must be at most 128 bytes with no whitespace, control character
or /; otherwise the site is skipped_invalid.Every build whose overlay keeps a wrapper stanza prints one line:
[overlay] wrappers repo=<label> stanzas=<s> sites=<n> minted=<m> duplicate=<d> skipped_nonliteral=<x> skipped_comment=<c> skipped_invalid=<i> (http=<h> queue_producer=<p> queue_consumer=<q> data_entity=<e> receiver=<r>).
A duplicate is a site whose node and owner edge the file already holds (an extractor
caught the same call, or two stanzas match one call).
Every table rejects unknown keys, so a typo is an error, not a silently ignored line. Errors
never fail a build. Each one is printed once as [overlay] error: .glia/overlay.toml:<line>: <message>.
version
other than 1 or 2 ignores the whole file. Version 1 is the 0.5.1 schema and loads exactly as
it did; version 2 adds the [[constraint]] fields rule, transitive, via and min_tier
(next section). A 0.5.1 glia ignores a version-2 file whole (version = 2 is not supported).glia check lists a dropped [[constraint]] / [[component]] /
[[layer]], and a whole file ignored while it holds one, under "Rules that check nothing"
(above), with the loader's message and a fix. The rules:[[edge]] category that is not an edge category name, or is DEFINES, CONTAINS
or CO_CHANGESroute_prefix that does not start with / or contains whitespacedata_entity wrapper without
name_arg, or with a flavor other than sql / nosql / graph)kind = "component" or
"layer" in a [[constraint]] (the model is declared only through its own sections)rule, transitive = true, a non-empty via or a min_tier in a version-1 file
(`rule` need `version = 2` (this file says version = 1))transitive on any kind but forbid_edge; via without transitive = true, or with an
empty path, a path that leaves the repo (..) or one path listed twicerule on any kind but invariant, or a blank one, or one over 16,384 charactersmin_tier other than fact / derived (heuristic included: a heuristic violation
never fails CI), on an allow, or on an invariant without ruleallow whose from / to is not a kept [[component]], names one component twice,
or sets scope..), or a path
an earlier component already claims (exact-equal paths only; nested paths are legal)[A-Za-z_][A-Za-z0-9_.]*[walk] skip or tests pattern, which drops that pattern only[ids], each on its own: a shape that breaks the grammar, a 17th shape, a
local_words entry that is empty or not lowercase ASCII, an invalid markdown glob, and an
[[ids.tracker]] with an empty path, files, id, list or stateversion = 2 adds four [[constraint]] fields. Everything else in the file reads as in version 1,
and a version-1 file keeps loading unchanged; a version-1 file that sets one of the four loses that
stanza only, with an error naming the fields.
version = 2
[[constraint]]
id = "web-not-into-api"
kind = "forbid_edge"
from = "web"
to = "services/api"
transitive = true # a path through any third scope counts, not only a direct edge
via = ["services/api/public"] # facades: a path may enter `to` here
categories = ["CALLS", "HTTP_CALLS"]
min_tier = "fact" # fact | derived (default derived)
[[constraint]]
id = "routes-are-handled"
kind = "invariant"
scope = "services/api"
text = "every route has a handler"
rule = 'violation(r) :- unhandled_route(r).' # Datalog over the graph and the prelude
min_tier = "derived"
rule (on an invariant; text stays required as the statement a person reads): Datalog text
whose violation/N rows are the rule's violations. It is evaluated at glia check time over
the graph plus the predicate prelude (docs/rules.md), never at load: the loader checks only
that it is a non-blank string of at most 16,384 characters, so a CONSTRAINT cell never depends
on the rules parser. A name the rule uses that this glia does not have (an unknown kind,
category or predicate) or a syntax error is a rule error at check time: glia check lists it
in its errors and exits 2, and glia gaps reports a rotted_rule row. An invariant without
rule is a statement only: listed as unchecked.transitive (on a forbid_edge): a path from from to to through any third scope counts.
The first node of to on a path is the hit. via names facade scopes (paths or project
labels, resolved like from / to) the walk never passes through, so a path that enters to
through a facade is allowed. IMPORTS chains over the module import graph, separately from the
symbol categories, unless the rule names both IMPORTS and CALLS. One violation per hit,
with its shortest best-tier path as the witness.min_tier (on a forbid_edge, a no_cycle, or an invariant with rule): fact or
derived, default derived. A violation whose tier is weaker is reported and does not fail
CI. A heuristic violation never fails CI under any min_tier (0.5.2 Q2), so heuristic is
not a value.A worked example. web/ is a TypeScript app and services/api/ a Python service whose
services/api/public/ package is its published surface. Under the first stanza above, a path from
web into services/api/public/ is allowed, and the walk never continues past the facade. A path
from web whose first node under services/api/ is anywhere else - an HTTP_CALLS hop straight
into a handler in services/api/internal/, or a chain of calls through a third scope such as
shared/ - is one violation per such node, the path its witness. The HTTP_CALLS hop is
resolver-paired (DERIVED), so under min_tier = "fact" that violation is listed and leaves the exit
code at 0; a path of CALLS edges the parsers bound (FACT) fails CI.
Storage. The fields go on the rule's CONSTRAINT entry only when set: "transitive":true,
"via":[resolved] beside "via_raw":[as written], "rule" and "min_tier". A version-1
stanza's entry is byte-identical to what 0.5.1 stored.
Rule text is declared only in this file (0.5.2 Q9). The cell API (glia cell set, glia_py's
set_cell / write_cell, a hand-edited .glia/cells.jsonl row) refuses a CONSTRAINT entry that
carries a rule key, whatever its source says: `rule` (Datalog text) is declared only in
.glia/overlay.toml [[constraint]] (0.5.2 Q9); the cell API cannot write one. A rule a model
proposes goes through the overlay file like any other stanza, written by glia overlay accept
(section "Rules through the loop").
glia check judges all three: a transitive forbid_edge as forbid_path violations (DB.3b), an
invariant with rule through the rules layer (DB.3a), each violation blocking only at or above
its min_tier. Only an invariant without rule is listed as unchecked (the unchecked= count
of its [check] line); [check] gate blocking=<b> advisory=<a> counts the gate.
glia check rules (forbid_edge, no_cycle) judge one pair of scopes at a time. A reflexion
model (Murphy, Notkin and Sullivan) states the architecture once: the components, the code each
owns, and the dependencies allowed between them. The checker then reports what the code does
against that model.
[[component]] maps a name to paths: repo-relative paths (. is the repo root) or
project labels. A file belongs to the component with the longest path above it, so
web/admin can be carved out of web. Optional: text, origin, anchor.[[layer]] groups components. Layers rank in file order, top first. A component may use a
component of any lower layer; with strict = true, only of the next layer down. A use
within one layer, or upward, is not allowed.[[constraint]] kind = "allow" permits from -> to between two components. categories
(edge category names) narrows the edges the model checks, as on forbid_edge.Storage. Each stanza is a CONSTRAINT entry (source overlay), so a loaded .gmap can be
checked without the file: a component is {"id":"component:<name>","kind":"component","name",
"paths":[resolved],"paths_raw":[..],...} (paths resolved like a rule's scope, so a label is
stored as its path), a layer {"id":"layer:<name>","kind":"layer","name","rank","components",
"strict"} (rank 0 is the top), and an allow keeps from / to as component names. Two paths
that resolve to one directory (a label and its path) count as one path claimed twice, and the
build rejects the later component.
Anchors. A component with anchor hangs on that qname. Otherwise it hangs on the PROJECT at its
first path, else the repo root's PROJECT, else (a repo with no root manifest) the smallest-id
MODULE under its first path. A layer, and an allow without anchor, hang on the repo root's
PROJECT, else wherever their first named component hung (the layer's first component, the
allow's from).
Evaluation (glia check, CC.5b). Every edge in the checked categories between two different
components is a dependency. An allowed dependency is a convergence. A dependency the model does
not allow is a divergence, reported as a violation with located evidence. An allow with no
dependency behind it is an absence: a FACT about the graph as built, with the coverage caveats of
the checked categories, never a violation. A model with components only (no layer, no allow) is
open: the dependency matrix is reported as observed and nothing diverges. Files that no
component owns are counted as unmapped.
A build with a model prints one line per repo:
[declared] model repo=<label> components=<c> layers=<l> allows=<a> anchored=<n> orphaned=<o>,
and a detail line for each stanza that did not anchor or was rejected. Component paths resolve
at build time, like rule scopes: rebuild after adding a project.
DEFINES, CONTAINS) or history edges (CO_CHANGES;
those come from the history snapshot, below).glia never calls a model. Three commands run the loop that writes this file, and the model step
between them (reading the code, writing stanzas) is an agent's or a person's. The glia-overlay
skill (skills/glia-overlay/SKILL.md; copy it to ~/.claude/skills/glia-overlay/) gives an agent
the loop's commands and its rules.
glia gaps <repo> lists the blind spots: unpaired endpoints and routes, <unresolved>
endpoint sinks, tag-only queues, dead-flagged symbols, and overlay rules that no longer
bind or that this glia can no longer read. Each row suggests a section to fill, and carries an id (gap:<16 hex>) that stays
the same across rebuilds: it is keyed by the node, stanza or sidecar row, never by a line
or an ordinal, so a stanza can name the gap it targets and a re-run shows it gone.glia overlay propose <repo> is the work list: the rows a stanza could close
(every category but wrapped_sink unless --category narrows it), each with the source lines
around it (--snippet-lines, default 3) and, on a suspected_edge row, a paste-ready [[edge]]
draft. Its guide points each suggest value at its section of this file. It writes nothing.[constants],
[entrypoints], [[route_prefix]], [[wrapper]], [[edge]] and [[constraint]] rules
(section "Rules through the loop", below), plus an optional version, each stanza under
# gap: <id> comment lines naming the rows it targets (above a constant's key, or above a
pattern's item in a multi-line qnames array). A gap line holds ids only. The links are
comments, so a candidate is plain overlay text, and it is all-valid or refused. A candidate with
no version is checked as version 2. Keep it outside .glia/, whose files are fingerprinted
build inputs.glia overlay try <repo> --candidate <file> builds the tree with the overlay as it is
(base), with every candidate stanza merged in (with) and, per graph stanza, with every stanza
but that one. It reports each node kind, edge category and gap category that moved, and per stanza
(wrapper#1, edge#2, constants.GATEWAY, entrypoints#1, constraint#1) its marginal effect (with, minus
the build without it), the linked gaps it closes (closed by it alone: back when it is left
out; the report's closed also counts gaps several stanzas close together) and a verdict. A
[[constraint]] stanza is judged by its rule on with, and a declared rule's violations moving
is part of the verdict ("Judging a rule", below). An
overlay never changes a parse, so every build reuses the cached parses; builds counts the
distinct overlay texts, at most 2 + the graph stanzas. --no-leave-one-out builds base and with
only: the totals and the verdict, no per-stanza rows. It writes only the parse cache.glia overlay accept <repo> --candidate <file> --only <stanza> (repeatable; default
every stanza) is the only writer of .glia/overlay.toml. It merges the chosen stanzas with their
# gap: lines and keeps the file's own comments and layout, refuses any result the loader would
not load as validated, and writes atomically. A constant or pattern the file already holds, or a
rule with the same id and the same body, is a duplicate and writes nothing. A rule whose id the
file already declares with another body is refused, and so is a rule glia check could not
evaluate. --dry-run prints the diff and writes nothing. Exit 0 when written (or a dry run, or
nothing changed), 1 when the result would not validate (nothing written: a constant pinned to
another value and a colliding rule id included), 2 on a usage or build error.gaps reports as an orphaned_rule (it binds nothing: the code was
renamed or deleted), a redundant_rule (the extractor now emits the same edge) or a
rotted_rule (a [[constraint]] whose rule this glia can no longer read: a syntax error, or
a node kind, edge category or predicate it does not have; detail quotes the first rule error
with its line and column in the rule text) is removed by its gap id:
glia overlay accept <repo> --remove <gap id>. The row's stanza is found by its
identity (an [[edge]]'s from / to / category, a [[constraint]] / [[decision]] / [[note]]
id, an [entrypoints] pattern), never by a line, and every stanza with that identity goes.
--candidate and --remove combine, so a renamed rule is replaced in one write. A
[[wrapper]], [[route_prefix]] or constant is never a rule row: a person removes one that
stopped doing anything.The verdict. try (per stanza and for the whole candidate) and glia gaps <repo> --overlay-delta
(for the whole file) judge by one rule. The overlay improved the graph when some gap category
fell, or some node kind or edge category other than DEFINES / CONTAINS rose; it regressed it
when some gap category rose. keep = improved and not regressed, review = both (a person
decides), drop = not improved. A stanza that closes a gap it links, and raises no gap category, is
a keep. A candidate with rules is judged by both parts: this graph rule, and its rules checked
by glia check ("Judging a rule", below). Accept only what the verdict keeps. The verdict
measures; it cannot tell whether an added edge is right, which is why a stanza is written from the
code it cites.
glia gaps <repo> --overlay-delta builds without and then with the overlay, counts each build
whole (nodes by kind, edges by category, gaps by category for the categories that need no repo
root) and prints [overlay] N rules, +M edges, orphans K→J, gaps G0→G1, verdict=<keep|review|drop>.
[entrypoints] is user config that --no-overlay still applies, so this delta does not see it;
try does.
Each step prints one stderr line: [overlay] propose repo=<label> rows=<n> ...,
[overlay] candidate file=<path> stanzas=<n> (route_prefix=<a> wrapper=<b> edge=<c> constants=<d> entrypoints=<e> constraint=<k>) gap_links=<g> errors=<x>,
[overlay] try repo=<label> stanzas=<n> builds=<b> verdicts keep=<k> review=<r> drop=<d> ... and
[overlay] accept repo=<label> added=<a> (route_prefix=<p> wrapper=<w> edge=<e> constants=<c> entrypoints=<n> constraint=<k>) removed=<r> duplicates=<u> ... dry_run=<bool> ....
On every build that finds the file, stderr carries
[overlay] loaded .glia/overlay.toml repo=<label> version=1 (walk=N project=N entrypoints=N constants=N route_prefix=N wrapper=N edge=N constraint=N decision=N note=N component=N layer=N) errors=E.
The counts are skip plus tests patterns for walk, qname patterns for entrypoints, keys for
constants, and stanzas for every other section.
From Python (glia_py, CK.1): overlay_propose(repo_paths, categories=None, top_k=20,
snippet_lines=3), overlay_try(repo_paths, candidate, leave_one_out=True) and
overlay_accept(repo_path, candidate=None, only=None, remove=None, dry_run=False) run the same
three steps and return the --json objects as dicts. The first of repo_paths is the primary
repo, whose .glia/overlay.toml the loop reads; the rest merge in, as --with does. candidate
is the candidate's TOML text, not a path. A refusal raises ValueError with the engine's message.
The markers end surface=py, and the candidate marker says file=-. They write what the
commands write: overlay_propose nothing, overlay_try only the primary repo's parse cache (one
repo), overlay_accept only .glia/overlay.toml.
A rule that an agent proposes reaches .glia/overlay.toml the way any other stanza does: a
candidate, glia overlay try, then glia overlay accept. A candidate may hold these
[[constraint]] kinds, each with an optional min_tier:
forbid_edge, with transitive and via if it needs them;no_cycle;invariant with a rule.Each rule stanza has the handle constraint#<n> (its position among the candidate's rules), so
--only constraint#2 picks one. Its id is its identity in the file.
Two kinds are refused in a candidate, and both still load from a file a person writes by hand:
kind = "allow": it waives a reflexion divergence, and that is a person's decision. (It also
names [[component]]s that a candidate cannot declare.)invariant without rule: glia check never evaluates prose, so try has nothing to judge.[[component]], [[layer]], [[decision]] and [[note]] are never candidate sections.
Version. The file moves to version = 2 only when a written rule sets rule, transitive, via
or min_tier. The move happens in place: version = 1 # schema becomes version = 2 # schema, and
nothing else in the file moves. A file with no version key never gets one, and accept never
lowers a version. A new file starts from version = 2 when its first rule needs it, and from
version = 1 otherwise, so a 0.5.1 glia can still read it. The move is refused when version 2
would also load a stanza the old version drops (typically a hand-written rule in a version-1
file): the merged .glia/overlay.toml would also load constraint=1 stanza(s) that version = 1 drops
(...), and accept exits 1 with nothing written. Raise the version by hand, check those stanzas,
then accept again.
Ids. Give each rule a fresh id. A rule whose id and body match one already in the file is a
duplicate. Bodies are compared by value, so formatting, key order and comments do not count. A
rule with that id and another body is refused (constraint "<id>" is already declared in
.glia/overlay.toml with another body, exit 1): a candidate adds rules and never edits one.
Readable rules. Accept refuses a rule that glia check cannot read: one that does not validate
against the code schema and the prelude (rule "<id>" does not validate at <line>:<column>:
<message>, the check rotted_rule uses), or one that defines no violation predicate. Nothing
is written. try builds such a stanza anyway and judges it. A rule whose scope holds no node is
not refused: accept never builds, the rule parses, and glia check then lists it under "Rules
that check nothing" (try judges it checks_nothing).
Origin. An unmarked stanza is origin = "llm", the loader's default, so a rule a model wrote is
marked as inference without any extra field.
glia drafts no rule. propose lists no rule rows: a rule states intent, and the graph holds only
what the code is. A rule glia could mine from structure would only restate the code as law, so the
evidence of intent stays prose (an unchecked invariant, an ADR or [[decision]], the person's
request), and the model step translates it.
try runs glia check once on every build whose graph declares a rule (the file's, the
candidate's, a cell-API rule) and keeps each violation as a hit keyed by its rule id, its rule
kind and its identity within the rule: a forbid_edge or a divergence by evidence row (<from>
-[<category>]-> <to>, plus one <n> row(s) past check::MAX_EVIDENCE hit for the rows check
cuts), a rule or forbid_path violation by its subject, a no_cycle by its witness's qnames
and member count. Two builds compare by those keys, never by a count, so a stanza that hides one
violation and adds another moves two hits. A hit takes its row's tier and location, and blocks
when glia check would exit 1 on it (its tier at or above the rule's min_tier). A build that
declares no rule is not checked, and a rule-free try prints and serialises exactly as it did
before rules.
A [[constraint]] stanza only adds a rule, so it costs no build: it is judged on with alone, and
its status is the first that applies:
| status | when |
|---|---|
duplicate |
the overlay as it is already declares its id (the same body, or a cell-API rule) |
error |
glia check reports an error for it on with (its message, on one line) |
orphaned |
with declares no rule of its id: an anchor that names no node, or a scope with no PROJECT at or above it and no MODULE under it; glia check lists it under "Rules that check nothing" |
checks_nothing |
with's check lists it as one that checks nothing: a from, to, via or no_cycle scope it names holds no located node (its reason and fix are glia check's, which lists it under "Rules that check nothing" and fails CI on it only with --strict) |
violated |
with holds a hit of it |
holds |
otherwise |
Its empty scopes are the scopes its rule's positive in_scope(_, "s") and under(_, "p")
literals name that hold no node on with: such a rule may hold vacuously. A negated literal never
counts (!in_scope(f, "legacy") holds for every node).
| verdict | a [[constraint]] stanza |
|---|---|
keep |
holds with no empty scope: true of the code as it is, and glia check keeps it true |
review |
holds on an empty scope (a misspelt scope, or one meant to stay empty), checks nothing (a misspelt scope, or one meant to fill later), or violated (the rule or the code is wrong; a blocking violation would make glia check exit 1, a heuristic one never does) |
drop |
error (glia check would exit 2), orphaned (it checks nothing; glia check lists it) or duplicate (adds nothing) |
A graph stanza keeps its graph verdict, except that keep becomes review when the build without
it and the build with it differ in a declared rule's hits, or a rule errors only with it: an
overlay stanza that adds or hides a rule's violation needs a person, as an allow does.
The candidate's verdict has two parts. The graph part is the graph rule on base and with, keep
demoted to review when base -> with moves a hit (or an error) of a rule the candidate does not
newly declare. The rule part is keep when every [[constraint]] stanza keeps, drop when every
one drops, and review otherwise; a candidate with no [[constraint]] stanza has none. The
verdict is the graph part when there is no rule part; otherwise review when either part is
review, keep when either keeps, and drop when both drop. A rule-only candidate's graph part is
drop (nothing in the graph moved), so its verdict is its rule part. The rule part needs only
base and with, so --no-leave-one-out judges it too, and builds stays at most 2 + the graph
stanzas.
The text form shows the rule part when there is one. The stanza table gains a rules column (a
rule stanza's status, <n> violation(s) (<tiers>; <b> blocking) when violated; a graph stanza's
moved hits as +<added> -<resolved> (<b> blocking)), and a rules (glia check on the build with
every stanza): block under it gives each such stanza's verdict and reason with up to five located
hits (+ on with, - gone with it; --json lists up to 20). The totals gain a rules: line (the
statuses, then violations +<a> -<r> (<b> blocking) from base to with and the rules that start to
error), the one place --no-leave-one-out names the statuses, and the verdict line reads
verdict: <v> - graph part <g> (<why>), rule part <r>. In --json and from Python the report
gains rules ({delta, statuses, graph_verdict, rule_verdict}), a rule stanza rule ({rule_id,
status, violations, tiers, blocking, hits}, plus error, reason / fix (a checks_nothing
rule: glia check's) and empty_scopes when set) and a graph
stanza that moves hits rules ({added, added_total, resolved, resolved_total, blocking,
errors}).
When a try has a rule part, a second stderr line follows [overlay] try ...:
[overlay] try-rules repo=<label> rules=<R> (holds=<h> violated=<v> error=<e> orphaned=<o> duplicate=<u> checks_nothing=<n>) empty_scopes=<s> violations +<a> -<r> blocking=<b> errors=<n> surface=<cli|py|engine>,
checks_nothing=<n> only when n > 0, R the candidate's [[constraint]] stanzas by status,
s those that hold on an empty scope, and the rest the base -> with hits. Each rule-bearing build
also prints check's own [check] line (with unanchored=<n> and a [check] declared not
checked: line when a rule checks nothing; a try reads no overlay file, so those count only the
empty scopes).
Git history is a build input too, but not an overlay section. glia history sync <repo>
(pyo3: history_sync(repo_path)) reads the repo's local git (no fetch, no remote, no author
or committer identity) and writes .glia/history-snapshot/ (commits.jsonl, blame.jsonl,
meta.json), replacing the previous one. The next build ingests it: churn ATTN cells on
MODULE nodes, blame-recency ATTN on symbols (with --blame), and heuristic CO_CHANGES edges
between modules that change together. The build itself never runs git, so re-sync when HEAD
moves. Flags: --max-commits N (default 2000), --since <date> (passed to git log --since),
--blame, --blame-max-files N (default 300). The snapshot is local and regenerable: when
.glia/.gitignore is absent, the sync creates one that lists it.
Since 0.5.3 (snapshot version 2; a version 1 snapshot still reads) the sync also keeps each
commit's message in messages.jsonl, one row per commits.jsonl row in the same order: the
subject (first non-empty line, at most 300 chars), the body (at most 4096 chars), the author time
and, when a merge whose subject names a branch (Merge pull request #N, Merge branch 'x',
Merge x into y) brought the commit in, that branch's label. Trailer lines (Co-Authored-By:,
Signed-off-by:, Reviewed-by: .. and a final block of Token: value lines) are dropped, so no
author or committer identity is kept, and every subject, body and label goes through the same
secret redaction as test output, when written and again when read. The build never reads the
file and data_hash does not cover it (it has its own messages_hash), so churn and
CO_CHANGES are the same with or without it; only query-time ranking reads it (the intent
ranking of review and find --files). --no-messages (pyo3: messages=False) keeps none and
removes an earlier one.
The sync prints [history] sync repo=<label> head=<12 hex> commits=N files=N renames=N binary=N
blame_files=N runs=N window=max:N[,since:<date>] messages=N redacted=N branch_labels=N
trailers_dropped=N surface=cli|pyo3 on stderr (one line; messages=off with --no-messages),
and a build that reads the snapshot prints [history] ingest repo=<label> .... A message
sidecar that does not match its snapshot is ignored with one [history] messages incomplete
dir=<d> reason=<r> line.
Since 0.5.3 the ranked answers can read one more snapshot, .glia/calibration-snapshot/
(calibration.json and meta.json, written last), made by glia history calibrate <repo>
(pyo3: history_calibrate(repo_path)). It holds the repo's own pick among a fixed set of
rankings per answer and query shape: the step builds the graph, replays the history snapshot
over it oldest commit first, and at each test commit scores every candidate ranking on
queries made from that commit's files, choosing with only the commits before it (leak-free and
online). The record keeps each (set, shape) row's pick, its fallback, the online selection's
mean recall@10 against the fallback's and the commit-cluster bootstrap 95% CI of that gain; a
pick that does not beat the fallback is recorded as the fallback.
The order is glia history sync <repo>, then glia history calibrate <repo>. Neither runs in a
build, calibrate never syncs (with no history snapshot it exits 1 naming glia history sync),
and a build never reads the record: only the ranked answers do, at query time. Flags:
--max-commits N (default 300, the newest test commits scored), --max-seeds N (default 5,
seed queries per test commit), --sets <group,..> (the candidate groups replayed, default every
registered group; an unknown name exits 1 listing the known ones), --json (the summary). Plain
output is one line per row, <set> <shape>: pick <name> over <q> queries (online recall@10 <x>
vs fallback <y>, delta <d> [<lo>, <hi>]), then wrote <dir>; a build error exits 2.
The sets: SELh0 (co-change first, the default) and SELs0 (under --no-cochange /
cochange=False), the structure group, rank impact / blast-radius, diff-impact,
review without a title and pack; SELt2 ranks review --title and SELf2t ranks
find --files (the review and find groups). An answer prints one [select] surface=<s>
set=<set> shape=<shape> pick=<name> source=calibrated|fallback|explicit reason=<r>
test_commits=<t> line, and ignores the record, falling back to its uncalibrated ranking, with
the reason: absent (no record), not_loaded, multi_repo (a merge: a calibration is per
repo), incomplete / version (a damaged or foreign record), build_stamp (written by another
glia build: any upgrade or parser change needs a recalibration), ranker_revision, no_set (no
row for the set and shape), candidate_set (the row was measured over other candidates),
refused (the replay refused the selection: its online recall@10 fell below a fallback that is
not one of the set's candidates, review's LX[IDR,P1] or find's LX[IDR,T1s], so the answer
uses that fallback, ruling EC-3; a refused SELh0 / SELs0 row records its fallback as the pick
and reads calibrated) or no_queries (the row scored nothing). A ranking chosen by flag is source=explicit. The step
prints [calibrate] done surface=cli|pyo3 sets=<n> test_commits=<t> ms=<ms> on stderr.
A compiler-grade SCIP index is a build input the same way: read once by a CLI step, stored as a
snapshot, never read by the build itself. Run the language's indexer (scip-python,
scip-typescript, scip-java, scip-go, rust-analyzer's scip), then
glia scip import <repo> <index.scip> [--prefix <dir>]. The import decodes the index (a
hand-written protobuf reader: no protobuf dependency, fields the schema adds later are skipped
and counted, one document in memory at a time, at most 256 MiB each) and writes
.glia/scip-snapshot/ (documents.jsonl, symbols.jsonl, meta.json), replacing the previous
one. Nothing is written when the index cannot be decoded, or when no document of it was kept.
What the snapshot holds is read against the repo's own source at import, never at build: the
identifier text at every definition (the build binds a definition by position and checks it by
that name), whether each reference is followed by a call paren (SCIP has no call role), and a hash
of every document's bytes. Offsets are converted in the document's position encoding (UTF-8,
UTF-16 or UTF-32 code units; unspecified reads as UTF-16). Document-local symbols and forward
definitions are dropped, and so is any document whose path leaves the repo. Document paths are
relative to --prefix (a directory in the repo, . for its root) when given, else to the
index's project_root; a root outside the repo (an index made in a container or on another
machine) reads them as repo-relative, with one [scip] warning: line.
The build never runs an indexer, so re-index and re-import when the code moves. Staleness is per document: a document whose file no longer hashes to the bytes the import read is skipped whole, so an index never places facts on code edited since; the other documents still apply. What the build makes of the snapshot:
unbound, and a symbol whose definitions bind two
different nodes is counted ambiguous and never used.CALLS (a call of a function or method, rule call_site)
or USES (callable_ref for a function or method used as a value, else reference,
reference_write on a write), emitter scip:<tool> (the indexer's name, lowercased, other
characters than a-z0-9_- as _), tier FACT (CE.1d), from the innermost function or method
holding the reference, else the innermost type, else the file's module. Import references add
nothing (the parser's IMPORTS are already FACT), and a node's reference to itself adds nothing
unless it is a call (recursion, kept; the rest counted self_refs). One edge per (from, to,
category), located at its first reference. An edge glia already has between the same nodes keeps
its category: the same category counts as confirmed, and when glia joins the two by any other
category (DEFINES and INHERITS_FROM included, and a heritage edge the index's relationship
just added) its classification stands and nothing is added (category_differs).is_implementation relationships between two bound definitions add the heritage
edges glia lacks, before the references are read (so a class header's reference to its base adds
no USES beside the heritage edge): IMPLEMENTS for a method implementing a method, or a class,
struct or enum implementing an interface; INHERITS_FROM for any other pair of types (an
interface extending an interface included, glia's own shape); rule implementation, located at
the implementing definition. A relationship between other kinds (an attribute implementing an
interface property) adds nothing and is counted other_kinds; one whose pair glia already joins
by the other heritage category counts category_differs.confirmed_fact), and when its emitter is not
the parser, extractor or graph stage (confirmed_other: a merge recomputes resolver and pass
edges by emitter, so a re-stamped one would come back twice). A name-only, below-Strong inferred
or unlocated one (tier HEURISTIC or DERIVED) is re-stamped scip:<tool> with rule
confirms:<old emitter>[/<old rule>], at its own site when the index binds its target there,
else at the index's, confidence Strong, tier FACT (upgraded); why notes confirmed by a SCIP
index (<tool>) at <file>:<line>; first emitted by <old emitter> (<old rule>) (CE.1e).CALLS / USES / INHERITS_FROM / IMPLEMENTS edge whose site the index binds to
another node, and not to the edge's target (which the index must know), is counted
contradicted and printed, never removed or changed (CE.1e).Delta caveat: glia delta and diff-impact build the base rev with the same untracked snapshot,
and the per-document hash check skips every file that differs from the indexed bytes, so a file
changed between the rev and the working tree carries SCIP edges only on the side the index was
made for. Those edges then show as added (or removed) in the delta.
The snapshot is local and regenerable: its directory holds a .gitignore of *, so it ignores
itself even in a repo whose .glia/.gitignore predates it, and the import creates
.glia/.gitignore when it is absent. Exits 0 on a written snapshot, 1 when the index cannot be
decoded or nothing was kept, 2 on a usage error.
The import prints [scip] decoded index=<file> documents=N occurrences=N symbols=N
external_symbols=N unknown_fields=N malformed_ranges=N after a clean decode, then [scip] import
repo=<label> tool=<tool>@<version> documents=N skipped=N defs=N refs=N calls=N symbols=N locals=N
forward=N bad_ranges=N encoding_unspecified=N surface=cli on stderr (one line each), and a build
that reads the snapshot prints [scip] ingest repo=<label> tool=<tool> documents=N stale=N defs=N
bound=N unbound=N ambiguous=N refs=N imports=N unowned=N added=N (calls=N uses=N) confirmed=N
category_differs=N self_refs=N and [scip] confirm repo=<label> upgraded=N confirmed_fact=N
confirmed_other=N relationships=N added_implements=N added_inherits=N contradicted=N
other_kinds=N, then at most 20 [scip] contradicts <from> -[<CATEGORY> <emitter>/<rule>]-> <to>
at <file>:<line>; index says <qname> lines, sorted by file and line.