2026-11-15 18:55:46 +01:00
/**
* 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 è
2026-11-15 19:01:42 +01:00
* 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
2026-11-15 18:55:46 +01:00
* `/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" ;
2026-11-15 19:01:42 +01:00
type Nodo = {
id : string ;
label? : string ;
source_file? : string ;
source_location? : string ;
} & Record < string , unknown >;
2026-11-15 18:55:46 +01:00
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 , "" );
}
2026-11-15 19:01:42 +01:00
/**
* 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 ;
}
2026-11-15 18:55:46 +01:00
/** Archi `references` da un documento ai file e ai simboli di codice che cita. */
export function archiDocumento ( grafo : Grafo , doc : string , testo : string ) : Arco [] {
2026-11-15 19:01:42 +01:00
const idDoc = idNodoDocumento ( grafo , doc );
if ( ! idDoc ) return [];
2026-11-15 18:55:46 +01:00
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 ;
}
2026-11-15 19:01:42 +01:00
/** 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 ;
}
2026-11-15 18:55:46 +01:00
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 ) {
2026-11-15 19:01:42 +01:00
const testo = readFileSync ( doc , "utf8" );
const nuovi = [... archiDocumento ( grafo , doc , testo ), ... archiRfc ( grafo , doc , testo )]. filter (
2026-11-15 18:55:46 +01:00
( 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 ;
}
2026-11-15 19:01:42 +01:00
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 ++ ;
}
2026-11-15 18:55:46 +01:00
writeFileSync ( file , JSON . stringify ( grafo ));
console . log ( `graphify-link-docs: ${ aggiunti } archi da ${ conArchi } / ${ elenco . length } documenti` );
}