A compiler over what this repository already states. Read-only in every subcommand documented here.
It is not a wiki, not a database, not a memory service, and not a second place to write things down. Every source it reads is a file somebody already maintains, everything it produces is derived and regenerable, and none of it outranks the file it came from. A result is a pointer to canonical knowledge, never a rewritten answer.
knowledge is not search. search is a literal scan over the durable operational
records — handovers, checkpoints, decisions, questions, prompts, the ledger — with no
index, because that corpus is a handful of files. knowledge covers the repository's
canonical artifacts: the policy, the profiles, the prompts, the milestone and issue
contracts, the claims matrix and the documents. Two corpora, two contracts, and neither
changes the other.
sources [--scope shared|operational|all]lists the curated source classes and the files each one discovers, with the class, the scope, the kind, a content hash and the repository-relative path.nodes [--scope ...] [--kind <k>]derives one node per canonical object: its identity, its kind, the source it came from and that source's hash. Exits10when two objects claim one identity.edges [--scope ...] [--type <t>]derives one edge per stated relationship, with the file and the field or line it was observed in.
No edge without provenance. Every edge names where the relationship was stated, and an edge missing any of from, to, type or provenance is refused rather than emitted with a blank — a blank source reads as "unknown" and is indistinguishable from one nobody recorded. An edge nobody can trace to a line is not a fact, it is a guess wearing a fact's clothes, and it is a guess the person best placed to notice it is wrong will never see.
Nothing is inferred from prose. The repository already states its relationships explicitly, in fields somebody maintains, and those are both free and correct. The edge types are a closed set; an undeclared one is an error rather than a new vocabulary word:
| type | from → to | stated in |
|---|---|---|
part_of | issue → milestone | the issue's milestone |
depends_on | issue → issue, milestone → milestone | the record's depends_on |
declares | milestone → claim | the milestone's claims |
specified_by | claim → document | docs/CLAIMS.yaml's source |
implemented_by | claim → implementation | its implementation |
tested_by | claim → test, doctrine → test | its test |
supports | doctrine → claim | the doctrine's claims |
references | document → document | an inline Markdown link |
Links are the one edge source that is not curated, and they are read conservatively. Fenced
code is dropped first, because a path inside a code sample is an example of a path and not a
reference to one. A target with a scheme, a protocol-relative target and an absolute path are
all skipped; a fragment is trimmed, because docs/CLI.md#session refers to docs/CLI.md.
Severity distinguishes what Majordomus owns from what an author wrote. A declared
relationship pointing at a file the repository does not contain is a FAIL: it is a broken
promise. A link in a document pointing at nothing is a WARN: a document may deliberately
point outside the repository and the target alone cannot tell the two apart. A relationship
pointing at a real file that this compiler does not model as one node — a container of many
objects, or something outside the curated set — is silent, because a report that is large by
design is a report people stop reading.
nodes and edges are two views of one derivation, so a defect in either exits 10 on
both. There is no clean node listing over a graph that is broken.
A node's identity is never its content hash. It is the object's own canonical id where
it has one — claim:policy-parse, issue:I0801, milestone:M003, profile:debugging —
and its repository path where it does not, as in document:docs/CONTINUITY.md. A hash says
whether something changed; it can never say what something is, because then every edit
would delete a node and create a stranger, and every reference to it would point at nothing
without anything saying so. The hash rides along on the node and is what freshness is
measured with.
The consequence is deliberate: editing a document keeps its node and moves its hash; renaming one is a delete and an add, and disturbs no other node.
A kind comes from structure, never from prose. It is decided by the source class the
file was discovered in and by fields the file itself declares. Nothing reads a body looking
for a word that suggests a type: a document that discusses roadmaps and milestones is a
document that discusses them. Where no rule applies the kind is unknown and the node is
still emitted, with one finding naming the class — an explicit unknown is information, and a
confident wrong answer is not.
A title is taken or left empty, never invented. From the record's own title or
description, or from a document's first level-one heading. A document with no heading gets
no title rather than its filename, because a filename standing in for a title reads like a
fact and is a guess.
Two stores have no id field of their own. A decision is keyed by its title and a question by its text — in both cases exactly what the ledger records and what a session envelope references, so three places name the same thing the same way and nothing maps between them. Answering a question rewrites the line it lives on, and the identity survives that: the appended answer is stripped, and only when what remains ends in the entry's opening date, so a question whose own text contains a dash is left whole rather than cut at a guess.
Discovery is driven by the repository index, not by a filesystem walk. A walk returns
build output, vendored trees and editor droppings; it returns them in an order that
differs between two machines; and it can return a file nobody meant to publish. Listing
tracked files instead gives repository truth in a stable order, and an untracked file is
never a source. Operational records are discovered from the state directory Majordomus
itself owns under .ai/local/, which is never tracked — and from nowhere else. No hidden
directory is scanned because it happens to exist.
The source classes are declared twice, by two owners: the repository declares its shared
sources in its AI layer, .ai/repo/knowledge/sources.yaml, and the tool ships the
operational classes, the records it writes itself, in share/knowledge-sources.yaml. The
scope of a class is decided by which file declared it; neither file names the other:
| scope | meaning |
|---|---|
shared | repository knowledge; may be projected to a public surface |
operational | this checkout's own working records; never part of a shared projection |
The scope is a property of the class, so the publication boundary is decided in one place
rather than at each producer. Every tracked pathspec carries the :(glob) prefix, under
which * does not cross a directory separator. That is not tidiness: without it,
docs/*.md also matches docs/claims/*.md, two classes silently overlap, and one file
becomes two nodes. test/cases/64_knowledge_discovery.sh fails on that mutation.
A class marked required that discovers nothing is reported as a WARN. The cost of a
curated list is that a path can be forgotten, and a forgotten path is indistinguishable
from a repository that does not have that file unless something says so.
$ majordomus knowledge sources --scope shared
policy shared policy 4f2a9c1d8b30 .ai/repo/policy.yaml
profile shared profile a1b2c3d4e5f6 .ai/repo/profiles/debugging.yaml
...
issue shared issue 0e5a7b9c3f51 .ai/repo/project/issues/I0801.yaml
claims shared claim 7c9e1b3d5f70 docs/CLAIMS.yaml
document shared document 9b1e2d4f8c3a docs/CONTINUITY.md
knowledge sources: 169 file(s) in scope shared