<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">PROP-047 — Progress Control: the campaign toolchain</title>
  <status stage="impl" state="work" comment="born of the owner&apos;s 2026-08-22 boundary ruling: the facts markup (grammar, IR, lint) is the universal lower layer and lives in PROP-043 (modules/vibe-facts); this document is the upper layer — vibevm&apos;s own refactoring-campaign toolchain, matured quietly until it is worth showing. Every section here moved verbatim from PROP-043 §§4–7/10 and parts of §1/§2/§11; anchors unchanged."/>
  <section id="mandate" title="1. Mandate — the process layer">
    <p p="1"><fact id="CAMPAIGN-LAYER-MANDATE" status="spec/done">**The owner's boundary ruling (2026-08-22, chat, near-verbatim):** «факты являются чем-то, поверх чего можно построить совершенно разные процессы рефакторингов, и не факт, что наша текущая кампания и способ вообще делать кампании — самый лучший. Всё, что касается самого синтаксиса, IR, операций над фактами — в модуль facts (это будет использовать широкая общественность); в progress оставить наши инструменты для рефакторингов vibevm и потихоньку доделывать, чтобы когда-то они стали достаточно хороши, чтобы показать миру.» This document is that progress layer: the tool, its config, evidence providers, campaign data contracts, and the maintenance discipline. The grammar it operates on is PROP-043 (`spec://org.vibevm.core/vibevm/modules/vibe-facts/PROP-043`), and the dependency points strictly upward: progress knows facts; facts never knows progress.</fact></p>
    <p p="2"><fact id="needs-intake" status="spec/done">The needs this layer answers (moved verbatim from the
founding motivation of PROP-043 §1):</fact></p>
    <list ordered="true" p="3">
      <item><fact id="NEED-TOOL" status="impl/done">an **algorithmic tool** that reports the state of the whole observed tree
   and enforces exhaustiveness when a campaign demands it;</fact></item>
      <item><fact id="NEED-CAMPAIGN-SUBSTRATE" status="impl/work">a substrate for the **actualization campaign**: mark every claim, verify it
   against the code, and drive the drift down — with the markup remaining
   afterwards as the steering mechanism for further development.</fact></item>
    </list>
    <list ordered="false" p="4">
      <item><fact id="SEP-ADAPTER" status="impl/done">The **vibevm adapter** contributes the `vibe progress` CLI surface (§3), the
  `facts.toml` discovery, and the specmap evidence provider (§4). All
  vibevm-specific knowledge lives here.</fact></item>
    </list>
  </section>
  <section id="config-moved" title="2. Scope configuration — moved to the facts home">
    <p p="5"><fact id="CONFIG-MOVED-TO-FACTS" status="spec/done">**Scope configuration is a facts-layer concern and
lives in PROP-043 §6** (owner correction 2026-08-22: «what is observed» is a
universal question every facts consumer answers — the campaign toolchain
merely consumes the same scope through the facts core). The config file is
`facts.toml`; `progress.toml` is read as a silent legacy fallback for the
transition. This heading keeps its number so the section map below stays
stable.</fact></p>
  </section>
  <section id="tool" title="3. The tool — `vibe progress`">
    <list ordered="false" p="6">
      <item><fact id="TOOL-ADAPTER" status="impl/done">A subcommand of `vibe` (adapter over the standalone core).</fact></item>
      <item><fact id="TOOL-OUTPUT-FORMS" status="impl/done">Native output is
  XML; `--md` renders the table form (source · stage · state · action ·
  comment); `--json` emits the state projections of §5.2.</fact></item>
      <item><fact id="TOOL-INCREMENTAL" status="impl/done">All subcommands are
  incremental over the content-hash cache (§5.1).</fact></item>
    </list>
    <list ordered="false" p="7">
      <item><fact id="CMD-SCAN" status="impl/done">**`scan`** {#scan} — parse the observed tree, build/update the cache and
  state projections.</fact></item>
      <item><fact id="CMD-CHECK" status="impl/done">**`check`** {#check} — validation gate: closed vocabularies (with
  nearest-value hints), well-formedness (unclosed point markers), placement
  rules (standalone-between-paragraphs), shorthand collisions, foreign-grammar
  non-collision, lossless folds. `--exhaustive` additionally requires **zero
  unmarked paragraphs** in scope — the campaign gate. Exit codes are stable
  for CI.</fact></item>
      <item><fact id="CMD-REPORT" status="impl/done">**`report`** — the tree status: XML native, `--md` table,
  `--json`. Filters: `--view done|todo|qa|remove|doc|terminal` (the six resolution
  views: `state=done` · `action=continue` · `stage=test&amp;state=plan|work` ·
  `action=remove` · `actionstage=doc` · derived terminal artifact closure), `--audience user|author|dev|agent`
  (`agent` admitted 2026-09-11 by PROP-057 through PROP-043 `##AUDIENCE-VALUES`;
  the `doc` view lists the documentation's obligations and feeds the coverage
  gate `vibe doc check --coverage`, PROP-057 §14),
  per-file and whole-project rollups, explicit-vs-computed columns, and the
  evidence column when a provider is wired (§4).</fact></item>
      <item><fact id="CMD-MIRROR" status="impl/done">**`mirror`** — materialize the per-file cache view (campaign
  working representation; §5.1) under the campaign zone.</fact></item>
      <item><fact id="CMD-WEAVE" status="impl/done">**`weave`** — algorithmic stitch of the observed corpus into one
  document for whole-context LLM loading. `--digest` emits the map form
  (headings + markers + unmarked counts — always fits); `--max-tokens N`
  shards the full form with a shard manifest. **Measured 2026-07-26** on the
  58-file wave-1 corpus: the full weave is **one shard of 1 138 441 bytes**
  (≈ a third of a 1M-token window, so the sharder never had to split) and
  `--digest` is **200 454 bytes**.</fact></item>
      <item><fact id="CMD-RESCAN" status="impl/done">**`rescan --baseline &lt;file&gt;`** {#rescan} — the recurrence entry point:
  three-way compare (sources ↔ markers ↔ baseline, §5.3) emitting
  new / changed(suspect) / carried-forward unit lists, plus
  "marker changed outside any campaign" flags.</fact></item>
      <item><fact id="CMD-BASELINE" status="impl/done">**`baseline [--out &lt;file&gt;]`** {#baseline} — write the campaign's
  `baseline.json` (§5.3), the file `rescan` consumes. Projects the cache's
  **fact-grain** verdicts onto the **unit** granularity the baseline contract
  is defined at: a fact rolls up into every unit whose span carries it, the
  worst verdict wins (`drift` &gt; `unverifiable` &gt; `confirmed`), evidence is the
  deduplicated union, and the marker snapshot is resolved by the same code path
  `rescan` compares against. It re-verifies nothing and invents no verdict — a
  unit with no judged fact is omitted rather than filled in, so the artifact
  fails toward re-verifying. Default output is `campaigns/&lt;id&gt;/baseline.json`.</fact></item>
      <item><fact id="CMD-SEAL" status="impl/done">**`seal &lt;path&gt;…`** — record that a file's verdicts hold for
  its **current** text: sets `content_hash` and `campaign.processed_hash` to the
  digest **recomputed from disk**, plus `verified_at`. Same shape as
  `##CMD-GATE` — the caller did the real re-derivation and this records it;
  the command computes, changes and invents no verdict. Reading the cached
  `content_hash` instead of the disk would defeat the purpose, since that field
  is refreshed only by `scan` and between scans compares one stale value with
  another. It **refuses** a file whose markers are not all judged (naming the
  count and the first few), refuses a path the cache does not carry, prints
  what it is vouching for before doing it, and is a no-op with no fresh
  timestamp when the digest already matches. **Its refusal is a *coverage*
  test, not a *recency* one** — the schema carries one date per file and none
  per verdict, so "every marker has a verdict" is checkable and "every verdict
  is fresh" is not; the operator asserting the seal is the real gate (F-075).</fact></item>
      <item><fact id="CMD-GATE" status="impl/done">**`gate`** {#gate} — record one gate's verdict into the campaign's
  gate panel in `campaign.json`. The automation seam: whoever ran the real
  gate reports the result here, and the dashboard reads it back out. Spawns
  nothing and computes nothing — gates are *recorded*, never run here.</fact></item>
      <item><fact id="CMD-RESUME" status="impl/done">**`resume`** {#resume} — render `RESUME.md` from the campaign journal and
  state (operates on the campaign zone when present; a no-op outside one).</fact></item>
    </list>
  </section>
  <section id="evidence" title="4. Evidence providers">
    <list ordered="false" p="8">
      <item><fact id="EVIDENCE-SEAM" status="impl/done">The core defines a seam: *given a unit, return external facts about it*.</fact></item>
      <item><fact id="EVIDENCE-SPECMAP" status="impl/done">The
  vibevm adapter wires **specmap** (PROP-014) into it: `implements` /
  `verifies` / `deviates` edge counts per unit.</fact></item>
      <item><fact id="EVIDENCE-MISMATCH-FLAGS" status="spec/done">`report` then flags
  **markup-vs-reality mismatches** — e.g. a unit marked `test/done` with zero
  `verifies` edges. An **unclassified** `freeze/done` specmap orphan retains
  the legacy implementation warning; a classified frozen fact is checked only
  against the artifact kinds in its own `@requires` set. `check` can gate on
  the worst mismatch without inventing an undeclared implementation
  obligation.</fact></item>
      <item><fact id="EVIDENCE-OPTIONAL" status="impl/done">A project without specmap runs with an empty evidence
  column; nothing in the core knows the provider's shape.</fact></item>
    </list>
    <p p="9"><fact id="VERDICTS-NOT-IN-MARKUP" status="impl/done">Verification *verdicts* (confirmed / drift / unverifiable) are campaign data
and live in the cache and baseline — **never in the markup** (§5.5).</fact></p>
    <p p="10"><fact id="FACT-GRAIN-EVIDENCE" status="impl/done">*Fact-grain evidence (2026-07-24, owner-directed):* the specmap side
recognises `@fact:&lt;ID&gt;` fact anchors as addressable units (PROP-014 §2.1, the
fact amendment's twin), so `implements`/`verifies` edges land **per fact**
and the provider's mismatch checks apply at the campaign grain, not only
per section.</fact></p>
    <p p="11"><fact id="ARTIFACT-SATISFACTION-SEAM" status="impl/done">The evidence
    seam also serves PROP-043 `##TERMINAL-PER-ARTIFACT`: for each declared
    terminal artifact it reports `satisfied | missing | unavailable` and exact
    locators when it has them. The facts core itself satisfies self-carried
    specification/decision/research/plan/disposition artifacts from their
    explicit declaration on the addressed `state=done` fact; the vibevm specmap adapter observes exact
    `implements`, `verifies` and `documents` relations; an external artifact
    stays unavailable until an observer can check its `ref`. A provider absence
    and a known zero are never conflated.</fact></p>
    <p p="12"><fact id="TERMINAL-OBSERVATION-SURFACE" status="impl/done">`report`
    renders the authored canonical `requires` set, each artifact observation,
    and the derived `unclassified | pending | terminal` result. A separate
    `terminal` view selects only terminal facts; the existing `done` view
    remains exactly `state=done`. XML/Markdown/JSON projections share one row
    model. Dashboard/state projections consume that result and never recompute
    it from partial cache fields.</fact></p>
    <p p="13"><fact id="TERMINAL-OBSERVATION-NOT-VERDICT" status="impl/done">A
    terminal observation answers only whether the fact's declared artifact set
    is currently satisfied by this provider snapshot. Campaign verdicts
    (`confirmed | drift | unverifiable`), lifecycle verification evidence and
    adoption remain separate axes. The observation is never written into
    authored markup, the tracked cache or a baseline as truth.</fact></p>
  </section>
  <section id="data" title="5. Data contracts">
    <p p="14"><fact id="DATA-DISCIPLINE" status="impl/done">All formats are schema-versioned (`"schema": 1`); all writes are atomic
(tmp + rename); the journal is append-only JSONL (a torn tail line is
discarded on read).</fact></p>
    <section id="cache" title="5.1 Cache (per-file records)">
      <p p="15"><fact id="CACHE-RECORD" status="impl/done">Per observed file: path, content-hash, extracted markers with positions,
unit/paragraph counts, unmarked count, rollup results; campaign fields when a
campaign is active: verdict per marker (`confirmed` / `drift` /
`unverifiable`), evidence refs, batch id, processed hash.</fact></p>
      <p p="16"><fact id="CACHE-TALLY-COMPUTED" status="impl/done">The per-file **verdict tally is computed on read**, never
stored beside the verdict map it counts (F-077, owner ruling 2026-07-26). A
stored tally is a second statement of the same fact with its own writer, and
this campaign measured three that had gone stale — including one that claimed a
drift row already closed. The map is the source; the count is a view of it.</fact></p>
    </section>
    <section id="state" title="5.2 State projections (dashboard food)">
      <list ordered="false" p="17">
        <item><fact id="STATE-FILES" status="impl/done">`campaign.json` (wave, stage-of-campaign, gates, counters, `updated_at`),
  `corpus.json` (per-file rollups and counts), `findings.json` (the stitching
  obligation ledger), `tasks.json` (both task corpora with statuses),
  `docdebt.json` (harvest cards, doc-coverage).</fact></item>
        <item><fact id="DASHBOARD-READS-ONLY" status="impl/done">The dashboard reads **only**
  these; it computes nothing and parses no Markdown ever.</fact></item>
      </list>
    </section>
    <section id="baseline" title="5.3 Baseline (inter-campaign contract)">
      <list ordered="false" p="18">
        <item><fact id="BASELINE-RECORD" status="impl/done">`baseline.json` — per unit: URI#anchor, unit content-hash at verdict time,
  verdict, evidence refs, date, named crates, marker snapshot. **Shipped:**
  `baseline.rs`'s `BaselineUnit` carries exactly these fields, with
  `Baseline::load`, `Baseline::store` (`baseline/project.rs`), the
  `##CMD-BASELINE` writer and the `rescan` CLI all live. `store` was claimed
  here before it existed and was built to match on 2026-07-26 (F-065); the
  round trip — write the baseline, rescan against it on an unchanged tree —
  is what pins the two halves together.</fact></item>
        <item><fact id="BASELINE-INVALIDATION" status="spec/done">Invalidation:
  unit hash changed ⇒ suspect; named crate has commits after the verdict date
  ⇒ suspect; marker diverged from snapshot without a campaign ⇒ flagged;
  otherwise carry-forward (plus a small random control sample, because
  code-side invalidation is deliberately coarse).</fact></item>
      </list>
    </section>
    <section id="campaign-zone" title="5.4 The campaign zone">
      <list ordered="false" p="19">
        <item><fact id="ZONE-LAYOUT" status="impl/done">`campaigns/&lt;id&gt;/` at the repository root: `baseline.json`, `deferrals.md`,
  `harvest/`, `tasks/`, and the ephemeral `run/` (journal.jsonl, state/,
  RESUME.md, mirror/).</fact></item>
        <item><fact id="ZONE-EXCLUDED" status="impl/done">Excluded from markup scope, from packaging, and from
  registries — always.</fact></item>
        <item><fact id="ZONE-LIFETIMES" status="impl/done">`run/` is disposable after close-out; the other four
  survive between campaigns.</fact></item>
        <item><fact id="PROCESS-LAW-ELSEWHERE" status="impl/done">Process law (journal step protocol, recovery
  rules, RESUME contract) lives in the campaign plan, not here.</fact></item>
      </list>
    </section>
    <section id="erasure" title="5.5 The erasure law">
      <list ordered="false" p="20">
        <item><fact id="ERASURE-LAW" status="impl/done">Delete every derived artifact — cache, state, journal, mirror, weave — and no
  *fact* is lost: the markup in the sources carries all knowledge.</fact></item>
        <item><fact id="BASELINE-ACCELERATION" status="spec/done">The one
  artifact worth keeping anyway is `baseline.json`: not knowledge but
  **acceleration** — its loss returns the next run's cost from O(delta) to
  O(corpus).</fact></item>
      </list>
    </section>
  </section>
  <section id="maintenance" title="6. Maintenance discipline">
    <p p="21"><fact id="maintenance-lead" status="spec/done">After the first campaign:</fact></p>
    <list ordered="false" p="22">
      <item><fact id="EDIT-UPDATES-MARKER" status="spec/done">**Edit a unit ⇒ update its marker in the same commit.** `vibe progress
  check` sits in the gate panel and yellows on divergence.</fact></item>
      <item><fact id="TASK-LOOP" status="spec/done">Task pipelines close the loop: an IMPL task cites markers on entry and
  updates them on exit (`impl/work → impl/done`, then `test/plan`).</fact></item>
      <item><fact id="FREEZE-NEEDS-EVIDENCE" status="impl/done">A classified
  `freeze/done` fact requires the artifact evidence its own `@requires` set
  names; freeze alone never manufactures an implementation obligation. An
  unclassified frozen fact retains the legacy implementation-edge warning
  during migration, so introducing terminal contracts cannot silently weaken
  an existing gate.</fact></item>
      <item><fact id="DOC-COVERAGE-RATCHET" status="spec/done">Doc-coverage (units lacking `documents` edges / doc-view closure) ratchets
  like specmap orphans.</fact></item>
      <item><fact id="PERIODIC-REVERIFICATION" status="spec/done">Periodic re-verification runs as a recurring campaign
  (O(delta) via §5.3) and as a health-audit category between runs.</fact></item>
    </list>
    <section id="fact-lifecycle" title="6.1 The life of a fact under an active campaign">
      <p p="23"><fact id="LIFECYCLE-WHY" status="impl/done">**Editing the corpus while a campaign judges it is the normal
case, not an exception** — the campaign exists precisely because the corpus is
being reworked. Three things can happen to a fact, they are not the same thing,
and only one of them announces itself.</fact></p>
      <list ordered="false" p="24">
        <item><fact id="LIFECYCLE-EDITED" status="impl/done">**A judged fact whose text moves comes due for
  re-judgement, and the tooling names it.** The freshness reader compares the
  text a fact was judged against with the text on disk and lists every fact that
  moved, by anchor. This is the case the machinery was built for.</fact></item>
        <item><fact id="LIFECYCLE-ADDED" status="impl/done">**A fact added to an already-judged file is unjudged, and
  NOTHING says so.** It does not enter the verdict total, it does not appear in
  any percentage, and no gate fires. It is discovered only by comparing the
  file's addressable anchors against its verdict map — which no shipped command
  prints today.</fact></item>
        <item><fact id="LIFECYCLE-DELETED" status="impl/done">**A fact removed from a document leaves its verdict
  behind, and the verdict keeps counting.** The cache is keyed by anchor and
  nothing prunes a key whose anchor is gone.</fact></item>
        <item><fact id="STALE-IS-NOT-REJUDGE" status="impl/done">**«The file moved» and «a judged fact moved» are
  different questions, and conflating them wastes the whole point.** A file goes
  stale the moment its bytes change — including when the change only ADDS facts,
  leaving every judged fact untouched. A corpus can carry five stale files and
  zero facts owed re-judgement. Read the per-fact answer, never the per-file
  one.</fact></item>
        <item><fact id="SEAL-IS-A-WHOLE-FILE-ASSERTION" status="impl/done">**Sealing refuses a file carrying any
  unjudged marker, and this is correct rather than inconvenient.** Sealing
  asserts that *every* verdict in the file is valid for its current text, so a
  partially-judged file may be left flagged but not vouched for. That refusal is
  the only mechanism today that makes an added fact visible at all.</fact></item>
      </list>
    </section>
    <section id="debt-clearance" title="6.2 Incremental debt clearance">
      <p p="25"><fact id="DEBT-IS-A-LIST-NOT-A-RATIO" status="impl/done">**The debt is enumerable, so it is paid item by
item and never by re-judging the corpus.** Three enumerable kinds: facts with no
verdict, facts whose text moved, verdicts whose anchor is gone. Each has names
and addresses; none is a percentage to be attacked wholesale. Re-judging
everything would redo work that nothing invalidated.</fact></p>
      <p p="26"><fact id="DEBT-UNIT-IS-THE-FILE" status="impl/done">**The unit of clearance is one file**, because sealing
is a whole-file assertion (`##SEAL-IS-A-WHOLE-FILE-ASSERTION`) — a file is
either clear or flagged, and there is no half-sealed state to leave behind.</fact></p>
      <p p="27"><fact id="DEBT-CHEAPEST-IS-THE-FILE-YOU-OPENED" status="impl/done">**The cheapest debt is in the file you
were going to read anyway.** Judging N facts in one document costs far less than
N facts in N documents, because the reading is shared; a session already editing
a document pays almost nothing to clear that document's backlog in the same
pass.</fact></p>
      <p p="28"><fact id="DEBT-PROCEDURE" status="impl/done">**The procedure, run on demand and never automatically:**</fact></p>
      <list ordered="true" p="29">
        <item><fact id="DEBT-STEP-MEASURE" status="spec/plan">**Measure.** Print the three kinds with the files behind
   them, worst first.</fact></item>
        <item><fact id="DEBT-STEP-PICK" status="impl/done">**Pick one file** — either the heaviest, or the one this
   session is about to touch anyway.</fact></item>
        <item><fact id="DEBT-STEP-JUDGE" status="impl/done">**Judge only its unjudged facts**, to the ordinary standard
   and clause by clause. A prescriptive fact is judged on coherence and on every
   referent resolving; a descriptive one is checked against the tree. Freshly
   authored text is not exempt from either.</fact></item>
        <item><fact id="DEBT-STEP-SEAL" status="impl/done">**Merge and seal.** A refusal to seal means something in the
   file was missed — that refusal is the check, not an obstacle.</fact></item>
        <item><fact id="DEBT-STEP-REPORT" status="impl/done">**Report how much was cleared**, so the number moves
   visibly rather than silently.</fact></item>
      </list>
      <p p="30"><fact id="DEBT-CLOSING-INCLUDES-JUDGING" status="spec/plan">**Content moved into a specification is judged
in the same pass that moves it.** An unjudged statement in a spec is the same
kind of tail as a dangling citation: the move is not finished until the corpus
knows about what arrived. Without this the standing ruling «significant content
moves into the specifications on closure» manufactures debt at every closure.</fact></p>
      <p p="31"><fact id="DEBT-ASK-AT-SESSION-START" status="spec/plan">**A session reports the debt when it restores
context** (owner ruling 2026-08-06) — one line in the resume report, beside the
gate state and the blockers. Reporting is not paying: the session says what the
debt is and waits, because clearing it is work like any other and its priority
is the owner's.</fact></p>
      <p p="32"><fact id="DEBT-MUST-BE-ASKABLE" status="spec/plan">**The debt is a question the tool answers, not a query
somebody reconstructs** (owner ruling 2026-08-06). «How much debt is there for
the periodic clearance» must be answerable by asking `vibe progress`, in the
same breath as the confirmed/drift figures — three counts and the files behind
them. A number that exists only in a hand-written query is a number nobody
looks at, and this whole subsection describes work that is invisible until it is
printed. **The campaign-side script is a stopgap; the durable home is the
shipped verb.**</fact></p>
      <p p="33"><fact id="DEBT-DO-NOT-JUDGE-BLIND" status="impl/done">**What must not happen: clearing the count by
judging without evidence.** A verdict written to move a number is the defect
this whole apparatus exists to remove, and it is cheapest to commit exactly when
someone is paying down a backlog.</fact></p>
    </section>
  </section>
  <section id="future" title="7. Out of scope / future">
    <list ordered="false" p="34">
      <item><fact id="FUT-SECOND-WAVE" status="spec/done">second-wave corpora (`vibevm/vibepacks/org.vibevm.world`,
  `org.vibevm.ai-native`, ~230–250 authored files) and the fractality specspace
  (explicitly excluded from wave 1 by owner decision);</fact></item>
      <item><fact id="FUT-DASHBOARD" status="spec/done">dashboard evolution beyond the minimal read-only page.</fact></item>
      <item><fact id="DASHBOARD-TERM" status="impl/done">(Terminology note:
  this surface is always called the **dashboard** — never "storefront", a
  term already taken by the vibevm store surface.)</fact></item>
    </list>
  </section>
</spec>
