Skip to content
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- **Loop closure (P5 of the substrate-v2 plan): doom-loop diagnosis, imagination, CUSUM
drift, checkpoint cadence.** `forge diagnose "<error>"` hashes each failure into a
signature (line numbers, addresses, timestamps, and absolute paths normalized out) and
counts recurrences in a 50-entry ring; the 3rd identical hit is thrash — it mints a
content-addressed `diagnosis` claim into the team ledger and tells the agent to STOP
retrying and escalate ONE model tier with the diagnosis as the prompt's head (the same
loop becomes a one-per-team event, not one-per-session). `forge imagine "<task>"` is the
static half of the consequence simulator (paper Eq. 4): entities → blast radius →
predicted breaks with confidence, plus the minimal dry-run test suite via weighted greedy
set cover (weight = file size as a duration proxy; classic ln-n approximation) and
`riskScore = Σ confidence` — the sandboxed worktree runner that executes the suite is the
P5 follow-up. `anchor.cusum()` adds the M4 one-sided CUSUM control chart (k = 0.35,
h = 1.0): sustained small drift alarms, a single exploratory spike drains back to zero.
`verify.checkpointCadence()` prices M6's "when to check?" as the optimal-stopping
threshold rule `n* = ⌈checkCost / (pErr·tokensPerStep·costPerToken)⌉`, clamped to
[1, 50] — every input measured or priced, no magic constants.

- **Context assembly + completeness gate (P4 of the substrate-v2 plan).** `forge context
"<task>"` makes what goes into the window a budgeted optimization and makes
*sufficiency* a computed set. The required-knowledge set `R(edit)` — the target's
Expand All @@ -20,6 +37,43 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
implements it?"), shown in `forge substrate` and — under `FORGE_ENFORCE=1` — blocking:
acting on missing context is acting on a guess. Incomplete context stops being a
feeling and starts being a set difference.
- **Generated-UI quality gate (P6 of the substrate-v2 plan).** Taste becomes measurable:
`src/uifingerprint.js` extracts a deterministic design fingerprint from CSS/JSX/Tailwind
classes — pure static parsing, no LLM, no screenshots — covering palette (HSL + 12-bin hue
histogram), spacing (base unit by residual-minimization approximate GCD, on-scale
fraction), font families, radius and shadow levels. Two distances gate generated UI:
`slopDistance` to a shipped, rationale-documented generic-template signature set
(default-Tailwind blue/indigo, stock Bootstrap, the AI-landing gradient) must stay HIGH,
and `conformance` to the project's own fingerprint — stored as a shared `fingerprint`
ledger claim via `mintProjectFingerprint` — must stay LOW; `uiGate` failures are
actionable per-feature edits, never a bare score. Scale-conformance checks
(spacing-on-base, radius/shadow level caps, palette bound) join `ASSERTABLE_CHECKS`.
`forge uicheck` gains `fingerprint <file...> [--mint]` and `design <file...>` (exit 1 on
fail) alongside the unchanged contrast math.
- **Local dashboard (P7 of the substrate-v2 plan).** `forge dash [--port N]` serves a
read-only lens on the substrate's state: a `node:http` stdlib server (localhost-only,
zero runtime deps) with ONE self-contained HTML page — inline CSS/JS, no CDN, no
framework, no build step. Panels: Ledger (claims with val bars, kind filter, contested
claims — val ∈ [0.4, 0.6] with ≥1 contradiction — and per-author trust), Cost/Cache
(stage counters + measured saved-token estimates from `.forge/metrics.jsonl`), and
Impact (atlas blast-radius explorer via `/api/impact?target=X`). Every claim row shows
its `forge ledger blame <id>` command — no unexplained scores anywhere in the UI. Data
is separated from serving (`dashData()` vs `serve()` in `src/dash.js`) so the payload
is tested without sockets, and corrupt/missing stores degrade to empty sections instead
of taking down the lens. The ratify/retract POSTs are a follow-up; this phase never
writes.
- **Measured cost report (P8 of the substrate-v2 plan).** `forge cost --stages [--json]`
computes per-stage cost factors as pure arithmetic over `.forge/metrics.jsonl`
(`src/cost_report.js`): gate halt rate, tier-weighted cache hit rate (exact 1.0 / near
0.85 / adapt 0.5), route saving priced against the always-premium baseline, and context
assembly — then composes `C = C₀ · Π(1 − fᵢ)` over ONLY the measured stages. A stage with
no events reports "no data", never a default; the composed figure is a lower bound whose
caveats name every unmeasured stage; the paper's 62 % routing figure is cited as context,
and ~90 % appears only as a labeled target. `substrateCheck` now meters the assumption
gate on the explicit path (one `gate` halt/pass line per decision; ambient hooks stay
write-free), `recordGate`/`recordRoute` give future stage wiring one obvious call each,
and `reports/cost-eval.md` scaffolds the paired-run harness report with a truthful
empty state.

- **Proof-carrying reuse cache (P3 of the substrate-v2 plan).** `forge reuse` turns
"reuse already-generated code" from prose into a deterministic system: verified code
Expand Down
66 changes: 66 additions & 0 deletions reports/cost-eval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Cost evaluation — measured stage factors (P8)

> Status: **no data yet.** This document is the artifact the P8 harness
> ([docs/plans/substrate-v2/05-cost-model.md](../docs/plans/substrate-v2/05-cost-model.md) §3)
> fills with measurements. Until a cell below holds a measured number, the only claimable
> figures are the paper's: 62 % routing saving on live tokens (paper §9). The plan's ~90 %
> composed figure is a **target**, not a result, and does not appear in this table.

## Methodology

The cost model is multiplicative — `C = C₀ · Π(1 − fᵢ)` over independent stages — so each
stage factor is measured separately and composed arithmetically, never asserted:

1. **Instrumentation.** Every substrate stage appends one line to `.forge/metrics.jsonl`
(`{t, stage, outcome, tokensIn, tokensOut, tier, savedEstimate, ref}` — `src/metrics.js`).
`forge cost --stages` computes the per-stage factors from those lines (`src/cost_report.js`);
a stage with no events reports **no data**, never a default.
2. **Paired runs.** Baseline (always-premium, read-everything, no cache) vs. substrate over
the same replay corpus (N ≥ 100 real tasks, stratified repeat-heavy / mixed / cold), the
paper §9 methodology: identical tokens repriced, so every saving is arithmetic on measured
tokens.
3. **Correctness guard (spec §3).** A saving counts only if the external verifier passes the
output. A routed-down answer that fails is not a saving; a cache hit that gets reverted is
recorded as a *negative* entry.

## Measured factors

| stage | factor | events | measured? |
|---|---|---|---|
| gate (M2 halt rate) | — | 0 | no data yet — run with metrics enabled |
| cache (reuse, tier-weighted) | — | 0 | no data yet — run with metrics enabled |
| route (vs always-premium) | — | 0 | no data yet — run with metrics enabled |
| context (assembly ρ) | — | 0 | no data yet — run with metrics enabled |
| **composed (measured stages only)** | — | 0 | nothing to compose yet |

Secondary counters (doom-loop halts avoided, M5 lean, avoided rework) are reported alongside
when populated — they are deliberately excluded from the multiplication (spec §1).

## How to populate this table

Metrics accrue as the substrate is actually used — each command below appends stage-tagged
lines to `.forge/metrics.jsonl`:

```sh
# gate + cache: every explicit pre-action check meters both stages
forge substrate "<task>"

# cache: explicit reuse queries and mints
forge reuse query "<what you are about to build>"

# then read the measured factors (and paste them here):
forge cost --stages # human table
forge cost --stages --json # machine-readable, for this report
```

Route and context events are emitted via `recordRoute` / a future context-assembly hook
(`src/cost_report.js`) as those stages gain live wiring.

## Caveats that ship with any number placed here

- Stage rates are **workload-dependent**: factors describe the recorded traffic of one repo,
not a general claim (spec §2 — repeat-heavy warm-ledger workloads differ from cold starts).
- The composed reduction is a **lower bound from measured stages only**; unmeasured stages
contribute nothing rather than a target.
- Until the paired-run harness with the correctness guard has run, per-stage factors from
live metrics are unguarded observational numbers, not eval results.
27 changes: 27 additions & 0 deletions src/anchor.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,33 @@ export function goalDrift(root, goal, opts = {}) {
};
}

/**
* M4 — one-sided CUSUM control chart over a drift-signal series (spec §5:
* docs/plans/substrate-v2/06-faculties-and-mechanisms.md). A raw threshold on a
* single checkpoint's drift Dₜ is noisy — one exploratory step legitimately wanders.
* CUSUM accumulates only the excess over the allowance k (Cₜ = max(0, Cₜ₋₁ + Dₜ − k))
* and alarms at Cₜ > h, which detects SUSTAINED small drift with provably minimal
* detection delay for a given false-alarm rate (classical sequential analysis),
* while a single within-tolerance spike drains back to zero instead of alarming.
* Defaults k = 0.35, h = 1.0 per the spec (calibration lands in P8). Pure.
* @param {number[]} signals drift per checkpoint, Dₜ ∈ [0, 1] (non-numeric → 0)
* @param {{k?: number, h?: number}} [opts]
* @returns {{alarm: boolean, C: number[], firstAlarm: number}} firstAlarm = index of
* the first checkpoint whose statistic crossed h, or -1 if none did.
*/
export function cusum(signals, { k = 0.35, h = 1.0 } = {}) {
const C = [];
let c = 0;
let firstAlarm = -1;
for (let i = 0; i < signals.length; i++) {
const d = Number(signals[i]);
c = Math.max(0, c + (Number.isFinite(d) ? d : 0) - k);
C.push(c);
if (firstAlarm < 0 && c > h) firstAlarm = i;
}
return { alarm: firstAlarm >= 0, C, firstAlarm };
}

export function renderAnchor(r) {
const lines = ["Forge anchor — goal-drift check", ""];
if (!r.changed.length)
Expand Down
Loading
Loading