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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`forge docs impact` — a reusable documentation-impact graph.** Where `docs check`
reconciles a fixed list of registries and `docs sync` scans a diff for raw identifiers,
`docs impact` answers the general question the project kept forgetting: _"I changed X —
which documented surfaces mention X and are now potentially stale?"_ It works in three
data-driven stages: (1) a pluggable extractor registry derives the **typed** entities
the project documents (command names, CLI flags, `FORGE_*` env vars, MCP tool names,
exported symbols, brand tokens, the version, `package.json` fields) from their canonical
sources — reusing `docs_check`'s `envVarsRead`/`srcFiles` and the `COMMANDS`/`TOOLS`
registries; (2) a word-boundary- and code-fence-aware scan of every doc surface (all
tracked `*.md`, `CITATION.cff`, the plugin manifests, the landing page, `package.json`)
builds an inverted index entity → `file:line`; (3) an impact query maps the entities a
git diff changed to every doc location that references them, ranked by confidence.
Advisory by default (`--strict` exits non-zero for CI); `--since <ref>`, `--staged`,
`--min-confidence <n>`, and `--json` flags; `docs check` prints a one-line advisory
pointing here when the working tree touched a documented entity. New `src/docs_impact.js`.

## [0.23.2] - 2026-07-20

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ that never clobbers your existing settings (skip it with `install.sh --no-settin
| | `forge tools` | primary-tool config — gitignore secondary-tool artifacts (`.cursor/`, `.gemini/`, …) for tools this repo doesn't use; `forge tools <name>` sets it, `--reset` clears |
| | `forge doctor` | pass/fail health check: tools, guards, MCP, drift, update |
| | `forge update` | self-update — `--check` reports if a newer version exists, bare applies it, `--to <version>` pins/downgrades |
| | `forge docs` | docs↔code drift — `check` reconciles commands/env/MCP/CHANGELOG; `sync` sweeps the diff for stale doc mentions |
| | `forge docs` | docs↔code drift — `check` reconciles commands/env/MCP/CHANGELOG; `sync` sweeps the diff for stale doc mentions; `impact` maps which docs reference the entities THIS diff changed |
| | `forge config` | provider setup — show / switch / add providers, set the default model |
| | `forge integrations` | opt-in third-party MCP servers (e.g. context7) — `add` records the managed set and writes only with `--yes` (`--adopt` claims a same-name entry you already had); `remove` reverses it |
| | `forge harden` | wire the pre-commit gate (gitleaks + commit gate) + sandbox settings |
Expand Down
41 changes: 41 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,46 @@ touched in the same diff still answers for **removed** symbols (updated-for-one-
isn't updated-for-the-rename); and `.forge/state.md` is never scanned — handoff writes
the changed-file list into it by design, so the gate's mtime check covers it instead.

### `forge docs impact` — change X, which docs mention X?

`docs sync` sweeps a diff for raw identifiers; `docs impact` is the general, reusable
version of the same idea, built as a **documentation-reference graph** rather than a
per-file rule. It works in three data-driven stages:

1. **Entity extraction.** A pluggable extractor registry derives the _typed_ set of
things the project documents — command names, CLI flags, `FORGE_*` env vars, MCP tool
names, exported symbols, brand tokens, the version, and `package.json` fields — from
their canonical sources (the same `COMMANDS`/`TOOLS` registries and `envVarsRead`
scanner `docs check` already uses). Adding a new entity type is one record in the
registry, not a new special case.
2. **Reference index.** A word-boundary- and code-fence-aware scan of every doc surface
(all tracked `*.md`, `CITATION.cff`, the plugin manifests, the landing page,
`package.json`) builds an inverted index: entity → every `file:line` that names it.
English-word-shaped entities (bare symbols like `build`) only count inside backticks
or fenced blocks, so prose doesn't false-positive.
3. **Impact query.** From a git diff of the canonical sources it computes which entities
_changed_ (a command renamed, a flag added, an env var removed, an export touched, the
version bumped) and returns every doc location that references them, ranked by
confidence.

```console
$ forge docs impact --since main
docs impact — diff vs main: 2 changed entities, 1 doc file(s) potentially stale
env `APP_CACHE_MODE` (REMOVED) — 2 reference(s):
README.md:88 (code, 0.95) [Environment] Set `APP_CACHE_MODE` to tune the cache.
version `0.23.0` (REMOVED) — 1 reference(s):
README.md:3 (prose, 0.75) install @scope/pkg@0.23.0
→ review each surface: update it, or confirm the mention is still correct.
```

Advisory by default (matching the fail-open ethos) — `--strict` exits non-zero when any
doc surface is impacted, for CI. `--since <ref>` sets the diff base (default: the session
baseline, then `HEAD`), `--staged` reads the index, `--min-confidence <n>` drops low-signal
hits, `--json` for tooling. `docs check` also prints a one-line advisory pointing here when
the working tree touched a documented entity. **Honest limits:** a token index catches a
doc that _names_ a changed entity; it cannot see a paraphrase that never uses the name, a
screenshot or diagram, or a design/wording choice with no textual anchor.

### `forge verify` — did it actually work?

The independent check: runs the real test suite and flags edited symbols that aren't in
Expand Down Expand Up @@ -997,6 +1037,7 @@ Plain `forge cost` remains the per-day spend view via `ccusage`.
| `forge doctor` | Health check: layers, install, drift, cortex. `forge doctor --fix` auto-repairs the safely fixable findings (missing settings hooks/permissions, ledger union-merge rule, stale `AGENTS.md`, non-executable guards) by reusing the same idempotent functions `forge init`/`forge sync` use, then re-checks. |
| `forge docs check` | Docs↔code drift: commands, env vars, MCP tools, CHANGELOG reconciled against the code (CI-gated on the forge repo itself). |
| `forge docs sync` | Diff-driven stale-docs sweep: UPDATED / STALE (file:line hits) / VERIFIED-UNAFFECTED per artifact (see the full section above). |
| `forge docs impact` | Documentation-reference graph: extract typed entities (commands/flags/env/MCP/symbols/version), index every doc surface, and report which docs reference the entities THIS diff changed — ranked by confidence (see the full section above). |
| `forge catalog` | Start-Here index of every tool / crew / guard. |
| `forge integrations` | Opt-in third-party MCP servers (e.g. `context7`, no longer installed by default). `integrations add <name>` prints the package, its network behaviour, and the files it touches, then writes only with `--yes`; the install is recorded in `.forge/forge.config.json` (`mcp.integrations`), so every later sync keeps emitting it. A same-name server you configured yourself is never overwritten unless you pass `--adopt` (recorded under `mcp.adopted`). `integrations remove <name>` reverses the add — it deletes only forge-owned entries/blocks/files and is a no-op when nothing is installed. |
| `forge brain` / `forge remember` | Portable project memory inlined into `AGENTS.md`. |
Expand Down
26 changes: 26 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,22 @@ async function run(argv) {
if (r.error || (argv.includes("--strict") && r.stale.length)) process.exitCode = 1;
return;
}
// `impact` — reusable documentation-impact graph: which documented surfaces
// reference the entities THIS diff changed (advisory; --strict exits 1).
if (sub === "impact") {
const { docsImpact, renderDocsImpact } = await import("./docs_impact.js");
const { newestBaseline } = await import("./docs_sync.js");
const staged = argv.includes("--staged");
const sinceIdx = argv.indexOf("--since");
const base = sinceIdx >= 0 ? argv[sinceIdx + 1] : newestBaseline(process.cwd()) || undefined;
const mcIdx = argv.indexOf("--min-confidence");
const minConfidence = mcIdx >= 0 ? Number(argv[mcIdx + 1]) : 0;
const r = docsImpact(process.cwd(), { base, staged, minConfidence });
if (json) console.log(JSON.stringify(r, null, 2));
else console.log(renderDocsImpact(r));
if (argv.includes("--strict") && r.impacted.length) process.exitCode = 1;
return;
}
// `check` — self-check of the forge package's own docs against its code (commands
// table, env reads, MCP registry, CHANGELOG).
const { docsCheck } = await import("./docs_check.js");
Expand All @@ -489,6 +505,16 @@ async function run(argv) {
console.log(`\n${r.issues.filter((i) => i.severity === "error").length} problem(s)`);
process.exitCode = 1;
}
// Advisory: if the working tree changed a documented entity, point at the impacted
// prose. Never fails the check (fail-open); best-effort, so a git-less tree is silent.
try {
const { docsImpact } = await import("./docs_impact.js");
const imp = docsImpact(process.cwd());
if (imp.impacted.length)
console.log(
`\n ! ${imp.summary.impactedFiles} doc file(s) may be stale from this diff — run \`${BRAND.cli} docs impact\` to review`,
);
} catch {}
return;
}
if (cmd === "integrations") {
Expand Down
30 changes: 29 additions & 1 deletion src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,35 @@ export const COMMANDS = {
dash: "live dashboard: ledger, metrics trends, radar, memory browser, timeline, blast radius",
report: "emit a static, self-contained HTML snapshot of .forge/ — opens offline, no server",
brand: "print the active brand token map",
docs: "docs↔code drift — check (registry reconcile) / sync (diff-driven stale-docs sweep)",
docs: {
summary:
"docs↔code drift — check (registry reconcile) / sync (diff-driven stale-docs sweep) / impact (reusable doc-reference graph: which docs mention what THIS diff changed)",
usage: "forge docs [check | sync | impact] [--since <ref> | --staged] [--strict] [--json]",
flags: [
{
flag: "--since <ref>",
desc: "impact: diff against this git ref (default: session baseline, then HEAD)",
},
{
flag: "--staged",
desc: "impact/sync: diff the staged index instead of the working tree",
},
{
flag: "--min-confidence <n>",
desc: "impact: drop reference hits below this confidence (0..1)",
},
{
flag: "--strict",
desc: "exit non-zero when stale/impacted docs are found (for CI; advisory otherwise)",
},
],
examples: [
"forge docs check",
"forge docs sync",
"forge docs impact",
"forge docs impact --since main",
],
},
integrations:
"opt-in third-party MCP servers (e.g. context7) — add records the managed set and writes only with --yes (--adopt claims a same-name entry); remove reverses it",
};
Expand Down
4 changes: 3 additions & 1 deletion src/docs_check.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ function readDoc(root, rel) {
return existsSync(p) ? readFileSync(p, "utf8") : "";
}

function srcFiles(root) {
// Exported so docs_impact.js reuses the SAME src-file inventory (recursive, .js only)
// the env-var extractor scans — one definition of "what counts as a source file".
export function srcFiles(root) {
const dir = join(root, "src");
if (!existsSync(dir)) return [];
// recursive: src/emit/*.js reads are part of the same env contract.
Expand Down
Loading
Loading