NAME
kb-document — ingest and review narrative documents at graduated abstraction levels
SYNOPSIS
kb document ingest PATH –project P [–title T] [–format F] [–dry-run] [–no-prompt]
kb document list [–project P]
kb document show ID
kb document review list [–project P] [–status S]
kb document draft SECTION_ID BODY –by WHO [–confidence N]
kb document review promote SECTION_ID
kb document tag –project P [–concept NAME,…] [–dry-run]
kb document fuzzy-tag –project P [–concept NAME,…] [–dry-run]
kb document frontmatter PATH [–accept FIELD,…] [–accept-keywords NAME,…] [–set FIELD=VALUE] [–dry-run]
kb document delete ID [–force] [–dry-run]
DESCRIPTION
A document is a narrative or article (Markdown, Fountain, or plain
text) ingested into two tables: one documents row plus one
document_sections row per structural unit, always including one
level = 'gist' row for the whole document. Unlike a
decision record, a document takes no required frontmatter –
title/author/publication-date/keywords are recognized when present
(antennaApp’s own frontmatter vocabulary: title, description, pubDate,
author, keywords) but never required, since a story or screenplay
usually has none.
Every [[Name]] wikilink in a section’s body, and every
frontmatter keyword, resolves to a concept and links to the record the
same way kb-ingest(1) tags decision records – case-insensitive, and
matching case becomes canonical for later mentions. A section’s
tag_density (how many known concepts its text mentions, tagged or not)
is a mechanical triage signal, not a link – see review, below.
A wikilink may carry a display alias after a bar:
[[the-long-fire/Anna|Anna]] links the concept before the
bar and shows the text after it. Only the part before the bar is the
concept’s name.
Story events (DR-0075, DR-0079). A file whose front matter has a narrative key (event, story_time, pov, characters, foreshadows, pays_off, precedes, learns or knows; place and status alone are ordinary front matter and do not count, but travel with an event) is one story event; a Fountain scene with a scene number (INT. KITCHEN - DAY #12A#) is an event too, with its own metadata in a one-line boneyard after the heading: /* story: story_time=1969-10-29; pov=Anna */. A scene with no number is not an event. story_time is a date (1969-10-29), a month, a year, circa YEAR, ordinal: N, or after:/before: LABEL with an optional offset such as +3d (units d, w, m, y). The summary says how many events the file yields and names every key or value it did not understand.
What a character learns or already knows (DR-0083). learns and knows each take items of the form “Character: fact”: learns says the character finds the fact out in this scene, knows says the character already knew it and the scene relies on it. In front matter they are YAML lists; in a boneyard or a story comment the key repeats, once per fact, because a fact may hold a comma: /* story: learns=Anna: the letter, which Cass wrote, is forged; learns=Bram: the debt */. A fact is the same fact whatever its case or spacing. A repeated learns of one fact by one character keeps the first. In a Markdown file with scene ids the front matter learns and knows are ignored, as event and story_time are.
Several scenes in one Markdown file (DR-0082). A heading with an id
is a scene and an event; a heading without one is an ordinary section.
Give the id on the heading, ## The fire {#12A}, or as
id=12A in a story comment on the line after the heading,
<!-- story: id=12A; story_time=1969-10-29; pov=Anna -->,
or both (if they differ the comment wins and ingest says so). The
comment takes the same keys as a Fountain boneyard, is left out of the
section’s text, and is read only directly after a heading. The file’s
front matter pov, place, status and characters are the defaults for
every scene; a scene’s comment overrides them field by field, and a list
in the comment replaces the front matter’s list. With scene ids, front
matter event and story_time are ignored (two scenes cannot share them)
and ingest says so. A heading ending in a Fountain-style number, #12A#,
is plain text, not an id. A scene id repeated in one file keeps the
first scene as the event and reports the rest. Headings inside fenced
code blocks are not headings, and a heading’s closing # marks and {#id}
are not part of its text. Events are derived from the file on every
ingest, a skipped one included, so running this again on the same paths
rebuilds them after an import or merge; they are not exported, imported,
merged or compared by check-db.
Sources a document cites (DR-0081). A front matter key sources lists the works the document rests on: a bare DOI (10.1234/sparql), an absolute URL, or source:ID for a source already in the knowledge base. Ingest links each to the document through document_sources: an existing identifier or id is linked, and a DOI or URL not yet known becomes a new source titled by its identifier. The links are rebuilt on every ingest of the file, a skipped one included. A file with the key manages the links it made (relationship cited): drop an entry and its link goes, and sources: [] clears them; links with another relationship, made by hand, are kept. A file with no sources key manages none. An entry that is not one of the three forms (a pasted doi: or doi.org prefix is refused, as kb source add refuses it), or a source:ID that names no source, is reported as a source warning and skipped, and never fails the ingest. The summary says how many sources were linked. See kb-source(1) for the registry and retraction checking.
Re-ingesting a changed file matches new sections to old ones by heading text: an unchanged section is left alone; a changed one updates its body and is flagged stale rather than losing its summary; a heading with no match is reported, never deleted.
- ingest
-
parse and segment PATH, creating the document and its sections on first
ingest, or reconciling a changed file against its existing rows. On a
terminal, a file with no frontmatter block (a note, say), or with a
block that has no title, is first offered one (a block’s other keys are
kept, and only the fields it lacks are asked about):
title,authoranddateCreatedare proposed one at a time as for the frontmatter subverb below (Enter accepts, text replaces, - skips, q stops), then the known concepts the text mentions askeywords, and the file is written only after a final confirmation. A new concept is never created here. Stopping, declining or input that ends leaves the file as it was, and the note is ingested as before.--no-promptturns the offer off; it is also off with--dry-run, with the global--json, and whenever standard input or output is not a terminal, and nothing makes it appear in those cases. A file whose frontmatter block has a title is never offered one. There is no directory form: ingest takes one file, and kb ingest is for decision records and skips dated notes. To bring a directory of notes in, loop over its files, and each one that lacks a title is offered the editor in turn (q skips the editor for that file, which is still ingested):
Add –no-prompt to ingest them all without being asked. A wikilink or frontmatter keyword that is not a valid concept name (one holding a control character) is not a tag: the document is ingested without it and the summary prints afor f in notes/*.md; do kb document ingest "$f" --project P; donetag warning:line naming it (tag_warningswith –json), once for each distinct link, exit 0. A file that has not changed (skipped) is not reported again. - list
- print matching documents, one per line
- show
- print one document’s metadata plus every section (heading, summary status, stale flag, linked concepts) in one view
- review list
- the triage queue – sections needing attention, with their size, tag_density and confidence signals. Defaults to unsummarized and drafted sections; –status narrows to one exact status, including “reviewed” for auditing
- draft
- write a gist or section summary. WHO is free text (“human” or a model identifier) – kb never calls a model itself, drafting is always supplied by the caller. Clears a section’s stale flag, since a fresh draft is not stale against itself
- review promote
- promote a drafted summary to reviewed – the only human action that makes a summary trusted. Only a reviewed summary is indexed for search or returned as content by a concept-tag query; a section’s raw body is never indexed at all, only its reviewed summary. It requires an interactive terminal on standard input and output (exit 2 without one, nothing written, no way to turn that off; see EXIT STATUS), so a model that drafted a summary cannot also make it trusted. The library call PromoteDocumentSummary has no such rule: a program that links the package, harvey for one, enforces its own
- tag
-
a pure file operation over every already-ingested document in –project:
inserts an explicit
[[Name]]wikilink, at its first safe occurrence, for each eligible concept. Never writes to the database – the next ingest of a changed file links it through the wikilink path above, unchanged. With no –concept, eligible means mentioned more than once in the file outside code spans (the same threshold density-linking already applies); –concept NAME,… forces exactly those names regardless of occurrence count, but each must already be a known concept, checked before any file is touched. A name already wikilinked anywhere in a file is left alone, so a second run is a no-op (a link hard-wrapped over lines, as [[deterministicoutput]], counts as the name written on one line); YAML frontmatter and the document’s own first H1 heading (its title) are never written into either, even if a concept name genuinely occurs there. –dry-run reports without writing. See DR-0029 (knowledge/decisions/). - fuzzy-tag
-
mirrors tag’s shape, catching what tag’s exact matching can’t: a typo,
plural, or simple tense variant of a known concept’s name (Levenshtein
distance, not paraphrase). Never bracket-wraps the near-miss text itself
– doing so would mint a duplicate concept from the misspelled or
inflected spelling. Instead inserts a
[^n]footnote marker at the match and a footnote definition carrying the canonical[[Concept]]elsewhere, so prose stays unedited and linking still resolves to the real concept. A concept already an exact match anywhere in the file is skipped entirely – fuzzy matching is additive, never a duplicate of what tag already covers. With no –concept, eligible means within a length-based distance threshold; –concept NAME,… bypasses that threshold for exactly the names given, each already a known concept, checked before any file is touched. A concept already footnoted (or wikilinked) anywhere in a file is left alone, so a second run is a no-op. –dry-run reports without writing. Decision records have their own verb, kb-record(1)’s fuzzy-tag: it adds the concept to the record’s tags: instead of a footnote, never edits an accepted record, and skips a concept only when the record already links it, not when its plain name appears in the prose. - frontmatter
-
propose-then-accept
title/author/dateCreated/dateModified/keywordsfor one document, PATH – unlike tag/fuzzy-tag, not scoped by –project, since a human reviews one file at a time. A bare invocation (no –accept/–accept-keywords/–set) only reports; nothing is ever written without an explicit selection.title/author/dateCreatedare absent-only – never overwritten once present.dateCreatedcomes from a YYYY-MM-DD at the start of the file’s name (notes/2026-09-29-worklog.md) when there is one, then git’s first commit, then the file’s time.authoris a layered signal: a byline in the document’s own prose wins, then git’s earliest-commit author, thengit config user.name– when signals disagree, the report shows all of them, not just the winner.dateModifiedis the one exception to absent-only: always recomputed from git’s most recent commit (filesystem mtime otherwise) and rewritten on every accepted run, since a stale value is worse than a missing one.keywordsdiffs the current list against two candidate sets: known concepts already mentioned in the text (a plain write on--accept-keywords), and new candidate terms distinctive to this document specifically, scored against the rest of its project (or the whole corpus) with the document itself excluded from that comparison – accepting one creates the concept first, the same checked-before-any- write disciplinetag’s--conceptalready holds to.--set FIELD=VALUE(repeatable) bypasses signal detection and the absent-only rule entirely – a human’s explicit assertion, not a proposal. Editing is surgical: only the frontmatter block changes, via a YAML node edit that leaves every other key, value, and comment in the block untouched; the document body is never modified. This module’s first process dependency – shells out togitfor provenance, falling back to filesystem timestamps whengititself fails (not a repository, or a genuinely untracked file). - delete
- delete a document, its sections, their concept links and the search entries of any promoted summaries. A document with a section whose summary is reviewed is refused (exit 1): a reviewed summary is human-gated data and deleting it loses it. –force deletes anyway. Ingesting the file again recreates the document, without the reviewed summaries. –dry-run reports and changes nothing.
Deletion is local to one database, with no tombstone: kb-merge(1) and
kb-import(1) from a database that still has the row bring it back, so
delete it there as well. agents/knowledge.jsonl is
re-exported from the database, so a delete reaches a database rebuilt
from it. See DR-0050 (knowledge/decisions/).
VOCABULARIES
- summary_status
- unsummarized, drafted, reviewed – promotion is a one-way, human-only action for a first pass; no path un-reviews a promoted summary yet
EXIT STATUS
The workspace convention, as described in kb(1): 0 success; 1 the command ran and the answer is no (no such document or section, a section that is not drafted so there is nothing to promote); 2 the command line is wrong. Setting a summary to reviewed is a person’s act: review promote exits 2, and writes nothing, unless standard input and standard output are both a terminal, with a message that a person must promote it. No flag or environment variable turns that off. draft, review list and every other verb here need no terminal, so a model or a script can draft but not make a summary trusted.
SEE ALSO
kb-record(1), kb-ingest(1), kb-search(1)