PROP-043 — The facts markup: the inline status grammar and its IR
01spec://org.vibevm.core/vibevm/modules/vibe-facts/PROP-043
02Status: RATIFIED 2026-07-24 (owner, in session — «ратифицирую PROP-043»)
and IMPLEMENTED: the markup language below is binding. The tool surface,
the data contracts and the campaign consumer moved verbatim to PROP-047 at the
2026-08-22 boundary split (##BOUNDARY-SPLIT); their statuses live there.
03Depends on / relates to (all as external companions — see the
separability law, §2): the addressable-specs flow (anchors, spec:// URIs),
PROP-035 (the document IR whose
Markdown/XML dual-frontend model this markup targets),
PROP-014 specmap
(spec↔code traceability — consumed by the progress layer through the
evidence-provider seam, PROP-047 §4),
PROP-029 (spec://
grammar), and PROP-047 —
the campaign toolchain built on this grammar. The first consumer was the
spec-actualization campaign
(vibevm/vibespecs/terraforms/SPEC-ACTUALIZATION-CAMPAIGN-v0.1.xml).
1. Motivation
- 04The spec tree is the project's memory, and it has accumulated every stage of thought — shipped contracts, half-executed plans, parked ideas, and prose that no longer matches the code.
- Document-level
**Status:**lines exist (~55 of them) but are free-form: fifteen vocabularies, unparseable, and silent below document granularity.
05The project needs (the tool and campaign-substrate needs live in PROP-047 §1 since the boundary split):
- 06a machine-readable markup that records, inline in the sources, where every document / section / paragraph / text fragment stands;
07The durable knowledge lives in the markup, in the sources. Every other artifact this PROP defines (cache, state projections, baselines) is derived acceleration, erasable without loss of any fact (PROP-047 §5.5, the erasure law).
2. The separability law
08The facts machinery is designed as a standalone layer hosted inside vibevm, extractable at any moment:
- 09The core (parser, model, rollup, renderers, cache) is its own crate with no dependency on vibe-core, vibe-spec, specmap, or any vibevm subsystem. Its input is a file tree plus a config; its output is the parsed model this document defines (the campaign data contracts consuming it are PROP-047 §5).
- Non-collision with neighbouring inline grammars (
@spec://,#use,#embed,#source) is a convention held by tests, not shared code: the core ships fixtures containing those foreign directives and asserts zero false matches. - The specs of this layer are self-contained: they cite neighbouring vibevm systems as external companions and never fold their content in (the campaign templates and the owner guide belong to the progress layer beside PROP-047).
10Never couple the core to a vibevm crate; never let a foreign subsystem
reach into the core's parsing; never split ONE layer's normative text across
documents. Owner-revised 2026-08-22 (##BOUNDARY-SPLIT): the original
one-document form of this clause predates the boundary — the system is now TWO
layers with exactly one home each (the facts grammar here, the campaign
toolchain in PROP-047), and the no-split law binds within each home.
11The facts/progress boundary (owner ruling 2026-08-22, chat, near-verbatim): «факты — то, поверх чего можно построить совершенно разные процессы рефакторингов; синтаксис, IR и операции над фактами — в модуль facts, и это можно показывать сразу; в progress остаются наши инструменты для рефакторингов vibevm, доделываемые до зрелости». The law: this document owns the universal lower layer — grammar (§3), parsing (§4), genre semantics (§5) — plus the adoption registry beside it (PROP-046); PROP-047 owns the upper layer — tool, config, evidence, campaign data contracts, maintenance discipline. The dependency points strictly upward (progress knows facts; facts never knows progress), and every unit of both documents kept its anchor through the split — only the doc-paths changed, with the old path carrying a tombstone pointer.
12The CLI follows the boundary. The markup lint is a facts
operation: vibe facts check [--exhaustive] becomes its durable home, with
vibe progress check kept as a transitional alias (printing the new spelling);
the gate panel switches to the facts spelling. Campaign verbs
(scan/mirror/seal/gate/baseline/rescan/resume/weave/report)
stay under vibe progress. The same wave repairs B-100: a bare --campaign
<id> resolves against campaigns/<id> instead of silently minting a
cwd-relative state zone. Landed: vibe facts check carries the lint
byte-identically (one implementation, two entries), the alias prints its
stderr note (suppressed under --json), the shared campaign resolver fails
loud on an unknown bare id with the existing zones listed and writes
nothing, and the gate panel's markup line runs the facts spelling —
proven live on the original B-100 scenario.
13Crate names lag the boundary deliberately. progress-core
remains the crate name of the facts core for now — renaming it is a mechanical
ripple (engine copies, sync-engines) with no consumer until the progress
layer's own maturation wave, which is when it happens. The spec boundary and
the CLI boundary are the ones the public sees.
3. The markup language
3.1 The <status> element
14One XML-shaped element, embedded in Markdown (and, later, native in XML documents — the frontend duality of PROP-035 §5):
15<status stage="impl" state="work"/> — point marker
<status stage="test" state="plan">wrapped text</status> — fragment wrapper
- 16A point marker MUST be self-closing (
/>). An unclosed<status …>point form is not well-formed XML and is acheckerror. - A fragment wrapper is the paired form around text inside a paragraph.
- Inside fenced code blocks, inline code spans, and URLs the element and the shorthand (§3.7) are not recognized — the scanner is fence-aware.
17Decision — element name status, not progress.
- 18Why:
<progress>is an HTML5 element: GitHub-class sanitizers strip it, andhtml:truerenderers (VS Code preview) draw a literal progress-bar widget mid-spec.statusis not an HTML element and renders inert. - Considered and rejected:
progress(HTML collision),vp/prg(unreadable), HTML comments (invisible in raw reading, defeating the point). - Revisit when: the XML storage frontend lands and element naming is re-grounded in a schema.
3.2 Attributes
| Attribute | Required | Values |
|---|---|---|
stage |
yes | idea · spec · impl · test · doc · freeze · unknown |
state |
yes | plan · work · done · hold · void |
action |
no | continue · drift · rework · remove |
actionstage |
no | any stage value; absent ⇒ the action targets stage |
audience |
no | CSV of user · author · dev · agent; absent ⇒ dev (agent admitted 2026-09-11 by PROP-057, implementation pending) |
comment |
no | free text |
ref |
no | spec://… URI, path, or task id (e.g. DRIFT-012) |
- 20Vocabularies are closed. Any value outside the tables is a
checkerror with a nearest-legal-value hint (typos likerewrokdie in CI, not in review). - New values enter only by amendment to this section.
21Multiple markers on one node: at most one status marker (stage/state), any
number of action markers — a unit may legitimately need
remove+actionstage="doc" and continue+actionstage="test" at once.
3.3 Stages
- 22
idea— a thought worth keeping, not yet specified. spec— being specified / specified.impl— being implemented / implemented.test— being tested / tested.doc— being documented / documented.freeze— freezing as a deliberate process:freeze/plan(we intend to freeze),freeze/work(final checks),freeze/done(frozen).unknown— looked at, not understood; explicit triage demand. Distinct from no marker ("nobody looked yet").
23Decision — freeze, not a terminal done stage.
- 24Why: freezing is a process (planned, executed, and later reversed —
unfreeze is an ordinary marker change back to
spec/idea/impl; history lives in git). Adonestage collided semantically with thedonestate. - Considered and rejected:
stage="done"(ambiguous againststate="done"); a booleanfrozenflag (hides the process). - Revisit when: never expected; the cycle-of-improvement premise is core.
- 25The stage order is fixed for aggregation:
idea < spec < impl < test < doc < freeze;unknownsits outside the order and compares below everything. - The order is a sort key for rollup, not a process law: test and doc interleave in reality, and falling back (impl → spec, spec → idea) is legal and expressed by simply changing the marker.
3.4 States
- 26
plan— intended, not started. work— in progress.done— done for that stage.hold— deliberately parked (neither worked nor discarded).void— the unit no longer asserts anything. Named for a void contract — without effect — deliberately not the programming sense of "still works, discouraged". The unit was either split into heirs and left as a pointer to them, or cancelled with no replacement; its text survives only so its name is not reused and inbound links do not break. It is neither work outstanding nor work completed but no claim at all, and §3.10 sorts it accordingly. Marking one is the author's judgment about their own corpus — nothing derives it.
3.5 Actions
- 27
continue— unfinished; carry on (spawns tasks in the campaign). drift— diverged from reality; reconcile. Operationally bound to the sync-from-code flow when the code is right and the spec is stale: the marker names the problem, that flow is the procedure.rework— exists but bad; redo (pairs with the feature-flag disable path).remove— bad or abandoned; delete (or demote toidea/holdarchive).
28actionstage narrows the target: action="remove" actionstage="doc" = "the
documentation of this is to be removed", while stage keeps describing the
unit itself.
3.6 Audience
- 29For whom this promise must eventually be told:
user(writes specs in their own project, installs dependencies; never opens the package internals),author(builds packages; wants depth),dev(vibevm's own developers — the spec tree itself serves them; the default), and — admitted 2026-09-11 by PROP-057 §14 —agent(a session reading on a user's behalf: the boot snippet, a skill's instructions, the machine-facing corpus; text for it obeys the agent-text laws of that PROP — a token budget, no narration, never in a boot prefix). The grammar itself knows nothing of lanes; where such text may appear is PROP-057's rule, not this vocabulary's. - Primary use:
actionstage="doc"markers with an audience are the obligations of the documentation — the promises a page for that audience must cite.vibe progress report --view doc --audience user|author|dev|agentlists them; it is the source of the coverage gatevibe doc check --coverage(PROP-057 §14, the ratchet PROP-047##DOC-COVERAGE-RATCHETnames), never a table of contents — navigation is derived from the page manifest (amended 2026-09-11; the earlier wording fed the two guides' tables of contents).
3.7 Shorthand
30@status:<stage>/<state> and @status:<stage> are macro-equivalents of a
point marker. The legacy spellings @<stage>/<state> and @<stage> mean
exactly the same and are still read, so a document written before the
qualified form keeps parsing.
- 31
@status:test/plan⇒<status stage="test" state="plan"/> @status:impl⇒<status stage="impl" state="work"/>— bare shorthand defaults tostate="work", with exactly one exception:@status:unknown⇒state="hold". (@status:freeze⇒freeze/work: "freezing now".)
- 32Why the key is named. These documents share the
@space with foreign annotation grammars — JSDoc, TypeScript directives, npm scopes, Java annotations, and plainx@yaddresses. Without a key the reader must tell its own tokens from those by a vocabulary of stages and states, which answers "is this mine?" with a dictionary lookup instead of with the token's own shape. - Disambiguation against the
@spec://directive grammar (in-place spec citations) applies to the legacy form only: after@specthe scanner looks ahead —://follows ⇒ foreign directive, not ours;/<state>, whitespace, or end-of-token follows ⇒ shorthand. The qualified form needs no lookahead:@status:spec/donecannot be confused with@spec://…, because the key is stated before the value. - A shorthand is recognized only as a standalone token at the start or end of a paragraph's text, never mid-sentence, never inside code or links.
3.8 Placement
33Six granularities, one rule each — and no ambiguous positions:
- 34Document — marker in the preamble, before the first heading. Pilot amendment (2026-07-24): a document that opens with its heading (the standard shape of this repo's specs) has no preamble; there, the standalone marker immediately after that first heading is the document marker, not a section marker.
- Section — marker on its own line immediately after the heading line (for any heading other than a preamble-less file's first one, per the amendment above). This is the only legal standalone position inside a body.
- Paragraph — marker inside the paragraph's own text: the first
token (right after the newlines, or right after the paragraph's
@fact:<ID>anchor) or the last token (right before them). - List item (fact amendment, 2026-07-24 — owner-directed) — every item of a bulleted or numbered list is a unit of its own, at every nesting level. Its marker — shorthand or XML form alike — sits inside the item's own text: the first or last token of the item (before any nested sub-items, which carry their own markers).
- 35Fact anchors — the anchored-when-marked law (owner, 2026-07-24;
spelling amended 2026-08-06). A stable fact address is written
@fact:<ID>as the first token of a paragraph or list item. The legacy spelling##<ID>means the same and is still read. - Why the key is named.
##<ID>was never a heading — an ATX heading requires a space after the hashes, and this markup is written closed up — but it reads like one to a markdown linter, to a parser outside CommonMark, and to the eye.@fact:states what the token is instead of relying on a reader knowing what it is not. - A fenced block is an example, not an assertion (owner,
2026-08-06). By default nobody is asked to believe what a fence says
and no agent is asked to run it. Marking the fact
@fact/code:<ID>makes the fence part of that fact's body: the fact then has an address, a verdict, and it comes due for re-judgement when the block's text moves. - Why the type exists. A fence carries no anchor of its own and cannot be given one — it is a payload, copied out and pasted elsewhere, and an anchor written inside would travel with the copy. Measured over this corpus: 372 fenced blocks carry zero facts while all 7255 text blocks carry theirs, so a claim inside a fence belonged to nobody, could not be judged, and could not be made stale. Two false statements survived exactly that way in one week.
- The known type set is
code, and that is a measurement. Fences are the only block kind falling outside fact bodies: the corpus holds no images at all, and 891 of 908 table rows and 84 of 96 block quotes already sit inside a fact. A type naming a block kind that is already covered would address nothing. - An unknown type is a
checkerror, as are a typed anchor that is not its block's last fact and one with no matching block below it. Ignoring an unimplemented type would let the grammar promise what it cannot do, and the author would learn years later that nothing read it. <ID>is[A-Za-z][A-Za-z0-9_-]*; the unit is then addressable asspec://…/<doc>#<ID>, sharing one address space with the heading{#anchor}s — a duplicate across both forms is acheckerror. The address is unchanged by the spelling: it names the id, never the opener.- Every unit that carries a status marker — paragraph or list item —
MUST also carry a
@fact:<ID>anchor; a marked, anchor-less unit is acheckerror. - The marker may stand immediately after the anchor
(
1. ##RULE-001 rule text @freeze/done— the owner's canonical example shape) or as the unit's last token.
- 36Decision — two anchor-id registers (owner ruling, 2026-07-24).
@fact:UPPER-SLUGnames a normative fact (a law, rule, carrier, changelog entry — content with binding weight);@fact:kebab-casenames a service unit (status lines, lead-ins, connective prose). - Why: the register itself carries the normativity signal at zero syntax cost; ratified from the PROP-029 re-pilot mix reviewed in session.
- Considered and rejected: single UPPER register (service units become shouty; the signal is lost); single kebab register (normative facts stop standing out; the re-pilot would need re-anchoring).
- Revisit when: the post-campaign fold (§3.9) shows the mixed registers confusing report consumers or check tooling.
- 37Table addressing (proposed 2026-07-24, this session — same
syntax, no new grammar). A
@fact:<ID>as the first token of the first cell of a body row addresses that row (| ##ROW-PKGREF pkgref | … |); in any other cell it addresses that cell; the whole table is addressed by the anchor of its lead paragraph (the target set: ##TBL-MIRRORS) or its section. - Positional schemes (
r2c3) are rejected — a row shuffle breaks them; a minted id travels with its row. - Cells stay exempt from the anchored-when-marked obligation (mint ids only where something cites them); a table that is really a list of facts is deconstructed into an anchored list instead.
- Table cell (fact amendment, 2026-07-24) — every non-empty body cell of a table is a unit of its own; its marker sits inside the cell's text, first or last token. Header rows and the delimiter row are structure, not units. A table whose rows are really a list of facts is better deconstructed into a list (§3.9); cell markup is for tables whose tabular shape is essential.
- Fragment — paired
<status>…</status>around text within a paragraph, list item, or cell — the form for an inline fact that cannot be pulled out of its sentence.
- 38A standalone marker between two paragraphs is a
checkerror — there is no "nearest paragraph" heuristic. - A section's span follows the owner-fixed IR rule (PROP-035 §5): from its heading to the next heading of the same or higher level.
3.9 The granularity doctrine — facts are the campaign grain
- 39Anchored units are the maintenance granularity. Between campaigns,
markup lives on
{#anchor}-ed units; reports citespec://…#anchor; this is the stable, refactor-proof form. - Facts are the campaign granularity (fact amendment, 2026-07-24 — owner-directed; supersedes the paragraph grain of the original text). An actualization campaign demands verbatim exhaustiveness at the grain of individual facts, because a paragraph routinely carries several and an LLM pass silently skips the inner ones. Operationally:
- every paragraph, list item, and non-empty table body cell carries its own marker — these are the countable units the exhaustive counter enforces;
- a paragraph that carries more than one fact is deconstructed — rewritten, sense-preserving and wording-preserving, into a bulleted or numbered list with one fact per item, each item marked. Most prose is expected to become lists; a paragraph stays prose only when it truly carries one fact (or none — connective tissue);
- a fact that cannot leave its sentence (an inline clause, an enumeration
inside one sentence that resists splitting) is wrapped as a
fragment:
<status …>the fact</status>; - campaign passes still add missing
{#anchor}s; deconstruction changes form only — semantic edits belong to the drift-correction stage, never to markup passes. - After a campaign, density is folded back: a section whose units agree
collapses to one unit marker (
checkverifies the fold is lossless); mixed sections stay fact-marked.
3.10 Inheritance and rollup
- 40Downward (defaulting): a node's marker covers unmarked descendants.
- Upward (aggregation): an unmarked node's computed status is the
worst-of its children per the §3.3 order (
unknownwins the bottom). voidis the one value outside the(stage, state)order: it sorts above every other pair regardless of stage, so worst-of never returns it while any live unit remains.worst-of {spec/void, impl/plan}isimpl/plan— the live part governs and the tombstone's stage does not drag the document back tospec;worst-of {done, void}isdone. A document whose every unit isvoidisvoid, which falls out of the same rule rather than being special-cased. This is a property of the pair: givingvoidthe top state slot within its stage would leave@spec/voidgoverning by stage anyway, which is why two of the three options originally proposed for it could not work.- An explicit marker always beats both directions. Reports show explicit and computed separately — a divergence is information, not noise.
3.11 Terminal artifact obligations
41Decision
(owner, 2026-09-09): terminality is independent of status. stage/state
continues to answer where a fact stands now, and done continues to mean
done only for that stage. An addressable fact may separately declare the
artifact kinds whose satisfaction closes the whole fact. No stage is a
mandatory successor of another, and no fact is forced toward source-code
implementation merely because its specification is complete.
42The Markdown form is
exactly one qualified trailing annotation
@requires:<kind>[,<kind>…], immediately before the fact's final
status shorthand admitted by §3.7 (qualified full/bare or legacy
full/bare), or final <status …/> point marker. @requires itself has
no legacy bare spelling. The XML-dialect equivalent
is a requires="<kind>[,<kind>…]" attribute on the named or generic
fact element, beside status; it is never an attribute of the standalone
document/section <status> element. Both forms lower to one fact-owned
semantic set.
43@fact:PROTOCOL The protocol text is the delivered artifact. @requires:specification @status:spec/done
@fact:PARSER The parser implements and proves the grammar. @requires:implementation,verification @status:test/done
- 44A requirements
annotation belongs only to an explicitly addressed fact. Paragraphs,
list items, quotes and table cells may carry it; a table cell loses its
usual anchor exemption when it does. A fragment, an unaddressed unit,
a document marker and a section marker cannot carry requirements.
@fact/code:may carry them normally; its bound fence remains part of the same fact body. - The complete
trailing cluster is
@requires:…then the status marker, with only whitespace between them and nothing after the status. A second contiguous@requirestoken in that trailing cluster, an empty member, a leading or trailing comma, or a trailing requirement with no final status is a parse error rather than a best-effort attachment. An occurrence separated from the final status by ordinary prose is not in the cluster and remains prose. - Fenced code,
inline code and link destinations remain opaque. Any look-alike outside
the exact trailing cluster remains ordinary Markdown even when its
comma-separated value happens to use legal artifact names; only the
cluster is grammar. The parser does not reserve every occurrence of the
English word
requires. - Requirements are a non-empty unordered set. The IR stores them in the fixed vocabulary order and every backend emits that canonical order. Authored duplicates are an error, not silently meaningful repetition. Changing presence, membership or spelling changes the fact content identity and therefore makes prior campaign verdicts due for re-judgement.
- Absent requirements
mean unclassified terminal contract, never an empty set and never
vacuous success. Existing facts therefore retain their exact status and
acquire no terminal claim. An explicitly empty set is invalid; a live
fact that requires nothing uses a self-carried artifact kind, while a
voidfact carries no requirements because it asserts no claim;state=voidplus a requirements annotation is a parse/check error, not an ordinary pending fact.
3.11.1 Closed artifact vocabulary
45The closed
vocabulary, in canonical order, is specification, implementation,
verification, documentation, decision, research, plan,
disposition, external. A new kind requires an amendment here before
parser, IR or wire support. These are closure artifacts, not document
genres: a research document may require an implementation, and a
contract fact may itself be a decision artifact.
- 46
specification,decision,researchandplanname the addressed fact body (plus its typed bound object, when any) as the delivered artifact. They need noimplementsedge merely to exist.state=doneasserts that this self-carried artifact is complete; independent campaign judgement may still confirm, drift or reject that assertion. implementationrequires independently observed implementing evidence for this exact fact address. A provider that knows the address and reports zero ismissing; no provider or an address outside its knowledge isunavailable, never silently satisfied.verificationrequires independently observed verifying evidence for the exact fact. An edge establishes attribution, not that a test run passed forever; campaign verdict freshness remains separate evidence and is never copied into authored markup.documentationis self-carried by a fact atdoc/done; otherwise it requires an independently observeddocumentsartifact/relation. Documentation about a fact and the fact's normative specification remain distinct.dispositionis a self-carried authored assertion that the addressed fact body is the durable outcome record for the thing it discusses. Atstate=donethe declaration itself satisfies the artifact presence; campaign judgement still confirms or rejects whether the body really records an accepted, rejected, deferred or superseded outcome. The core never guesses those semantics from prose.externalnames an artifact outside the fact body and therefore requires exactly one non-empty factrefplus an observer capable of checking that locator. In Markdown that ref is the existingrefattribute of the final status point marker; in XML it is the existingrefattribute of the named/generic fact element. Both lower through the fact'sstatus.refpivot member to one semantic fact locator. A shorthand status cannot carry a ref, and a missing/empty/duplicate ref is an error. A ref with no observer isunavailable, not proof by pointer.
3.11.2 Derived terminal observation
- 47The closed derived
outcome is
unclassified | pending | terminal.unclassifiedmeansrequiresis absent.pendingmeans the set is present but the fact is not atstate=done, carries any action, or at least one required artifact is missing or unavailable.terminalmeans the non-empty set is present, the fact is atstate=done, no action remains, and every required artifact is satisfied. Parser/check errors are invalid input, not a fourth outcome. - Each required
kind is reported as
satisfied | missing | unavailable, with exact evidence locators when available. Overallpendingnever erases that distinction. The markup declares the closure contract; current evidence observes it. Neither side forges the other. - No stage
ordering implies an artifact.
test/donedoes not by itself prove an implementation;impl/donedoes not imply documentation; andfreeze/donedoes not imply source code. Only the declared set plus its artifact observations decides terminality. - Status
rollup remains the existing worst-of
(stage,state)operation. Terminality aggregates separately as classified, terminal, pending and unclassified counts; it never changes the stage sort key. The existingdonereport view remains stage-local, while a separate terminal view selects only derivedterminalfacts. - A section/document marker cannot fold away fact-owned requirements. Classified facts remain fact-grained until a future explicitly specified aggregate requirements carrier can preserve every set losslessly. A fold that would drop requirements is a check error.
- The old
generic evidence heuristic “every
freeze/*needs animplementsedge” is superseded for classified facts: freeze uses the same declared artifact set as every other stage. During migration an unclassified frozen fact retains the legacy warning rather than silently losing a check. A specification-only frozen contract becomes honest by declaringrequires=specification.
3.11.3 Compatibility and projections
- 48Old source,
parse sidecars, caches and baselines decode missing requirements as
unclassified. No existing source bytes, status, verdict or
doneview changes. A newly annotated fact deliberately changes its content hash; this is semantic evolution, so no compatibility layer preserves its prior judgement. - Every fact-preserving Markdown/XML pivot and authored-fact query carries the normalized set. The requirements metadata query exposes the authored set and keeps authoring, adoption, relations and lifecycle evidence as separate axes; it does not invent a portable terminal verdict from partial inputs. A progress report may render its own explicitly named evidence observation, including unavailable, but never persist it as authored truth.
- Adding the set to a strict public machine wire requires a new wire epoch and independent canonical identity frames: explicit presence, canonical count and canonical kind order. Additive Rust deserialization compatibility is not permission to change a strict JTD epoch in place.
3.11.4 Never
- 49Never
infer whole-fact terminality from
state=donealone. - Never infer an implementation artifact from a later stage or from freeze.
- Never treat absent requirements as an empty satisfied set.
- Never persist a live evidence-derived terminal observation back into source markup or a source-only cache.
4. Parsing rules
- 50Markdown frontend first: the scanner operates on the document tree
(headings → units per the PROP-035 §5 body-span rule), then recognizes
<status>elements and shorthand in text nodes only — never inside fenced code, inline code, or link targets. - Countable units (fact amendment, 2026-07-24): inside a text block
the scanner recognizes list items (
-/*/+andN./N)lines, with their indented continuation lines, at every nesting level) and table rows (|-delimited); the units the exhaustive counter walks are: plain paragraphs, the lead lines of a block before its first list item, each list item, and each non-empty body cell of a table. Header and delimiter rows of a table are structure. A marker counts for the unit whose text carries it (first/last token of that unit, or a fragment wrapper inside it). - Fact anchors (fact amendment, 2026-07-24): a
@fact:<ID>first token of a paragraph or list item is that unit's anchor, recorded alongside the heading anchors; the scanner enforces the anchored-when-marked law (§3.8) — a marked unit with no anchor, and a duplicate id, arecheckerrors. An opener inside code spans/fences is opaque, as all markup is. - Typed fact — the fence joins the body (owner ruling variant D
2026-08-06; built 2026-08-20, B-068):
@fact/code:<ID>is a fact definition in the same position and id namespace as@fact:<ID>, whose body is its own unit plus the first fenced block after it (blank lines between are fine; any other block breaks the adjacency and is a parse error, as is a typed fact that is not the last fact of its text block). The attached fence enters the fact's content hash — editing the fence stales the fact — while staying opaque to marker/anchor scans, as all fences are.codeis the one implemented type: an unknown type (@fact/<t>:) is a parse error naming<t>, never a silent skip — a grammar must not promise what it cannot check. By default a fence remains an example belonging to no fact; the typed form is the opt-in that turns it into a judgeable assertion. - The element grammar is XML: attributes quoted, point markers self-closed. A future XML storage frontend consumes the same attribute schema natively; the markup language does not change.
- Foreign inline grammars (
@spec://,#use,#embed,#source,<!-- REVIEW: … -->) are opaque text to this scanner (§2).
5. Genre semantics
- 51Every genre is in scope — contracts, design docs, research, plans, manual tests: a design decision the code ignores is first-class drift.
- Genre informs which
artifact kinds an author declares, but no genre silently supplies a terminal
status. A contract may end as specification, implementation, verification or
a frozen combination; research may require its own study plus dispositions;
a campaign plan may itself be the artifact while its execution is a separate
fact.
##TERMINALITY-IS-ORTHOGONALand the explicit@requiresset replace the former one-shape claim that every contract ends atfreeze/donewith an implementation edge.
6. Scope configuration — facts.toml
- 52Optional dev-mode mechanics, configured by a
facts.tomlat the package root (theclippy.tomlpattern — tool config, not manifest pollution);progress.tomlis read as a silent legacy fallback for the transition (owner correction 2026-08-22: the observed tree is a facts-layer concern). - Include-style globs name what is observed (not gitignore-style excludes):
53schema = 1
include = ["spec/**/*.md", "packages/**/*.md"] # the default when absent
- 54Default excludes (applied always, even under explicit includes),
in two kinds. By directory, matched against any path component:
vibedeps/,.vibe/,refs/,fixtures/,campaigns/,target/,node_modules/,**/vendor/. By file name, matched against the basename wherever it sits:LICENSE.md. - Rationale: regenerated dependency copies must never carry authored markup (PROP-009's install-never-edits-authored-spec law), third-party and test-asserted content is off-limits, and the campaign zone (PROP-047 §5.4) is not itself corpus. A licence is the same case one granularity down — verbatim text the observing project neither authored nor is the source of truth for, replaced wholesale from upstream — so it needed the file-name kind rather than a directory.
- Project-side
exclude— an optional list of globs inprogress.toml, matched against the/-separated repo-relative path and applied after the includes and after both default kinds. It exists for what an include glob cannot say: everything under this subtree except these named files — a derived index, a generated projection, anything whose own words make a hand edit a defect. this section is include-style so that nothing is observed by accident, and an enumerated exclude list serves that purpose exactly as well as an enumerated include list; both are explicit and both are reviewable. It must not become a wildcard escape hatch, so: a pattern matching no observed file is reported by name on every subcommand, the count of files it removes is printed byscan, an invalid glob is a clean error naming the pattern, and an absent key behaves as a config that never had one. - A nested package with its own
progress.tomlowns its subtree (the host aggregates; it does not reach in) — this is how a specspace keeps its own cadence.
7. Out of scope / future
- 55XML document storage (arrives with the PROP-035 XML frontend — this markup is already native to it);
- extraction into a standalone distributable product (the separability law keeps it cheap);