Sign In

@ghostlygawd/codeweb

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ghostlygawd/codeweb - npm Package Compare versions

Comparing version
0.10.0
to
0.11.0
+17
scripts/stamp-screens.mjs
// Stamp assets/screens/.template-stamp with the hash of the report template.
// The brand-sync gate (tests/brand-sync.test.mjs) compares the two: when the template
// changes, the committed screenshots are presumed stale until they are re-shot (or
// consciously re-verified against the change) and this script is re-run. The stamp is a
// review checkpoint, not a pixel proof — it exists so a template change can never ship
// with old screenshots silently.
import { createHash } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const hash = createHash('sha256')
.update(readFileSync(join(ROOT, 'scripts', 'report-template.html')))
.digest('hex');
writeFileSync(join(ROOT, 'assets', 'screens', '.template-stamp'), hash + '\n');
console.log('stamped assets/screens/.template-stamp = ' + hash.slice(0, 12) + '…');
+3
-3

@@ -8,4 +8,4 @@ {

"metadata": {
"description": "codeweb — the living map of your codebase. Deterministic call/import graph, 27 read-only MCP tools, ambient pre/post-edit hooks, and a self-contained interactive report.",
"version": "1.0.0"
"description": "codeweb — your agents break less code and burn fewer tokens. Deterministic call/import graph, 27 MCP tools, ambient pre/post-edit hooks, and a self-contained interactive report.",
"version": "0.11.0"
},

@@ -16,5 +16,5 @@ "plugins": [

"source": "./",
"description": "Map any repo in seconds: /codeweb builds the graph; hooks brief every session and impact-check every edit; 27 deterministic MCP tools answer impact, callers, duplication, dead code, and risk before the agent writes."
"description": "Your agents break less code and burn fewer tokens. /codeweb maps any repo in seconds. Hooks brief every session and impact-check every edit. 27 deterministic MCP tools answer impact, callers, duplication, dead code, and risk before your agents write."
}
]
}
{
"name": "codeweb",
"version": "0.10.0",
"description": "Dissect a codebase to its atomic parts (functions, classes, symbols), wire them into a living system web (call/import graph), tag each node's domain, and surface cross-domain overlap and consolidation opportunities. Renders a self-contained interactive HTML map for humans, and exposes 27 deterministic read-only query tools over MCP (impact, callers, duplication, risk, dead code, …) so a coding agent can check what already exists and what an edit breaks before writing. Works on the current project or any external repo you want to review before adopting.",
"version": "0.11.0",
"description": "Your agents break less code and burn fewer tokens. codeweb maps the repo into a deterministic call/import graph. Your agents get 27 MCP tools: impact, callers, duplication, risk, dead code, and more. /codeweb builds the map; hooks brief every session and impact-check every edit. You get an interactive HTML map of it all. Also maps any repo you want to review before adopting.",
"author": {

@@ -6,0 +6,0 @@ "name": "GhostlyGawd",

#!/usr/bin/env node
// codeweb-diff bin — old-syntax Node guard (see bin/codeweb.mjs), then the diff gate CLI: the
// quickstart path to the regression verdict (exit 1 on a new cycle/duplication/lost callers).
// quickstart path to the regression verdict (exit 1 on a new cycle, new duplication, or a
// non-exported symbol losing every caller — the orphan-gate semantics).
var major = parseInt(process.versions.node.split('.')[0], 10);
if (major < 22) {
console.error('codeweb needs Node >= 22 (you have ' + process.version + '). Install the current LTS: https://nodejs.org');
process.exit(1);
process.exit(2); // setup error — never 1, which codeweb-diff reserves for "regression found" (API F2)
}
import('../scripts/diff.mjs');

@@ -7,5 +7,5 @@ #!/usr/bin/env node

console.error('codeweb-mcp needs Node >= 22 (you have ' + process.version + '). Install the current LTS: https://nodejs.org');
process.exit(1);
process.exit(2); // setup error — never 1, which codeweb-diff reserves for "regression found" (API F2)
}
process.env.CODEWEB_BIN = '1';
import('../scripts/mcp-server.mjs');

@@ -7,4 +7,4 @@ #!/usr/bin/env node

console.error('codeweb needs Node >= 22 (you have ' + process.version + '). Install the current LTS: https://nodejs.org');
process.exit(1);
process.exit(2); // setup error — never 1, which codeweb-diff reserves for "regression found" (API F2)
}
import('../scripts/query.mjs');

@@ -9,4 +9,4 @@ #!/usr/bin/env node

console.error('codeweb needs Node >= 22 (you have ' + process.version + '). Install the current LTS: https://nodejs.org');
process.exit(1);
process.exit(2); // setup error — never 1, which codeweb-diff reserves for "regression found" (API F2)
}
import('../scripts/run.mjs');

@@ -14,3 +14,3 @@ {

],
"description": "codeweb: when the session starts in a .codeweb-mapped repo, inject the ~2KB day-one briefing (areas, load-bearing symbols, entry points, tests, known issues) so the agent starts oriented instead of exploring (fail-open; inert until /codeweb has mapped the repo)",
"description": "codeweb: when the session starts in a .codeweb-mapped repo, inject the ~2KB day-one briefing (domains, load-bearing symbols, entry points, tests, known issues) so the agent starts oriented instead of exploring (fail-open; inert until /codeweb has mapped the repo)",
"id": "codeweb:session-brief"

@@ -17,0 +17,0 @@ }

@@ -122,3 +122,4 @@ #!/usr/bin/env node

for (const id of out.lostCallers) lines.push(` ${id} lost all callers (now uncalled)`);
lines.push(' -> run `node scripts/diff.mjs` or re-run /codeweb for the full delta.');
lines.push(' (call-caller preflight — stricter than the CI gate, which exempts exported symbols)');
lines.push(' -> full verdict: codeweb_diff (agents) or node scripts/diff.mjs; /codeweb re-maps.');
return lines.join('\n');

@@ -125,0 +126,0 @@ }

@@ -133,4 +133,8 @@ #!/usr/bin/env node

try {
// API.md F10: this hook is ADVISORY — context only. permissionDecision:'allow' silently
// auto-approved edits to mapped load-bearing files, overriding whatever permission flow the
// user configured; no doc claimed that power. The card now ships alone and the host's own
// permission decision stands.
process.stdout.write(JSON.stringify({
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', additionalContext: msg },
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg },
}) + '\n');

@@ -137,0 +141,0 @@ } catch { /* ignore */ }

@@ -8,3 +8,4 @@ #!/usr/bin/env node

import { readFileSync, existsSync } from 'node:fs';
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';

@@ -16,3 +17,3 @@ import { dirname, join, resolve } from 'node:path';

import { bump, attachActivity } from '../scripts/lib/stats.mjs';
import { checkStaleness } from '../scripts/lib/cli.mjs'; // R3: the change-based nudge
import { checkStaleness, SRC_RE } from '../scripts/lib/cli.mjs'; // R3: the change-based nudge
import { loadStaleStamps } from '../scripts/lib/stale-stamps.mjs'; // R3: stamps without the graph parse

@@ -35,2 +36,25 @@ import { readHistory } from '../scripts/lib/history.mjs'; // R1/R8: the progression line

// Returns the briefing text for a SessionStart payload, or null (unmapped / unreadable).
// COMPREHENSION #3: the marketplace promises "hooks brief every session", but on an unmapped
// repo this hook exited with zero output — the first session after install taught "the plugin
// doesn't work". One line, once per workspace (home-dir stamp keyed by cwd), only when the cwd
// actually has source to map; the quiet-by-default posture survives. Fail-open everywhere.
function unmappedNudge(cwd) {
try {
const entries = readdirSync(cwd).slice(0, 200);
const hasSource = entries.some((f) => SRC_RE.test(f))
|| entries.some((d) => ['src', 'lib', 'app'].includes(d)
&& (() => { try { return readdirSync(join(cwd, d)).slice(0, 100).some((f) => SRC_RE.test(f)); } catch { return false; } })());
if (!hasSource) return null;
const stampPath = join(homedir(), '.codeweb', 'nudged.json');
let doc = null; try { doc = JSON.parse(readFileSync(stampPath, 'utf8')); } catch { /* first nudge */ }
const dirs = (doc && doc.dirs) || {};
if (dirs[cwd]) return null;
dirs[cwd] = new Date().toISOString();
const keys = Object.keys(dirs);
if (keys.length > 100) for (const k of keys.slice(0, keys.length - 100)) delete dirs[k];
try { mkdirSync(join(homedir(), '.codeweb'), { recursive: true }); writeFileSync(stampPath, JSON.stringify({ dirs })); } catch { /* stamp is best-effort */ }
return "[codeweb] this repo isn't mapped yet — run /codeweb (or codeweb_map) to turn on briefs and impact cards.";
} catch { return null; }
}
export function preview(raw) {

@@ -40,3 +64,3 @@ let input; try { input = JSON.parse(raw); } catch { input = {}; }

const graphPath = findGraph(cwd);
if (!graphPath) return null;
if (!graphPath) return unmappedNudge(cwd);
// finding 23: the brief is a pure function of the graph — the report stage pre-rendered it, so

@@ -43,0 +67,0 @@ // the common path is stat + one small read instead of parse + index of the whole graph (97ms on

{
"name": "@ghostlygawd/codeweb",
"version": "0.10.0",
"version": "0.11.0",
"type": "module",
"description": "See what an edit breaks before you write it — deterministic call/import graph of your codebase, 27 MCP tools for coding agents, and a self-contained interactive map. Claude Code plugin & MCP server; zero deps, runs 100% locally.",
"description": "Your agents break less code and burn fewer tokens. codeweb maps your repo into a deterministic call/import graph; 27 MCP tools answer impact, callers, and duplication before agents write. Claude Code plugin & MCP server; zero deps, runs 100% locally.",
"keywords": [

@@ -7,0 +7,0 @@ "mcp",

+135
-467
<div align="center">
<img src="assets/brand/hero.svg" alt="codeweb — the living map of your codebase" width="840">
<img src="assets/brand/banner.png" alt="codeweb — your coding agents grep. codeweb knows." width="100%">
[![CI](https://github.com/GhostlyGawd/codeweb/actions/workflows/ci.yml/badge.svg)](https://github.com/GhostlyGawd/codeweb/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/%40ghostlygawd%2Fcodeweb?style=flat-square&color=c6f24e)](https://www.npmjs.com/package/@ghostlygawd/codeweb)
[![license: MIT](https://img.shields.io/npm/l/%40ghostlygawd%2Fcodeweb?style=flat-square&color=3fb950)](LICENSE)
[![deterministic engine](https://img.shields.io/badge/engine-deterministic-c6f24e?style=flat-square)](#how-it-works)
[![MCP server](https://img.shields.io/badge/MCP-server-a371f7?style=flat-square)](#use-it-as-an-mcp-tool)
[![sponsor](https://img.shields.io/badge/%E2%99%A5-sponsor-ea4aaa?style=flat-square)](https://github.com/sponsors/GhostlyGawd)
[![license: MIT](https://img.shields.io/npm/l/%40ghostlygawd%2Fcodeweb?style=flat-square&color=8a8794)](LICENSE)
**Your coding agent greps. codeweb knows.**
**Free & MIT-licensed. Runs entirely on your machine — no account, no server, no telemetry. Reads your code; never executes it.**
<br><sub>DETERMINISTIC · READ-ONLY · ZERO-DEPENDENCY</sub>
Every serious change starts with the same questions: *who uses this? what breaks if I change it?
does this already exist? is this dead?* Today an agent answers them by grepping and reading whole
files — thousands of tokens per question, and it still guesses. codeweb maps the repo's call/import
graph once (~3 s for 3,000 symbols), then answers those questions **exactly, in milliseconds, for
about a kilobyte each** — as **27 deterministic MCP tools** (MCP is the open protocol coding agents
like Claude Code, Cursor, and Windsurf use to call tools; no LLM in codeweb's loop) and
a self-contained **interactive map for you**.
**[Website](https://ghostlygawd.github.io/codeweb/)**&nbsp;·&nbsp;[See it in action](#see-it-in-action)&nbsp;·&nbsp;[Install](#install)&nbsp;·&nbsp;[Use](#use)&nbsp;·&nbsp;[For agents (MCP)](#use-it-as-an-mcp-tool)&nbsp;·&nbsp;[How it works](#how-it-works)&nbsp;·&nbsp;[Changelog](CHANGELOG.md)
Measured on [vite](https://github.com/vitejs/vite) (3,000+ symbols), graded by the TypeScript
compiler as an independent referee ([`bench/results/oracle-ab.json`](bench/results/oracle-ab.json)):
</div>
| The question | codeweb | grep |
|---|---|---|
| *"Who depends on X?"* (30 symbols) | **100% of compiler-verified files, better precision than grep, 0.7 KB, one call** | 100% of files but 3× the tokens, as raw text lines the agent must still read |
| *"What breaks if I change X?"* | **one ~1 KB answer** | no transitive operator: ~5 recursive rounds, **126× the tokens** |
| *"Does this already exist? Is this dead? Did my edit break structure?"* | one call each (`find_similar` / `deadcode` / `diff` gate) | not answerable by search |
**Your agents break less code and burn fewer tokens.**
Don't take vite's word for it — **run the same referee on your own repo**:
`npm run bench -- <path>/.codeweb/graph.json` (context cost always; recall/precision graded by the
TypeScript compiler wherever `typescript` resolves — same engine as the published results).
codeweb reads your code and maps it: every function, and every call between them
(~3 s for 3,000 symbols). It's static analysis — no LLM — so the same code always produces
the same map.
In the frontier-agent A/B on v0.9.0's budgeted responses — the replies agents actually receive —
codeweb lifted caller-discovery recall **+0.31 at equal token cost** vs grep (all 5 engine-frozen
reps positive). And the byproduct is the part you can
see: the map also surfaces **duplication, dead code, hotspots, and tangled domains** — where your
codebase does the same work twice, which neither you nor the agent can see from inside a file.
Your coding agents query the map instead of grepping. With grep, agents miss more than half of
a function's real callers ([measured](https://ghostlygawd.github.io/codeweb/research.html)).
They break the code they can't see.
**[Website](https://ghostlygawd.github.io/codeweb/)**&nbsp;·&nbsp;[See it in action](#see-it-in-action)&nbsp;·&nbsp;[Install](#install)&nbsp;·&nbsp;[Use](#use)&nbsp;·&nbsp;[For agents (MCP)](#use-it-as-an-mcp-tool)&nbsp;·&nbsp;[How it works](#how-it-works)&nbsp;·&nbsp;[Changelog](CHANGELOG.md)
- **For agents:** an MCP server with 27 tools — `codeweb_impact`, `codeweb_callers`,
`codeweb_find_similar`, and 24 more.
- **Answers:** exact, instant, tiny. Your agents keep their context for the real work.
- **For you:** the supporting view — an interactive map of the whole codebase.
</div>
The map also shows things you can't see from inside one file: **duplicated logic, dead code,
hotspots, and tangled domains**.
---
## Try it on your repo
```
cd your-project
npx -y @ghostlygawd/codeweb .
```
Three seconds for 3,000 symbols. Open `.codeweb/report.html` — that's your map.
## See it in action
One command runs the whole deterministic pipeline and drops an interactive map at
`<target>/.codeweb/report.html`. **Every screenshot below is that actual generated report**, codeweb
pointed read-only at **[axios](https://github.com/axios/axios)** — 274 product symbols across 8
areas (tests and tooling hidden by default). No mockups; regenerate them any time with
`node scripts/screenshot.mjs`.
Every screenshot below is a real generated report of **axios** (274 symbols, 8 domains).
No mockups.
> **▶ Read the full [axios case study](docs/case-study-axios.md):** on a library downloaded ~50M
> times a week, codeweb body-confirmed **3 real duplications** (two byte-identical across files),
> dismissed 12 false positives, and produced a cycle-safe merge plan for each. Or **[click around
> this exact map yourself](https://ghostlygawd.github.io/codeweb/demo/)** — it's live on GitHub Pages.
codeweb found 3 real duplications in axios and dismissed 12 false positives —
[the case study](docs/case-study-axios.md). Or
[click around the live map](https://ghostlygawd.github.io/codeweb/demo/).
### Know what an edit breaks — before you write
That's the whole point. Ask *if I change this function, what else moves?* — and codeweb answers from
structure, not a guess. Click any node in the [living map](https://ghostlygawd.github.io/codeweb/) and
its **blast radius** lights up: every function transitively affected, and the domains it crosses. It's
the `codeweb_impact` tool — the same answer an agent gets over MCP, before it writes a line.
Click any function in the [living map](https://ghostlygawd.github.io/codeweb/) and its
**blast radius** lights up: everything your change would touch.
Your agents get the same answer over MCP (`codeweb_impact`) — before they write a line.
<div align="center">
<img src="assets/screens/06-blast-radius.png" alt="codeweb blast radius: AxiosError selected in the axios graph — its area expanded in place, 58 users listed in the inspector, cross-area dependencies lit, neighboring areas highlighted" width="760">
<br><sub>Selecting <code>AxiosError</code> in axios lights up its <b>58 users across the areas that depend on it</b> — try it yourself in the <a href="https://ghostlygawd.github.io/codeweb/">living map</a>.</sub>
<img src="assets/screens/axios-blast-radius.png" alt="codeweb blast radius: AxiosError selected in the axios graph — the selected block wears the accent with a viewfinder frame, blast edges lit across three domains, 27 callers listed in the inspector" width="760">
<br><sub>Selecting <code>AxiosError</code> in axios lights up its <b>27 callers across the domains that depend on it</b> — try it yourself in the <a href="https://ghostlygawd.github.io/codeweb/">living map</a>.</sub>
</div>

@@ -80,3 +70,3 @@

<img src="assets/screens/05-axios-graph.png" alt="codeweb Graph tab on axios: a force-directed domain map (adapters, helpers, core, cancel, defaults, platform) on a dark canvas" width="100%">
<img src="assets/screens/axios-graph.png" alt="codeweb Graph tab on axios: eight domain blocks (helpers, core, adapters, cancel, defaults, platform) sized by symbol count and linked by stippled call edges" width="100%">

@@ -89,5 +79,5 @@ ### Findings — stop guessing what to refactor

<img src="assets/screens/05-axios-findings.png" alt="codeweb Findings tab on axios: ranked duplication, hotspots, and likely-dead code, with a clickable detail panel" width="100%">
<img src="assets/screens/axios-findings.png" alt="codeweb Findings tab on axios: ranked duplication, hotspots, and likely-dead code, with a clickable detail panel" width="100%">
### See duplication density, and where areas tangle
### See duplication density, and where domains tangle

@@ -97,10 +87,10 @@ <table>

<td width="50%" valign="top">
<img src="assets/screens/05-axios-treemap.png" alt="codeweb Treemap on axios: every file sized by lines of code, duplication density carried by a slate-to-red lightness ramp">
<br><b>Treemap</b> — every file sized by lines of code; the brighter red a block, the more of it
<img src="assets/screens/axios-treemap.png" alt="codeweb Treemap on axios: every file sized by lines of code, duplication density carried by a dark-to-lime lightness ramp">
<br><b>Treemap</b> — every file sized by lines of code; the brighter a block, the more of it
is duplicated. The bright blocks are your consolidation targets, at a glance.
</td>
<td width="50%" valign="top">
<img src="assets/screens/05-axios-matrix.png" alt="codeweb Matrix on axios: a heatmap of call coupling between domains">
<br><b>Matrix</b> — area-to-area coupling. A big off-diagonal cell means two areas are tangled:
merge them, or put a clean interface between them.
<img src="assets/screens/axios-matrix.png" alt="codeweb Matrix on axios: a heatmap of call coupling between domains">
<br><b>Matrix</b> — domain-to-domain coupling. A big off-diagonal cell means two domains are
tangled: merge them, or put a clean interface between them.
</td>

@@ -117,43 +107,29 @@ </tr>

codeweb is the missing **atomic-analysis + overlap-detective** layer. Where `repo-scan`
classifies *files* and flags duplicate *modules*, and `codebase-onboarding` writes high-level
architecture docs, codeweb works at **symbol resolution**: functions, classes, and methods, the
call/import edges between them, the semantic domain each belongs to, and the cross-domain
overlap graph.
codeweb works at **symbol resolution** — functions, classes, and methods, and the call/import
edges between them. File-level scanners can tell you two *modules* look alike; codeweb tells you
two *functions* are the same work, who calls each, and what merging them would break.
## Proven effective — measured, not just claimed
## Benchmarks
We didn't only assert codeweb works; we **pre-registered hypotheses and measured it**, applying the
same rigor codeweb brings to code: independent oracles, a pinned cross-language corpus, confidence
intervals, and adversarial review. **32 of 33 pre-registered checks pass**
([the full check-by-check receipt](bench/preregistration.md), with the frozen registration preserved
at tag `v0.8.0` for timestamp proof) — and the testing was rigorous enough to **find and fix two real
bugs** the engine's own 286-test suite had missed.
- **Finding callers before an edit** — agents found **74%** of a function's real callers with
codeweb, **44%** with grep, at the same context spend. Missed callers are how edits break
working code.
- **"What breaks if I change this?"** — one codeweb call, one small answer. Agents grepping
for the same answer needed ~5 rounds of search and **126× the tokens**, and still guessed.
- **Duplicate detection** — codeweb caught **every planted duplicate with zero false alarms**,
including renamed copies. Text search catches renamed copies 0% of the time.
- **Trust the answers** — checked against the TypeScript compiler and other independent
implementations **490,000+ times: zero disagreements**.
- **Speed** — first map in **~3 s** on a 3,000-symbol repo. Queries answer in **~0.1 s**.
A repo twice the size maps in ~1.3× the time.
- **Known limits** — re-mapping after huge edits is slower than we'd like. On simple tasks,
agents did fine without codeweb.
- **Correctness held against independent oracles** — **zero observed disagreements across >490,000
comparisons** (cycles, impact, callers/callees, context-pack); 0 violations over 20,000 edit-safety trials.
- **Detection is accurate** — exact-clone **F1 1.0** (vs 0.67 name-match), renamed-clone recall **1.0
structural vs 0.0 lexical**, reuse-ranking **MRR 0.99**.
- **It scales** — runtime grows **sub-quadratically** (sub-linear in this corpus, b=0.33); structural
queries answer in **~95–120 ms** on a 3,201-symbol graph; zero required dependencies.
- **It's honest** — the one pass/fail miss (incremental speedup at high churn) is reported as a measured
curve; the agent A/B capstone returned a null (no headroom on clean tasks) and says so plainly.
- **And it measurably helps a frontier agent** — the v0.9.0 discovery pilot, run on the budgeted
responses agents actually receive, found codeweb lifts caller-discovery **recall +0.31 ± 0.04 at
equal token cost** vs grep (all 5 engine-frozen reps positive; precision +0.23 ± 0.08 —
[`bench/experiments/efficiency-pilot.reps5-v090.json`](bench/experiments/efficiency-pilot.reps5-v090.json)).
An earlier 8-rep run on a different base model also showed ~34% fewer tool-calls and ~44% fewer
tokens; those savings **did not replicate** on the current frugal base agent, and we report that
rather than quoting the better number. The harder edit-quality capstone stays an honest null.
Methodology, raw data, and per-claim receipts:
[the evidence ledger](https://ghostlygawd.github.io/codeweb/research.html). Benchmark your own
repo: `npm run bench -- <path>/.codeweb/graph.json`. CI re-runs the performance budgets on
every PR; breaking a published number fails the build.
> **▶ Every number above is a receipt — see the [evidence ledger](https://ghostlygawd.github.io/codeweb/research.html).**
> The benchmark harnesses and raw results live in [`bench/`](bench/); every number regenerates with
> `node bench/run-all.mjs`, and `npm run bench:all -- --gate` re-measures the standing budgets
> **in CI on every PR** — a change that breaks a published number fails the build
> ([`bench/budgets.json`](bench/budgets.json) is the promise ledger). (The retired manuscript and
> pre-registration remain in git history, last at `v0.8.0`.)
codeweb also keeps a local tally of what it actually did for you — `npm run stats`:
And the value codeweb delivers during real work is counted where it accrues — the strictly-local
outcome ledger (`npm run stats`, surfaced in every session brief) prints a receipt shaped like:
```

@@ -163,17 +139,16 @@ codeweb this month: 41 pre-edit card(s) · 5 card-named caller(s) followed · 2 regression(s) flagged · 120 queries served

## Two modes
Considering a dependency? Point codeweb at any repo you don't own yet
(`/codeweb https://github.com/owner/repo`): it clones *read-only*, maps it, and appends an
adoption review. codeweb never executes target code.
- **Internal** — map your own codebase and find consolidation opportunities to restructure.
- **External** — clone a third-party repo *read-only* (e.g. a Claude Code plugin you found on
GitHub), fully map it, and get an adoption review before you commit to using it. codeweb
never executes target code.
## Install
**Free & MIT-licensed. Runs entirely on your machine — no account, no server, no telemetry. Reads
your code; never executes it.** Zero required dependencies — it runs on an empty `node_modules`
(CI-verified); you need **Node.js ≥ 22**. One *optional* wasm grammar (`web-tree-sitter`) sharpens
extraction when present and is never required. Releases are published from CI with **npm provenance**
— verify any install with `npm audit signatures`.
your code; never executes it.**
- Requires **Node.js ≥ 22**. That's it.
- Zero required dependencies — runs on an empty `node_modules`, CI-verified.
- One *optional* wasm grammar (`web-tree-sitter`) sharpens extraction. Never required.
- Releases publish from CI with **npm provenance**. Verify with `npm audit signatures`.
**Using Claude Code?** The plugin adds the `/codeweb` command, ambient pre-edit impact cards, and

@@ -204,27 +179,26 @@ all 27 tools:

git clone https://github.com/GhostlyGawd/codeweb.git
node codeweb/scripts/run.mjs /path/to/your/project # map lands in /path/to/your/project/.codeweb
# kick the tires on bundled sample code first (no stakes, ~2s):
node codeweb/scripts/run.mjs codeweb/bench/corpus/flask --out-dir /tmp/flask-map
node codeweb/scripts/run.mjs /path/to/your/project
```
Requires **Node.js ≥ 22** — the whole deterministic pipeline (extract → cluster → overlap → render)
runs on Node, no external dependencies. Static-analysis tools (universal-ctags, ripgrep, madge,
etc.) are *optional* — they only sharpen the agent fallback path; the default engine reads the code
directly.
Every bin, flag, and exit code is tabled in [`docs/cli.md`](docs/cli.md).
**In your editor:** [`editor/vscode-codeweb`](editor/vscode-codeweb/) is a zero-dependency VS Code
extension that shows **`N callers · blast M`** CodeLens above every mapped symbol (served from the
nearest `.codeweb/graph.json`, same numbers as `codeweb_callers`/`codeweb_impact`), with
click-through into the interactive report.
**In your editor:** [`editor/vscode-codeweb`](editor/vscode-codeweb/) shows a
**`N callers · blast M`** lens above every mapped symbol. Click through into the report.
## What you can do — three jobs
## What you can do
Everything below serves one of three jobs. Skim for yours; each section carries the full flags.
Each link lands on full docs, flags, and examples in **[the reference](docs/reference.md)**.
- **Know before you edit** — who calls this, what breaks, does this already exist: `impact`,
`context-pack`, `find`, `find-similar`, and the ambient pre-edit card.
- **Gate every edit** — the structural regression verdict on edits, PRs, and architecture rules:
`diff`, `ci-gate`, `review`, `fitness`, and the post-edit hook.
- **Clean up, ranked** — consolidation and dead-code work ordered by evidence: `optimize`,
`deadcode`, `hotspots`, `campaign`, `trend`.
- **Know before you edit** — who calls this, what breaks, does this already exist.
→ [Query the graph](docs/reference.md#query-the-graph-for-agents--humans) ·
[context & pre-flight](docs/reference.md#agent-tools--context--pre-flight-context-pack-simulate-edit)
- **Gate every edit** — a structural regression verdict on edits, PRs, and architecture rules.
→ [The `diff` verdict](docs/reference.md#guard-agent-edits-diff) ·
[the PR gate](docs/reference.md#gate-every-pr-github-action) ·
[the capability suite](docs/reference.md#agent-capability-suite-write--review--optimize)
- **Clean up, ranked** — consolidation and dead-code work, ordered by evidence.
→ [`optimize`](docs/reference.md#advise-consolidations-optimizemjs) ·
[`hotspots`](docs/reference.md#find-the-hotspots--where-to-refactor-first-hotspotsmjs) ·
[`campaign`](docs/reference.md#plan-a-whole-optimization-campaign-campaignmjs) ·
[`trend`](docs/reference.md#track-duplication-over-time-trendmjs)

@@ -243,272 +217,27 @@ ## Use

## Outputs (under `<target>/.codeweb/`)
Everything lands in `<target>/.codeweb/` — `graph.json` for machines, `report.html` for you,
markdown twins for both. [Every output file, explained →](docs/reference.md#outputs-under-targetcodeweb)
| File | What it is |
|---|---|
| `graph.json` | The machine-readable web: `nodes`, `edges`, `domains`, `overlaps`, plus `meta` (target root, engine, languages, stats). |
| `report.html` | Self-contained interactive map — force-directed graph, domain tree, clickable node details, ranked overlap tab. No network/CDN required. |
| `report.md` | The same map as plain markdown — domains, top nodes, ranked overlaps. |
| `overlap.md` | The ranked consolidation opportunities in plain markdown. |
| `optimize.md` | The consolidation advisory — duplicate-logic findings tiered **ready / blocked / review**, each pre-flighted against the gate's cycle check (the `optimize.mjs` report). |
| `fragment.json` | The raw extractor output (atomic nodes + edges) before clustering — the pipeline's first stage. |
## Query the graph (for agents & humans)
Once `graph.json` exists, `scripts/query.mjs` answers the structural questions an agent needs
before it edits — read-only, deterministic, no LLM in the loop:
```
node scripts/query.mjs <graph.json> --impact <symbol> # blast radius: transitive callers + domains touched
node scripts/query.mjs <graph.json> --callers <symbol> # direct callers
node scripts/query.mjs <graph.json> --callees <symbol> # direct callees
node scripts/query.mjs <graph.json> --cycles # file-level dependency cycles (SCCs)
node scripts/query.mjs <graph.json> --orphans # uncalled & unexported (dead-code candidates)
```
`<symbol>` is a node id (`file:label`) or a bare label (a label matching several nodes operates on
the union, reported in `matched`). Add `--json` for stable, machine-readable output. Exit codes:
`0` success (even when empty), `1` symbol not found, `2` usage/IO error. Example — *"what could I
break if I change the state store?"*:
```
$ node scripts/query.mjs .codeweb/graph.json --impact lib/state-store/index.js:get
impact of lib/state-store/index.js:get: 120 functions across 12 domains
```
> `--orphans` is a *candidate* list: extraction deliberately drops ambiguous call edges (precision
> over recall), so genuinely-called functions and entrypoints can surface — cross-check before deleting.
## Guard agent edits (`diff`)
`scripts/diff.mjs` compares two `graph.json` snapshots (before vs after an edit) and flags
structural **regressions**, so it can run as a PostToolUse hook or CI gate:
```
node scripts/diff.mjs <before.json> <after.json> [--json]
```
It reports nodes/edges/overlaps/cycles/orphans added & removed plus the cross-domain coupling
delta, and **exits 1** (listing `regressions`) when an edit introduces a new dependency cycle, a
new duplication finding, or makes an existing symbol lose all its callers. It **exits 0** for pure
removals — deleting code/cycles/dups is an improvement, not a regression — and a brand-new uncalled
node is reported but does not trip the gate (agents add functions before wiring them).
## Gate every PR (GitHub Action)
`scripts/ci-gate.mjs` turns the `diff` gate into CI: it builds the graph for the PR base and head and
**fails the build on a structural regression** (a new cycle, a new duplication, or a symbol that
loses all its callers). Drop it into any repo (full spec: [`docs/ci-gate.md`](docs/ci-gate.md)):
```yaml
# .github/workflows/codeweb-gate.yml
on: pull_request
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # required — the gate diffs against the PR base
- uses: GhostlyGawd/codeweb/.github/actions/codeweb-gate@main
with: { target: src, comment: true } # comment posts the structural review on the PR
```
Locally: `node scripts/ci-gate.mjs --base <ref> [--target <subdir>]`. Pure removals never trip the
gate; a brand-new uncalled function is reported but doesn't fail the build.
## Advise consolidations (`optimize.mjs`)
Where `diff.mjs` *gates* (pass/fail on an edit), `optimize.mjs` *advises*: it reads a graph's
body-confirmed `overlaps[]` and ranks the `duplicate-logic` findings into consolidation
opportunities, **pre-flighting each proposed merge against the gate's own cycle check** — without
editing a line of source.
```
node scripts/optimize.mjs <graph.json> [--json] # or set CODEWEB_WS
```
Each opportunity is tiered: **ready** (body-confirmed ≥60%, not drifted, and the simulated merge
stays acyclic → the gate would pass, duplication −1), **blocked** (the naive merge would introduce a
new file cycle → the gate would reject it; needs a neutral home), or **review** (drifted copies,
merely-structural confidence, or non-`duplicate-logic` findings — human/agent judgement required).
Low/refuted findings are excluded outright. It picks a canonical survivor (most-called, tie-broken by
LOC then id) and reports the copies removed, callers rewired, blast radius, and LOC reclaimed. It is
**advisory only** — it never writes code and never exits non-zero on a clean read; the merge stays a
human + gate decision.
## Track duplication over time (`trend.mjs`)
A one-shot map tells you where you are; `trend.mjs` tells you which way you're heading — is the
codebase consolidating or sprawling? It charts **body-confirmed duplication** and **cross-domain
coupling** across snapshots, with a sparkline and a rising/falling verdict:
```
node scripts/trend.mjs --git <repo> --last 10 [--focus <subdir>] [--json] # snapshot the last N commits
node scripts/trend.mjs a.json b.json c.json [--labels …] [--json] # or chart pre-built snapshots
```
The `--git` mode checks out each of the last N commits into an **ephemeral worktree** (read-only
over your working tree), runs the deterministic pipeline, and records the metrics — so you can watch
duplication trend down as you consolidate, or catch it creeping up in review.
## Find the hotspots — where to refactor first (`hotspots.mjs`)
In a large repo the first question is *where do I even start?* `hotspots.mjs` answers it with the
**complexity × fan-in × churn** model — the riskiest, most-depended-on, most-churned symbols rank
first. Cyclomatic complexity and max nesting depth are computed during the body scan (every
`function`/`method` node carries `complexity` and `maxDepth`), so this needs no extra tooling; churn
is optional (`--git`, or `--churn <map.json>`).
```
$ node scripts/hotspots.mjs <graph.json>
codeweb hotspots: axios/lib — 253 symbol(s) ranked by complexity x fan-in x churn
weights: complexity 0.5, fanIn 0.3, churn 0.2
0.533 adapters/fetch.js:factory [cx 147 in 1 churn 0]
0.347 adapters/http.js:httpAdapter [cx 102 in 0 churn 0]
0.312 core/mergeConfig.js:mergeConfig [cx 33 in 6 churn 0]
0.270 helpers/toFormData.js:toFormData [cx 50 in 3 churn 0]
```
Every row shows its raw components, so the ranking is auditable rather than a black box. Add `--json`
for machine output; also surfaced as the `codeweb_hotspots` MCP tool.
## Plan a whole optimization campaign (`campaign.mjs`)
`optimize` (ready merges), `deadcode` (safe deletes), and `break-cycles` (verified cuts) are three
separate advisors. `campaign.mjs` composes them into **one ordered, individually-gated, ROI-ranked
worklist** with cumulative projected deltas — "auto-optimize this codebase, at any scale." Crucially,
every step is pre-flighted so that applying the steps **in order** never introduces a cycle that
wasn't there before: a safe campaign is safe as a *sequence*, not merely per step. It is a read-only
plan — codeweb never writes source; the agent (+ the gate) executes each step.
```
$ node scripts/campaign.mjs <graph.json>
codeweb campaign: axios/lib — 80 step(s): 2 cut, 77 delete, 1 merge
projected: -12 LOC, 2 cycle(s) broken (all steps stay gate-green in order)
[DELETE] adapters/fetch.js:duplex (roi 0; +0 LOC, +0 cycle; cumulative -0 LOC)
…each of 80 steps tagged [CUT|DELETE|MERGE] with its own gate verdict + cumulative delta
```
`--budget N` keeps the top-N ROI prefix; `--json` emits per-step `{op, gate:{ok}, delta, cumulative,
roi}`. Also surfaced as `codeweb_campaign`.
## Onboard in dependency order (`reading-order.mjs`)
To understand a codebase — or one domain — fast, `reading-order.mjs` emits a **foundations-first**
reading path: the depended-upon leaves before the orchestrators that call them, bounded to a budget.
A curated tour instead of blind grep.
```
$ node scripts/reading-order.mjs <graph.json> --budget 6
codeweb reading-order: 6 symbol(s) — read top-down (foundations first):
1. core/AxiosError.js:AxiosError
foundation — 18 in-scope caller(s)
2. cancel/CanceledError.js:CanceledError
foundation — 5 in-scope caller(s)
```
Scope it with `--scope domain|file|symbol <value>`; cycles degrade gracefully (members ordered by
fan-in, never a crash). Deterministic and read-only; also the `codeweb_reading_order` MCP tool.
## Measured coverage — "is this symbol actually tested?" (`coverage.mjs`)
`codeweb_tests` answers from test-kind call edges (a heuristic). Feed codeweb a real coverage
report and the answers become **measured**:
```
node --test --experimental-test-coverage --test-reporter=lcov > lcov.info # Node's own runner
node scripts/coverage.mjs .codeweb/graph.json lcov.info # or a c8/istanbul JSON
```
Every instrumented symbol gets `covered`/`hits` facts, and `explain`, `--tests`, and
`context-pack` answers say `covered by the recorded run (peak N hits)` or — the loud one —
`⚠ NOT covered by the recorded test run` before an agent edits an unguarded symbol. Optional and
explicit (absent input leaves graphs byte-identical); `codeweb_refresh` drops stale annotations
and says how to restore them.
## Agent tools — context & pre-flight (`context-pack`, `simulate-edit`)
Two read-only tools that move work off the LLM and into the graph (full spec:
[`docs/agent-tools.md`](docs/agent-tools.md)):
```
node scripts/context-pack.mjs <graph.json> <symbol> [--json] # minimal context to edit <symbol>
node scripts/simulate-edit.mjs <graph.json> --delete <sym> | --merge <a,b> [--into <id>] | --move <sym> --to <file>
```
`context-pack` returns the **blast-radius-scoped** context for a symbol — its body, the direct
callers (call sites, with body), the direct callees (location-only), and the transitive impact set
(ids only) — so an agent edits with a small window instead of reading whole files. `simulate-edit`
predicts the regression gate's **structural verdict** (`{newCycles, lostCallers, ok}`) for a
hypothetical delete/merge/move **without performing it**, so doomed edits are discarded cheaply. Both
share the pure `applyEdit` primitive in `graph-ops.mjs` with `optimize.mjs` (one truth), and are
covered by property tests that pin the tool's output to an independent oracle.
## Agent capability suite (write · review · optimize)
A set of read-only, deterministic tools that make an agent better at the three jobs — each pinned by
property tests against an independent oracle (full spec: [`docs/agent-tools-v2.md`](docs/agent-tools-v2.md)):
| Tool | Job | What it answers |
|---|---|---|
| `find-similar.mjs <graph> --body/--stdin/--signature [--structural]` | **write** | "Does code like this already exist?" — ranks existing bodies by token-shingle similarity (or, with `--structural`, by identifier-normalized *skeleton* similarity, catching renamed/Type-2 clones), so the agent reuses instead of re-implementing. |
| `placement.mjs <graph> --calls <ids>` | **write** | Where a new symbol belongs (domain + file by callee gravity) and whether it duplicates something. |
| `query.mjs <graph> --tests <symbol>` | **write** | The tests that exercise a symbol — run the right subset after an edit. |
| `review.mjs <graph> --changed <files> [--before g] [--gate]` | **review** | Maps a change to its changed symbols, blast radius, domains, and a fan-in-ranked review order; structural regression gate. |
| `fitness.mjs <graph> --rules codeweb.rules.json` | **review** | Checks architectural invariants (forbidden deps, layering, no-cycles, fan-in/loc caps); fails on violation. |
| `risk.mjs <graph> [--changed] [--churn/--git]` | **review** | Ranks symbols by change-risk (fan-in × fan-out × loc × blast × churn) for triage. |
| `codemod.mjs <graph> --merge <ids> --into <id> [--write]` | **optimize** | Plans a consolidation merge (deletions + caller rewrites + projected gate); `--write` applies it, gated + reversible. |
| `break-cycles.mjs <graph>` | **optimize** | For each dependency cycle, the cheapest edge to sever — *verified* to break it. |
| `deadcode.mjs <graph>` | **optimize** | Tiers orphans into safe-to-delete vs review-first (test-guarded / entrypoint-like). |
| `annotate.mjs --suppress <fingerprint> [--note …]` | **review** | Records a false-positive suppression in `.codeweb/annotations.json` (never touches source); `overlap`/`deadcode` then hide that finding and report a `suppressedCount`. Fingerprints are identity-based, so a genuinely *new* issue can't hide behind an old suppression. |
Plus **graph freshness**: `extract-symbols.mjs --cache <path>` re-scans only changed files **and
reuses per-file edges** (incremental edge derivation, guarded by a global symbol-set signature;
`--full` forces a from-scratch rebuild that is byte-identical to the incremental one), and
`refresh.mjs <graph>` re-extracts a graph's nodes+edges from disk so mid-edit queries stay accurate.
Nodes now carry a `signature` (params/returns) and, for functions/methods, `complexity` + `maxDepth`;
edges from test files are a distinct `test` kind (so production `--callers` exclude tests). All of the
above are also exposed over MCP (below).
## Use it as an MCP tool
`scripts/mcp-server.mjs` is a zero-dependency MCP (Model Context Protocol) stdio server exposing all
**27** of codeweb's queries + the capability suite as tools any MCP client can call mid-task:
`codeweb_map` (build/rebuild the graph over MCP), `codeweb_brief` (the day-one repo page —
call it first), `codeweb_find` (concept search — free text like
*"retry backoff"* ranked into starting symbols, no name needed), `codeweb_callers/callees/impact/
cycles/orphans/diff`, the edit-loop tools `codeweb_context/refresh`, the intelligence tools
`codeweb_hotspots/campaign/reading_order`, the pre-flight + hygiene loop
`codeweb_simulate` (the gate's verdict for a hypothetical delete/merge/move — before any edit),
`codeweb_annotate` (false-positive suppression memory, sidecar-only), and `codeweb_stats` (the
local value receipt), plus `codeweb_tests/find_similar/placement/review/
fitness/risk/break_cycles/deadcode/codemod` (the last is plan-only — `--write` is not exposed).
`scripts/mcp-server.mjs` is a zero-dependency MCP (Model Context Protocol) stdio server. It gives
any MCP client all **27 tools**, grouped by moment: orient, read the structure, check before
writing, gate the edit, clean up.
**Installing the plugin registers the server automatically** (`.claude-plugin/plugin.json` carries
the `mcpServers` entry). Standalone — without the plugin — register it from npm (or a clone):
**Installing the plugin registers the server automatically.** Standalone:
```
claude mcp add codeweb -- npx -y -p @ghostlygawd/codeweb codeweb-mcp
claude mcp add codeweb -- node /abs/path/to/codeweb/scripts/mcp-server.mjs # clone variant
```
or in an `.mcp.json`:
Built for agents, not just reachable by them:
```json
{ "mcpServers": { "codeweb": { "command": "node", "args": ["/abs/path/to/codeweb/scripts/mcp-server.mjs"] } } }
```
- **`graph` is optional everywhere** — the server finds the nearest map on its own. No map yet?
The error names `codeweb_map`, which builds one.
- **Budgeted responses** — top items and true totals. A context answer that weighed ~300 KB now
weighs ~10 KB.
- **Staleness awareness** — stale results say so and point at `codeweb_refresh`.
Built for agents, not just reachable by them:
[All 27 tools, grouped and explained →](docs/reference.md#the-mcp-server-tool-by-tool)
- **`graph` is optional everywhere** — the server resolves the nearest `.codeweb/graph.json` above
its cwd (or `CODEWEB_WS`). No graph yet? The error names `codeweb_map`, which builds one (~3s for
a 3k-symbol repo) without leaving MCP.
- **Budgeted responses by default** — list-heavy tools answer with a one-line `summary`, the top-N
most relevant items, TRUE totals, and an explicit `more.remaining`; `full: true` (or
`limit`/`offset`) overrides. A `codeweb_context` that used to weigh ~300KB on a busy symbol now
answers in ~10KB of call-site windows.
- **Staleness awareness** — when the graph no longer matches disk, query results say so and point
at `codeweb_refresh`.
- The handshake carries `instructions` teaching the loop: *context → edit → refresh → diff-gate*.
## How it works

@@ -525,6 +254,5 @@

1. **Extract** (`extract-symbols.mjs`) — parse every source file into atomic nodes (functions,
classes, methods) and call/import edges. Unresolved bare calls only wire to a global
definition when the name is unambiguous; multi-def names drop the edge rather than fabricate a
false hub. Each function/method node also gets a `signature`, cyclomatic `complexity`, and
`maxDepth`; edges are cached per file (incremental, byte-identical to a full rebuild) so refreshes scale.
classes, methods) and call/import edges. When a bare call could belong to several definitions,
codeweb drops the edge rather than guess. Per-file caching keeps re-extraction incremental,
byte-identical to a full rebuild.
2. **Cluster** (`cluster3.mjs`) — strip genuine utility hubs, then group nodes into

@@ -539,65 +267,11 @@ directory-anchored semantic domains.

For languages the extractor can't parse (or when the deterministic engine is skipped entirely), codeweb **falls back** to the
agent path: parallel `codeweb-dissector` agents extract nodes + edges per subsystem, the
fragments merge into one graph by node id, and `codeweb-domain-mapper` tags domains and detects
overlaps. Both paths emit the same `graph.json` schema, so clustering, overlap, and rendering are
shared. In **external** mode, either path appends an adoption verdict (risk, deps, architecture).
For languages the extractor can't parse, codeweb **falls back** to the agent path:
`codeweb-dissector` agents extract nodes and edges per subsystem, and `codeweb-domain-mapper`
tags domains and overlaps.
## Components
Both paths emit the same `graph.json` schema. In **external** mode, either path appends an
adoption verdict.
```
codeweb/
├── .claude-plugin/plugin.json
├── commands/codeweb.md # /codeweb trigger
├── scripts/ # the deterministic engine (default fast path)
│ ├── run.mjs # orchestrator — one command, runs all stages per target
│ ├── extract-symbols.mjs # stage 1: source -> atomic nodes + edges (JS/TS/Python/Rust/Go)
│ ├── cluster3.mjs # stage 2: hub-strip + directory-anchored domains
│ ├── overlap.mjs # stage 3: body-confirmed duplication/overlap detection
│ ├── build-report.mjs # stage 4: graph.json -> interactive report.html + report.md
│ ├── report-template.html # the renderer's self-contained HTML shell
│ ├── query.mjs # structural queries (callers/callees/tests/impact/cycles/orphans)
│ ├── diff.mjs # graph-delta / post-edit regression gate (before vs after)
│ ├── trend.mjs # duplication + coupling over snapshots / git history (dashboard)
│ ├── ci-gate.mjs # CI gate: before(base)-vs-after(working tree) diff, exits 1 on regression
│ ├── optimize.mjs # advise: rank body-confirmed dups into gated consolidation opportunities
│ ├── context-pack.mjs # agent context: blast-radius-scoped window to edit a symbol
│ ├── simulate-edit.mjs # agent pre-flight: predict the gate's verdict for delete/merge/move
│ ├── refresh.mjs # F0: re-extract a graph's nodes+edges from disk (cached, fast)
│ ├── find-similar.mjs # F1: rank existing bodies vs a candidate (reuse-at-write-time)
│ ├── placement.mjs # F2: suggest a new symbol's domain/file + reuse warnings
│ ├── review.mjs # F5: structural review of a change (blast radius, regressions)
│ ├── fitness.mjs # F6: architectural fitness-rule checker
│ ├── risk.mjs # F7: change-risk ranking for review triage
│ ├── codemod.mjs # F8: consolidation edit plan (+ gated/reversible --write)
│ ├── deadcode.mjs # F10: confidence-tiered dead-code workflow
│ ├── break-cycles.mjs # F9: cheapest verified cut per dependency cycle
│ ├── hotspots.mjs # rank symbols by complexity x fan-in x churn (where to refactor first)
│ ├── campaign.mjs # compose optimize+deadcode+break-cycles into one gated ROI worklist
│ ├── reading-order.mjs # foundations-first reading path for onboarding (bounded by budget)
│ ├── annotate.mjs # record false-positive suppressions in .codeweb/annotations.json
│ ├── mcp-server.mjs # MCP stdio server exposing all queries + the capability suite
│ └── lib/
│ ├── graph-ops.mjs # shared pure graph primitives (index, cycles, orphans, impact, reviewImpact, …)
│ ├── shingles.mjs # F1: shared token-shingle/jaccard (also used by overlap.mjs)
│ ├── skeleton.mjs # identifier-normalized skeleton for Type-2 (renamed) clone detection
│ ├── complexity.mjs # cyclomatic complexity + nesting depth (the hotspot inputs)
│ ├── dup-check.mjs # incremental duplication check over changed symbols (edit gate)
│ ├── annotations.mjs # finding fingerprints + false-positive suppression memory
│ ├── hotspots.mjs # the complexity x fan-in x churn blend (shared with tests)
│ ├── campaign.mjs # the ordered/gated/ROI campaign planner (pure)
│ ├── reading-order.mjs # foundations-first DAG linearization
│ └── risk.mjs # F7: the change-risk formula + weights (one truth)
├── agents/ # fallback path (unparseable langs / no deterministic engine)
│ ├── codeweb-dissector.md # atomic dissection (parallel, read-only)
│ └── codeweb-domain-mapper.md # domain tagging + overlap detection
├── skills/codebase-anatomy/
│ ├── SKILL.md # orchestration brain (fast path default, agents fallback)
│ └── references/
│ ├── graph-schema.md
│ ├── overlap-heuristics.md
│ └── engine-detection.md
├── assets/ # brand art (logo, hero, animated demo) + report screenshots
└── README.md
```
Curious how the repo is laid out? [The component map lives in the
reference.](docs/reference.md#components)

@@ -612,8 +286,6 @@ ## Roadmap

_Recently shipped: an **agent-intelligence suite** — refactoring **hotspots** (complexity × fan-in ×
churn), a gated ROI-ranked optimization **campaign** planner, a foundations-first **reading-order**,
**Type-2 (renamed) clone** detection, false-positive **suppression memory**, and a growing MCP
surface (**27 tools today**) · a **[live interactive demo](https://ghostlygawd.github.io/codeweb/demo/)** on GitHub
Pages · Go and Rust on the fast path · duplication-over-time trend (`trend.mjs`) · a one-command CI
regression gate + GitHub Action._
_Recently shipped: the agent-intelligence suite (**hotspots**, **campaign**, **reading-order**,
Type-2 clone detection, suppression memory — 27 tools today) · a
**[live interactive demo](https://ghostlygawd.github.io/codeweb/demo/)** · Go and Rust on the fast
path · duplication-over-time trend · the one-command CI regression gate + GitHub Action._

@@ -623,7 +295,6 @@ ## Versioning & releases

codeweb follows [Semantic Versioning](https://semver.org/) and keeps a
[Keep a Changelog](https://keepachangelog.com/)-formatted [`CHANGELOG.md`](CHANGELOG.md). Every new
capability, benchmark, or fix is recorded there and shipped as a **tagged GitHub release** — product,
marketing, and research move as one front, never lost in commit history.
[Keep a Changelog](https://keepachangelog.com/)-formatted [`CHANGELOG.md`](CHANGELOG.md). Every
capability, benchmark, and fix is recorded there and ships as a **tagged GitHub release**.
One source of truth keeps it honest. The version lives in `package.json`; the MCP tool count lives in
One source of truth keeps it honest: the version lives in `package.json`, the MCP tool count in
`scripts/mcp-server.mjs`. Everything else is derived and verified:

@@ -638,8 +309,5 @@

`check-consistency` runs in CI, applying codeweb's own "fail on regression" philosophy to its public
comms. What it actually gates: version strings across every surface, the derived MCP tool count
(including prose mentions of tool and language counts in the README, the site, the skill, and the
npm description), the CHANGELOG entry for the current version, and that every evidence file the
ledger cites exists on disk. Prose it can't reach (the already-published npm page) is fixed at the
next release, which re-runs the same gate.
`check-consistency` runs in CI. It gates version strings on every surface, every prose mention of
the tool and language counts, the CHANGELOG entry for the current version, and every evidence
file the ledger cites.

@@ -660,15 +328,15 @@ ## About

[Sponsorship](https://github.com/sponsors/GhostlyGawd) funds development — mainly the AI bills
from benchmarking, and new language support. Details on the
[Sponsoring](https://github.com/sponsors/GhostlyGawd) supports the project — and it's
advertising: top sponsors get their logo at the top of this README, and every sponsor joins
the supporters list beneath it. Details on the
[support page](https://ghostlygawd.github.io/codeweb/support.html).
**Enterprise support**: email support with an SLA, onboarding help, and priority on feature
requests. **$3–6k/yr**, limited to a few customers. Contact via the GitHub profile.
Running codeweb at an org and want help? Email via the GitHub profile.
## Handoffs
If you have them installed, codeweb's domain map and overlap list feed naturally into
`refactor-cleaner` (act on the consolidation list), `codebase-onboarding` (use the domain map for
a guide), and `code-tour` (anchor a tour to the symbol index). None are required — without them,
the ideal second step is simply: apply the top **ready** merge from `optimize.md`, re-run
codeweb's outputs feed naturally into `refactor-cleaner`, `codebase-onboarding`, and `code-tour`,
if you have them. None are required.
The ideal second step either way: apply the top **ready** merge from `optimize.md`, re-run
codeweb, and watch the findings count drop.

@@ -10,9 +10,10 @@ #!/usr/bin/env node

// node annotate.mjs --list [--dir <.codeweb>] [--json]
// Exit: 0 ok, 2 usage.
// (or set CODEWEB_WS, or run from a mapped repo)
// Exit: 0 ok, 2 usage / no mapped workspace.
import { resolve } from 'node:path';
import { resolve, join, dirname } from 'node:path';
import { addSuppression, loadAnnotations } from './lib/annotations.mjs';
const USAGE = 'usage: annotate.mjs (--suppress <fingerprint> [--note "..."] | --list) [--dir <.codeweb>] [--json]';
import { die, emitJson, finish, parseArgs } from './lib/cli.mjs';
const USAGE = 'usage: annotate.mjs (--suppress <fingerprint> [--note "..."] | --list) [--dir <.codeweb>] [--json] (or set CODEWEB_WS, or run from a mapped repo)';
import { die, emitJson, finish, findTarget, parseArgs } from './lib/cli.mjs';

@@ -27,8 +28,24 @@ // finding 24: THE flag loop (lib/cli.mjs parseArgs) — one unknown-flag policy, --help included.

note: { type: 'string', default: '' },
dir: { type: 'string', default: '.codeweb' },
dir: { type: 'string', default: null },
},
});
const { json, list, note, dir } = opts, fp = opts.suppress;
const annDir = resolve(dir);
// API F7 (+ ERRORS.md #8): the ONLY sidecar-mutating CLI defaulted --dir to ./.codeweb RELATIVE
// TO CWD and mkdir -p'd it — a suppression written from the wrong directory landed in a fresh,
// orphaned .codeweb (with a success message) that no tool would ever read: annotations are read
// beside the GRAPH. The default now follows the one graph-addressing contract (CODEWEB_WS ->
// nearest .codeweb above cwd) and ERRORS instead of creating, when unmapped.
let annDir;
if (dir != null) annDir = resolve(dir);
else if (process.env.CODEWEB_WS) annDir = resolve(process.env.CODEWEB_WS);
else {
const near = findTarget(join(process.cwd(), 'x')); // findTarget walks up from a FILE's dir; anchor so the walk starts AT cwd
if (!near) {
die(`no mapped workspace found — checked CODEWEB_WS and every .codeweb above ${process.cwd()}. A suppression written to an unmapped directory would never be read: run from the mapped repo, pass --dir <target>/.codeweb, or set CODEWEB_WS.\n${USAGE}`, 2);
}
annDir = dirname(near.baseline);
console.error(`[codeweb] using ${annDir} (nearest .codeweb above cwd)`);
}
if (list) {

@@ -35,0 +52,0 @@ const ann = loadAnnotations(annDir);

@@ -13,3 +13,3 @@ #!/usr/bin/env node

const USAGE = 'usage: break-cycles.mjs <graph.json> [--limit N] [--json] (or set CODEWEB_WS)';
const USAGE = 'usage: break-cycles.mjs <graph.json> [--limit N] [--offset N] [--json] (or set CODEWEB_WS)';
import { emitJson, finish, loadGraph, capList, parseArgs } from './lib/cli.mjs';

@@ -20,5 +20,9 @@

usage: USAGE,
flags: { json: { type: 'bool', default: false }, limit: { type: 'number', default: null } },
flags: {
json: { type: 'bool', default: false },
limit: { type: 'number', default: null, min: 0 }, // API F3: one pagination dialect (limit/offset)
offset: { type: 'number', default: 0, min: 0 },
},
});
const { json, limit } = opts;
const { json, limit, offset } = opts;
const { graph } = loadGraph(pos[0], { usage: USAGE });

@@ -28,5 +32,6 @@

const capped = capList(cycles, limit);
// API F3: `count` stays the true total, `more` carries nextOffset so the remainder is reachable.
const capped = capList(cycles, limit, offset);
const payload = { target: graph.meta?.target || 'target', summary: `${cycles.length} file dependency cycle(s), ${cycles.filter((c) => c.verified).length} with a verified cheapest cut`, count: cycles.length, cycles: capped.items };
if (capped.truncated) payload.more = { remaining: capped.remaining };
if (capped.truncated) payload.more = { remaining: capped.remaining, nextOffset: capped.offset + capped.items.length };
if (json) { emitJson(payload); } else {

@@ -33,0 +38,0 @@

@@ -20,3 +20,3 @@ #!/usr/bin/env node

import { fileURLToPath } from 'node:url';
import { execFileSync, spawnSync } from 'node:child_process';
import { spawnSync } from 'node:child_process';
import { gateComment } from './lib/gate-md.mjs';

@@ -45,3 +45,10 @@ import { die, parseArgs } from './lib/cli.mjs';

const buildGraph = (srcDir, label, ws) => {
execFileSync(node, [join(HERE, 'run.mjs'), srcDir, '--target', label, '--out-dir', ws], { stdio: 'ignore' });
// ERRORS.md #3: stdio:'ignore' reduced every pipeline failure to "gate error: Command failed:
// <argv>" — the child's own diagnosis (wrong root, no source, node version) never surfaced.
// Capture and forward the stderr tail; a build failure is a SETUP error (exit 2), never a verdict.
const r = spawnSync(node, [join(HERE, 'run.mjs'), srcDir, '--target', label, '--out-dir', ws], { encoding: 'utf8', maxBuffer: 1 << 26 });
if (r.status !== 0) {
const tail = (r.stderr || '').trim().split('\n').slice(-8).join('\n');
throw new Error(`graph build failed for "${label}" (exit ${r.status}):\n${tail || '(no stderr captured)'}`);
}
return join(ws, 'graph.json');

@@ -48,0 +55,0 @@ };

@@ -8,5 +8,5 @@ #!/usr/bin/env node

// (it never re-introduces the byName guessing the extractor refuses). Built on ./lib/graph-ops.mjs
// (shares applyEdit / structuralRegressions / chooseCanonical — one truth with simulate-edit/optimize).
// (shares applyEdit / gateVerdict / chooseCanonical — one truth with simulate-edit/optimize).
//
// Usage: node codemod.mjs <graph.json> (--opportunity <ovId> | --merge <ids> --into <id>) [--json] [--write]
// Usage: node codemod.mjs [graph.json] (--opportunity <ovId> | --merge <ids> --into <id>) [--json] [--write] (or set CODEWEB_WS, or run from a mapped repo)
// Exit: 0 ok, 1 predicted/actual regression (no net change), 2 usage/IO/ambiguous.

@@ -18,7 +18,7 @@

import { fileURLToPath } from 'node:url';
import { normalizeGraph, buildIndex, callersOf, importersOf, impactOf, applyEdit, structuralRegressions, chooseCanonical, resolveSymbol } from './lib/graph-ops.mjs';
import { normalizeGraph, buildIndex, callersOf, importersOf, impactOf, applyEdit, structuralRegressions, chooseCanonical, resolveSymbol, gateVerdict } from './lib/graph-ops.mjs';
import { maskAligned } from './lib/masking.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const USAGE = 'usage: codemod.mjs <graph.json> (--opportunity <ovId> | --merge <ids> [--into <id>]) [--json] [--write]'; // F14a: --into is optional (survivor inferred)
const USAGE = 'usage: codemod.mjs [graph.json] (--opportunity <ovId> | --merge <ids> [--into <id>]) [--json] [--write] (or set CODEWEB_WS, or run from a mapped repo)'; // F14a: --into is optional (survivor inferred)
import { die, emitJson, finish, loadGraph, parseArgs } from './lib/cli.mjs';

@@ -38,6 +38,7 @@

const { json, merge, into } = opts, doWrite = opts.write, opp = opts.opportunity;
const graphPath = pos[0] || (process.env.CODEWEB_WS ? `${process.env.CODEWEB_WS}/graph.json` : null);
if (!graphPath || (opp == null && merge == null)) die(USAGE, 2);
if (opp == null && merge == null) die(USAGE, 2);
const { graph, abs } = loadGraph(graphPath, { usage: USAGE });
// API F7: codemod died on usage before loadGraph could discover — no walk-up. The graph
// positional is optional now; THE one loader resolves arg -> CODEWEB_WS -> nearest .codeweb.
const { graph, abs } = loadGraph(pos[0], { usage: USAGE });

@@ -72,4 +73,4 @@ const index = buildIndex(graph);

const after = applyEdit(graph, { kind: 'merge', ids, into: canonical });
const sr = structuralRegressions(graph, after);
const projectedGate = { newCycles: sr.newCycles, lostCallers: sr.lostCallers, ok: sr.newCycles.length === 0 && sr.lostCallers.length === 0 };
const verdict = gateVerdict(graph, after, { exemptExported: false, scope: 'edges-only' });
const projectedGate = { newCycles: verdict.checks.newCycles, lostCallers: verdict.checks.lostCallers.map((l) => l.id), ok: verdict.ok, check: verdict.check };

@@ -90,3 +91,3 @@ const deletions = losers.map((id) => { const n = byId.get(id); return { id, file: n.file, range: [n.line, n.line + (n.loc || 1) - 1] }; });

if (!root || !existsSync(root)) die(`--write needs graph.meta.root on disk (got ${root || 'none'})`, 2);
if (!projectedGate.ok) { writeResult = { applied: false, reason: 'the gate predicts a regression — refusing to write', projectedGate }; code = 1; }
if (!projectedGate.ok) { writeResult = { applied: false, reason: 'the pre-flight predicts a regression (new cycle or lost caller) — refusing to write', projectedGate }; code = 1; }
else {

@@ -195,9 +196,11 @@ // a loser whose label differs from the canonical's must have a GLOBALLY-UNIQUE label to rewrite

console.log(`codeweb codemod: merge ${ids.length} -> keep ${canonical}`);
console.log(` removes ${losers.length} copy(ies), rewires ${rewrites.length} caller(s), ~${locReclaimed} LOC, blast ${plan.blastRadius}`);
console.log(` projected gate: ${projectedGate.ok ? 'PASS' : 'BLOCK'}${projectedGate.ok ? '' : ` (${projectedGate.newCycles.length} new cycle, ${projectedGate.lostCallers.length} lost-caller)`}`);
console.log(` removes ${losers.length} copy(ies) · ${rewrites.length} caller/importer site(s) to re-check · ~${locReclaimed} LOC · blast radius ${plan.blastRadius}`);
console.log(` projected: ${projectedGate.ok ? 'PASS — no new cycles; no surviving symbol loses its last caller' : `BLOCK — ${projectedGate.newCycles.length} new cycle(s), ${projectedGate.lostCallers.length} lost caller(s)`}`);
console.log(' (checks cycles + lost callers — stricter than the diff.mjs/CI gate on exports; does not count duplication)');
console.log(' deletions:'); for (const d of deletions) console.log(` ${d.file}:${d.range[0]}-${d.range[1]} (${d.id})`);
console.log(' rewrites:'); for (const r of rewrites) console.log(` ${r.file}:${r.line} (${r.callerId})`);
if (writeResult) console.log(` write: ${writeResult.applied ? `APPLIED to ${writeResult.filesTouched.length} file(s)` : `NOT applied — ${writeResult.reason}`}`);
else console.log(' (plan-only — pass --write to apply, gated + reversible)');
console.log(' caller/importer sites to re-check (codemod renames tokens and repoints imports — it never ADDS an import):');
for (const r of rewrites) console.log(` ${r.file}:${r.line} (${r.callerId})`);
if (writeResult) console.log(` write: ${writeResult.applied ? `applied — deleted ${losers.length} definition(s) in ${writeResult.filesTouched.length} file(s); re-extract gate ok (0 new cycles, 0 lost callers). Verify imports at the sites above.` : `NOT applied — ${writeResult.reason}`}`);
else console.log(' (plan-only — --write applies it, and auto-reverts only if the post-edit re-extract regresses; after a successful apply, undo is git\'s job)');
finish(code);
}

@@ -112,4 +112,7 @@ #!/usr/bin/env node

};
if (capSafe.truncated) payload.moreSafe = { remaining: capSafe.remaining };
if (capReview.truncated) payload.moreReview = { remaining: capReview.remaining };
// API F3 (§4 convention: nextOffset rides wherever `remaining` is emitted). A single --offset
// paging BOTH tiers in lockstep would over-skip the shorter tier — genuinely ambiguous — so the
// tiers carry nextOffset only (the page boundary), without an offset param.
if (capSafe.truncated) payload.moreSafe = { remaining: capSafe.remaining, nextOffset: capSafe.offset + capSafe.items.length };
if (capReview.truncated) payload.moreReview = { remaining: capReview.remaining, nextOffset: capReview.offset + capReview.items.length };

@@ -121,3 +124,5 @@ if (json) { emitJson(payload); } else {

if (deadScope.excluded) console.log(` scope: product — ${scopeNote(deadScope)}`); // #6: counted, never silent
console.log(`\nsafe to delete (no caller, not exported, no test):`);
// MICROCOPY A4: the heading is the label people act on — the hedge rides IN it, before the
// list, not in a note after both lists. "safe to delete" asserted safety the caveat then undid.
console.log(`\ndelete candidates (no caller, not exported, no test — extraction can miss dynamic calls; cross-check before deleting):`);
for (const o of payload.safe) console.log(` ${o.id} [${o.domain}] (${o.loc} loc)`);

@@ -130,4 +135,5 @@ if (payload.moreSafe) console.log(` … +${payload.moreSafe.remaining} more`);

if (!review.length) console.log(' (none)');
console.log(`\nnote: ${CAVEAT}.`);
// MICROCOPY A5: the false-positive door, visible where the false positive is staring at you.
console.log(`\nfalse positive? suppress it: node scripts/annotate.mjs --suppress <fingerprint> --note "why" (fingerprints: --json)`);
finish();
}

@@ -10,5 +10,7 @@ #!/usr/bin/env node

//
// Regression (exit 1) = a NEW dependency cycle, a NEW duplication finding, or an EXISTING symbol that
// lost all its callers. A brand-new uncalled node is reported but is NOT a gate failure (agents
// legitimately add functions before wiring them). Exit: 0 ok, 1 regressions, 2 usage/IO.
// Regression (exit 1) = a NEW dependency cycle, a NEW confirmed duplication, or an EXISTING
// non-exported symbol newly orphaned (exported symbols are exempt HERE — the edit-time preflights
// flag those too; the payload's verdict.check names which semantics ran). A brand-new uncalled
// node is reported but is NOT a gate failure (agents legitimately add functions before wiring
// them). Exit: 0 ok, 1 regressions, 2 usage/IO.
//

@@ -15,0 +17,0 @@ // Schema note (finding #28): rename detection is O(removed × added), skipped when either side exceeds

@@ -99,2 +99,5 @@ #!/usr/bin/env node

// API F3 (behavior BUG FIX): `count` was the capped length (top.length after slice(0, k)) —
// contradicting the fleet-wide "count is the true total" contract; the real match total was
// discarded and truncation was invisible. `count` is now the TRUE total; `more` marks the cap.
const payload = {

@@ -104,4 +107,5 @@ candidate: { source: body != null ? 'body' : stdin ? 'stdin' : 'signature', shingles: candidate.size, mode: structural ? 'structural' : 'lexical' },

bodyLineCap: BODY_LINE_CAP, // finding #26: existing bodies shingled on their first N lines (candidate uncapped)
matches: top, count: top.length, scanned,
matches: top, count: matches.length, scanned,
};
if (matches.length > top.length) payload.more = { remaining: matches.length - top.length };

@@ -108,0 +112,0 @@ if (json) { emitJson(payload); } else {

@@ -7,3 +7,3 @@ #!/usr/bin/env node

//
// Usage: node fitness.mjs <graph.json> [--rules codeweb.rules.json] [--json]
// Usage: node fitness.mjs [graph.json] [--rules codeweb.rules.json] [--json] (or set CODEWEB_WS, or run from a mapped repo)
// rules file: { "rules": [ { "id", "type", "severity"?, ...params } ] } (severity default "error")

@@ -14,6 +14,6 @@ // Exit: 0 ok, 1 when >=1 error-severity violation, 2 usage/IO/unknown-rule.

import { resolve, join, dirname } from 'node:path';
import { normalizeGraph, buildIndex, fileCycles } from './lib/graph-ops.mjs';
import { buildIndex, fileCycles } from './lib/graph-ops.mjs';
const USAGE = 'usage: fitness.mjs <graph.json> [--rules codeweb.rules.json] [--json]';
import { die, emitJson, finish, parseArgs } from './lib/cli.mjs';
const USAGE = 'usage: fitness.mjs [graph.json] [--rules codeweb.rules.json] [--json] (or set CODEWEB_WS, or run from a mapped repo)';
import { die, emitJson, finish, loadGraph, parseArgs } from './lib/cli.mjs';

@@ -26,10 +26,6 @@ // finding 24: THE flag loop (lib/cli.mjs parseArgs) — one unknown-flag policy, --help included.

const { json } = opts, rulesArg = opts.rules;
const graphPath = pos[0] || (process.env.CODEWEB_WS ? `${process.env.CODEWEB_WS}/graph.json` : null);
if (!graphPath) die(USAGE, 2);
const gAbs = resolve(graphPath);
if (!existsSync(gAbs)) die(`graph not found: ${gAbs}`, 2);
let graph;
try { graph = normalizeGraph(JSON.parse(readFileSync(gAbs, 'utf8'))); }
catch (e) { die(`invalid JSON in ${gAbs}: ${e.message}`, 2); }
// API F7: fitness honored CODEWEB_WS but not the walk-up, with hand-rolled load errors. THE one
// loader now (arg -> env -> nearest .codeweb above cwd, shared not-found/corrupt messages).
const { graph, abs: gAbs } = loadGraph(pos[0], { usage: USAGE });

@@ -36,0 +32,0 @@ // locate rules: --rules, else codeweb.rules.json beside the graph, else in cwd

@@ -16,3 +16,3 @@ #!/usr/bin/env node

const USAGE = 'usage: hotspots.mjs <graph.json> [--limit N] [--churn <map.json> | --git] [--all] [--json]'; // F10: --limit was real but hidden
const USAGE = 'usage: hotspots.mjs <graph.json> [--limit N] [--offset N] [--churn <map.json> | --git] [--all] [--json]'; // F10: --limit was real but hidden
import { die, emitJson, finish, capList, loadGraph, parseArgs } from './lib/cli.mjs';

@@ -25,3 +25,4 @@

json: { type: 'bool', default: false },
limit: { type: 'number', default: null },
limit: { type: 'number', default: null, min: 0 }, // API F3: one pagination dialect (limit/offset)
offset: { type: 'number', default: 0, min: 0 },
churn: { type: 'string', default: null },

@@ -32,3 +33,3 @@ git: { type: 'bool', default: false },

});
const { json, limit, all } = opts, churnPath = opts.churn, useGit = opts.git;
const { json, limit, offset, all } = opts, churnPath = opts.churn, useGit = opts.git;
const { graph, abs } = loadGraph(pos[0], { usage: USAGE });

@@ -41,5 +42,6 @@

const full = rankHotspots(graph, { churn, allRoles: all });
const capped = capList(full.ranked, limit);
// API F3: `count` stays the true total, `more` carries nextOffset so the remainder is reachable.
const capped = capList(full.ranked, limit, offset);
const payload = { target: graph.meta?.target || 'target', summary: `${full.count} symbol(s) ranked by complexity x fan-in x churn`, ...full, ranked: capped.items };
if (capped.truncated) payload.more = { remaining: capped.remaining };
if (capped.truncated) payload.more = { remaining: capped.remaining, nextOffset: capped.offset + capped.items.length };

@@ -46,0 +48,0 @@ if (json) { emitJson(payload); } else {

@@ -78,3 +78,3 @@ // brief-core — the day-one briefing: everything an agent burns its first 20-50k tokens

if (b.domains.length) {
L.push('areas:');
L.push('domains:');
for (const d of b.domains) {

@@ -81,0 +81,0 @@ // AI-IDEAS Idea 3: the narration sidecar's one-liner says what the area is FOR — always

@@ -10,3 +10,3 @@ // codeweb shared CLI harness — the one place stdout/exit/graph-loading plumbing lives.

import { readFileSync, existsSync, statSync, writeFileSync, renameSync, rmSync } from 'node:fs';
import { resolve, dirname, join } from 'node:path';
import { resolve, dirname, join, basename } from 'node:path';
import { normalizeGraph } from './graph-ops.mjs';

@@ -40,2 +40,34 @@ import { sha1 } from './hash.mjs';

*/
// CLI review "first fix": the parser coaches instead of walling. Levenshtein for did-you-mean —
// tiny inputs (flag names/arg keys), plain DP. Exported: the MCP layer's unknown-argument
// near-miss (API F4) uses the same tier — one implementation, per codeweb's own gate.
export function editDistance(a, b) {
const m = a.length, n = b.length;
const row = Array.from({ length: n + 1 }, (_, j) => j);
for (let i = 1; i <= m; i++) {
let prev = row[0]; row[0] = i;
for (let j = 1; j <= n; j++) {
const cur = row[j];
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
prev = cur;
}
}
return row[n];
}
const nearestFlag = (name, flags) => {
const cands = [...Object.keys(flags), 'help'];
let best = null, bestD = Infinity;
for (const c of cands) {
const d = editDistance(name.toLowerCase(), c);
if (d < bestD) { best = c; bestD = d; }
}
return bestD <= 2 || (best && best.startsWith(name)) ? best : null;
};
// Usage strings name the script file; when invoked through a bin wrapper (codeweb-query,
// codeweb-diff), say the name the user actually typed.
const speakUsage = (usage) => {
const invoked = basename(process.argv[1] || '');
return invoked.startsWith('codeweb') ? String(usage).replace(/^usage: \S+\.mjs/, `usage: ${invoked.replace(/\.mjs$/, '')}`) : usage;
};
export function parseArgs(argv, spec) {

@@ -47,21 +79,34 @@ const flags = spec.flags || {};

for (let i = 0; i < argv.length; i++) {
const t = argv[i];
if (t === '--help' || t === '-h') { console.log(spec.usage); process.exit(0); }
let t = argv[i];
if (t === '--help' || t === '-h') { console.log(speakUsage(spec.usage)); process.exit(0); }
if (t.startsWith('-') && t !== '-') {
// accept --flag=value (the convention every other CLI taught people)
let inline;
const eq = t.indexOf('=');
if (eq > 1) { inline = t.slice(eq + 1); t = t.slice(0, eq); }
const name = t.replace(/^--?/, '');
const f = flags[name];
if (!f) die(`unknown flag: ${t}\n${spec.usage}`, 2);
if (f.type === 'bool') { opts[name] = true; continue; }
const v = argv[++i];
if (v === undefined) die(`flag ${t} needs a value\n${spec.usage}`, 2);
if (!f) {
const near = nearestFlag(name, flags);
die(`unknown flag: ${t}${near ? ` (did you mean --${near}?)` : ''}\n${speakUsage(spec.usage)}`, 2);
}
if (f.type === 'bool') {
if (inline !== undefined) {
if (inline !== 'true' && inline !== 'false') die(`flag ${t} is a switch — use ${t} or ${t}=false\n${speakUsage(spec.usage)}`, 2);
opts[name] = inline === 'true';
} else opts[name] = true;
continue;
}
const v = inline !== undefined ? inline : argv[++i];
if (v === undefined) die(`flag ${t} needs a value\n${speakUsage(spec.usage)}`, 2);
if (f.type === 'number' || f.type === 'float') {
const n = f.type === 'float' ? parseFloat(v) : parseInt(v, 10);
if (Number.isNaN(n)) die(`flag ${t} needs a number (got "${v}")\n${spec.usage}`, 2);
if (Number.isNaN(n)) die(`flag ${t} needs a number (got "${v}")\n${speakUsage(spec.usage)}`, 2);
// FORMS F14c: flags can declare a floor (min: 0 on limits/offsets) — a negative limit
// silently minted empty pages with a nextOffset:0 loop instead of an error.
if (f.min !== undefined && n < f.min) die(`flag ${t} must be >= ${f.min} (got ${v})\n${spec.usage}`, 2);
if (f.min !== undefined && n < f.min) die(`flag ${t} must be >= ${f.min} (got ${v})\n${speakUsage(spec.usage)}`, 2);
opts[name] = n;
} else if (f.type === 'pair') {
const v2 = argv[++i];
if (v2 === undefined) die(`flag ${t} needs two values\n${spec.usage}`, 2);
if (v2 === undefined) die(`flag ${t} needs two values\n${speakUsage(spec.usage)}`, 2);
opts[name] = [v, v2];

@@ -127,3 +172,11 @@ } else opts[name] = v;

}
if (!graphPath) die(usage || 'usage: <graph.json> required (or set CODEWEB_WS, or run from a mapped repo)', 2);
if (!graphPath) {
// ERRORS R1: 20 tools passed {usage} here and REPLACED the shared cause+remedy with a bare
// usage wall — syntax-blame for an environment problem. Append, never substitute.
die([
`no map found — checked the graph argument, CODEWEB_WS, and every .codeweb/ above ${process.cwd()}.`,
'map this repo first: npx -y @ghostlygawd/codeweb <repo root> (in Claude Code: /codeweb)',
`then: ${speakUsage(usage || 'usage: <graph.json> [flags]')}`,
].join('\n'), 2);
}
const abs = resolve(graphPath);

@@ -130,0 +183,0 @@ if (!existsSync(abs)) die(`graph not found: ${abs} — build it first (run /codeweb, or: node scripts/run.mjs <target> --out-dir <target>/.codeweb)`, 2);

@@ -71,3 +71,5 @@ // codeweb context-pack core — the one payload assembler behind BOTH transports (finding 20).

};
if (cappedCallers.truncated) payload.moreCallers = { remaining: cappedCallers.remaining };
// API F3 (§4 convention: nextOffset rides wherever `remaining` is emitted). One offset param
// paging callers AND callees in lockstep would be ambiguous, so the tiers carry nextOffset only.
if (cappedCallers.truncated) payload.moreCallers = { remaining: cappedCallers.remaining, nextOffset: cappedCallers.offset + cappedCallers.items.length };
// finding 23: a caller that already swept staleness (the MCP server's per-burst memo) threads

@@ -82,4 +84,4 @@ // the verdict in; the CLI leaves it undefined and computes here. Same function, same verdict.

}
if (cappedCallees.truncated) payload.moreCallees = { remaining: cappedCallees.remaining };
if (cappedCallees.truncated) payload.moreCallees = { remaining: cappedCallees.remaining, nextOffset: cappedCallees.offset + cappedCallees.items.length };
return payload;
}

@@ -10,3 +10,3 @@ // lib/diff-core.mjs — the structural delta + regression verdict between two PARSED graph snapshots.

import { buildIndex, fileCycles, orphans, edgeKey } from './graph-ops.mjs';
import { buildIndex, fileCycles, orphans, edgeKey, gateVerdict } from './graph-ops.mjs';
import { jaccard, capBody } from './shingles.mjs'; // finding #28: body cap for long-body rename shingling

@@ -142,4 +142,6 @@ import { structuralShingles } from './skeleton.mjs'; // rename detection: a rename IS a Type-2 clone

ok: regressions.length === 0,
// API §5: the shared verdict object — same fields, same check label, on every presenter.
verdict: gateVerdict(before, after, { exemptExported: true, newDuplications: overlapsAdded, scope: 'full' }),
};
return { payload, code: payload.ok ? 0 : 1 };
}

@@ -571,2 +571,49 @@ // codeweb shared graph primitives — pure functions over a graph.json object (see graph-schema.md).

// API review F1/§5: ONE verdict, five presenters. "The gate" shipped as three predicates wearing
// one name — diff.mjs/CI keyed on the orphan set (any in-edge kind, exported symbols exempt),
// simulate/the post-edit hook/codemod on call-callers alone (exports count), review --gate on the
// latter plus duplication. gateVerdict is the one place both semantics live, and the strictness
// difference is a DECLARED parameter, not an undocumented fork:
// exemptExported: true -> the orphan gate (diff.mjs / CI): blocks a surviving non-exported
// symbol newly matching the orphan predicate. Exported symbols that
// lost every in-edge are still LISTED, flagged exempted:true —
// visible, never silently dropped.
// exemptExported: false -> the call-caller preflight (simulate / post-edit hook / codemod):
// blocks any surviving symbol whose call-callers drop to zero.
// Duplication belongs to the presenter's own pipeline (diff's confirmed-overlap delta, review's
// incremental pass): pass the list via newDuplications, or null when this scope can't see it.
export function gateVerdict(before, after, { exemptExported = false, newDuplications = null, scope = 'edges-only' } = {}) {
let newCycles, lostCallers;
if (exemptExported) {
const b = normalizeGraph(before), a = normalizeGraph(after);
const bIx = buildIndex(b), aIx = buildIndex(a);
const bIds = new Set(b.nodes.map((n) => n.id));
const bOrph = new Set(orphans(b, bIx).map((o) => o.id));
const aOrph = new Set(orphans(a, aIx).map((o) => o.id));
lostCallers = [...aOrph]
.filter((id) => bIds.has(id) && !bOrph.has(id))
.map((id) => ({ id, exported: false, exempted: false }));
for (const n of a.nodes) { // the exempt set, listed: exported survivors that lost every in-edge
if (n.exports && bIds.has(n.id) && !aIx.hasIncoming.has(n.id) && bIx.hasIncoming.has(n.id)) {
lostCallers.push({ id: n.id, exported: true, exempted: true });
}
}
lostCallers.sort((x, y) => (x.id < y.id ? -1 : x.id > y.id ? 1 : 0));
const bCycles = new Set(fileCycles(b).map((c) => c.join('|')));
newCycles = fileCycles(a).filter((c) => !bCycles.has(c.join('|')));
} else {
const sr = structuralRegressions(before, after);
const aExports = new Map(normalizeGraph(after).nodes.map((n) => [n.id, !!n.exports]));
newCycles = sr.newCycles;
lostCallers = sr.lostCallers.map((id) => ({ id, exported: aExports.get(id) || false, exempted: false }));
}
const blocking = lostCallers.filter((l) => !l.exempted);
return {
ok: newCycles.length === 0 && blocking.length === 0 && !(newDuplications && newDuplications.length),
check: exemptExported ? 'orphan-gate' : 'call-caller-preflight',
scope,
checks: { newCycles, lostCallers, ...(newDuplications !== null ? { newDuplications } : {}) },
};
}
// finding 14: the Spec O-1 delta simulator, hoisted from optimize.mjs so EVERY merge chain stops

@@ -573,0 +620,0 @@ // cloning the whole graph + re-running full SCC per candidate (campaign measured 289ms/candidate

@@ -20,3 +20,6 @@ // codeweb reading-order (F8) — a minimal, dependency-ordered reading path for understanding a scope at

function scopeIdsOf(graph, index, scope) {
// API F3: exported so the CLI can report the TRUE in-scope total beside a truncated order (the
// budget cut used to be invisible). `index` is consulted only for the `symbol` closure — callers
// may pass null for the other kinds. readingOrder's return shape is oracle-pinned; do not widen it.
export function scopeIdsOf(graph, index, scope) {
const kind = (scope && scope.kind) || 'all';

@@ -23,0 +26,0 @@ if (kind === 'domain') return graph.nodes.filter((n) => (n.domain || 'unassigned') === scope.value).map((n) => n.id);

@@ -129,3 +129,3 @@ #!/usr/bin/env node

const body = o.bodySim != null ? ` · **Body:** ${(o.bodySim * 100).toFixed(0)}%` : '';
const keep = o.canonical ? `\nKeep \`${o.canonical}\` · removes ${o.removesNodes} copy(ies) · rewires ${o.callersRewired} caller(s) · blast ${o.blastRadius} · ~${o.locSaved} LOC` : '';
const keep = o.canonical ? `\nKeep \`${o.canonical}\` · removes ${o.removesNodes} copy(ies) · rewires ${o.callersRewired} caller(s) · blast radius ${o.blastRadius} · ~${o.locSaved} LOC` : '';
return [`### ${o.id} · [${o.severity.toUpperCase()}] ${o.title}`,

@@ -159,7 +159,7 @@ `**Gate:** ${o.gate}${body} · **Confidence:** ${o.confidence}`, keep, ``, `**→ ${o.recommendation}**`, ``].join('\n');

console.log(` if all ready merges applied: -${t.duplicationRemovable} duplication finding(s), ~${t.locReclaimable} LOC reclaimed (gate would stay green)`);
const TAG = { ready: 'READY ', blocked: 'BLOCKED', review: 'JUDGE ' };
const TAG = { ready: 'READY ', blocked: 'BLOCKED ', review: 'JUDGEMENT' };
for (const o of opportunities) {
console.log(`\n[${TAG[o.tier]} ${o.severity.toUpperCase().padEnd(6)} ${o.confidence.padEnd(6)}${o.bodySim != null ? ` ${(o.bodySim * 100).toFixed(0).padStart(3)}%` : ' '}] ${o.id} ${o.title.replace(/`/g, '')}`);
console.log(` gate: ${o.gate}`);
if (o.canonical) console.log(` keep \`${o.canonical}\` · removes ${o.removesNodes} copy(ies) · rewires ${o.callersRewired} caller(s) · blast ${o.blastRadius} · ~${o.locSaved} LOC`);
if (o.canonical) console.log(` keep \`${o.canonical}\` · removes ${o.removesNodes} copy(ies) · rewires ${o.callersRewired} caller(s) · blast radius ${o.blastRadius} · ~${o.locSaved} LOC`);
console.log(` -> ${o.recommendation}`);

@@ -166,0 +166,0 @@ }

@@ -8,3 +8,3 @@ #!/usr/bin/env node

//
// Usage: node placement.mjs <graph.json> --calls <id|label,...> [--name <label>] [--body <file>] [--json]
// Usage: node placement.mjs [graph.json] --calls <id|label,...> [--name <label>] [--body <file>] [--json] (or set CODEWEB_WS, or run from a mapped repo)
// Exit: 0 ok, 2 usage/IO.

@@ -19,3 +19,3 @@

const HERE = dirname(fileURLToPath(import.meta.url));
const USAGE = 'usage: placement.mjs <graph.json> --calls <id|label,...> [--name <label>] [--body <file>] [--json]';
const USAGE = 'usage: placement.mjs [graph.json] --calls <id|label,...> [--name <label>] [--body <file>] [--json] (or set CODEWEB_WS, or run from a mapped repo)';
import { die, emitJson, finish, loadGraph, parseArgs } from './lib/cli.mjs';

@@ -34,6 +34,7 @@

const { json, calls, name, body } = opts;
const graphPath = pos[0] || (process.env.CODEWEB_WS ? `${process.env.CODEWEB_WS}/graph.json` : null);
if (!graphPath || calls == null) die(USAGE, 2);
if (calls == null) die(USAGE, 2);
const { graph, abs } = loadGraph(graphPath, { usage: USAGE });
// API F7: placement died on usage before loadGraph could discover — no walk-up. The graph
// positional is optional now; THE one loader resolves arg -> CODEWEB_WS -> nearest .codeweb.
const { graph, abs } = loadGraph(pos[0], { usage: USAGE });

@@ -40,0 +41,0 @@ const index = buildIndex(graph);

@@ -10,4 +10,4 @@ #!/usr/bin/env node

import { resolve } from 'node:path';
import { normalizeGraph } from './lib/graph-ops.mjs';
import { readingOrder } from './lib/reading-order.mjs';
import { normalizeGraph, buildIndex } from './lib/graph-ops.mjs';
import { readingOrder, scopeIdsOf } from './lib/reading-order.mjs';

@@ -38,2 +38,10 @@ const USAGE = 'usage: reading-order.mjs <graph.json> [--scope domain|file|symbol <value>] [--budget N] [--json]';

const payload = { target: graph.meta?.target || 'target', scope, budget, count: order.length, order };
// API F3: the budget cut was INVISIBLE — a truncated path looked complete. When the order fills
// the budget, report the in-scope remainder as an explicit `more` marker (never a silent cut).
// The lib's return shape is oracle-pinned, so the true total comes from the exported scope
// resolver (index built only for the `symbol` closure — the other kinds are plain node filters).
if (order.length >= budget) {
const total = new Set(scopeIdsOf(graph, scopeKind === 'symbol' ? buildIndex(graph) : null, scope)).size;
if (total > order.length) payload.more = { remaining: total - order.length };
}

@@ -44,3 +52,4 @@ if (json) { emitJson(payload); } else {

order.forEach((o, i) => console.log(` ${String(i + 1).padStart(3)}. ${o.id}\n ${o.why}`));
if (payload.more) console.log(` … +${payload.more.remaining} more in scope (raise --budget for the full path)`);
finish();
}

@@ -8,3 +8,3 @@ #!/usr/bin/env node

//
// Usage: node refresh.mjs <graph.json> [--cache <path>] [--json]
// Usage: node refresh.mjs [graph.json] [--cache <path>] [--json] (or set CODEWEB_WS, or run from a mapped repo)
// Exit: 0 ok, 2 usage / missing meta.root.

@@ -14,8 +14,8 @@

import { spawnSync } from 'node:child_process';
import { resolve, dirname, join } from 'node:path';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const USAGE = 'usage: refresh.mjs <graph.json> [--cache <path>] [--json]';
import { die, emitJson, finish, atomicWrite, SCAN_CACHE_NAME, parseArgs } from './lib/cli.mjs';
const USAGE = 'usage: refresh.mjs [graph.json] [--cache <path>] [--json] (or set CODEWEB_WS, or run from a mapped repo)';
import { die, emitJson, finish, atomicWrite, SCAN_CACHE_NAME, loadGraph, parseArgs } from './lib/cli.mjs';

@@ -28,7 +28,9 @@ // finding 24: THE flag loop (lib/cli.mjs parseArgs) — one unknown-flag policy, --help included.

const { json, cache } = opts;
const graphPath = pos[0];
if (!graphPath) die(USAGE, 2);
const abs = resolve(graphPath);
if (!existsSync(abs)) die(`graph not found: ${abs}`, 2);
// API F7 (COPY.md #9): refresh required a positional and hand-rolled a weaker error — no
// CODEWEB_WS, no walk-up. It now uses THE one loader (arg -> env -> nearest .codeweb above cwd,
// shared errors). The graph is re-read RAW below: the written bytes must come from the on-disk
// JSON + the fresh fragment, never from normalizeGraph's in-memory back-fills (its mutation runs
// post-write for the sidecars — see the #25 note below).
const { abs } = loadGraph(pos[0], { usage: USAGE });
let graph;

@@ -35,0 +37,0 @@ try { graph = JSON.parse(readFileSync(abs, 'utf8')); }

@@ -51,2 +51,6 @@ /**

'README.md',
'docs/reference.md',
'docs/agent-tools.md',
'tests/README.md',
'.claude-plugin/marketplace.json',
'site/content/index.html',

@@ -115,3 +119,3 @@ 'site/content/product.html',

[/("version":\s*")[^"]+(")/, `$1${version}$2`],
[/(\d+)(\s+deterministic read-only query tools)/, `${count}$2`],
[/(\d+)(\s+MCP tools)/, `${count}$2`],
],

@@ -124,2 +128,8 @@ },

{
// DOCS/COMPREHENSION: marketplace.json said 1.0.0 while every other surface said the real
// version — the one manifest the sync never touched.
file: '.claude-plugin/marketplace.json',
subs: [[/("version":\s*")[^"]+(")/, `$1${version}$2`]],
},
{
// SEO F2: the MCP-registry manifest tracks the package version (top-level + the npm

@@ -172,5 +182,30 @@ // package entry) so a release republish never ships a stale shelf listing.

if (plugin.version !== version) problems.push(`plugin.json version ${plugin.version} != package.json ${version}`);
const advertised = (plugin.description.match(/(\d+)\s+deterministic read-only query tools/) || [])[1];
const advertised = (plugin.description.match(/(\d+)\s+MCP tools/) || [])[1];
if (advertised && Number(advertised) !== count) problems.push(`plugin.json advertises ${advertised} tools; MCP server exposes ${count}`);
// CHARTER.md "Done looks like" #2: the ratified job line is the one identity statement, read
// from the charter itself, and it must appear on every public listing surface. Identity drift
// fails the gate like a stale version string. (No CHARTER.md — e.g. test fixtures — no check.)
const charterPath = join(root, 'CHARTER.md');
if (existsSync(charterPath)) {
const jobLine = (readText(charterPath).match(/\*\*"([^"]+)"\*\*/) || [])[1];
if (!jobLine) {
problems.push('CHARTER.md has no bolded, quoted job line to enforce');
} else {
const productPath = join(root, 'site', 'data', 'product.json');
const surfaces = [
['README.md', existsSync(join(root, 'README.md')) ? readText(join(root, 'README.md')) : null],
['site/data/product.json (tagline)',
existsSync(productPath) ? (JSON.parse(readText(productPath)).tagline || '') : null],
['package.json (description)', JSON.parse(readText(join(root, 'package.json'))).description || ''],
['.claude-plugin/plugin.json (description)', plugin.description || ''],
];
for (const [label, text] of surfaces) {
if (text !== null && !text.includes(jobLine)) {
problems.push(`${label} is missing the charter job line "${jobLine}"`);
}
}
}
}
const skill = readText(join(root, 'skills', 'codebase-anatomy', 'SKILL.md'));

@@ -226,3 +261,4 @@ const skillVer = (skill.match(/^version:\s*(.+)$/m) || [])[1];

if (existsSync(productPath)) {
for (const c of JSON.parse(readText(productPath)).claims || []) {
const productData = JSON.parse(readText(productPath));
for (const c of productData.claims || []) {
const m = /(\d+)\s*\/\s*(\d+)\s+tools/.exec(c.metric || '');

@@ -233,2 +269,13 @@ if (m && (Number(m[1]) !== count || Number(m[2]) !== count)) {

}
// The elevator carried "24 MCP query tools" for a release while 27 shipped: prose inside this
// DATA file feeds site templates, but lived outside both the PROSE_FILES sweep (content files
// only) and the structured checks above. Scan every string value, so a stale count anywhere
// in the site data fails the gate like any other prose surface.
const strings = [];
(function walk(v) {
if (typeof v === 'string') strings.push(v);
else if (Array.isArray(v)) v.forEach(walk);
else if (v && typeof v === 'object') Object.values(v).forEach(walk);
})(productData);
problems.push(...scanProseCounts(strings.join('\n'), 'site/data/product.json (prose)', { toolCount: count, langCount }));
}

@@ -235,0 +282,0 @@

@@ -94,3 +94,17 @@ #!/usr/bin/env node

const payload = { ...impact, filesChanged: hunks.map((h) => h.file).sort(), structural, newDuplications };
// F1/API §5: the labeled verdict object — same fields as diff/simulate/codemod, so "the gate"
// names one thing everywhere. Without --before this run can only see duplication; the check
// label says so instead of implying the structural half ran.
const expOf = new Map(graph.nodes.map((n) => [n.id, !!n.exports]));
const verdict = {
ok: !hasRegression,
check: structural ? 'call-caller-preflight' : 'duplication-only',
scope: 'full',
checks: {
newCycles: structural?.newCycles ?? [],
lostCallers: (structural?.lostCallers ?? []).map((id) => ({ id, exported: expOf.get(id) || false, exempted: false })),
newDuplications,
},
};
const payload = { ...impact, filesChanged: hunks.map((h) => h.file).sort(), structural, newDuplications, verdict };
const code = (gate && hasRegression) ? 1 : 0;

@@ -97,0 +111,0 @@

@@ -17,3 +17,3 @@ #!/usr/bin/env node

const USAGE = 'usage: risk.mjs <graph.json> [--changed <file,...>] [--limit N] [--churn <map.json> | --git] [--all] [--json]'; // F10: --limit was real but hidden
const USAGE = 'usage: risk.mjs <graph.json> [--changed <file,...>] [--limit N] [--offset N] [--churn <map.json> | --git] [--all] [--json]'; // F10: --limit was real but hidden
import { die, emitJson, finish, capList, loadGraph, parseArgs } from './lib/cli.mjs';

@@ -26,3 +26,4 @@

json: { type: 'bool', default: false },
limit: { type: 'number', default: null },
limit: { type: 'number', default: null, min: 0 }, // API F3: one pagination dialect (limit/offset)
offset: { type: 'number', default: 0, min: 0 },
changed: { type: 'string', default: null },

@@ -34,3 +35,3 @@ churn: { type: 'string', default: null },

});
const { json, limit, changed, all } = opts, churnPath = opts.churn, useGit = opts.git;
const { json, limit, offset, changed, all } = opts, churnPath = opts.churn, useGit = opts.git;
const { graph, abs } = loadGraph(pos[0], { usage: USAGE });

@@ -70,6 +71,8 @@

const capped = capList(ranked, limit);
// API F3: one pagination dialect — `count` stays the true total, `more` carries nextOffset so the
// advertised remainder is actually reachable (it used to name a remainder no offset could fetch).
const capped = capList(ranked, limit, offset);
const payload = { target: graph.meta?.target || 'target', summary: `${ranked.length} symbol(s) ranked by change-risk${changed != null ? ' (changed only)' : ''}`, weights: RISK_WEIGHTS, maxes, count: ranked.length, ranked: capped.items, excluded: riskScope.excluded, excludedByRole: riskScope.excludedByRole };
if (riskScope.excluded) payload.summary += ` — ${scopeNote(riskScope)}`;
if (capped.truncated) payload.more = { remaining: capped.remaining };
if (capped.truncated) payload.more = { remaining: capped.remaining, nextOffset: capped.offset + capped.items.length };

@@ -81,7 +84,10 @@ if (json) { emitJson(payload); } else {

if (riskScope.excluded) console.log(` scope: product — ${scopeNote(riskScope)}`); // #6: counted, never silent
for (const r of ranked.slice(0, 15)) {
// CLI.md 5.2: text mode printed a hard-coded top-15 of the UNCAPPED list — --limit was accepted
// and silently ignored. Render the capped page; the classic top-15 stays the no-flag default.
for (const r of (limit != null ? payload.ranked : payload.ranked.slice(0, 15))) {
const c = r.components;
console.log(` ${r.risk.toFixed(3)} ${r.id} [in ${c.fanIn} out ${c.fanOut} loc ${c.loc} blast ${c.blast} churn ${c.churn}]`);
}
if (payload.more) console.log(` … +${payload.more.remaining} more (rerun with --offset ${payload.more.nextOffset})`);
finish();
}

@@ -15,2 +15,8 @@ #!/usr/bin/env node

// Read-only over the target; never executes target code.
//
// Stream contract (CLI.md 5.1/6.1 — the fleet convention this file predated): stderr carries
// PROGRESS (the [run] stage lines, children's output, failure lines); stdout carries the RESULT
// (everything from `[run] done -> …` on: banner, since-last delta, artifact list, receipt,
// next steps — or, with --json, exactly one machine-readable line). So `codeweb . | grep mapped`
// works and `codeweb . 2>/dev/null` shows the results, like every other tool in the fleet.

@@ -36,3 +42,3 @@ import { execFileSync } from 'node:child_process';

const USAGE = `usage: run.mjs [<SRC>] [--target <label>] [--out-dir <dir>] [--open] [--full] [--allow-empty]
const USAGE = `usage: run.mjs [<SRC>] [--target <label>] [--out-dir <dir>] [--open] [--full] [--allow-empty] [--json]
<SRC> path to the codebase to map (default: current directory)

@@ -45,2 +51,4 @@ --target <label> display label stamped into the map (default: last two path segments of <SRC>)

--allow-empty permit a target with no supported source (writes an empty map)
--json machine mode: one JSON result line on stdout ({ws, symbols, actionable,
reused, version}); stage progress stays on stderr
--stages <phase> partial pipeline; only 'through-overlap' (extract+cluster+overlap, skip

@@ -62,2 +70,3 @@ optimize+report) — the trend fast path; never writes the stage memo

coverage: { type: 'string', default: null }, // #13: measured-execution annotation after the map
json: { type: 'bool', default: false }, // CLI.md 5.1: the flagship's machine mode
},

@@ -68,3 +77,3 @@ });

// silent nonsense map.
const opts = { src: pos[0] ?? '.', target: flags.target, outDir: flags['out-dir'], open: flags.open, serve: flags.serve, full: flags.full, allowEmpty: flags['allow-empty'], stages: flags.stages, coverage: flags.coverage };
const opts = { src: pos[0] ?? '.', target: flags.target, outDir: flags['out-dir'], open: flags.open, serve: flags.serve, full: flags.full, allowEmpty: flags['allow-empty'], stages: flags.stages, coverage: flags.coverage, json: flags.json };
// finding #42: --stages is a partial pipeline. Only 'through-overlap' is valid — any other value dies

@@ -79,5 +88,9 @@ // with usage (exit 2), so a typo can never silently run a different phase set. A partial run computes

// <SRC> must mean the same thing as a relative --out-dir. Fail here with one clean line, not a
// stage-level stack trace.
// stage-level stack trace. API.md F2: a wrong path is an INPUT error — exit 2 like the sibling
// input errors below (bad --stages, missing --coverage) and every loadGraph tool, never 1 (the
// stage-failure code): a typo'd path and a real pipeline failure must be distinguishable to CI.
// CLI.md 7.2: this validation runs BEFORE the workspace mkdir below, so a bad target never mints
// a .codeweb directory on the way out.
opts.src = resolve(opts.src);
if (!existsSync(opts.src)) { console.error(`[run] target not found: ${opts.src}`); process.exit(1); }
if (!existsSync(opts.src)) { console.error(`[run] target not found: ${opts.src}`); process.exit(2); }
// FORMS F9: --coverage names a FILE — check it now, not after five stages of work on a large

@@ -94,2 +107,4 @@ // repo (the map used to build fully, then die on an lcov typo with an "aborting" frame that

// orphaned maps in the npx cache where nothing could ever find them.
// CLI.md 7.2: created only AFTER the target validation above — mkdirSync({recursive}) on an
// unvalidated <SRC> would fabricate the missing target directory itself just to die inside it.
const ws = opts.outDir ? resolve(opts.outDir) : join(opts.src, '.codeweb');

@@ -123,6 +138,8 @@ mkdirSync(ws, { recursive: true });

try {
execFileSync(node, [file, ...args], { stdio: 'inherit', env: useEnv ? env : process.env, cwd: ROOT });
// CLI.md 6.1: children's stdout is progress by definition — route it to OUR stderr so stdout
// stays the result channel (and --json stays one parseable line). Child stderr passes through.
execFileSync(node, [file, ...args], { stdio: ['ignore', 2, 'inherit'], env: useEnv ? env : process.env, cwd: ROOT });
} catch (e) {
// The stage already printed its own diagnostics (stdio inherited) — add one clean line, not a
// raw execFileSync stack dump.
// The stage already printed its own diagnostics (stdout -> our stderr, stderr inherited) —
// add one clean line, not a raw execFileSync stack dump.
console.error(`\n[run] stage '${label}' failed${typeof e?.status === 'number' ? ` (exit ${e.status})` : ''} — aborting`);

@@ -136,5 +153,6 @@ process.exit(1);

// ACTIVATION A2: optimize's per-item advisory dump (~90 lines on a real repo) buried the result
// under logistics. The stage runs CAPTURED: its headline lines still print (stdout, as before),
// the dump lives in optimize.md with a one-line pointer here; CODEWEB_VERBOSE=1 restores the
// firehose. Returns stdout so the banner can scrape the ready/LOC pair.
// under logistics. The stage runs CAPTURED: its headline lines still print (on stderr — stage
// chatter under the CLI.md 6.1 stream contract), the dump lives in optimize.md with a one-line
// pointer here; CODEWEB_VERBOSE=1 restores the firehose. Returns stdout so the banner can scrape
// the ready/LOC pair.
const runCapture = (label, file, args) => {

@@ -154,4 +172,4 @@ console.error(`\n[run] ${label}`);

const shown = (verbose ? out : lines.slice(0, 3).join('\n')).trimEnd();
if (shown) console.log(shown);
if (!verbose && lines.length > 4) console.log(` full advisory: ${join(ws, 'optimize.md')}`);
if (shown) console.error(shown);
if (!verbose && lines.length > 4) console.error(` full advisory: ${join(ws, 'optimize.md')}`);
console.error(`[run] ${label} done in ${Date.now() - t0}ms`);

@@ -274,50 +292,65 @@ return out;

console.error(`\n[run] done -> ${ws} · codeweb v${VERSION}`);
if (partial) {
console.error(`[run] ${ws}/graph.json · overlap.md · fragment.json (through-overlap: no report)`);
// CLI.md 5.1/6.1: everything from here down is the RESULT — it prints on stdout so pipes and
// `2>/dev/null` both work. --json replaces the whole text block with ONE machine-readable line
// (symbols/actionable are null when no banner exists, e.g. a --stages through-overlap run;
// reused mirrors the stage memo: true when the downstream stages did not execute this run).
if (opts.json) {
console.log(JSON.stringify({
ws,
symbols: banner ? banner.symbols : null,
actionable: banner ? banner.actionable : null,
reused: reusable,
version: VERSION,
}));
} else {
// ACTIVATION A3: the banner leads with the RESULT (what the map found), not logistics. Numbers
// come from the graph itself (memo-cached on reuse) + optimize's headline; never recomputed here.
if (banner) {
const ready = banner.ready > 0 ? ` · ${banner.ready} ready merge(s)` : '';
const loc = banner.loc > 0 ? ` (~${banner.loc} LOC reclaimable)` : '';
console.error(`[run] mapped ${banner.symbols} symbols -> ${banner.actionable} actionable finding(s)${ready}${loc} — details: optimize.md`);
}
// RETENTION R1: the re-map is a PROGRESS REPORT — measured deltas only, never projections.
if (sinceLast) {
const { prev, cur } = sinceLast;
const when = prev.at ? ` (${String(prev.at).slice(0, 10)})` : '';
console.error(`[run] since last map${when}: dups ${prev.confirmed} -> ${cur.confirmed} · cycles ${prev.cycles} -> ${cur.cycles} · symbols ${prev.symbols} -> ${cur.symbols}`);
}
console.error(`[run] ${ws}/report.html · report.md · overlap.md · optimize.md · graph.json · fragment.json`);
// #10: the value receipt shows up where the user already is — one line, only when non-empty.
let receipt = null;
try {
const { readStats, lifetimeTotals, monthLine } = await import('./lib/stats.mjs');
receipt = monthLine(lifetimeTotals(readStats(join(ws, 'graph.json'))));
} catch { /* receipt must never break the pipeline */ }
if (receipt) {
// Returning user (the hooks/MCP have accrued activity here): receipt instead of onboarding.
console.error(`[run] codeweb here so far: ${receipt} (full receipt: scripts/stats.mjs)`);
if (!opts.open) console.error(`[run] open ${join(ws, 'report.html')} in your browser (or re-run with --open)`);
// REVENUE §3.2: the ONE in-product ask, at the receipt high point only — local counters,
// 30-day throttle, never on first contact, never on agent/failure surfaces.
console.log(`\n[run] done -> ${ws} · codeweb v${VERSION}`);
if (partial) {
console.log(`[run] ${ws}/graph.json · overlap.md · fragment.json (through-overlap: no report)`);
} else {
// ACTIVATION A3: the banner leads with the RESULT (what the map found), not logistics. Numbers
// come from the graph itself (memo-cached on reuse) + optimize's headline; never recomputed here.
if (banner) {
const ready = banner.ready > 0 ? ` · ${banner.ready} ready merge(s)` : '';
const loc = banner.loc > 0 ? ` (~${banner.loc} LOC reclaimable)` : '';
console.log(`[run] mapped ${banner.symbols} symbols -> ${banner.actionable} actionable finding(s)${ready}${loc} — details: optimize.md`);
}
// RETENTION R1: the re-map is a PROGRESS REPORT — measured deltas only, never projections.
if (sinceLast) {
const { prev, cur } = sinceLast;
const when = prev.at ? ` (${String(prev.at).slice(0, 10)})` : '';
console.log(`[run] since last map${when}: dups ${prev.confirmed} -> ${cur.confirmed} · cycles ${prev.cycles} -> ${cur.cycles} · symbols ${prev.symbols} -> ${cur.symbols}`);
}
console.log(`[run] ${ws}/report.html · report.md · overlap.md · optimize.md · graph.json · fragment.json`);
// #10: the value receipt shows up where the user already is — one line, only when non-empty.
let receipt = null;
try {
const { sponsorAskDue, recordSponsorAsk } = await import('./lib/stats.mjs');
if (sponsorAskDue(join(ws, 'graph.json'))) {
console.error('[run] codeweb is free — sponsoring pays for its benchmarks: https://github.com/sponsors/GhostlyGawd');
recordSponsorAsk(join(ws, 'graph.json'));
}
} catch { /* the ask must never break the pipeline */ }
} else {
// ACTIVATION A5: first map of this repo — the three moves that turn one run into a habit.
// #5 still holds: the map's whole point is to be LOOKED AT, so seeing it is step 1.
const openCmd = process.platform === 'win32' ? 'start ""' : process.platform === 'darwin' ? 'open' : 'xdg-open';
console.error(`[run] next:`);
console.error(`[run] 1. ${opts.open ? 'the map is opening in your browser' : `see the map: ${openCmd} ${join(ws, 'report.html')}`}`);
console.error(`[run] 2. live queries in Claude Code: claude mcp add codeweb -- npx -y -p @ghostlygawd/codeweb codeweb-mcp`);
console.error(`[run] 3. after edits: re-run codeweb here — the refresh is cache-warm (seconds, not a re-map)`);
const { readStats, lifetimeTotals, monthLine } = await import('./lib/stats.mjs');
receipt = monthLine(lifetimeTotals(readStats(join(ws, 'graph.json'))));
} catch { /* receipt must never break the pipeline */ }
if (receipt) {
// Returning user (the hooks/MCP have accrued activity here): receipt instead of onboarding.
console.log(`[run] codeweb here so far: ${receipt} (full receipt: scripts/stats.mjs)`);
if (!opts.open) console.log(`[run] open ${join(ws, 'report.html')} in your browser (or re-run with --open)`);
// REVENUE §3.2: the ONE in-product ask, at the receipt high point only — local counters,
// 30-day throttle, never on first contact, never on agent/failure surfaces (--json included:
// the suppressed block never prints, so the throttle is never burned unseen).
try {
const { sponsorAskDue, recordSponsorAsk } = await import('./lib/stats.mjs');
if (sponsorAskDue(join(ws, 'graph.json'))) {
console.log('[run] codeweb is free — sponsoring pays for its benchmarks: https://github.com/sponsors/GhostlyGawd');
recordSponsorAsk(join(ws, 'graph.json'));
}
} catch { /* the ask must never break the pipeline */ }
} else {
// ACTIVATION A5: first map of this repo — the three moves that turn one run into a habit.
// #5 still holds: the map's whole point is to be LOOKED AT, so seeing it is step 1.
const openCmd = process.platform === 'win32' ? 'start ""' : process.platform === 'darwin' ? 'open' : 'xdg-open';
console.log(`[run] next:`);
console.log(`[run] 1. ${opts.open ? 'the map is opening in your browser' : `see the map: ${openCmd} ${join(ws, 'report.html')}`}`);
console.log(`[run] 2. live queries in Claude Code: claude mcp add codeweb -- npx -y -p @ghostlygawd/codeweb codeweb-mcp`);
console.log(`[run] 3. after edits: re-run codeweb here — the refresh is cache-warm (seconds, not a re-map)`);
}
}
// reach: --serve keeps the process alive serving THIS workspace on localhost (Ctrl-C to stop).
if (opts.serve) run('serve', S('scripts/serve.mjs'), [ws], false);
}
// reach: --serve keeps the process alive serving THIS workspace on localhost (Ctrl-C to stop).
if (!partial && opts.serve) run('serve', S('scripts/serve.mjs'), [ws], false);
#!/usr/bin/env node
// codeweb simulate-edit — predict the regression gate's STRUCTURAL verdict for a hypothetical edit,
// WITHOUT performing it. Lets an agent discard doomed edits for ~zero cost before generating a line.
// Scoped to structuralRegressions (new file-cycles + symbols that lose all callers) — the same
// Scoped to the call-caller preflight (gateVerdict exemptExported:false — new file-cycles +
// surviving symbols that lose all call-callers, exports included) — the same
// subset the post-edit hook enforces. Duplication delta needs the full body-confirmed pipeline and

@@ -18,3 +19,3 @@ // is intentionally OUT OF SCOPE (documented, not silently dropped). Read-only; never writes.

import { resolve } from 'node:path';
import { normalizeGraph, resolveSymbol, suggestSymbols, applyEdit, structuralRegressions } from './lib/graph-ops.mjs';
import { normalizeGraph, resolveSymbol, suggestSymbols, applyEdit, structuralRegressions, gateVerdict } from './lib/graph-ops.mjs';

@@ -81,5 +82,9 @@ const USAGE = 'usage: simulate-edit.mjs <graph.json> (--delete <sym> | --merge <s1,s2,..> [--into <id>] | --move <sym> --to <file>) [--json]';

const { newCycles, lostCallers } = structuralRegressions(graph, after);
const projected = { newCycles, lostCallers, ok: newCycles.length === 0 && lostCallers.length === 0 };
const payload = { op: opName, target, into: intoOut, to: toOut, projected };
// F1/API §5: the shared gateVerdict is the oracle; `projected` keeps its legacy shape for
// existing consumers, `verdict` carries the labeled check (call-caller preflight, edges-only).
const verdict = gateVerdict(graph, after, { exemptExported: false, scope: 'edges-only' });
const newCycles = verdict.checks.newCycles;
const lostCallers = verdict.checks.lostCallers.map((l) => l.id);
const projected = { newCycles, lostCallers, ok: verdict.ok };
const payload = { op: opName, target, into: intoOut, to: toOut, projected, verdict };

@@ -89,8 +94,8 @@ if (json) { emitJson(payload); } else {

console.log(`simulate-edit: ${opName} ${target.join(', ')}${intoOut ? ` -> ${intoOut}` : ''}${toOut ? ` -> ${toOut}` : ''}`);
console.log(`projected gate: ${projected.ok ? 'PASS — the gate would accept this edit (exit 0)' : 'BLOCK — the gate would reject this edit (exit 1)'}`);
console.log(`projected: ${projected.ok ? 'PASS — no new cycles; no surviving symbol loses its last caller' : 'BLOCK — new cycle or a symbol losing its last caller (details below)'}`);
if (newCycles.length) console.log(` new file cycle(s): ${newCycles.map((c) => c.join(' -> ')).join(' | ')}`);
if (lostCallers.length) console.log(` symbol(s) left with no callers: ${lostCallers.join(', ')}`);
console.log(' (structural pre-flight: duplication delta is out of scope — run the full pipeline for that.)');
console.log(' (checks cycles + lost callers — stricter than the diff.mjs/CI gate, which exempts exported symbols; duplication delta needs the full pipeline.)');
finish();
}
}

@@ -15,4 +15,4 @@ # Security

outcome ledger in `.codeweb/stats.json` is documented in-file as strictly local and never
transmitted) · require dependencies (it runs on an empty `node_modules`; one *optional* wasm
grammar sharpens extraction).
transmitted — set `CODEWEB_NO_STATS=1` to disable even that) · require dependencies (it runs on
an empty `node_modules`; one *optional* wasm grammar sharpens extraction).

@@ -19,0 +19,0 @@ **Supply chain:** releases are published from CI with **npm provenance attestation** (SLSA) —

---
name: codebase-anatomy
description: Dissect a codebase to atomic nodes (functions, classes, symbols), wire the call/import web, tag each node's domain, and build a cross-domain overlap graph that ranks consolidation/de-duplication opportunities, then render an interactive HTML map. Use to restructure your own codebase into well-defined non-duplicative systems, OR to fully map and review an external repo (git URL / owner-repo) before adopting it. Triggers include "map this codebase", "dependency/relationship graph", "find duplication/overlap", "atomic dissection", "review this repo before I use it", and the /codeweb command.
version: 0.10.0
version: 0.11.0
metadata:

@@ -6,0 +6,0 @@ origin: community

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display