Generated from
cat-harness/skills/kg/kg-core/content-context-and-state-graphs.md— do not edit here.
Content, context and state graphs
An instance declares directories, and each says what kind of graph it holds. Until 2026-09-20 a kind answered one question — does this render as a website — and a consumer had no way to tell the subject matter from a record about it, or a record it may write from one it may not.
One question settles every kind
Does a running process WRITE it, READ it, or is it the SUBJECT?
| layer | the process… | example |
|---|---|---|
content | produces it — that is usually the point | a skill, a schema, a folio chapter |
context | reads it and never writes it. Static for the duration of an instance; it changes only when a human directs an authoring act, outside any process | an agent-memory entry |
state | writes it as it runs — a bean’s status changes because a step completed | a bean, a workflow instance, a QA verdict |
derived | produces it from a source, and would regenerate it rather than re-author it. Not the working corpus | a library/ section |
content’s example used to be “a library section” and that was wrong —
see §”The case that added derived” below. It is a folio chapter now.
It is holds on GraphTypologyDef in schemas/cat-harness.ts, and it is
required: tsc refuses a kind that has not said.
A step that writes to a context graph is a defect, not an update. That is
the whole reason the layer exists, and it is what a consumer could not
previously ask.
Two supporting questions, for a hard case
The one above decides almost everything. When it does not, these two do, and they have agreed with each other in every case here.
Does it stand on its own? Detach a node from everything else. A skill still instructs, a schema still constrains, a library section still reads. A bean names work on something that is no longer there; a QA verdict judges an absent subject. If detaching it empties it, it is not content.
(A library section passes this and is still not content — standing on its own
rules out state, not derived. The section below is that case.)
Would you regenerate it or re-author it? Content and context are written
by somebody with an intention. Live state is arrived at — re-running is how
you would get it back. A .po catalogue is authored; a health report is
arrived at.
Where they disagree with each other, say so rather than picking: a shape this axis has not met is how a vocabulary acquires a category nobody can use.
The classification
Ask the code, not this page — graphTypologiesOfLayer("context") returns the live
answer. No count appears here on purpose: a count in prose is a claim
nothing checks, and this repository has corrected two of them.
What is worth writing down is the reasoning for the kinds that are not obvious from their name.
memory is context, and it is the kind the third layer was added for.
The owner, settling bean mhh9, 2026-09-20:
put memory under state/context as static, during a process. it does not change. agents dont work on it (except when an authoring agent is directed by human). todos, beans are not static
Every clause of that is the definition. A process reads memory; no step writes it. It changes when a human directs an authoring agent, which is an act outside any instance.
todos is state, and that is the pair worth understanding. todos/todos.json states a 2×2 — memory vs workflow management, human vs agent
— and puts todos/ and agent memory in the same memory row. This axis cuts
across that row. A todo is an outstanding item a process closes; a memory
entry is an established fact nothing mid-process revises. Same row of that
2×2, opposite sides of this line. Neither model is wrong; they are asking
different questions, and a reader who assumes they align will expect memory to
behave like a todo.
fsh-guts is context, and it is the one that reads like content. It
holds structured material that could be rendered — which is what makes it
hard — and the fact it carries is where something got to: abandoned,
superseded, decided against. But no running step writes it. Relocating
something there is a human-directed act, and
deletion-requires-confirmation is the
skill that says so in as many words. Read, never written by a process. It was
state for the few hours between the axis landing and mhh9 being settled,
and the refinement moved it by the same criterion that moved memory.
qa and health are state. A verdict is where a review got to, and
the sweep that produced it writes it. health is the same shape one level
out — about the repository rather than its artefacts. Both fail the
stand-alone question outright: detached from what they judge, they assert
nothing. This is also why the kg-qa/ tree mirrors each subject’s path
rather than being flat — a verdict’s identity is partly the thing it is about.
attestations is state too, and it is the half of qa that stays on
main. Arc 3fva split every QA verdict by whether it can be regenerated.
The DERIVED half (qa, and health with it) is state a CI run writes, and
its record now lives on the orphan qa-reports branch keyed by commit — the
checkout’s test/results/ is a working copy, still committed until bean
5hox removes it. The JUDGEMENT half — agent, human and baseline-pair
attestations, qa-attestations/v1 under test/attestations/ — is written by
a review step too, so it is state by the same question, but it is the one
state graph here that cannot be rebuilt by re-running its writer. That is
why it stays on main (owner ruling D2 (a)) and why a merge conflict in it
is a person’s to read rather than a generator’s to repeat. The layer answers
who writes it; where it is kept answers can it be reproduced — two
questions, and the arc is what made the second one visible.
uploads is state and library is content. The clearest proof the
axis is not about file type: the same PDF is state in one directory and content
in the other. uploads/ is a queue, and ingestion writes and drains it; the
declaration already says these files are not L1 and read as absent to every
corpus consumer. library/ is what ingestion produced.
What a consumer may assume
| content | context | state | |
|---|---|---|---|
| means what it says without resolving a reference | yes | no | no |
| a step may write it | no — authoring writes content, and that is the subject of a governed process, not bookkeeping | no — a write is a defect | yes |
| can be stale | it is wrong or right | yes — against a codebase that moved on | yes |
| same node in two instances is the same thing | yes | no | no |
Of an unregistered kind, nothing. graphLayer() returns undefined, and
the three predicates are deliberately not each other’s negations. Reading
an absence as content is how somebody asking for a skill gets handed a QA
verdict.
Use processMayWrite() rather than === "state". It is the question
isStateGraph is usually being asked in service of, and naming it means a
fourth layer is one edit rather than a search.
Where each layer lives
The layer a kind already declares (holds) decides where its graph lives by
default. The declaration makes it explicit and checkable.
layer (holds) | example | default ref | keyed by | write shape |
|---|---|---|---|---|
content | chapters, skills, processes | main | — | PR |
context | interaction prefs, roles | main | — | PR |
state | beans, workflow instances, todos | dedicated branch-store branches (e.g. cat/cat-harness/beans, cat/cat-harness/todos) | tip — one living tree | fetch → splice → push, never -f |
derived (verdicts) | QA sidecars | cat/cat-harness/qa-reports (arc 3fva) | commit | append under main/<sha>/, pr/<n>/<sha>/ |
| rendered | the docs site | gh-pages | commit (the build stamp) | replace, previews kept |
| cache | Lake builds | lake-cache/* | toolchain + inputs | one key per branch, pruned |
The distinction that matters between state and derived: state is not
regenerable. A QA verdict can be recomputed from the tree, so a commit-keyed
snapshot per run is right and losing one costs a rerun. A bean is somebody’s
decision; it must be a single living tree whose history is the audit trail, and
a lost write is lost work. So state is tip-keyed (storage.keyedBy: tip),
stored on dedicated branch-store branches like cat/cat-harness/beans, todos,
and fsh-guts, and its write loop must splice, never replace.
Where a graph is STORED — storage.keyedBy
The layer says who may write a graph. The directory’s storage field says
where the graph lives when that is not main. The two are independent, but
only process-written graphs are candidates for storage:
keyedBy | what the branch holds | for | API |
|---|---|---|---|
commit | one entry per commit (main/<sha>/, pr/<n>/<sha>/) | qa — derived verdicts read against a baseline | scripts/qa-store.ts |
tip | ONE live copy; the tip IS the current state | beans, todos — state with one truth at a time | scripts/branch-store.ts |
route | one entry per published site route | the auto-doc page families — derived, regenerable | scripts/branch-store.ts |
route is not a synonym for tip, and the difference is REGENERABILITY
Both live at a branch tip, so the temptation is to read them as one keying with two names. The question that separates them is the one §3.1 of the state-branch proposal already asks: can this be recomputed from the tree?
- A bean cannot. It is somebody’s decision, a lost write is lost work, so a
tip-keyed change carries
expect(the blob its author read) and two sessions editing one file is aconflictthe caller settles. - A rendered page can. Nobody authored either side, the newer generation is
simply right, and a lost write costs a rerun. So a route-keyed write carries
no
expect— andbranch-store.tsrefuses one that does rather than honouring it.
The refusal is the part to understand. Ignoring an expect here would let a
caller believe it had the tip-keyed guarantee while getting last-write-wins,
which is the vacuous-pass shape. And an expect arriving on a route-keyed
write means the caller thinks the page has two writers — the premise of
route-keying failing, which is worth a loud stop. If a generated page really
does have two writers, route is the wrong keying for it and tip is right.
Four rules (bean 2h76, owner rulings 2026-10-02; route from bean 1j3q,
owner 2026-10-03 — “Add route-keyed storage, then move it off main”):
- One ref per state subgraph. Beans are on
cat/cat-harness/beansand todos oncat/cat-harness/todos, not on one sharedstatebranch. The owner: “go with cat/cat-harness/todos and cat/cat-harness/beans as their own named sub-graph branches”. - Not every named subgraph gets a branch. The owner: “(not all named
subgraphs get own branch, especially not semi-static KG content)”. Skills,
schemas, processes, roles and the declarations themselves are
contentorcontext. They change by PR and stay onmain. - A
qadirectory is commit-keyed. The schema refuseskeyedBy: "tip"and"route"on one, because either would be read as if it were keyed by commit, and a verdict is addressed by the commit it judges — there is no route to key it by. The guard names both values rather than onlytip: one that named a single keying would admit every keying added after it. - The keying is PASSED to the store, never sniffed from the branch. A
manifest whose
keyedBydisagrees with what the caller opened for iscorrupt, in both directions. A store that adopted whatever the branch claimed would read a route-keyed branch as state the moment somebody pushed the wrong manifest.
A branch-tip read is hit / miss / corrupt / unknown, and a branch without a
state-manifest/v1 root manifest is corrupt, not a hit — for a route-keyed
branch too. The $schema string says state-manifest on both, and that is a
choice: the two seeded branches already carry it, the format is identical in
every field, and keyedBy inside the manifest is the discriminator, which
is this repository’s own rule that the file declares what it is. The name is
historical; bean 1j3q records it as such. A write is spliced
onto the tip and pushed without -f, retrying when the tip moves. To edit
a file somebody else may be editing, pass the blob you read as expect. A tip
that disagrees then answers conflict and pushes nothing, so the edit is not
silently lost.
Until a declaration sets storage, main is authoritative. That holds for
route too: no generator writes a route-keyed branch yet, and the first family
(docs/uml/) is a follow-on bean to 1j3q — deliberately separate, because 49
of 94 gated checks write page files and a flip of all of them cannot be
bisected. A directory
with no storage field lives in the checkout, whatever branches exist. The
seeded cat/cat-harness/beans and cat/cat-harness/todos say
authoritative: false in their manifests until the steward-run flip on #1850.
Adding a kind
Answer the question and write the answer in holds, with the reason beside it
as a comment. Three rules about how:
Do not add a value without a case that needs it. context arrived because
content and state alone forced two unlike things together — a bean, which a
step rewrites, and a memory entry, which no step may touch — and a consumer
told only “this is state” could not tell whether writing to it was normal or a
bug. That is the bar: a real consumer that cannot answer a real question.
There is no “could not determine”. A third state is right when a check looked and could not tell, and wrong when an author is registering a kind they are defining. Declining to say moves the burden to every consumer, which is the position this axis exists to end.
holds is part of a kind’s identity. sameKind compares type,
renderable and holds, so two layers registering one name on different
layers is a conflict that throws rather than a diamond where the first silently
wins. summary, skill and schema are descriptive and are not compared.
The case that added derived — library/, 2026-09-20
The fourth layer exists because the two supporting questions disagreed, and the instruction above is to say so rather than pick. This is what saying so looked like.
Owner, 2026-09-20: “library is static (only if we materialize assets or
not)” and “can duplicate asset into a folio and work there.” Bean hqku.
| question | answer for a library/ section |
|---|---|
| does a process write it? | yes — document-ingestion.bpmn |
| does it stand on its own? | yes — a section still reads. Says content |
| regenerate, or re-author? | regenerate, from the ingested source. Says not-content |
Both candidate answers were wrong, and each was ruled out by a rule rather than by taste:
contextis ruled out because it carries “a step that writes to it is a defect, not an update” — and a declared process writeslibrary/. Choosing it would have madedocument-ingestiona defect by this axis’s own rule.contentis ruled out by the sweep rule, “qa-sweep is only on active/working content” (owner, same day). A QA finding against a derived section is a finding against its generator, so a sweep that judgeslibrary/sends a reviewer to fix the wrong file.
What derived buys, concretely: walkBlocks skips a directory whose declared
graphs are all non-content, so library/ leaves the sweep with no
directory name written down anywhere — the same way fsh-guts/ did. A
hardcoded skip would have been the alternative, and a hardcoded path is what
this declaration exists to remove.
What it does NOT mean. derived is not “unimportant” and not “not
committed”. library/ is L1 corpus, committed and greppable, and every
knowledge-graph reference to a source still resolves through it. The layer says
where a finding belongs, not what the material is worth.
Classifying a kind is not a ruling on a directory’s contents
cat-harness is correctly content. For a few hours on 2026-09-20,
skills/memory/ sat inside a cat-harness directory and held context.
Both were true at once, and that is the case that shows why the two
questions are separate: classifying a kind says what that KIND holds, not what
every node beneath a directory of that kind is.
The nodes have since moved to their own declared memory/ graph (bean
07xs), as their own change — relocating a directory as a side effect of
adding a classification is the shape #395 refused and bean auap did
separately. So the example is now history rather than a live gap, and it is
kept because the rule it illustrates is not: a directory of one kind may
contain a node of another, and the containing declaration is not a claim about
its contents.
The layer is not the source — a branch is not “where state lives”
A declared subgraph also says where its content COMES FROM — the checkout’s
directory or a declared repository branch, overridable by the instance config
(source, directory-conventions §”Where a
subgraph gets its content”; bean l4ay). That is a different axis from this
one, and neither is derived from the other.
The temptation runs one way: beans and todos are state and are moving to
their own branches, so “state lives on a branch” reads like a rule. It is not.
todos is state while its source is directory on main, and stays state
when the config mounts it from cat/cat-harness/todos. qa is state and is
keyed by commit on its branch, never by tip. A context graph mounted from a
branch is still read-only to every process, because what a step may do is
processMayWrite(kind) — asked of the KIND, never of where the bytes came from.
So: ask processMayWrite for whether to write, and the resolved source for
HOW to write — a commit for directory, a spliced push for branch
(processes/kg/mount-subgraph.bpmn). A consumer that branches on one to answer
the other has merged two questions this repository keeps apart.
A declared ASSET carries a layer too
The axis is not only about directories. instance-readme and
agent-instructions are declared assets, and both hold context — the
owner’s own words on issue #592: “its static content at process runtime and
treated as an asset like memories”. README.md and AGENTS.md are read at
session start and authored by a human, so a step that rewrites one is a defect
by the same rule a step writing to memory/ is.
One rule, not two spellings. layerIsWritable(layer) is the whole of it;
processMayWrite(kind) and processMayWriteAsset(role) both call it. They
were nearly written as two === "state" comparisons, which is how a context
directory and a context asset come to mean different things the day a fourth
layer is added — and the word stops carrying a rule the moment that is
possible.
Where an asset’s layer is declared is the one difference. A directory’s
layer is a property of its graph TYPOLOGY; an asset’s is a property of its ROLE,
in ASSET_ROLES, because an asset has no kind. A per-asset layer would be
eleven instances answering one question, and check:asset-roles rejects it —
asked of the raw declaration, since KgAssetSchema strips an unknown key
without a word.
Creation at INITIALISATION is not a process write. A context asset that
does not exist yet has to come from somewhere, and bootstrap writing one when
it is absent is not the write this rule forbids: initialisation is not
process runtime. The rule governs a process operating on an instance that
exists; initialisation is the act that brings one into being, so “never
written by a process” cannot mean “never created” without leaving every
instance without one.
That is not hypothetical. initialize-harness.bpmn ends with
A_WriteRootReadme, whose skill (bootstrap/skills/root-readme.md) writes
the root README.md when there is none — an instance-readme, which is
context. It never replaces one that exists: an existing README is
authored content, and the link and install status go in a marker pair the
harness’s readme_sync owns from then on. Bean 7sfm.
Where this page and that skill disagree, this page wins and the skill is the copy that drifted — it says so itself.
See also
directory-conventions— what a declaration is and how a path resolves. This page is the axis; that one is the mechanism.agent-memory— what a memory entry promises, and the injection budget. This page says what layer it is on.process-state,bpmn-processes— the largest writer of a live-state graph: a workflow instance is a token’s position in a diagram that lives in a content graph.todo-manager,bean-coordination— the work plan, and why a bean is never deleted. A state record that vanishes leaves a sibling unable to tell abandonment from accident.instance-publication— astategraph’s nodes are never published. That rule, and every other rule about what reaches the site, lives there (bean4ak5item 4).