PROP-047 — Progress Control: the campaign toolchain
1. Mandate — the process layer
01The 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.
02The needs this layer answers (moved verbatim from the founding motivation of PROP-043 §1):
- 03an algorithmic tool that reports the state of the whole observed tree and enforces exhaustiveness when a campaign demands it;
- 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.
- 04The vibevm adapter contributes the
vibe progressCLI surface (§3), thefacts.tomldiscovery, and the specmap evidence provider (§4). All vibevm-specific knowledge lives here.
2. Scope configuration — moved to the facts home
05Scope 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.
3. The tool — vibe progress
- 06A subcommand of
vibe(adapter over the standalone core). - Native output is
XML;
--mdrenders the table form (source · stage · state · action · comment);--jsonemits the state projections of §5.2. - All subcommands are incremental over the content-hash cache (§5.1).
- 07
scan{#scan} — parse the observed tree, build/update the cache and state projections. 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.--exhaustiveadditionally requires zero unmarked paragraphs in scope — the campaign gate. Exit codes are stable for CI.report— the tree status: XML native,--mdtable,--json. Filters:--view done|todo|qa|remove|doc|terminal(the six resolution views:state=done·action=continue·stage=test&state=plan|work·action=remove·actionstage=doc· derived terminal artifact closure),--audience user|author|dev|agent(agentadmitted 2026-09-11 by PROP-057 through PROP-043##AUDIENCE-VALUES; thedocview lists the documentation's obligations and feeds the coverage gatevibe 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).mirror— materialize the per-file cache view (campaign working representation; §5.1) under the campaign zone.weave— algorithmic stitch of the observed corpus into one document for whole-context LLM loading.--digestemits the map form (headings + markers + unmarked counts — always fits);--max-tokens Nshards 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--digestis 200 454 bytes.rescan --baseline <file>{#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.baseline [--out <file>]{#baseline} — write the campaign'sbaseline.json(§5.3), the filerescanconsumes. 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>unverifiable>confirmed), evidence is the deduplicated union, and the marker snapshot is resolved by the same code pathrescancompares 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 iscampaigns/<id>/baseline.json.seal <path>…— record that a file's verdicts hold for its current text: setscontent_hashandcampaign.processed_hashto the digest recomputed from disk, plusverified_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 cachedcontent_hashinstead of the disk would defeat the purpose, since that field is refreshed only byscanand 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).gate{#gate} — record one gate's verdict into the campaign's gate panel incampaign.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.resume{#resume} — renderRESUME.mdfrom the campaign journal and state (operates on the campaign zone when present; a no-op outside one).
4. Evidence providers
- 08The core defines a seam: given a unit, return external facts about it.
- The
vibevm adapter wires specmap (PROP-014) into it:
implements/verifies/deviatesedge counts per unit. reportthen flags markup-vs-reality mismatches — e.g. a unit markedtest/donewith zeroverifiesedges. An unclassifiedfreeze/donespecmap orphan retains the legacy implementation warning; a classified frozen fact is checked only against the artifact kinds in its own@requiresset.checkcan gate on the worst mismatch without inventing an undeclared implementation obligation.- A project without specmap runs with an empty evidence column; nothing in the core knows the provider's shape.
09Verification verdicts (confirmed / drift / unverifiable) are campaign data and live in the cache and baseline — never in the markup (§5.5).
10Fact-grain evidence (2026-07-24, owner-directed): the specmap side
recognises @fact:<ID> 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.
11The 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.
12report
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.
13A
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.
5. Data contracts
14All 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).
5.1 Cache (per-file records)
15Per 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.
16The 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.
5.2 State projections (dashboard food)
- 17
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). - The dashboard reads only these; it computes nothing and parses no Markdown ever.
5.3 Baseline (inter-campaign contract)
- 18
baseline.json— per unit: URI#anchor, unit content-hash at verdict time, verdict, evidence refs, date, named crates, marker snapshot. Shipped:baseline.rs'sBaselineUnitcarries exactly these fields, withBaseline::load,Baseline::store(baseline/project.rs), the##CMD-BASELINEwriter and therescanCLI all live.storewas 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. - 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).
5.4 The campaign zone
- 19
campaigns/<id>/at the repository root:baseline.json,deferrals.md,harvest/,tasks/, and the ephemeralrun/(journal.jsonl, state/, RESUME.md, mirror/). - Excluded from markup scope, from packaging, and from registries — always.
run/is disposable after close-out; the other four survive between campaigns.- Process law (journal step protocol, recovery rules, RESUME contract) lives in the campaign plan, not here.
5.5 The erasure law
- 20Delete every derived artifact — cache, state, journal, mirror, weave — and no fact is lost: the markup in the sources carries all knowledge.
- 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).
6. Maintenance discipline
21After the first campaign:
- 22Edit a unit ⇒ update its marker in the same commit.
vibe progress checksits in the gate panel and yellows on divergence. - Task pipelines close the loop: an IMPL task cites markers on entry and
updates them on exit (
impl/work → impl/done, thentest/plan). - A classified
freeze/donefact requires the artifact evidence its own@requiresset 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. - Doc-coverage (units lacking
documentsedges / doc-view closure) ratchets like specmap orphans. - Periodic re-verification runs as a recurring campaign (O(delta) via §5.3) and as a health-audit category between runs.
6.1 The life of a fact under an active campaign
23Editing 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.
- 24A 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.
- 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.
- 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.
- «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.
- 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.
6.2 Incremental debt clearance
25The 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.
26The 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.
27The 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.
28The procedure, run on demand and never automatically:
- 29Measure. Print the three kinds with the files behind them, worst first.
- Pick one file — either the heaviest, or the one this session is about to touch anyway.
- 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.
- Merge and seal. A refusal to seal means something in the file was missed — that refusal is the check, not an obstacle.
- Report how much was cleared, so the number moves visibly rather than silently.
30Content 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.
31A 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.
32The 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.
33What 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.
7. Out of scope / future
- 34second-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); - dashboard evolution beyond the minimal read-only page.
- (Terminology note: this surface is always called the dashboard — never "storefront", a term already taken by the vibevm store surface.)