@ghostlygawd/codeweb 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.claude-plugin/plugin.json +30 -0
  2. package/CHANGELOG.md +480 -0
  3. package/LICENSE +21 -0
  4. package/README.md +573 -0
  5. package/agents/codeweb-dissector.md +56 -0
  6. package/agents/codeweb-domain-mapper.md +63 -0
  7. package/commands/codeweb.md +57 -0
  8. package/hooks/hooks.json +47 -0
  9. package/hooks/post-edit-diff.mjs +83 -0
  10. package/hooks/pre-edit-impact.mjs +94 -0
  11. package/hooks/session-brief.mjs +53 -0
  12. package/package.json +64 -0
  13. package/scripts/annotate.mjs +46 -0
  14. package/scripts/bench-ts-engine.mjs +59 -0
  15. package/scripts/bench.mjs +133 -0
  16. package/scripts/break-cycles.mjs +0 -0
  17. package/scripts/brief.mjs +28 -0
  18. package/scripts/build-report.mjs +182 -0
  19. package/scripts/campaign.mjs +61 -0
  20. package/scripts/check-consistency.mjs +45 -0
  21. package/scripts/ci-gate.mjs +86 -0
  22. package/scripts/cluster3.mjs +112 -0
  23. package/scripts/codemod.mjs +130 -0
  24. package/scripts/context-pack.mjs +118 -0
  25. package/scripts/deadcode.mjs +96 -0
  26. package/scripts/diff.mjs +0 -0
  27. package/scripts/explain.mjs +87 -0
  28. package/scripts/extract-symbols.mjs +1307 -0
  29. package/scripts/find-similar.mjs +86 -0
  30. package/scripts/find.mjs +50 -0
  31. package/scripts/fitness.mjs +90 -0
  32. package/scripts/grammars/PROVENANCE.md +36 -0
  33. package/scripts/grammars/tree-sitter-c-sharp.wasm +0 -0
  34. package/scripts/grammars/tree-sitter-go.wasm +0 -0
  35. package/scripts/grammars/tree-sitter-java.wasm +0 -0
  36. package/scripts/grammars/tree-sitter-python.wasm +0 -0
  37. package/scripts/grammars/tree-sitter-rust.wasm +0 -0
  38. package/scripts/grammars/tree-sitter-typescript.wasm +0 -0
  39. package/scripts/hotspots.mjs +53 -0
  40. package/scripts/lib/annotations.mjs +51 -0
  41. package/scripts/lib/bench-core.mjs +178 -0
  42. package/scripts/lib/brief-core.mjs +92 -0
  43. package/scripts/lib/campaign.mjs +73 -0
  44. package/scripts/lib/claims-check.mjs +44 -0
  45. package/scripts/lib/cli.mjs +140 -0
  46. package/scripts/lib/complexity.mjs +68 -0
  47. package/scripts/lib/dup-check.mjs +51 -0
  48. package/scripts/lib/find-core.mjs +85 -0
  49. package/scripts/lib/gate-md.mjs +50 -0
  50. package/scripts/lib/graph-ops.mjs +319 -0
  51. package/scripts/lib/hotspots.mjs +34 -0
  52. package/scripts/lib/query-core.mjs +85 -0
  53. package/scripts/lib/reading-order.mjs +53 -0
  54. package/scripts/lib/reliance.mjs +143 -0
  55. package/scripts/lib/risk.mjs +18 -0
  56. package/scripts/lib/shingles.mjs +39 -0
  57. package/scripts/lib/skeleton.mjs +41 -0
  58. package/scripts/lib/stats.mjs +92 -0
  59. package/scripts/lib/ts-engine.mjs +605 -0
  60. package/scripts/mcp-server.mjs +370 -0
  61. package/scripts/optimize.mjs +152 -0
  62. package/scripts/overlap.mjs +380 -0
  63. package/scripts/placement.mjs +93 -0
  64. package/scripts/query.mjs +94 -0
  65. package/scripts/reading-order.mjs +36 -0
  66. package/scripts/refresh.mjs +73 -0
  67. package/scripts/release-utils.mjs +158 -0
  68. package/scripts/release.mjs +62 -0
  69. package/scripts/report-template.html +730 -0
  70. package/scripts/review.mjs +112 -0
  71. package/scripts/risk.mjs +77 -0
  72. package/scripts/run.mjs +96 -0
  73. package/scripts/screenshot.mjs +104 -0
  74. package/scripts/simulate-edit.mjs +70 -0
  75. package/scripts/stats.mjs +31 -0
  76. package/scripts/trend.mjs +147 -0
  77. package/scripts/version-sync.mjs +19 -0
  78. package/skills/codebase-anatomy/SKILL.md +174 -0
  79. package/skills/codebase-anatomy/references/engine-detection.md +49 -0
  80. package/skills/codebase-anatomy/references/graph-schema.md +120 -0
  81. package/skills/codebase-anatomy/references/overlap-heuristics.md +52 -0
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: codeweb-domain-mapper
3
+ description: Read-only domain-tagging and overlap-detection pass for the codeweb plugin. Given the merged node/edge graph, clusters nodes into semantic domains, assigns a domain to every node, and detects cross-domain overlap (duplicated logic, parallel implementations, tangled responsibilities) with ranked consolidation recommendations. Returns JSON.
4
+ tools: Read, Grep, Glob
5
+ model: opus
6
+ ---
7
+
8
+ # codeweb-domain-mapper
9
+
10
+ You turn a raw symbol web into a **domain map** and an **overlap graph**. You run once, after
11
+ all dissectors have produced the merged `{nodes, edges}`. Your output is data — return one JSON
12
+ object and nothing else.
13
+
14
+ ## Inputs
15
+
16
+ - The merged graph: `nodes[]` (with empty `domain`) and `edges[]`.
17
+ - Access to the repo (read-only) to confirm semantics when names are ambiguous.
18
+ - The **overlap heuristics** reference — apply its kinds and severity rubric.
19
+
20
+ ## Step 1 — Assign domains
21
+
22
+ A **domain** is a coherent area of responsibility (e.g. `auth`, `billing`, `persistence`,
23
+ `http-routing`, `rendering`, `notifications`). Derive domains from: directory structure,
24
+ naming, what each symbol does, and edge clustering (densely-connected groups tend to be one
25
+ domain). Assign every node exactly one `domain`. Aim for 4–15 domains for a typical repo —
26
+ not one-per-file, not one-for-everything. Produce a `domains[]` summary with a node count and
27
+ one-line description each.
28
+
29
+ ## Step 2 — Detect overlaps
30
+
31
+ Find places where **separate parts of the system do overlapping work**. For each, classify the
32
+ `kind`:
33
+
34
+ - `duplicate-logic` — the same algorithm/check re-implemented in 2+ places.
35
+ - `parallel-impl` — competing implementations of one capability (e.g. two HTTP clients).
36
+ - `shared-responsibility` — one concern smeared across many domains with no owner.
37
+ - `tangled-domain` — a node whose behaviour mixes multiple domains and should be split.
38
+
39
+ Assign `severity` (high|medium|low) per the heuristics reference (weight by duplication count,
40
+ blast radius, and divergence risk). For each overlap give concrete `evidence` (the specific
41
+ symbols and what they share) and a `recommendation` that names the consolidation target — the
42
+ single well-defined system the overlapping pieces should collapse into.
43
+
44
+ ## Step 3 — Rank
45
+
46
+ Order `overlaps[]` by severity then by how many nodes/domains they touch. The top items are the
47
+ codebase's best simplification opportunities.
48
+
49
+ ## Output
50
+
51
+ Return exactly one JSON object — no prose, no fences:
52
+
53
+ ```json
54
+ {
55
+ "nodes": [ { "id": "...", "domain": "auth" } ],
56
+ "domains": [ { "name": "auth", "nodes": 12, "summary": "..." } ],
57
+ "overlaps": [ { "id": "ov1", "title": "...", "kind": "duplicate-logic", "severity": "high",
58
+ "domains": ["auth","billing"], "nodes": ["..."], "evidence": "...", "recommendation": "..." } ]
59
+ }
60
+ ```
61
+
62
+ In `nodes` you only need `id` + `domain` (the orchestrator merges these back onto the full
63
+ nodes). Include every node id.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: codeweb
3
+ description: Dissect a codebase into atomic nodes, wire the system web, tag domains, and surface overlap/consolidation opportunities as an interactive HTML map. Works on the current project or an external repo (path or git URL).
4
+ ---
5
+
6
+ # /codeweb
7
+
8
+ Invoke the **codebase-anatomy** skill to build a "biological web" of a codebase and an
9
+ overlap graph for restructuring.
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ /codeweb [target] [flags]
15
+ ```
16
+
17
+ - `target` — a local path (default: current directory `.`) **or** a git URL / `owner/repo`
18
+ to clone read-only and review before adopting.
19
+ - `--depth module|symbol|auto` — granularity (default `auto`: module-level overview, then
20
+ symbol-level on the densest/most-overlapping subsystems).
21
+ - `--engine hybrid|read|tools` — how edges are extracted (default `hybrid`: static-analysis
22
+ tools when available, agent reading otherwise).
23
+ - `--focus <glob>` — restrict atomic dissection to a path glob (e.g. `src/payments/**`).
24
+ - `--mode internal|external` — `internal` = restructure my own code; `external` = explore a
25
+ third-party repo and produce an adoption review. Auto-detected from `target` if omitted.
26
+ - `--open` — open the generated `report.html` after rendering.
27
+
28
+ ## What it does
29
+
30
+ 1. Scopes and acquires the target (clones external repos read-only; never executes their code).
31
+ 2. Dissects the code to atomic nodes (functions, classes, methods, exported symbols).
32
+ 3. Wires the relationship web (calls, imports, inheritance) using the hybrid engine.
33
+ 4. Tags every node with a semantic domain and clusters the domains.
34
+ 5. Builds the overlap graph — duplicated logic, parallel implementations, tangled domains —
35
+ and ranks consolidation opportunities.
36
+ 6. Runs the consolidation advisor — tiers each duplicate-logic finding into **ready** (a
37
+ body-confirmed merge the regression gate would accept), **blocked** (a merge that would
38
+ introduce a new dependency cycle), or **review** (drifted/structural) — and writes `optimize.md`.
39
+ 7. Renders a self-contained interactive HTML map plus `graph.json`, `overlap.md`, and `optimize.md`.
40
+ 8. In external mode, adds an adoption review (risk, dependency, architecture verdict).
41
+
42
+ ## Instructions for Claude
43
+
44
+ Activate the `codebase-anatomy` skill and follow its workflow. Parse the arguments above from
45
+ `$ARGUMENTS`. If `target` is a git URL or `owner/repo`, treat it as **external mode** unless
46
+ overridden. For JavaScript/TypeScript/Python targets, prefer the skill's **fast path** — a single
47
+ deterministic command:
48
+
49
+ ```bash
50
+ node "${CLAUDE_PLUGIN_ROOT}/scripts/run.mjs" "<target>" --target "<label>" --out-dir "<target>/.codeweb"
51
+ ```
52
+
53
+ which runs extract → cluster → overlap → optimize → render in one shot; fall back to the agent-based
54
+ dissection only for languages it can't parse or `--engine read`. Write all outputs under
55
+ `<target>/.codeweb/` (or a temp dir for cloned external repos) and finish by reporting the artifact
56
+ paths and the top consolidation opportunities — lead with the **ready** tier from `optimize.md`
57
+ (merges the regression gate would accept), then call out anything **blocked** by a projected cycle.
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "startup|resume|clear",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-brief.mjs\"",
11
+ "timeout": 10
12
+ }
13
+ ],
14
+ "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)",
15
+ "id": "codeweb:session-brief"
16
+ }
17
+ ],
18
+ "PreToolUse": [
19
+ {
20
+ "matcher": "Edit|Write|MultiEdit",
21
+ "hooks": [
22
+ {
23
+ "type": "command",
24
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/pre-edit-impact.mjs\"",
25
+ "timeout": 10
26
+ }
27
+ ],
28
+ "description": "codeweb: before an edit in a .codeweb-mapped target, surface one line of blast-radius awareness (symbols + dependents in the file) so contract changes get impact-checked first (advisory; fail-open; inert until /codeweb has mapped the target)",
29
+ "id": "codeweb:pre-edit-impact"
30
+ }
31
+ ],
32
+ "PostToolUse": [
33
+ {
34
+ "matcher": "Edit|Write|MultiEdit",
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/post-edit-diff.mjs\"",
39
+ "timeout": 30
40
+ }
41
+ ],
42
+ "description": "codeweb: after an edit to a .codeweb-mapped target, flag new dependency cycles or symbols that lost all callers (edges-only subset; fail-open, non-blocking, inert until /codeweb has mapped the target)",
43
+ "id": "codeweb:post-edit-diff"
44
+ }
45
+ ]
46
+ }
47
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // codeweb PostToolUse hook — structural regression check after an edit.
3
+ //
4
+ // Reads the PostToolUse payload on stdin. IF the edited file is a source file under a
5
+ // `.codeweb/`-mapped target (i.e. /codeweb has been run, leaving `<target>/.codeweb/graph.json`),
6
+ // re-extracts that target and compares to the baseline via structuralRegressions(): a NEW file
7
+ // dependency cycle, or a symbol that lost ALL its callers, is surfaced to the agent.
8
+ //
9
+ // FAIL-OPEN by construction: any parse error, missing baseline, or extraction failure exits 0
10
+ // silently, so the hook can never block or break an edit. It is INERT until a target is mapped
11
+ // (no `.codeweb/graph.json` up-tree -> no-op), and only checks the edges-only regression subset
12
+ // (cycles + lost-callers) — run `scripts/diff.mjs` or re-run /codeweb for the full delta.
13
+
14
+ import { readFileSync, existsSync } from 'node:fs';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, join, resolve } from 'node:path';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { tmpdir } from 'node:os';
19
+ import { structuralRegressions } from '../scripts/lib/graph-ops.mjs';
20
+ import { bump, correlateEdit } from '../scripts/lib/stats.mjs';
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url));
23
+ const EXTRACT = join(HERE, '..', 'scripts', 'extract-symbols.mjs');
24
+ import { SRC_RE, findTarget } from '../scripts/lib/cli.mjs'; // Spec E: one truth (was a stale local copy missing five languages)
25
+
26
+
27
+ // Returns { root, newCycles, lostCallers } when an edit introduces a structural regression, else null.
28
+ export function check(raw) {
29
+ let input; try { input = JSON.parse(raw); } catch { return null; }
30
+ const fp = input?.tool_input?.file_path || input?.tool_input?.filePath;
31
+ if (!fp || !SRC_RE.test(fp)) return null;
32
+ const t = findTarget(fp);
33
+ if (!t) return null;
34
+ let baseline; try { baseline = JSON.parse(readFileSync(t.baseline, 'utf8')); } catch { return null; }
35
+ const tmpOut = join(tmpdir(), `codeweb-hook-${process.pid}.json`);
36
+ try {
37
+ // Incremental: the per-file scan cache lives beside the graph, so an edit re-scans only the
38
+ // changed file(s) instead of the whole target on every keystroke-batch.
39
+ const cache = join(dirname(t.baseline), 'scan-cache.json');
40
+ execFileSync(process.execPath, [EXTRACT, t.root, '--no-ctags', '--cache', cache, '--out', tmpOut], { stdio: 'ignore' });
41
+ } catch { return null; }
42
+ let after; try { after = JSON.parse(readFileSync(tmpOut, 'utf8')); } catch { return null; }
43
+ const reg = structuralRegressions(baseline, after);
44
+ if (!reg.newCycles.length && !reg.lostCallers.length) return null;
45
+ return { root: t.root, ...reg };
46
+ }
47
+
48
+ function format(out) {
49
+ const lines = [`[codeweb] ⚠ structural regression after edit (target: ${out.root}):`];
50
+ for (const c of out.newCycles) lines.push(` new dependency cycle: ${c.join(' <-> ')}`);
51
+ for (const id of out.lostCallers) lines.push(` ${id} lost all callers (now uncalled)`);
52
+ lines.push(' -> run `node scripts/diff.mjs` or re-run /codeweb for the full delta.');
53
+ return lines.join('\n');
54
+ }
55
+
56
+ // Execute as a hook only when run directly (not when imported by tests).
57
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
58
+ let raw = '';
59
+ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
60
+ let out = null;
61
+ try { out = check(raw); } catch { /* fail-open */ }
62
+ try {
63
+ const fp = JSON.parse(raw)?.tool_input?.file_path || JSON.parse(raw)?.tool_input?.filePath;
64
+ const t = fp && SRC_RE.test(fp) && findTarget(fp);
65
+ if (t) {
66
+ bump(t.baseline, 'postEditChecks');
67
+ if (out) bump(t.baseline, 'regressionsFlagged', out.newCycles.length + out.lostCallers.length);
68
+ // advice-followed correlation: did this edit touch a caller file the last card warned about?
69
+ const rel = resolve(fp).slice(t.root.length + 1).replace(/\\/g, '/');
70
+ correlateEdit(t.baseline, rel);
71
+ }
72
+ } catch { /* receipt only */ }
73
+ if (out) {
74
+ const msg = format(out);
75
+ process.stderr.write(msg + '\n');
76
+ try {
77
+ process.stdout.write(JSON.stringify({
78
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: msg },
79
+ }) + '\n');
80
+ } catch { /* ignore */ }
81
+ }
82
+ process.exit(0); // ALWAYS non-blocking
83
+ }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // codeweb PreToolUse hook — one line of blast-radius awareness BEFORE an edit lands.
3
+ //
4
+ // The post-edit gate catches a regression after the fact; this is the missing other half: when the
5
+ // agent is about to edit a file in a `.codeweb`-mapped target, surface how load-bearing that file
6
+ // is (symbols + in-repo callers + the top symbol) so contract-changing edits get checked with
7
+ // codeweb_impact/codeweb_context FIRST. One line, advisory, never blocks.
8
+ //
9
+ // FAIL-OPEN and cheap: any parse/read problem exits 0 silently; unmapped targets are a no-op; the
10
+ // whole check is one JSON parse + an in-memory count (~50-100ms on a 3k-symbol graph).
11
+
12
+ import { readFileSync, existsSync } from 'node:fs';
13
+ import { execFileSync } from 'node:child_process';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { dirname, join, resolve, relative } from 'node:path';
16
+ import { bump, recordPendingCard } from '../scripts/lib/stats.mjs';
17
+
18
+ // set by preview() when a card is embedded — the caller FILES the card warned about
19
+ // (docs/specs/card-correlation.md: a later edit touching one = advice followed)
20
+ let lastCardMeta = null;
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url));
23
+ const EXPLAIN = join(HERE, '..', 'scripts', 'explain.mjs');
24
+ import { SRC_RE, findTarget } from '../scripts/lib/cli.mjs'; // Spec E: one truth (was a duplicated walk + a trailing language list)
25
+
26
+ // Returns the one-line advisory for an edit payload, or null (not mapped / not source / no signal).
27
+ export function preview(raw) {
28
+ let input; try { input = JSON.parse(raw); } catch { return null; }
29
+ const fp = input?.tool_input?.file_path || input?.tool_input?.filePath;
30
+ if (!fp || !SRC_RE.test(fp)) return null;
31
+ const t = findTarget(fp);
32
+ if (!t) return null;
33
+ let graph; try { graph = JSON.parse(readFileSync(t.baseline, 'utf8')); } catch { return null; }
34
+ const rel = relative(t.root, resolve(fp)).replace(/\\/g, '/');
35
+ const nodes = (graph.nodes || []).filter((n) => n.file === rel && n.kind !== 'module');
36
+ if (!nodes.length) return null;
37
+ const inCount = new Map();
38
+ for (const e of graph.edges || []) {
39
+ if (e.kind !== 'call' && e.kind !== 'import' && e.kind !== 'ref') continue;
40
+ inCount.set(e.to, (inCount.get(e.to) || 0) + 1);
41
+ }
42
+ let total = 0, top = null;
43
+ for (const n of nodes) {
44
+ const c = inCount.get(n.id) || 0;
45
+ total += c;
46
+ if (!top || c > top.c) top = { label: n.label, c };
47
+ }
48
+ if (total === 0) return null; // nothing depends on this file — stay quiet
49
+ let msg = `[codeweb] editing ${rel}: ${nodes.length} symbol(s), ${total} in-repo dependent edge(s)` +
50
+ (top && top.c > 0 ? ` (most depended-on: ${top.label} ×${top.c})` : '') + '.';
51
+ // AMBIENT context: don't just point at the tools — inject the ~1KB explain card for the file's
52
+ // most-depended-on symbol, so the blast radius arrives without the agent having to ask. This is
53
+ // the attack on the edit-quality null: awareness with zero discipline required. Fail-open.
54
+ if (top && top.c > 0) {
55
+ try {
56
+ const topNode = nodes.slice().sort((a, b) => (inCount.get(b.id) || 0) - (inCount.get(a.id) || 0))[0];
57
+ const r = execFileSync(process.execPath, [EXPLAIN, t.baseline, topNode.id, '--json'], { encoding: 'utf8', timeout: 8000, maxBuffer: 1 << 22 });
58
+ const card = JSON.parse(r).cards?.[0];
59
+ if (card) {
60
+ msg += `\n ${card.summary}`;
61
+ if (card.topCallers?.length) msg += `\n top callers: ${card.topCallers.slice(0, 4).join(', ')}`;
62
+ if (card.tests?.length) msg += `\n tests: ${card.tests.slice(0, 2).join(', ')}`;
63
+ const callerFiles = [...new Set((card.topCallers || [])
64
+ .map((id) => id.slice(0, id.lastIndexOf(':')))
65
+ .filter((f) => f && f !== rel))]; // the SUBJECT file never counts as "following the advice"
66
+ if (callerFiles.length) lastCardMeta = { baseline: t.baseline, symbol: topNode.id, files: callerFiles };
67
+ }
68
+ } catch { /* card is a bonus, never a blocker */ }
69
+ }
70
+ msg += `\n → codeweb_context for a bounded edit window; codeweb_impact for the full blast radius.`;
71
+ return msg;
72
+ }
73
+
74
+ // Execute as a hook only when run directly (not when imported by tests).
75
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
76
+ let raw = '';
77
+ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
78
+ let msg = null;
79
+ try { msg = preview(raw); } catch { /* fail-open */ }
80
+ if (msg) {
81
+ try {
82
+ const fp = JSON.parse(raw)?.tool_input?.file_path || JSON.parse(raw)?.tool_input?.filePath;
83
+ const t = fp && findTarget(fp);
84
+ if (t) bump(t.baseline, 'cardsDelivered');
85
+ if (lastCardMeta) recordPendingCard(lastCardMeta.baseline, lastCardMeta.symbol, lastCardMeta.files);
86
+ } catch { /* receipt only */ }
87
+ try {
88
+ process.stdout.write(JSON.stringify({
89
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', additionalContext: msg },
90
+ }) + '\n');
91
+ } catch { /* ignore */ }
92
+ }
93
+ process.exit(0); // ALWAYS non-blocking
94
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ // codeweb SessionStart hook — the day-one briefing, injected before the first token is spent.
3
+ //
4
+ // A new session in a mapped repo starts oriented instead of exploring: one ~2KB page (areas,
5
+ // load-bearing symbols, entry points, test layout, known issues) from the already-built graph.
6
+ // FAIL-OPEN and cheap: unmapped cwd is a silent no-op; any error exits 0 with no output.
7
+
8
+ import { readFileSync, existsSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { dirname, join, resolve } from 'node:path';
11
+ import { normalizeGraph, buildIndex } from '../scripts/lib/graph-ops.mjs';
12
+ import { buildBrief, renderBrief } from '../scripts/lib/brief-core.mjs';
13
+ import { bump, attachActivity } from '../scripts/lib/stats.mjs';
14
+
15
+ function findGraph(startDir) {
16
+ let dir = resolve(startDir);
17
+ for (let i = 0; i < 40; i++) {
18
+ const cand = join(dir, '.codeweb', 'graph.json');
19
+ if (existsSync(cand)) return cand;
20
+ const parent = dirname(dir);
21
+ if (parent === dir) break;
22
+ dir = parent;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ // Returns the briefing text for a SessionStart payload, or null (unmapped / unreadable).
28
+ export function preview(raw) {
29
+ let input; try { input = JSON.parse(raw); } catch { input = {}; }
30
+ const cwd = input?.cwd || process.cwd();
31
+ const graphPath = findGraph(cwd);
32
+ if (!graphPath) return null;
33
+ let graph; try { graph = normalizeGraph(JSON.parse(readFileSync(graphPath, 'utf8'))); } catch { return null; }
34
+ const brief = attachActivity(buildBrief(graph, buildIndex(graph)), graphPath);
35
+ const text = renderBrief(brief);
36
+ return `[codeweb] this repo is mapped (${graphPath}).\n${text}`;
37
+ }
38
+
39
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
40
+ let raw = '';
41
+ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
42
+ let msg = null;
43
+ try { msg = preview(raw); } catch { /* fail-open */ }
44
+ if (msg) {
45
+ try { const input = JSON.parse(raw); bump(findGraph(input?.cwd || process.cwd()), 'briefInjected'); } catch { /* receipt only */ }
46
+ try {
47
+ process.stdout.write(JSON.stringify({
48
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: msg },
49
+ }) + '\n');
50
+ } catch { /* ignore */ }
51
+ }
52
+ process.exit(0);
53
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@ghostlygawd/codeweb",
3
+ "version": "0.9.0",
4
+ "type": "module",
5
+ "description": "The living map of your codebase — deterministic call/import graph engine, 24 MCP tools for coding agents, and a self-contained interactive report.",
6
+ "keywords": [
7
+ "mcp",
8
+ "code-analysis",
9
+ "call-graph",
10
+ "dependency-graph",
11
+ "claude-code",
12
+ "agent-tools",
13
+ "dead-code",
14
+ "duplication"
15
+ ],
16
+ "homepage": "https://github.com/GhostlyGawd/codeweb#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/GhostlyGawd/codeweb.git"
20
+ },
21
+ "license": "MIT",
22
+ "bin": {
23
+ "codeweb": "scripts/run.mjs",
24
+ "codeweb-mcp": "scripts/mcp-server.mjs"
25
+ },
26
+ "files": [
27
+ "scripts",
28
+ "hooks",
29
+ "skills",
30
+ "agents",
31
+ "commands",
32
+ ".claude-plugin",
33
+ "CHANGELOG.md",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "test": "node --test \"tests/**/*.test.mjs\"",
44
+ "build:site": "node site/build.mjs",
45
+ "version-sync": "node scripts/version-sync.mjs",
46
+ "check-consistency": "node scripts/check-consistency.mjs",
47
+ "release": "node scripts/release.mjs",
48
+ "optimize": "node scripts/optimize.mjs",
49
+ "trend": "node scripts/trend.mjs",
50
+ "context-pack": "node scripts/context-pack.mjs",
51
+ "simulate-edit": "node scripts/simulate-edit.mjs",
52
+ "hotspots": "node scripts/hotspots.mjs",
53
+ "campaign": "node scripts/campaign.mjs",
54
+ "bench": "node scripts/bench.mjs",
55
+ "bench:all": "node bench/all.mjs",
56
+ "stats": "node scripts/stats.mjs",
57
+ "reading-order": "node scripts/reading-order.mjs",
58
+ "annotate": "node scripts/annotate.mjs",
59
+ "bench:ts-engine": "node scripts/bench-ts-engine.mjs"
60
+ },
61
+ "optionalDependencies": {
62
+ "web-tree-sitter": "0.26.9"
63
+ }
64
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ // codeweb annotate (F7) — record (or list) false-positive suppressions for finding fingerprints in
3
+ // <dir>/annotations.json (default .codeweb). This is the ONLY mutation in the suppression workflow and
4
+ // it writes to .codeweb metadata, NEVER to source. Idempotent by fingerprint. Built on
5
+ // ./lib/annotations.mjs. A finding's fingerprint comes from the tools that emit it (deadcode/overlap).
6
+ //
7
+ // Usage:
8
+ // node annotate.mjs --suppress <fingerprint> [--note "why"] [--dir <.codeweb>] [--json]
9
+ // node annotate.mjs --list [--dir <.codeweb>] [--json]
10
+ // Exit: 0 ok, 2 usage.
11
+
12
+ import { resolve } from 'node:path';
13
+ import { addSuppression, loadAnnotations } from './lib/annotations.mjs';
14
+
15
+ const USAGE = 'usage: annotate.mjs (--suppress <fingerprint> [--note "..."] | --list) [--dir <.codeweb>] [--json]';
16
+ import { die, emitJson, finish } from './lib/cli.mjs';
17
+
18
+ const argv = process.argv.slice(2);
19
+ let json = false, list = false, fp = null, note = '', dir = '.codeweb';
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const t = argv[i];
22
+ if (t === '--json') json = true;
23
+ else if (t === '--list') list = true;
24
+ else if (t === '--suppress') fp = argv[++i];
25
+ else if (t === '--note') note = argv[++i];
26
+ else if (t === '--dir') dir = argv[++i];
27
+ }
28
+ const annDir = resolve(dir);
29
+
30
+ if (list) {
31
+ const ann = loadAnnotations(annDir);
32
+ if (json) emitJson(ann);
33
+ else {
34
+ console.log(`codeweb annotations (${annDir}): ${ann.suppressions.length} suppression(s)`);
35
+ for (const s of ann.suppressions) console.log(` ${s.fingerprint} ${s.verdict}${s.note ? ` — ${s.note}` : ''}`);
36
+ finish(0);
37
+ }
38
+ } else {
39
+ if (!fp) die(USAGE, 2);
40
+ const ann = addSuppression(annDir, fp, { note, verdict: 'false-positive' });
41
+ if (json) emitJson({ dir: annDir, fingerprint: fp, suppressions: ann.suppressions.length });
42
+ else {
43
+ console.log(`codeweb annotate: suppressed ${fp} in ${annDir} (${ann.suppressions.length} total). Source untouched.`);
44
+ finish(0);
45
+ }
46
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // Benchmark the optional tree-sitter complexity tier END-TO-END vs the regex F4 default, on a real
3
+ // target. This is the "performance gate" from docs/backlog-ast-tree-sitter.md (risk #3): does the
4
+ // parse cost stay acceptable before the tier is ever considered for default-on?
5
+ //
6
+ // Usage: node scripts/bench-ts-engine.mjs [target-dir] (default: bench/corpus/axios)
7
+ // --no-ctags is forced so symbol discovery is identical on both sides; the ONLY variable is which
8
+ // engine computes complexity. Reports best-of-N wall time (subprocess) + per-symbol overhead.
9
+
10
+ import { spawnSync } from 'node:child_process';
11
+ import { existsSync, writeFileSync } from 'node:fs';
12
+ import { resolve } from 'node:path';
13
+
14
+ const argvAll = process.argv.slice(2);
15
+ let outPath = null;
16
+ const positional = [];
17
+ for (let i = 0; i < argvAll.length; i++) {
18
+ if (argvAll[i] === '--out') outPath = argvAll[++i];
19
+ else positional.push(argvAll[i]);
20
+ }
21
+ const target = positional[0] || 'bench/corpus/axios';
22
+ if (!existsSync(target)) { console.error(`[bench] target not found: ${target}`); process.exit(1); }
23
+ const EXTRACT = resolve('scripts/extract-symbols.mjs');
24
+ const REPS = 3;
25
+
26
+ function timeRun(extra) {
27
+ const t0 = performance.now();
28
+ const r = spawnSync(process.execPath, [EXTRACT, target, '--no-ctags', ...extra], { encoding: 'utf8', maxBuffer: 1 << 28 });
29
+ const ms = performance.now() - t0;
30
+ if (r.status !== 0) { console.error(r.stderr); throw new Error('[bench] extract failed'); }
31
+ const frag = JSON.parse(r.stdout);
32
+ return { ms, symbols: frag.nodes.length, withCx: frag.nodes.filter((n) => n.complexity != null).length, engine: frag.meta.complexityEngine || 'regex' };
33
+ }
34
+ const best = (extra) => { let b = null; for (let i = 0; i < REPS; i++) { const r = timeRun(extra); if (!b || r.ms < b.ms) b = r; } return b; };
35
+
36
+ console.log(`[bench] target: ${target} (best of ${REPS} runs each, --no-ctags)`);
37
+ // The tier is DEFAULT-ON since v9, so the regex arm must opt out explicitly — a no-flag arm
38
+ // would silently benchmark tree-sitter against itself (the bug this line fixes).
39
+ const regex = best(['--engine', 'regex']);
40
+ const ts = best(['--engine', 'tree-sitter']);
41
+ if (ts.engine === 'regex') { console.error('[bench] tree-sitter engine unavailable (optional dep not installed) — aborting'); process.exit(1); }
42
+
43
+ const overhead = ts.ms - regex.ms;
44
+ const perSym = ts.withCx > 0 ? overhead / ts.withCx : 0;
45
+ console.log(`[bench] symbols: ${regex.symbols} (${regex.withCx} with complexity)`);
46
+ console.log(`[bench] regex F4: ${regex.ms.toFixed(0).padStart(6)} ms`);
47
+ console.log(`[bench] tree-sitter: ${ts.ms.toFixed(0).padStart(6)} ms (${ts.engine})`);
48
+ console.log(`[bench] overhead: ${('+' + overhead.toFixed(0)).padStart(6)} ms = ${(ts.ms / regex.ms).toFixed(2)}x = +${perSym.toFixed(3)} ms/symbol`);
49
+ if (outPath) {
50
+ const payload = {
51
+ bench: 'ts-engine (cold extraction, regex vs tree-sitter tier)', target, reps: REPS,
52
+ symbols: regex.symbols, withComplexity: regex.withCx,
53
+ regexMs: Math.round(regex.ms), treeSitterMs: Math.round(ts.ms),
54
+ overheadMs: Math.round(overhead), factor: +(ts.ms / regex.ms).toFixed(2),
55
+ perSymbolMs: +perSym.toFixed(3), engine: ts.engine,
56
+ };
57
+ writeFileSync(outPath, JSON.stringify(payload, null, 2) + '\n');
58
+ console.log(`[bench] wrote ${outPath}`);
59
+ }