FKF — Fab Knowledge Format (v0.1)
What this is. FKF is the format fab-kit uses for the
docs/memory/knowledge tree: a directory bundle of markdown files with YAML frontmatter, plus generated index and log files. FKF is a profile of OKF v0.1 (Open Knowledge Format, GoogleCloudPlatform/knowledge-catalog) — every FKF bundle is a conforming OKF bundle, and FKF additionally requires and fixes a handful of things OKF leaves open.Scope:
docs/memory/only. FKF governs the post-implementation memory tree. It does not apply todocs/specs/— specs remain human-curated, frontmatter-free, and free-form per a fab-kit design principle (specs are human-curated).
Canonical standard. This file is the canonical, authoritative FKF standard. It is maintained as
docs/site/fkf.mdin the fab-kit repository, published at https://shll.ai/fab-kit/fkf, and shipped verbatim into the kit cache as$(fab kit-path)/reference/fkf.md— the copy deployed skills cite; both homes carry identical bytes. Design rationale, OKF lineage, non-scope discussion, adoption/migration history, and the glossary live in the non-normative design companion: https://github.com/sahil87/fab-kit/blob/main/docs/specs/fkf.md. Section numbering is preserved from the original design doc — the gaps (§1, §4, §9–§11 are companion-only) are intentional, so every existing “FKF §N” citation resolves identically here.Editing this standard. Edit
docs/site/fkf.md(in the fab-kit repo), then runscripts/sync-fkf.shto refresh the shipped kit copysrc/kit/reference/fkf.md; a CI drift-guard test fails on any divergence between the two files.
2. Conformance
A docs/memory/ tree conforms to FKF v0.1 if all of the following hold:
- Every non-reserved
.mdfile carries a parseable YAML frontmatter block. - Every such block contains
type: memoryand a non-emptydescription. - Reserved filenames —
index.mdandlog.md— follow their generated structures (§5, §6) and are written only byfab memory-index. - Cross-links between memory files use the bundle-relative form (§7).
Items 1–2 are the OKF conformance floor (specialized: type is fixed, description is promoted
to required). Items 3–4 are FKF’s added strictness. As in OKF, consumers SHOULD degrade
gracefully — a missing optional body section or an unknown extra frontmatter key does not make a
file non-conforming.
3. Concept Documents (memory files)
A memory file = a YAML frontmatter block + a markdown body, at
docs/memory/{domain}/{name}.md or docs/memory/{domain}/{sub-domain}/{name}.md.
3.1 type (required, constant)
type: memory
type is OKF’s sole required field — its machine-routing discriminator. fab’s memory files are
homogeneous (every file is “a documented area of system behavior”), so type carries no
distinguishing signal and is fixed to the constant memory. The value is stamped by tooling
(the memory-file template and every memory writer), never hand-curated — so “required” costs the
author nothing.
3.2 description (required, curated)
description: "One-line summary used by the generated domain-index row."
OKF recommends description; FKF requires it, because it is load-bearing: the generated
domain index reads each file’s row Description from this field, and the always-load context layer
routes on it. It is the one hand-curated frontmatter field — authored by every memory writer
(hydrate, /docs-hydrate-memory, /docs-reorg-memory, /docs-distill-memory) and kept accurate on every edit.
Length: a one-line index-row summary, capped at 500 characters (blocking past 1000).
description: is a routing signal, not a summary of record. It MUST be a single-line frontmatter
scalar and SHOULD stay at or below 500 characters — the unit is characters (runes), measured
on the value after quote-stripping. Detail (requirements, design decisions, prose) belongs in the
file BODY (## Overview, ## Requirements, ## Design Decisions), never in the description. The cap
enforces in two tiers: fab memory-index emits a non-fatal advisory stderr warning for a
description in the 501–1000 range (over the soft cap — a trim nag that never fails
fab memory-index --check), and BLOCKS — fails --check, joining the blocking class below — for a
gross over-cap description strictly longer than 1000 characters (2× the soft cap; the
advisory-only posture demonstrably failed — 33×/50×-cap descriptions shipped straight through the nag).
Co-locating the description with the file (rather than in the index) is deliberate: editing a
description never touches the hot, churn-prone index row. It cannot be auto-derived from the
H1/Overview without loss (Overview prose contains literal | pipes that break index tables, and an
extracted first sentence degrades the routing signal).
No change-ids in description: (enforced/blocking). The description MUST NOT carry change-ids —
neither a trailing — xu0k-style suffix nor a (d9rs)-style citation. It is a routing signal, not a
provenance record; change-id citations belong in the body (§3.3), never in the description. This ban
is enforced: fab memory-index BLOCKS (fails --check, joining the blocking class below) on a
description: carrying a registry-gated change-id — a full YYMMDD-XXXX-slug folder-name token
whose registered folder matches, or a bare registered 4-char id (the same false-positive-free registry
gating the log.md change-id attribution uses, so code/yaml/any unregistered 4-char word never
trips it). Detection covers topic files and domain/sub-domain index.md stubs alike.
Blocking content class (§3.2 escalations + malformed frontmatter). Because the index reads the
description:frontmatter verbatim, an offending description silently propagates into the generated row (the drift check alone cannot catch it — committed byte-identical to regenerated).fab memory-indextreats four signatures as BLOCKING — they makefab memory-index --checkfail (exit ≥ 1) independent of index drift, enumerating the offending file(s): (a) an unclosed frontmatter block (opens---with no subsequent standalone---); (b) adescription:value that starts with a quote but fails quote-stripping (an unterminated quote, e.g. a closing fence glued on as"…text…"---); (c) a registry-gated change-id indescription:(the §3.2 ban above); and (d) a gross over-capdescription:(> 1000 characters). This is a blocking signal distinct from the advisory warnings (the 501–1000 length nag, plus the per-topic-file debt meters — narration density, file size,_unsorted/non-empty, and broken bundle-relative links — which never affect the exit code) and from the destructive-loss tier (§6.4): each is fixed by editing the offending file (repair the frontmatter, or trim/de-cite thedescription:), not by a reorg, so it is not a--checktier-2 category and does not fire the refuse-before-regen guards (which key on exit == 2; exit 2 still wins when a tier-2 loss co-occurs). Validation is stderr/exit-code only — it never changes the rendered index bytes.
3.3 Body (conventional headings, recommended — not mandated)
The body is standard markdown. FKF adopts OKF’s posture: conventional headings are recommended where they apply, not required. A file is conforming without any particular section. The conventional structure:
---
type: memory
description: "One-line summary used by the generated domain-index row."
---
# {File Name}
**Domain**: {domain}
## Overview
<!-- 1-2 sentences describing what this file covers. -->
## Requirements
### Requirement: {Name}
{RFC 2119 text: MUST / SHALL / SHOULD / MAY}
#### Scenario: {Name}
- **GIVEN** {precondition}
- **WHEN** {action}
- **THEN** {expected outcome}
## Design Decisions
### {Decision Title}
**Decision**: {chosen approach}
**Why**: {rationale}
**Rejected**: {alternative and why it was worse}
*Introduced by*: {change-name}
The pipeline’s hydrate step writes into ## Requirements and ## Design Decisions, and
review/intake read from them — so these headings remain the target shape and SHOULD be used
wherever the content warrants. But a small reference-pointer file need not invent a GIVEN/WHEN/THEN
scenario: the rule is SHOULD-use-these-conventional-headings-where-they-apply, not
MUST-have-these-sections.
Body style: state current truth in present tense (normative). The body describes what IS, not how it came to be:
- Present tense, current truth. Every statement describes the current contract. A memory file is a statement of record, not an accumulated log of edits.
- No transition narration. Never narrate a change as a change — no “renamed X→Y in {id}”, no
“this inverts/supersedes {id}‘s claim”, no “was
old.value”. Describe the current value; the previous one is not the body’s concern. - Superseded behavior is never described in the body. The previous state belongs to the
per-folder generated
log.md(§6, the dated what), git history (the diff), and archived change folders (the full design) — the body carries only what IS. Consolidating a section to current truth (dropping the superseded description) is the correct edit, not a loss. (Sole sanctioned exception:_shared/removed-domains.md, whose body is removal records — a citation-carrying tombstone ledger, not transition narration — protected by thefab memory-index --checktier-2 tombstone-loss guard and thedocs-reorg-memorycarve-out that authors it.) - Provenance is citation-only, and headings carry none. The sole permitted provenance in a body
is a trailing
(change-id)citation and the*Introduced by*: {change-name}field on a Design Decision. A citation marks where a current fact came from; it does not narrate a transition. Citations are deliberately preserved — a 6-char(id)cheaply defends a deliberate, easily-”fixed”-away behavior against future regressions. Heading text names its topic, never a change: a heading is## Dispatch States, never### Dispatch States (xu0k)or## xu0k — dispatch states. Change-ids appear only as trailing citations in body text — never in heading text. - No operational TODOs. Follow-up work items — TODOs, “still needs X”, next-step checklists — are
never memory-body content. They belong in the project backlog (
fab/backlog.md) or the originating change folder. A memory body states what IS, not what remains to be done. - Rationale survives distillation — as Design Decisions entries. “Don’t re-break this” content
lives in Design Decisions’
Why/Rejectedas durable, present-tense design intent — a rejected alternative is a design fact, not transition narration. Token savings come from dropping narration, never rationale. Any why, rejected alternative, or constraint explanation goes into a## Design Decisionsentry in the four-field shape (Decision / Why / Rejected / Introduced by), never as inline narration in Overview/Requirements prose. The changelog-bullet shape is banned inside## Design Decisions: an entry like- **{change-id} — retired X**is change history (log.md’s job, §6), not a design decision — a DD entry heading is a decision title, never a change-id.
No
## Changelogsection. Per-file changelog tables are removed in FKF — change history lives in the per-folder generatedlog.md(§6). This is the single biggest FKF divergence from the pre-FKF memory format.
3.4 Optional frontmatter
FKF neither requires nor forbids the other OKF-recommended fields (title, tags,
timestamp, resource). resource (a URI to an underlying asset) is typically absent —
memory files document behavior, not addressable assets. Per OKF, consumers MUST preserve
unknown frontmatter keys on round-trip and MUST NOT reject a file for carrying them.
5. Index Files (index.md) — generated
Every directory holding ≥1 non-index .md carries a generated index.md. All index tiers are
generated artifacts written solely by fab memory-index — agents never hand-edit index rows.
The render is a pure function of folder contents + each file’s description: frontmatter — so
the output is byte-stable / idempotent: two branches cannot produce conflicting
hand-edits to the same row, and any residual textual conflict auto-resolves by re-running
fab memory-index post-merge.
FKF is stricter than OKF here: OKF permits hand-written, auto-generated, or consumer-synthesized indexes. FKF forbids hand-editing — generation is the single writer.
Three tiers:
- Root (
docs/memory/index.md) — domains-only:| Domain | Description |. Each domain row’s Description is read from that domainindex.md’sdescription:frontmatter (round-tripped by the generator). No inlined per-file column (it silently drifts). - Domain (
docs/memory/{domain}/index.md) — file rows:| File | Description |. Description from each topic file’s frontmatter. The index carries no dates — it is a pure function of content, so it is branch-independent and idempotent (recency lives inlog.md). Carries its owndescription:frontmatter (the source for the root row). When sub-domains exist, appends a## Sub-Domainstable (| Sub-Domain | Description |) — emitted only when sub-domains exist, so a flat domain index is byte-identical to a sub-domain-free one. - Sub-domain (
docs/memory/{domain}/{sub-domain}/index.md) — same file-row contract as a domain index; carries its owndescription:frontmatter (the source for the parent’s## Sub-Domainsrow).
The one curated input to index generation is the description: frontmatter on topic files
and on domain/sub-domain index files. Everything else in an index is derived.
Stub-before-index. When a new domain/sub-domain is created, its
index.mdstub (carrying onlydescription:frontmatter) is written beforefab memory-indexruns; the command fills the generated body and round-trips the description. This is the Index Ownership model — it avoids the contradiction of one step hand-editing an index the next step both generates and forbids editing.
Merge policy: regenerate, never hand-merge (normative)
Because an index.md (and a log.md, §6) is a pure function of its folder’s contents, two branches
that both touch a folder produce conflicting edits to the same generated rows. On any merge
conflict in a generated docs/memory/**/index.md or log.md, agents MUST NOT hand-merge the
generated file. The procedure is mechanical:
- Resolve the conflicts in the topic files only (and any
.status.yaml/log.seed.mdseed inputs the generation reads) — never in theindex.md/log.mditself. - Re-run
fab memory-index. - Take its output wholesale as the resolution (
git addthe regenerated index/log files).
fab memory-index --check at review-pr backstops staleness. Byte-stability makes the
regenerate-wholesale resolution always correct, so there is never a reason to reconcile a
generated file by hand.
Optional
.gitattributesmerge-driver (non-normative aside — documentation only). A project MAY reduce the friction by registering a custom merge driver that resolves generated index/log conflicts by taking either side and deferring to the nextfab memory-indexregen. It is not auto-installed (no tooling change, no migration) — a convenience opted into by hand:docs/memory/**/index.md merge=fab-regen docs/memory/**/log.md merge=fab-regengit config merge.fab-regen.name "fab memory-index regenerates this" git config merge.fab-regen.driver "true" # accept either side; regen fixes itThe driver only suppresses the conflict marker; the mandatory
fab memory-indexre-run (step 2) then produces the correct bytes — it does not replace the regenerate step.
fkf_version on the root index — see §8.
6. Log Files (log.md) — generated (C-lite)
Each domain and sub-domain folder carries a generated log.md recording that folder’s change
history. log.md is a generated artifact written solely by fab memory-index (same
single-writer, byte-stable discipline as index.md). It replaces the per-file ## Changelog
tables that FKF removes from memory files (§3.3).
6.1 The C-lite model
log.md is assembled from two sources, neither of which any agent hand-edits:
- Git history, keyed to the folder — the when, the which file, and the change ID. This is
a projection of
git log(the sole consumer of the batched git pass, now that the index is dateless), so it is always accurate and never conflicts. - A per-change one-line summary — the what, written once into the change’s own
.status.yamlsummary:field (§6.3). Because each change touches only its own.status.yaml, the summary has zero conflict surface.
The generator joins them: for each commit touching a file in the folder, it emits one entry
under that commit’s date, carrying the file, the change’s summary, and the change ID.
6.2 Format
# Log — {domain}
<!-- Generated by `fab memory-index` from git history + per-change summaries. Do not hand-edit. -->
## 2026-06-13
- **Update** [migrations](/distribution/migrations.md) — surfaces the optional `agent.tiers`
per-stage-model override as a fully-commented config reference block; additive, no schema change. (260613-l3ja)
## 2026-06-12
- **Update** [migrations](/distribution/migrations.md) — drops the dead `stage_directives:` block. (260612-c5tr)
- **Update** [migrations](/distribution/migrations.md) — path-cite conformance; no migration shipped. (260612-tb6f)
- Entries are date-grouped, newest first; ISO
YYYY-MM-DDdate headings (OKF convention). - Each entry: an optional leading bold verb (
**Update**/**Creation**/**Deprecation**— OKF-conventional, derived from the change’schange_type/ removal markers), a bundle-relative link to the file that changed, the change’ssummary, and the(change-id)in parens. - The descriptive line is one line per change per file — deliberately not the paragraph-length
prose the pre-FKF changelog rows carried. Durable, long-form why belongs in the memory file’s
## Design Decisionssection (it is durable design intent, not dated history);log.mdcarries the dated what.
6.3 The summary: source field
The per-change summary line lives in the change’s .status.yaml:
summary: "surfaces the optional agent.tiers per-stage-model override as a commented config block"
- Written once during the change (authored at hydrate, or carried from the intake), via the fab CLI — single-change-touched, so conflict-free.
- Read by
fab memory-indexwhen generatinglog.md. - Absence degrades gracefully: a change with no
summaryprojects with the change slug in place of the descriptive line.
6.4 Freeze-on-write generation
log.md is not re-derived from live git on each run. Git history is not fixed — squash-merge
rewrites commit subjects and counts, and branch-deletion makes the original commits unreachable — so a
from-scratch projection yields a different result per contributor and across time. The existing
log.md is therefore authoritative and write-once: the generator reads it back, treats its entries
as immutable (never reworded, re-dated, or dropped), and uses the git projection only to discover
new entries to append. A re-run on unchanged history MUST be a byte-for-byte no-op.
- Append/dedup key. New entries are keyed on
(file-base, change-id)— deliberately not the git commit hash, which squash plus branch-delete makes unreachable (the exact operation being defended against), whereas the change-id survives in the change folder name and the change registry, independent of git. Only an attributable projected entry (one whose commit resolves to a registered change-id) participates; it is appended only when no existing entry already records that pair. - Unattributable commits are frozen, not re-projected. A commit that resolves to no registered change-id (a migration, a direct-default-branch edit, or a squash that dropped the branch token) has no key to append on, so it MUST NOT be added after first write. Unattributable lines already present in the file stay verbatim. Accepted tradeoff: tooling commits leave no log trace — they are not memory-domain history.
- Bootstrap is not a special mode. The first run on a folder with no
log.mdis simply the first append into an empty log (plus thelog.seed.mdseed-merge), projecting-and-freezing through the same code path as every later run. There is deliberately no first-generation flag — it would invite a re-run that re-introduces the churn this model exists to prevent. - Rebuild is the destructive escape hatch. Regenerating with
--rebuilddiscards the frozen state and re-projects every entry from current git (including unattributable commits). It can rewrite or drop frozen lines, so it is reserved for a corrupted log or a deliberate re-baseline — never the default path. - Drift checking compares against the merge, not a fresh projection. A valid frozen log legitimately
contains lines that current git can no longer produce, so a conformance check MUST compare the
committed file against the freeze-on-write merge result. A committed log that is a valid superset
of that merge passes. A failure means either a missing attributable
(file-base, change-id)entry the merge would append (the log was not regenerated and committed) or a hand-edited frozen line the merge cannot reproduce (the single-writer discipline was violated). Both are benign drift, never destructive loss — alog.mdis a projection, not a curated row table.
7. Cross-links — bundle-relative
Links between memory files use the bundle-relative absolute form: a path beginning with /,
interpreted relative to the bundle root (docs/memory/).
See [migrations](/distribution/migrations.md) and [configuration](/_shared/configuration.md).
FKF picks OKF’s recommended link form (over plain relative) for a concrete reason:
docs-reorg-memorymoves files between domains (splits/merges). Bundle-relative links survive a move — the reorg skill rewrites far fewer links — whereas plain relative links break on every move and must be rewritten in bulk. As in OKF, the relationship type (parent/child, references, depends-on) is conveyed by surrounding prose, not a typed link field, and consumers MUST tolerate broken links (a missing target is not malformed).
Links out of the bundle (to source files, specs, external URLs) use ordinary repo-relative or absolute-URL forms as appropriate — the bundle-relative rule governs memory↔memory links.
8. Versioning
The bundle declares its FKF version in the root index.md frontmatter — the only index.md
permitted to carry frontmatter beyond the generator’s own output:
---
fkf_version: "0.1"
---
FKF emits fkf_version, not OKF’s okf_version, because an FKF bundle is a superset of
OKF — claiming bare okf_version would under-state what the bundle guarantees. fab memory-index
writes fkf_version into the root index on generation.
Minor versions add backward-compatible features; major versions may break. Per OKF, consumers SHOULD attempt best-effort consumption rather than refusing an unknown version.