Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ flowchart TD

**blast radius** — the set of files an edit is predicted to impact, read from the code
graph. `forge impact` computes it; the pipeline surfaces it before the model touches
anything.
anything. The analysis is **hazard-aware**: SCC-aware propagation (a change to any file
in a circular-dependency cluster impacts all co-members, via Tarjan from `forge rank`)
and a data-driven threshold derived from PageRank centrality and ledger incident history
(`effectiveThreshold = base / (1 + hazard)`). `--basic` reverts to the fixed-threshold
mode.

The verdict is **advisory by default** — it reports, it does not block. Set
`FORGE_ENFORCE=1` to turn the strongest signals into a hard block:
Expand Down Expand Up @@ -340,6 +344,30 @@ exported symbols, brand tokens, version, package.json fields) reused from `docs_
an inverted entity → `file:line` index over every doc surface, and a diff-scoped impact
query ranked by confidence. Advisory by default; `--strict` exits non-zero for CI.

**Load-bearing code detector (`src/rank.js`, `forge rank`).** Fuses three classical graph
readings of the atlas — weighted PageRank centrality (deterministic power iteration over
sorted node ids), iterative Tarjan SCC (circular-dependency clusters), iterative
Hopcroft–Tarjan articulation points (chokepoint files) — with the team's own incident
history from the evidence ledger (`val()`-weighted lesson and session-summary claims that
name each file). The join `hazard = centralityNorm × (1 + history)` means structurally
central code that has hurt before outranks equally central code that hasn't. Exposed as the
`rank_code` MCP tool and the `forge rank` CLI command.

**Parallel-session conflict radar (`src/collide.js`, `forge collide`).** Reads recent
foreign-session summaries from the team-merged ledger and computes per-file collision risk
via the same noisy-OR model lessons use: `risk = 1 − ∏(1 − recᵢ × strengthᵢ)` over
sessions that touched overlapping files or their 1-hop import neighbors. No server, no
presence protocol — teammate summaries arrive via `forge ledger sync` / `git pull`. Exposed
as the `collide_check` MCP tool.

**Machine-owned doc surfaces (`src/docs_render.js`, `forge docs render`).** The
auto-maintenance layer that keeps tables and diagrams in sync with the code registries.
Four marker-managed blocks (commands table in README, groups and MCP-tools tables in GUIDE,
repo-map diagram in ARCHITECTURE) are regenerated from `COMMANDS`/`GROUPS`/`TOOLS`; six
"N MCP tools" count phrases are auto-corrected; and every mermaid block across all `.md`
and `.mdx` files receives the branded `%%{init` theme. Registry-derived blocks are CI-gated
errors when stale; tree-derived output is advisory.

**Deliberately not wired:** `checkpointCadence` (optimal-stopping check spacing) still
has no runtime step-loop to consume it — wiring it would mean inventing one. It stays
library math with tests until a real consumer exists.
Expand Down Expand Up @@ -459,6 +487,9 @@ forgekit/
dash.js # localhost-only read-only dashboard over the ledger, metrics, and blast radius (node:http, one HTML page)
metrics.js # stage-tagged .forge/metrics.jsonl — the measured events every cost figure is computed from
cost_report.js # per-stage cost factors as pure arithmetic over metrics.jsonl; composes ONLY measured stages
rank.js # load-bearing code: weighted PageRank centrality × ledger incident history, Tarjan SCC (circular deps), Hopcroft–Tarjan articulation points (chokepoints)
collide.js # parallel-session conflict radar: noisy-OR risk over recent foreign sessions that touched overlapping files or their import neighbors
docs_render.js # machine-owned doc surfaces: registry-derived tables (commands, groups, MCP tools) + tree-derived repo map, auto-normalized mermaid themes
source/
rules.json # THE canonical rules source (git · testing · security · style)
substrate.json # cognitive-substrate defaults (thresholds, routing, llm knobs)
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **impact: hazard-aware blast radius.** `forge impact` now fuses the code graph with
team memory: SCC-aware propagation (from `forge rank` Tarjan cycles — a change to any
file in a circular-dependency cluster impacts all co-members) and a data-driven
threshold derived from PageRank centrality and ledger incident history
(`effectiveThreshold = base / (1 + hazard)`). No new constants — every enhancement is
computed from infrastructure already in the codebase. `--basic` flag reverts to the
fixed-threshold mode for comparison.

### Fixed

- **docs: refresh post-v0.30.0 staleness.** ARCHITECTURE.md repo layout and component
descriptions updated for `rank.js`, `collide.js`, `docs_render.js`; mintlify Labs card
now lists `rank` and `collide`; mermaid theme normalization extended to `.mdx` files
(6 mintlify diagrams were rendering in default blue/grey instead of the branded palette).

## [0.30.0] - 2026-08-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ that never clobbers your existing settings (skip it with `install.sh --no-settin
| | `forge config` | provider setup — show / switch / add providers, set default model |
| **Substrate** | `forge substrate` | one pre-action gate: assumptions, route, impact, scope, memory, verify |
| | `forge preflight` | assumption check — what a task names that the repo doesn't define |
| | `forge impact` | predict blast radius for a symbol or file from the atlas graph |
| | `forge impact` | hazard-aware blast radius — SCC-aware propagation + data-driven threshold from PageRank centrality and ledger incident history |
| | `forge scope` | decompose files into independent clusters (+ coupled files you didn't name) |
| | `forge context` | budgeted context assembly + completeness gate — what an edit NEEDS known |
| | `forge route` | recommend the cheapest capable model for a task (+ gateway config) |
Expand Down
8 changes: 6 additions & 2 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,15 @@ and pin explicit IDs if a family scored wrong.

### `forge impact <symbol|file>` — what will this edit break?

Reverse-dependency blast radius from the atlas graph. Run `forge atlas build` first.
Hazard-aware blast radius from the atlas graph, fused with team memory:
SCC-aware propagation (a change to any file in a circular-dependency cluster
impacts all co-members) and a data-driven threshold from PageRank centrality
and ledger incident history. `--basic` reverts to the fixed-threshold mode.
Run `forge atlas build` first.

```console
$ forge impact verifyToken
Forge impact — blast radius
Forge impact — blast radius (hazard-aware)

target: verifyToken ✓ found
impacted files: 3
Expand Down
2 changes: 1 addition & 1 deletion mintlify/cli/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ noticing. Commands are organized into six groups (the last is experimental).
</Card>
<Card title="Labs (experimental)" icon="flask">
Experimental — may change or move: `taste`, `uicheck`, `imagine`, `lean`, `anchor`,
`diagnose`, `dash`, `report`, `deja`, `reuse`. Documented in the reference pages above.
`diagnose`, `dash`, `report`, `deja`, `reuse`, `rank`, `collide`. Documented in the reference pages above.
</Card>
</CardGroup>

Expand Down
6 changes: 4 additions & 2 deletions mintlify/cli/substrate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ forge route gateway # emit LiteLLM gateway config

## `forge impact`

Predict the blast radius for a symbol or file from the atlas graph.
Hazard-aware blast radius — SCC-aware propagation and data-driven threshold from
PageRank centrality and ledger incident history. `--basic` reverts to fixed-threshold
mode.

```bash
forge impact <symbol-or-file>
forge impact <symbol-or-file> [--json] [--basic]
```

## `forge collide`
Expand Down
1 change: 1 addition & 0 deletions mintlify/concepts/config-compiler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ config. The four layers are _how the brain is expressed_; the compiler is _how i
delivered_.

```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%%
flowchart TD
S["source/ · rules.json · substrate.json · mcp.json"] -->|"forge sync — content-hash + DO-NOT-EDIT headers"| N["native configs · CLAUDE.md · AGENTS.md · .cursor · .gemini · .aider"]
S -. configures .-> L
Expand Down
1 change: 1 addition & 0 deletions mintlify/concepts/pre-action-gate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and returns a single verdict. It composes the individually-callable stages —
`verify` — into one pre-action contract.

```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%%
flowchart TD
RE["referenced entities"] --> INTAKE
subgraph INTAKE["intake"]
Expand Down
2 changes: 2 additions & 0 deletions mintlify/concepts/proof-carrying-memory.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ materializes from the ledger. (`FORGE_LEDGER_ONLY=0` restores the legacy file st
one-release escape hatch.)

```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%%
flowchart LR
subgraph EV["local events"]
direction TB
Expand Down Expand Up @@ -102,6 +103,7 @@ dependencies still resolve. Otherwise it falls through to generation and mints a
claim on the way back.

```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%%
flowchart LR
SP["spec"] --> FP["fingerprint · MinHash + LSH"]
FP --> LD["match ladder · exact to near to adapt to miss"]
Expand Down
1 change: 1 addition & 0 deletions mintlify/guides/team-memory.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ property-tested to be commutative, associative, and idempotent — so two teamma
ledgers converge to the same state no matter who syncs first.

```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%%
flowchart LR
A["your ledger"] <-->|"git union-merge · conflict-free"| B["teammate ledger"]
A --> M["merged read view"]
Expand Down
1 change: 1 addition & 0 deletions mintlify/guides/zero-config-onboarding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ paying off on day two. (It is low-configuration, not zero-configuration: you sti
the CLI, run `forge init` in each repo, and some paths assume Bash, Git, and `jq`.)

```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#201a15','primaryTextColor':'#f2ede7','primaryBorderColor':'#372c22','lineColor':'#f26430','secondaryColor':'#272019','tertiaryColor':'#171310','edgeLabelBackground':'#201a15','clusterBkg':'#171310','clusterBorder':'#4a3b2e','fontFamily':'ui-sans-serif, system-ui, sans-serif','fontSize':'14px'},'flowchart':{'curve':'basis','padding':10,'nodeSpacing':36,'rankSpacing':44}}}%%
flowchart TD
I["forge init"] --> Cfg["every tool configured from one source"]
Cfg --> Work["you work as usual"]
Expand Down
52 changes: 49 additions & 3 deletions src/atlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,18 @@ export function impactLLM(atlas, target, { run = buildRunner() } = {}) {
});
}

/**
* Build a file → SCC-id index from the output of rank.cycles(). Files in the
* same SCC share an id; files not in any cycle are absent from the map.
* @param {string[][]} sccs each entry is a sorted list of files in one SCC
* @returns {Map<string, number>}
*/
export function buildSccIndex(sccs) {
const index = new Map();
for (let i = 0; i < sccs.length; i++) for (const file of sccs[i]) index.set(file, i);
return index;
}

/**
* @param {object} atlas
* @param {string} target
Expand All @@ -660,11 +672,13 @@ export function impactLLM(atlas, target, { run = buildRunner() } = {}) {
* @param {boolean} [opts.llm]
* @param {(p:string)=>string} [opts.run]
* @param {(file:string, target:string)=>boolean} [opts.verify]
* @param {Map<string, number>} [opts.sccIndex] file-to-SCC-id (from buildSccIndex)
* @param {Map<string, number>} [opts.hazards] file-to-hazard-score (from rankReport)
*/
export function impact(
atlas,
target,
{ threshold = 0.1, maxHops = 6, decay = 0.85, llm, run, verify } = {},
{ threshold = 0.1, maxHops = 6, decay = 0.85, llm, run, verify, sccIndex, hazards } = {},
) {
const starts = targetIds(atlas, target);
const startSet = new Set(starts);
Expand All @@ -691,12 +705,18 @@ export function impact(
if (startSet.has(edge.source)) continue;
const nextConfidence =
current.confidence * (EDGE_WEIGHT[edge.kind] || 0.5) * (edge.confidence ?? 1) * decay;
if (nextConfidence < threshold) continue;
const srcNode = nodeById.get(edge.source);
const srcFile = srcNode?.file;
const effectiveThreshold =
hazards && srcFile && hazards.has(srcFile)
? threshold / (1 + hazards.get(srcFile))
: threshold;
if (nextConfidence < effectiveThreshold) continue;
const prev = visited.get(edge.source);
if (prev && prev.confidence >= nextConfidence) continue;
const item = {
id: edge.source,
node: nodeById.get(edge.source) || {
node: srcNode || {
id: edge.source,
name: edge.source,
kind: "unknown",
Expand All @@ -714,6 +734,32 @@ export function impact(
path: item.path,
edgeKinds: item.edgeKinds,
});
if (sccIndex && srcFile != null && sccIndex.has(srcFile)) {
const sccId = sccIndex.get(srcFile);
for (const node of atlas.nodes || []) {
if (node.file === srcFile || !sccIndex.has(node.file)) continue;
if (sccIndex.get(node.file) !== sccId) continue;
if (startSet.has(node.id)) continue;
const prevScc = visited.get(node.id);
if (prevScc && prevScc.confidence >= nextConfidence) continue;
const sccItem = {
id: node.id,
node,
confidence: Number(nextConfidence.toFixed(4)),
hopDistance: current.hop + 1,
path: [...current.path, edge.source, node.id],
edgeKinds: [...current.edgeKinds, edge.kind, "scc"],
};
visited.set(node.id, sccItem);
queue.push({
id: node.id,
confidence: nextConfidence,
hop: current.hop + 1,
path: sccItem.path,
edgeKinds: sccItem.edgeKinds,
});
}
}
}
}
const impacted = [...visited.values()].sort((a, b) => b.confidence - a.confidence);
Expand Down
9 changes: 5 additions & 4 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -1686,21 +1686,22 @@ HANDLERS.preflight = async (argv) => {
HANDLERS.impact = async (argv) => {
const { predictImpact } = await import("./substrate.js");
const json = argv.includes("--json");
const basic = argv.includes("--basic");
const target = argv
.slice(1)
.filter((a) => a !== "--json")
.filter((a) => a !== "--json" && a !== "--basic")
.join(" ");
if (!target) {
console.error("usage: forge impact <symbol|file> [--json]");
console.error("usage: forge impact <symbol|file> [--json] [--basic]");
process.exitCode = 1;
return;
}
const r = predictImpact(process.cwd(), target);
const r = predictImpact(process.cwd(), target, { basic });
if (json) {
console.log(JSON.stringify(r, null, 2));
return;
}
heading(`${BRAND.brand} impact — blast radius\n`);
heading(`${BRAND.brand} impact — blast radius${basic ? "" : " (hazard-aware)"}\n`);
console.log(` target: ${target} ${r.found ? "✓ found" : "not found"}`);
console.log(` impacted files: ${r.impactedFiles.length}`);
for (const file of r.impactedFiles.slice(0, 20)) console.log(` - ${file}`);
Expand Down
18 changes: 17 additions & 1 deletion src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,23 @@ export const COMMANDS = {
preflight: "assumption check — what a task names that the repo doesn't define",
config: "provider setup — show / switch / add providers, set default model",
route: "recommend the cheapest capable model for a task (+ gateway config)",
impact: "predict blast radius for a symbol or file from the atlas graph",
impact: {
summary:
"hazard-aware blast radius — SCC-aware propagation + data-driven threshold from PageRank centrality and ledger incident history",
usage: "forge impact <symbol|file> [--json] [--basic]",
flags: [
{ flag: "--json", desc: "machine-readable report" },
{
flag: "--basic",
desc: "skip hazard-aware enhancements (fixed threshold, no SCC expansion)",
},
],
examples: [
"forge impact src/atlas.js",
"forge impact computeTax --json",
"forge impact src/val.js --basic",
],
},
collide: {
summary:
"parallel-session conflict radar — who else recently touched the files (or their import neighbors) you are editing",
Expand Down
9 changes: 7 additions & 2 deletions src/docs_check.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,17 @@ function checkMcpTools(docs, issues) {
/** Every tracked Markdown file, so diagram checks cover the WHOLE doc set — not just the
* four prose docs. Falls back to a recursive walk when git is unavailable (tmp fixtures). */
function markdownFiles(root) {
const tracked = git(root, ["ls-files", "*.md"]);
const tracked = git(root, ["ls-files", "*.md", "*.mdx"]);
if (tracked) return tracked.split("\n").filter(Boolean);
if (!existsSync(root)) return [];
return readdirSync(root, { recursive: true })
.map(String)
.filter((f) => f.endsWith(".md") && !f.includes("node_modules") && !f.startsWith(".git/"));
.filter(
(f) =>
(f.endsWith(".md") || f.endsWith(".mdx")) &&
!f.includes("node_modules") &&
!f.startsWith(".git/"),
);
}

// The branded Mermaid theme every diagram shares (see README's `%%{init …}%%`). Without it
Expand Down
Loading
Loading