Grafo: collega documenti e codice, esclude .claude/

scripts/graphify-link-docs.ts aggiunge gli archi references dai percorsi e dai
simboli citati nei .md ai file e ai simboli del codice. .graphifyignore toglie
le skill dal grafo. Documentato in docs/GRAFO.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-11-15 18:55:46 +01:00
1 parent cd4761418e
commit f9c6263662
4 files changed
+198

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Skill e impostazioni di Claude Code: non sono documentazione del progetto
.claude/
+16
View File
@@ -50,6 +50,22 @@ In Claude Code: `/graphify query "..."`, oppure leggere `graphify-out/GRAPH_REPO
Il grafo riporta il commit da cui è stato costruito (`GRAPH_REPORT.md`, sezione «Graph
Freshness»): confrontalo con `git rev-parse HEAD` per sapere se è vecchio.
## Collegamento documenti ↔ codice
L'estrazione semantica dei documenti crea pochissimi archi verso il codice, anche quando il
documento cita il file. Lo script [scripts/graphify-link-docs.ts](../scripts/graphify-link-docs.ts)
li aggiunge in modo deterministico: per ogni percorso citato nei `.md` (`src/lib/rosa.ts`)
crea l'arco `references` dal documento al file, e ai simboli citati tra backtick (`useIo`) se
stanno in quel file. Sono archi EXTRACTED: il percorso è scritto nel testo.
```bash
bun scripts/graphify-link-docs.ts # idempotente, gratis
```
Va rilanciato dopo ogni `/graphify . --update`, perché rielaborare un documento ne cancella
gli archi. Sopravvive invece a `graphify update .`. Un documento che cita un file solo per nome
(`rosa.ts`, senza `src/lib/`) non viene collegato: scrivi il percorso completo.
Nota: `graphify update .` su un grafo già completo conserva i nodi semantici dei documenti
(verificato). I `.md` nuovi o modificati entrano però solo come struttura (titoli e sezioni):
per l'estrazione semantica serve `/graphify . --update`, che rielabora solo quei file.
+109
View File
@@ -0,0 +1,109 @@
/**
* Collega i documenti al codice nel grafo graphify: dove un `.md` cita un file (`src/lib/rosa.ts`)
* aggiunge l'arco `references` dal documento al file, e dal documento ai simboli che cita in
* backtick (`useIo`) se stanno in uno dei file citati. Gli archi sono EXTRACTED: il percorso è
* scritto nel testo. Idempotente: rimuove i propri archi e li rifà. Va rilanciato dopo ogni
* `/graphify . --update` (rielabora i documenti e perde gli archi di quelli cambiati).
*
* bun scripts/graphify-link-docs.ts # aggiorna graphify-out/graph.json
*/
import { readFileSync, readdirSync, writeFileSync, existsSync, statSync } from "node:fs";
import { join } from "node:path";
const MARCA = "graphify-link-docs";
type Nodo = { id: string; label?: string; source_file?: string; source_location?: string };
type Arco = Record<string, unknown> & { source: string; target: string; _via?: string };
type Grafo = { nodes: Nodo[]; links: Arco[] } & Record<string, unknown>;
/** Percorsi di file del repo citati nel testo, con la prima riga in cui compaiono. */
export function estraiPercorsi(testo: string): Map<string, number> {
const trovati = new Map<string, number>();
const re = /\b(?:src|test|supabase|mailer|public)\/[\w./$@-]+?\.(?:tsx?|mjs|js|sql)\b/g;
testo.split("\n").forEach((riga, i) => {
for (const m of riga.matchAll(re)) if (!trovati.has(m[0])) trovati.set(m[0], i + 1);
});
return trovati;
}
/** Identificatori citati tra backtick (`useIo`, `inRosa()`), senza parentesi. */
export function estraiSimboli(testo: string): Set<string> {
const s = new Set<string>();
for (const m of testo.matchAll(/`([A-Za-z_$][\w$]*)(?:\(\))?`/g)) s.add(m[1] as string);
return s;
}
/** Id del nodo-documento, come lo costruisce graphify: percorso senza estensione, in minuscolo. */
export function idDocumento(percorso: string): string {
return percorso
.replace(/\.[^./]+$/, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_|_$/g, "");
}
/** Archi `references` da un documento ai file e ai simboli di codice che cita. */
export function archiDocumento(grafo: Grafo, doc: string, testo: string): Arco[] {
const idDoc = idDocumento(doc);
if (!grafo.nodes.some((n) => n.id === idDoc)) return [];
const archi: Arco[] = [];
const base = (target: string, riga: number): Arco => ({
source: idDoc,
target,
relation: "references",
confidence: "EXTRACTED",
confidence_score: 1.0,
weight: 1.0,
_origin: "semantic",
_via: MARCA,
source_file: doc,
source_location: `L${riga}`,
});
const simboli = estraiSimboli(testo);
for (const [percorso, riga] of estraiPercorsi(testo)) {
const file = grafo.nodes.find((n) => n.source_file === percorso && n.source_location === "L1");
if (!file) continue;
archi.push(base(file.id, riga));
for (const n of grafo.nodes) {
if (n.source_file !== percorso || n.id === file.id) continue;
if (n.label && simboli.has(n.label.replace(/\(\)$/, ""))) archi.push(base(n.id, riga));
}
}
return archi;
}
function documenti(dir: string, fuori: string[] = []): string[] {
return readdirSync(dir).flatMap((nome) => {
const p = join(dir, nome);
if (fuori.some((f) => p.startsWith(f))) return [];
return statSync(p).isDirectory() ? documenti(p, fuori) : p.endsWith(".md") ? [p] : [];
});
}
if (import.meta.main) {
const file = "graphify-out/graph.json";
if (!existsSync(file))
throw new Error("graphify-out/graph.json assente: lancia `graphify update .`");
const grafo: Grafo = JSON.parse(readFileSync(file, "utf8"));
grafo.links = grafo.links.filter((a) => a._via !== MARCA);
const gia = new Set(grafo.links.map((a) => `${a.source}>${a.target}`));
const elenco = [
...documenti("docs", ["docs/archive"]),
"AGENTS.md",
"PROJECT_STATE.md",
"README.md",
];
let aggiunti = 0;
let conArchi = 0;
for (const doc of elenco) {
const nuovi = archiDocumento(grafo, doc, readFileSync(doc, "utf8")).filter(
(a) => !gia.has(`${a.source}>${a.target}`),
);
for (const a of nuovi) gia.add(`${a.source}>${a.target}`);
if (nuovi.length) conArchi++;
grafo.links.push(...nuovi);
aggiunti += nuovi.length;
}
writeFileSync(file, JSON.stringify(grafo));
console.log(`graphify-link-docs: ${aggiunti} archi da ${conArchi}/${elenco.length} documenti`);
}
+71
View File
@@ -0,0 +1,71 @@
/** Check del collegamento documenti ↔ codice nel grafo: `bun test/unit/graphify-link-docs.test.ts`. */
import assert from "node:assert/strict";
import {
archiDocumento,
estraiPercorsi,
estraiSimboli,
idDocumento,
} from "../../scripts/graphify-link-docs";
// --- percorsi: solo file del repo, con la prima riga in cui compaiono ---------
const testo =
"Intro\nVedi `src/lib/rosa.ts` e src/routes/partita.$id.tsx.\nDi nuovo src/lib/rosa.ts, www.x.it/a.ts";
const p = estraiPercorsi(testo);
assert.deepEqual([...p.keys()], ["src/lib/rosa.ts", "src/routes/partita.$id.tsx"]);
assert.equal(p.get("src/lib/rosa.ts"), 2, "tiene la prima riga");
// --- simboli: solo tra backtick, senza parentesi ------------------------------
assert.deepEqual(
[...estraiSimboli("`useIo` e `inRosa()` ma non useRosa né `a b`")],
["useIo", "inRosa"],
);
// --- id del documento come lo costruisce graphify -----------------------------
assert.equal(idDocumento("docs/modules/allenatore.md"), "docs_modules_allenatore");
assert.equal(idDocumento("docs/decisions/DD-034.md"), "docs_decisions_dd_034");
// --- archi: file citato + simboli citati che ci stanno dentro -----------------
const grafo = {
nodes: [
{ id: "docs_modules_allenatore", label: "Modulo" },
{ id: "src_lib_rosa", label: "rosa.ts", source_file: "src/lib/rosa.ts", source_location: "L1" },
{
id: "src_lib_rosa_useio",
label: "useIo()",
source_file: "src/lib/rosa.ts",
source_location: "L160",
},
{
id: "src_lib_rosa_altro",
label: "altro()",
source_file: "src/lib/rosa.ts",
source_location: "L9",
},
{ id: "src_lib_x", label: "x.ts", source_file: "src/lib/x.ts", source_location: "L1" },
],
links: [],
};
const archi = archiDocumento(
grafo,
"docs/modules/allenatore.md",
"File: `src/lib/rosa.ts` (`useIo`)",
);
assert.deepEqual(
archi.map((a) => a.target),
["src_lib_rosa", "src_lib_rosa_useio"],
"il file e solo il simbolo citato; non altro() né file non citati",
);
assert.equal(archi[0]?.source, "docs_modules_allenatore");
assert.equal(archi[0]?.["confidence"], "EXTRACTED");
assert.deepEqual(
archiDocumento(grafo, "docs/non-nel-grafo.md", "src/lib/rosa.ts"),
[],
"documento ignoto: nessun arco",
);
assert.deepEqual(
archiDocumento(grafo, "docs/modules/allenatore.md", "src/lib/inesistente.ts"),
[],
"file non nel grafo",
);
console.log("graphify-link-docs: ok");