graphify-link-docs: RFC, concetti con nome uguale e nodo documento alternativo
Collega i documenti alle RFC che nominano, unisce con archi describes (INFERRED) i concetti che hanno lo stesso nome esatto di un simbolo, e trova il nodo del documento anche quando l'estrazione gli ha dato un id diverso (25 documenti, tra cui DD-026, prima restavano senza archi). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
916d3ab0d8
commit
d57f81c176
3 files changed
+178
-5
No files matched your search
@@ -62,6 +62,10 @@ stanno in quel file. Sono archi EXTRACTED: il percorso è scritto nel testo.
|
||||
bun scripts/graphify-link-docs.ts # idempotente, gratis
|
||||
```
|
||||
|
||||
Collega anche i documenti alle RFC che nominano (`RFC 8188/8291`) e unisce i concetti dei
|
||||
documenti che hanno lo stesso nome esatto di un simbolo del codice (`mediePagelle()`): questi
|
||||
ultimi sono archi `describes` INFERRED, perché si basano solo sul nome.
|
||||
|
||||
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.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* 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
|
||||
* scritto nel testo. Collega anche i documenti alle RFC che nominano (`RFC 8188/8291`) e i
|
||||
* concetti dei documenti che hanno lo stesso nome di un simbolo (`mediePagelle()`). 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
|
||||
@@ -12,7 +13,12 @@ import { join } from "node:path";
|
||||
|
||||
const MARCA = "graphify-link-docs";
|
||||
|
||||
type Nodo = { id: string; label?: string; source_file?: string; source_location?: string };
|
||||
type Nodo = {
|
||||
id: string;
|
||||
label?: string;
|
||||
source_file?: string;
|
||||
source_location?: string;
|
||||
} & Record<string, unknown>;
|
||||
type Arco = Record<string, unknown> & { source: string; target: string; _via?: string };
|
||||
type Grafo = { nodes: Nodo[]; links: Arco[] } & Record<string, unknown>;
|
||||
|
||||
@@ -42,10 +48,21 @@ export function idDocumento(percorso: string): string {
|
||||
.replace(/^_|_$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Il nodo che rappresenta il documento. Di solito ha l'id derivato dal percorso, ma
|
||||
* l'estrazione ne dà a volte uno diverso (`docs_decisions_dd_026_dd_026`): in quel caso vale il
|
||||
* nodo `document` di quel file, e se ce ne sono più d'uno il primo.
|
||||
*/
|
||||
export function idNodoDocumento(grafo: Grafo, doc: string): string | undefined {
|
||||
const id = idDocumento(doc);
|
||||
if (grafo.nodes.some((n) => n.id === id)) return id;
|
||||
return grafo.nodes.find((n) => n.source_file === doc && n["file_type"] === "document")?.id;
|
||||
}
|
||||
|
||||
/** 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 idDoc = idNodoDocumento(grafo, doc);
|
||||
if (!idDoc) return [];
|
||||
const archi: Arco[] = [];
|
||||
const base = (target: string, riga: number): Arco => ({
|
||||
source: idDoc,
|
||||
@@ -72,6 +89,78 @@ export function archiDocumento(grafo: Grafo, doc: string, testo: string): Arco[]
|
||||
return archi;
|
||||
}
|
||||
|
||||
/** Numeri di RFC nominati nel testo: «RFC 8291», «RFC-8291», «RFC 8188/8291». */
|
||||
export function estraiRfc(testo: string): Set<string> {
|
||||
const numeri = new Set<string>();
|
||||
for (const m of testo.matchAll(/RFC[ -]?(\d{4}(?:\/\d{4})*)/g))
|
||||
for (const n of (m[1] as string).split("/")) numeri.add(n);
|
||||
return numeri;
|
||||
}
|
||||
|
||||
const senzaParentesi = (etichetta: string) => etichetta.replace(/\(\)$/, "");
|
||||
const arcoDa = (
|
||||
source: string,
|
||||
target: string,
|
||||
doc: string,
|
||||
extra: Record<string, unknown>,
|
||||
): Arco => ({
|
||||
source,
|
||||
target,
|
||||
weight: 1.0,
|
||||
_origin: "semantic",
|
||||
_via: MARCA,
|
||||
source_file: doc,
|
||||
...extra,
|
||||
});
|
||||
|
||||
/** Archi dal documento ai nodi-RFC che nomina (le RFC sono nodi creati dall'estrazione). */
|
||||
export function archiRfc(grafo: Grafo, doc: string, testo: string): Arco[] {
|
||||
const idDoc = idNodoDocumento(grafo, doc);
|
||||
if (!idDoc) return [];
|
||||
const numeri = estraiRfc(testo);
|
||||
return grafo.nodes
|
||||
.filter((n) => n.label?.startsWith("RFC-") && numeri.has(n.label.slice(4)))
|
||||
.map((n) =>
|
||||
arcoDa(idDoc, n.id, doc, {
|
||||
relation: "references",
|
||||
confidence: "EXTRACTED",
|
||||
confidence_score: 1.0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concetti dei documenti con lo stesso nome di un simbolo del codice (`mediePagelle()` nel
|
||||
* modulo, `mediePagelle` in `pagelle.ts`): l'arco `describes` li unisce. Solo corrispondenze
|
||||
* esatte e uniche; è un'inferenza sul nome, quindi INFERRED.
|
||||
*/
|
||||
export function archiConcetti(
|
||||
grafo: Grafo & { nodes: Array<Nodo & Record<string, unknown>> },
|
||||
): Arco[] {
|
||||
const codice = new Map<string, Nodo[]>();
|
||||
for (const n of grafo.nodes)
|
||||
if (n["_origin"] === "ast" && n["file_type"] === "code" && n.label) {
|
||||
const k = senzaParentesi(n.label);
|
||||
codice.set(k, [...(codice.get(k) ?? []), n]);
|
||||
}
|
||||
const archi: Arco[] = [];
|
||||
for (const n of grafo.nodes) {
|
||||
if (n["_origin"] === "ast" || !n.label || !n.source_file?.endsWith(".md")) continue;
|
||||
if (!["concept", "rationale", "code"].includes(n["file_type"] as string)) continue;
|
||||
const trovati = codice.get(senzaParentesi(n.label));
|
||||
if (trovati?.length !== 1) continue;
|
||||
archi.push(
|
||||
arcoDa(n.id, (trovati[0] as Nodo).id, n.source_file, {
|
||||
relation: "describes",
|
||||
confidence: "INFERRED",
|
||||
confidence_score: 0.95,
|
||||
source_location: n.source_location ?? "L1",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return archi;
|
||||
}
|
||||
|
||||
function documenti(dir: string, fuori: string[] = []): string[] {
|
||||
return readdirSync(dir).flatMap((nome) => {
|
||||
const p = join(dir, nome);
|
||||
@@ -96,7 +185,8 @@ if (import.meta.main) {
|
||||
let aggiunti = 0;
|
||||
let conArchi = 0;
|
||||
for (const doc of elenco) {
|
||||
const nuovi = archiDocumento(grafo, doc, readFileSync(doc, "utf8")).filter(
|
||||
const testo = readFileSync(doc, "utf8");
|
||||
const nuovi = [...archiDocumento(grafo, doc, testo), ...archiRfc(grafo, doc, testo)].filter(
|
||||
(a) => !gia.has(`${a.source}>${a.target}`),
|
||||
);
|
||||
for (const a of nuovi) gia.add(`${a.source}>${a.target}`);
|
||||
@@ -104,6 +194,12 @@ if (import.meta.main) {
|
||||
grafo.links.push(...nuovi);
|
||||
aggiunti += nuovi.length;
|
||||
}
|
||||
for (const a of archiConcetti(grafo)) {
|
||||
if (gia.has(`${a.source}>${a.target}`)) continue;
|
||||
gia.add(`${a.source}>${a.target}`);
|
||||
grafo.links.push(a);
|
||||
aggiunti++;
|
||||
}
|
||||
writeFileSync(file, JSON.stringify(grafo));
|
||||
console.log(`graphify-link-docs: ${aggiunti} archi da ${conArchi}/${elenco.length} documenti`);
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
/** Check del collegamento documenti ↔ codice nel grafo: `bun test/unit/graphify-link-docs.test.ts`. */
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
archiConcetti,
|
||||
archiDocumento,
|
||||
archiRfc,
|
||||
estraiRfc,
|
||||
estraiPercorsi,
|
||||
estraiSimboli,
|
||||
idDocumento,
|
||||
@@ -68,4 +71,74 @@ assert.deepEqual(
|
||||
"file non nel grafo",
|
||||
);
|
||||
|
||||
// --- RFC: tutte le forme, anche «8188/8291» ------------------------------------
|
||||
assert.deepEqual([...estraiRfc("cifrata (RFC 8188/8291), poi RFC-8030 e RFC 123")].sort(), [
|
||||
"8030",
|
||||
"8188",
|
||||
"8291",
|
||||
]);
|
||||
const conRfc = {
|
||||
nodes: [
|
||||
{ id: "docs_decisions_dd_026", label: "DD" },
|
||||
{ id: "r8291", label: "RFC-8291" },
|
||||
{ id: "r8030", label: "RFC-8030" },
|
||||
],
|
||||
links: [],
|
||||
};
|
||||
assert.deepEqual(
|
||||
archiRfc(conRfc, "docs/decisions/DD-026.md", "cifrata (RFC 8188/8291)").map((a) => a.target),
|
||||
["r8291"],
|
||||
"solo le RFC che esistono come nodi",
|
||||
);
|
||||
|
||||
// --- concetti: nome esatto e unico, solo dai documenti --------------------------
|
||||
const conc = {
|
||||
nodes: [
|
||||
{
|
||||
id: "c1",
|
||||
label: "mediePagelle()",
|
||||
source_file: "docs/modules/pagelle.md",
|
||||
file_type: "concept",
|
||||
},
|
||||
{
|
||||
id: "k1",
|
||||
label: "mediePagelle()",
|
||||
source_file: "src/lib/pagelle.ts",
|
||||
file_type: "code",
|
||||
_origin: "ast",
|
||||
},
|
||||
{ id: "c2", label: "Altra cosa", source_file: "docs/modules/pagelle.md", file_type: "concept" },
|
||||
{ id: "c3", label: "dup()", source_file: "docs/x.md", file_type: "concept" },
|
||||
{ id: "k2", label: "dup()", source_file: "src/a.ts", file_type: "code", _origin: "ast" },
|
||||
{ id: "k3", label: "dup()", source_file: "src/b.ts", file_type: "code", _origin: "ast" },
|
||||
],
|
||||
links: [],
|
||||
};
|
||||
const ac = archiConcetti(conc);
|
||||
assert.deepEqual(
|
||||
ac.map((a) => [a.source, a.target]),
|
||||
[["c1", "k1"]],
|
||||
"esatto e unico; ambiguo scartato",
|
||||
);
|
||||
assert.equal(ac[0]?.["confidence"], "INFERRED");
|
||||
|
||||
// --- nodo-documento con id diverso da quello derivato dal percorso -------------
|
||||
const altro = {
|
||||
nodes: [
|
||||
{
|
||||
id: "docs_decisions_dd_026_dd_026",
|
||||
label: "DD-026",
|
||||
source_file: "docs/decisions/DD-026.md",
|
||||
file_type: "document",
|
||||
},
|
||||
{ id: "src_lib_rosa", label: "rosa.ts", source_file: "src/lib/rosa.ts", source_location: "L1" },
|
||||
],
|
||||
links: [],
|
||||
};
|
||||
assert.equal(
|
||||
archiDocumento(altro, "docs/decisions/DD-026.md", "src/lib/rosa.ts")[0]?.source,
|
||||
"docs_decisions_dd_026_dd_026",
|
||||
"usa il nodo document del file se l'id derivato non esiste",
|
||||
);
|
||||
|
||||
console.log("graphify-link-docs: ok");
|
||||
Reference in new issue
Block a user