@gmickel/gno 1.4.2 → 1.5.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.
- package/assets/skill/SKILL.md +11 -1
- package/assets/skill/cli-reference.md +9 -1
- package/assets/skill/mcp-reference.md +16 -0
- package/package.json +1 -1
- package/src/cli/commands/graph.ts +237 -4
- package/src/cli/program.ts +31 -0
- package/src/core/graph-analysis.ts +201 -0
- package/src/mcp/tools/index.ts +58 -4
- package/src/mcp/tools/links.ts +304 -7
- package/src/mcp/tools/query.ts +2 -0
- package/src/pipeline/explain.ts +2 -0
- package/src/pipeline/fusion.ts +28 -0
- package/src/pipeline/graph-retrieval.ts +368 -0
- package/src/pipeline/hybrid.ts +45 -3
- package/src/pipeline/types.ts +18 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/GraphView.tsx +237 -16
- package/src/serve/public/pages/Search.tsx +1 -0
- package/src/serve/routes/api.ts +2 -0
- package/src/store/sqlite/adapter.ts +343 -16
- package/src/store/types.ts +120 -0
|
@@ -30,6 +30,10 @@ import type {
|
|
|
30
30
|
FtsResult,
|
|
31
31
|
FtsSearchOptions,
|
|
32
32
|
GetGraphOptions,
|
|
33
|
+
GraphEdgeConfidence,
|
|
34
|
+
GraphEdgeAudit,
|
|
35
|
+
GraphLinkType,
|
|
36
|
+
GraphReportNode,
|
|
33
37
|
GraphResult,
|
|
34
38
|
IndexStatus,
|
|
35
39
|
IngestErrorInput,
|
|
@@ -45,6 +49,7 @@ import type {
|
|
|
45
49
|
import type { SqliteDbProvider } from "./types";
|
|
46
50
|
|
|
47
51
|
import { buildUri, deriveDocid, stripUriIndex } from "../../app/constants";
|
|
52
|
+
import { analyzeGraphCommunities } from "../../core/graph-analysis";
|
|
48
53
|
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
49
54
|
import { migrations, runMigrations } from "../migrations";
|
|
50
55
|
import { err, ok } from "../types";
|
|
@@ -807,6 +812,59 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
807
812
|
}
|
|
808
813
|
}
|
|
809
814
|
|
|
815
|
+
async getDocumentsByDocids(
|
|
816
|
+
docids: string[],
|
|
817
|
+
options: {
|
|
818
|
+
collection?: string;
|
|
819
|
+
activeOnly?: boolean;
|
|
820
|
+
} = {}
|
|
821
|
+
): Promise<StoreResult<DocumentRow[]>> {
|
|
822
|
+
try {
|
|
823
|
+
if (docids.length === 0) {
|
|
824
|
+
return ok([]);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const uniqueDocids = [
|
|
828
|
+
...new Set(docids.filter((docid) => docid.trim().length > 0)),
|
|
829
|
+
];
|
|
830
|
+
if (uniqueDocids.length === 0) {
|
|
831
|
+
return ok([]);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const db = this.ensureOpen();
|
|
835
|
+
const rows: DbDocumentRow[] = [];
|
|
836
|
+
const SQL_PARAM_LIMIT = options.collection ? 899 : 900;
|
|
837
|
+
|
|
838
|
+
for (let i = 0; i < uniqueDocids.length; i += SQL_PARAM_LIMIT) {
|
|
839
|
+
const batch = uniqueDocids.slice(i, i + SQL_PARAM_LIMIT);
|
|
840
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
841
|
+
const clauses = [`docid IN (${placeholders})`];
|
|
842
|
+
const params: string[] = [...batch];
|
|
843
|
+
|
|
844
|
+
if (options.activeOnly ?? true) {
|
|
845
|
+
clauses.push("active = 1");
|
|
846
|
+
}
|
|
847
|
+
if (options.collection) {
|
|
848
|
+
clauses.push("collection = ?");
|
|
849
|
+
params.push(options.collection);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const sql = `SELECT * FROM documents WHERE ${clauses.join(" AND ")} ORDER BY id`;
|
|
853
|
+
rows.push(...db.query<DbDocumentRow, string[]>(sql).all(...params));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return ok(rows.map(mapDocumentRow));
|
|
857
|
+
} catch (cause) {
|
|
858
|
+
return err(
|
|
859
|
+
"QUERY_FAILED",
|
|
860
|
+
cause instanceof Error
|
|
861
|
+
? cause.message
|
|
862
|
+
: "Failed to get documents by docids",
|
|
863
|
+
cause
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
810
868
|
async listDocumentsPaginated(options: {
|
|
811
869
|
collection?: string;
|
|
812
870
|
limit: number;
|
|
@@ -2333,17 +2391,43 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2333
2391
|
)
|
|
2334
2392
|
ORDER BY t.id LIMIT 1
|
|
2335
2393
|
`;
|
|
2394
|
+
|
|
2395
|
+
const wikiBestRank = (
|
|
2396
|
+
collectionExpr: string,
|
|
2397
|
+
targetRefExpr: string
|
|
2398
|
+
): string => `
|
|
2399
|
+
SELECT MIN(${wikiOrder("t", targetRefExpr)}) FROM documents t
|
|
2400
|
+
WHERE t.active = 1
|
|
2401
|
+
AND t.collection = ${collectionExpr}
|
|
2402
|
+
AND ${wikiMatch("t", targetRefExpr)}
|
|
2403
|
+
`;
|
|
2404
|
+
|
|
2405
|
+
const wikiBestRankMatchCount = (
|
|
2406
|
+
collectionExpr: string,
|
|
2407
|
+
targetRefExpr: string
|
|
2408
|
+
): string => `
|
|
2409
|
+
SELECT COUNT(*) FROM documents t
|
|
2410
|
+
WHERE t.active = 1
|
|
2411
|
+
AND t.collection = ${collectionExpr}
|
|
2412
|
+
AND ${wikiMatch("t", targetRefExpr)}
|
|
2413
|
+
AND ${wikiOrder("t", targetRefExpr)} = (${wikiBestRank(
|
|
2414
|
+
collectionExpr,
|
|
2415
|
+
targetRefExpr
|
|
2416
|
+
)})
|
|
2417
|
+
`;
|
|
2336
2418
|
interface ResolvedEdgeRow {
|
|
2337
2419
|
source_id: number;
|
|
2338
2420
|
source_docid: string;
|
|
2339
2421
|
target_id: number;
|
|
2340
2422
|
target_docid: string;
|
|
2341
2423
|
link_type: "wiki" | "markdown";
|
|
2342
|
-
|
|
2424
|
+
match_rank: number | null;
|
|
2425
|
+
match_count: number | null;
|
|
2343
2426
|
}
|
|
2344
2427
|
|
|
2345
|
-
interface
|
|
2346
|
-
|
|
2428
|
+
interface UnresolvedByTypeRow {
|
|
2429
|
+
link_type: "wiki" | "markdown";
|
|
2430
|
+
unresolved: number;
|
|
2347
2431
|
}
|
|
2348
2432
|
|
|
2349
2433
|
interface NodeMetaRow {
|
|
@@ -2369,7 +2453,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2369
2453
|
tgt.id as target_id,
|
|
2370
2454
|
tgt.docid as target_docid,
|
|
2371
2455
|
dl.link_type,
|
|
2372
|
-
|
|
2456
|
+
CASE dl.link_type
|
|
2457
|
+
WHEN 'wiki' THEN (${wikiBestRank(
|
|
2458
|
+
"COALESCE(dl.target_collection, src.collection)",
|
|
2459
|
+
"dl.target_ref_norm"
|
|
2460
|
+
)})
|
|
2461
|
+
WHEN 'markdown' THEN 5
|
|
2462
|
+
END as match_rank,
|
|
2463
|
+
CASE dl.link_type
|
|
2464
|
+
WHEN 'wiki' THEN (${wikiBestRankMatchCount(
|
|
2465
|
+
"COALESCE(dl.target_collection, src.collection)",
|
|
2466
|
+
"dl.target_ref_norm"
|
|
2467
|
+
)})
|
|
2468
|
+
WHEN 'markdown' THEN 1
|
|
2469
|
+
END as match_count
|
|
2373
2470
|
FROM documents src
|
|
2374
2471
|
JOIN doc_links dl ON dl.source_doc_id = src.id
|
|
2375
2472
|
JOIN documents tgt ON tgt.id = CASE dl.link_type
|
|
@@ -2387,8 +2484,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2387
2484
|
END
|
|
2388
2485
|
WHERE src.active = 1 AND tgt.active = 1
|
|
2389
2486
|
${edgeCollectionClause}
|
|
2390
|
-
|
|
2391
|
-
ORDER BY weight DESC, src.id ASC, tgt.id ASC
|
|
2487
|
+
ORDER BY src.id ASC, tgt.id ASC, dl.link_type ASC
|
|
2392
2488
|
`;
|
|
2393
2489
|
|
|
2394
2490
|
const resolvedEdgeRows = db
|
|
@@ -2403,9 +2499,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2403
2499
|
}
|
|
2404
2500
|
const unresolvedQuery = `
|
|
2405
2501
|
SELECT
|
|
2406
|
-
|
|
2502
|
+
link_type,
|
|
2503
|
+
COUNT(*) as unresolved
|
|
2407
2504
|
FROM (
|
|
2408
2505
|
SELECT
|
|
2506
|
+
dl.link_type,
|
|
2409
2507
|
CASE dl.link_type
|
|
2410
2508
|
WHEN 'wiki' THEN (
|
|
2411
2509
|
${wikiBestMatch(
|
|
@@ -2426,11 +2524,21 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2426
2524
|
WHERE src.active = 1
|
|
2427
2525
|
${unresolvedCollectionClause}
|
|
2428
2526
|
)
|
|
2527
|
+
WHERE target_id IS NULL
|
|
2528
|
+
GROUP BY link_type
|
|
2429
2529
|
`;
|
|
2430
|
-
const
|
|
2431
|
-
.query<
|
|
2432
|
-
.
|
|
2433
|
-
const
|
|
2530
|
+
const unresolvedRows = db
|
|
2531
|
+
.query<UnresolvedByTypeRow, string[]>(unresolvedQuery)
|
|
2532
|
+
.all(...unresolvedParams);
|
|
2533
|
+
const unresolvedByType: Record<"wiki" | "markdown", number> = {
|
|
2534
|
+
wiki: 0,
|
|
2535
|
+
markdown: 0,
|
|
2536
|
+
};
|
|
2537
|
+
for (const row of unresolvedRows) {
|
|
2538
|
+
unresolvedByType[row.link_type] = row.unresolved;
|
|
2539
|
+
}
|
|
2540
|
+
const totalEdgesUnresolved =
|
|
2541
|
+
unresolvedByType.wiki + unresolvedByType.markdown;
|
|
2434
2542
|
|
|
2435
2543
|
const outNeighbors = new Map<number, Set<number>>();
|
|
2436
2544
|
const inNeighbors = new Map<number, Set<number>>();
|
|
@@ -2481,6 +2589,46 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2481
2589
|
.filter((row): row is NodeMetaRow & { degree: number } => row !== null)
|
|
2482
2590
|
.sort((a, b) => b.degree - a.degree || a.id - b.id);
|
|
2483
2591
|
|
|
2592
|
+
const toReportNode = (
|
|
2593
|
+
row: NodeMetaRow & { degree: number }
|
|
2594
|
+
): GraphReportNode => ({
|
|
2595
|
+
id: row.docid,
|
|
2596
|
+
uri: row.uri,
|
|
2597
|
+
title: row.title,
|
|
2598
|
+
collection: row.collection,
|
|
2599
|
+
relPath: row.rel_path,
|
|
2600
|
+
degree: row.degree,
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2603
|
+
const isolatedBaseParams: (string | number)[] = [];
|
|
2604
|
+
const isolatedBaseConditions = ["active = 1"];
|
|
2605
|
+
if (collection) {
|
|
2606
|
+
isolatedBaseConditions.push("collection = ?");
|
|
2607
|
+
isolatedBaseParams.push(collection);
|
|
2608
|
+
}
|
|
2609
|
+
if (connectedIdList.length > 0) {
|
|
2610
|
+
const placeholders = connectedIdList.map(() => "?").join(",");
|
|
2611
|
+
isolatedBaseConditions.push(`id NOT IN (${placeholders})`);
|
|
2612
|
+
isolatedBaseParams.push(...connectedIdList);
|
|
2613
|
+
}
|
|
2614
|
+
const isolatedWhereClause = isolatedBaseConditions.join(" AND ");
|
|
2615
|
+
const isolatedCountRow = db
|
|
2616
|
+
.query<{ cnt: number }, (string | number)[]>(
|
|
2617
|
+
`SELECT COUNT(*) as cnt FROM documents WHERE ${isolatedWhereClause}`
|
|
2618
|
+
)
|
|
2619
|
+
.get(...isolatedBaseParams);
|
|
2620
|
+
const isolatedTotal = isolatedCountRow?.cnt ?? 0;
|
|
2621
|
+
const isolatedExampleRows = db
|
|
2622
|
+
.query<NodeMetaRow, (string | number)[]>(
|
|
2623
|
+
`SELECT id, docid, uri, title, collection, rel_path
|
|
2624
|
+
FROM documents
|
|
2625
|
+
WHERE ${isolatedWhereClause}
|
|
2626
|
+
ORDER BY id ASC
|
|
2627
|
+
LIMIT 10`
|
|
2628
|
+
)
|
|
2629
|
+
.all(...isolatedBaseParams)
|
|
2630
|
+
.map((row) => ({ ...row, degree: 0 }));
|
|
2631
|
+
|
|
2484
2632
|
let totalNodes = linkedOnly ? connectedNodes.length : 0;
|
|
2485
2633
|
if (!linkedOnly) {
|
|
2486
2634
|
const countParams: string[] = [];
|
|
@@ -2542,9 +2690,95 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2542
2690
|
const selectedDocids = new Set(nodes.map((node) => node.id));
|
|
2543
2691
|
const nodeDocids = new Set(nodes.map((node) => node.id));
|
|
2544
2692
|
|
|
2693
|
+
const confidenceRank: Record<GraphEdgeConfidence, number> = {
|
|
2694
|
+
explicit: 1,
|
|
2695
|
+
inferred: 2,
|
|
2696
|
+
ambiguous: 3,
|
|
2697
|
+
similarity: 4,
|
|
2698
|
+
};
|
|
2699
|
+
const classifyResolvedEdge = (
|
|
2700
|
+
linkType: "wiki" | "markdown",
|
|
2701
|
+
matchRank: number | null,
|
|
2702
|
+
matchCount: number | null
|
|
2703
|
+
): { confidence: GraphEdgeConfidence; audit: GraphEdgeAudit } => {
|
|
2704
|
+
if (linkType === "markdown") {
|
|
2705
|
+
return {
|
|
2706
|
+
confidence: "explicit",
|
|
2707
|
+
audit: { resolution: "exact-path", matchCount: 1 },
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
const count = matchCount ?? 0;
|
|
2712
|
+
if (count > 1) {
|
|
2713
|
+
return {
|
|
2714
|
+
confidence: "ambiguous",
|
|
2715
|
+
audit: {
|
|
2716
|
+
resolution: "ambiguous-fallback",
|
|
2717
|
+
matchCount: count,
|
|
2718
|
+
},
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
if (matchRank === 1 || matchRank === 2) {
|
|
2723
|
+
return {
|
|
2724
|
+
confidence: "explicit",
|
|
2725
|
+
audit: { resolution: "exact-title", matchCount: count || 1 },
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
if (matchRank === 5 || matchRank === 6) {
|
|
2729
|
+
return {
|
|
2730
|
+
confidence: "explicit",
|
|
2731
|
+
audit: { resolution: "exact-path", matchCount: count || 1 },
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
return {
|
|
2736
|
+
confidence: "inferred",
|
|
2737
|
+
audit: { resolution: "path-fallback", matchCount: count || 1 },
|
|
2738
|
+
};
|
|
2739
|
+
};
|
|
2740
|
+
const mergeAudit = (
|
|
2741
|
+
current: {
|
|
2742
|
+
type: GraphLinkType;
|
|
2743
|
+
weight: number;
|
|
2744
|
+
confidence: GraphEdgeConfidence;
|
|
2745
|
+
audit: GraphEdgeAudit;
|
|
2746
|
+
},
|
|
2747
|
+
nextConfidence: GraphEdgeConfidence,
|
|
2748
|
+
nextAudit: GraphEdgeAudit
|
|
2749
|
+
): void => {
|
|
2750
|
+
if (
|
|
2751
|
+
confidenceRank[nextConfidence] < confidenceRank[current.confidence]
|
|
2752
|
+
) {
|
|
2753
|
+
current.confidence = nextConfidence;
|
|
2754
|
+
current.audit = {
|
|
2755
|
+
...nextAudit,
|
|
2756
|
+
matchCount: Math.max(
|
|
2757
|
+
current.audit.matchCount ?? 0,
|
|
2758
|
+
nextAudit.matchCount ?? 0
|
|
2759
|
+
),
|
|
2760
|
+
};
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
if (
|
|
2764
|
+
nextAudit.matchCount !== undefined &&
|
|
2765
|
+
(current.audit.matchCount ?? 0) < nextAudit.matchCount
|
|
2766
|
+
) {
|
|
2767
|
+
current.audit = {
|
|
2768
|
+
...current.audit,
|
|
2769
|
+
matchCount: nextAudit.matchCount,
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
|
|
2545
2774
|
const edgeMap = new Map<
|
|
2546
2775
|
string,
|
|
2547
|
-
{
|
|
2776
|
+
{
|
|
2777
|
+
type: GraphLinkType;
|
|
2778
|
+
weight: number;
|
|
2779
|
+
confidence: GraphEdgeConfidence;
|
|
2780
|
+
audit: GraphEdgeAudit;
|
|
2781
|
+
}
|
|
2548
2782
|
>();
|
|
2549
2783
|
for (const row of resolvedEdgeRows) {
|
|
2550
2784
|
if (
|
|
@@ -2554,7 +2788,23 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2554
2788
|
continue;
|
|
2555
2789
|
}
|
|
2556
2790
|
const key = `${row.source_docid}:${row.target_docid}:${row.link_type}`;
|
|
2557
|
-
|
|
2791
|
+
const { confidence, audit } = classifyResolvedEdge(
|
|
2792
|
+
row.link_type,
|
|
2793
|
+
row.match_rank,
|
|
2794
|
+
row.match_count
|
|
2795
|
+
);
|
|
2796
|
+
const existing = edgeMap.get(key);
|
|
2797
|
+
if (existing) {
|
|
2798
|
+
existing.weight += 1;
|
|
2799
|
+
mergeAudit(existing, confidence, audit);
|
|
2800
|
+
} else {
|
|
2801
|
+
edgeMap.set(key, {
|
|
2802
|
+
type: row.link_type,
|
|
2803
|
+
weight: 1,
|
|
2804
|
+
confidence,
|
|
2805
|
+
audit,
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2558
2808
|
}
|
|
2559
2809
|
|
|
2560
2810
|
// Phase 3: Similarity edges (if requested)
|
|
@@ -2661,7 +2911,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2661
2911
|
// Keep max score
|
|
2662
2912
|
const existing = edgeMap.get(key);
|
|
2663
2913
|
if (!existing || clampedScore > existing.weight) {
|
|
2664
|
-
edgeMap.set(key, {
|
|
2914
|
+
edgeMap.set(key, {
|
|
2915
|
+
type: "similar",
|
|
2916
|
+
weight: clampedScore,
|
|
2917
|
+
confidence: "similarity",
|
|
2918
|
+
audit: { resolution: "similarity", score: clampedScore },
|
|
2919
|
+
});
|
|
2665
2920
|
}
|
|
2666
2921
|
}
|
|
2667
2922
|
} catch {
|
|
@@ -2687,9 +2942,39 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2687
2942
|
target: parts[1] ?? "",
|
|
2688
2943
|
type: val.type,
|
|
2689
2944
|
weight: val.weight,
|
|
2945
|
+
confidence: val.confidence,
|
|
2946
|
+
audit: val.audit,
|
|
2690
2947
|
};
|
|
2691
2948
|
});
|
|
2692
2949
|
|
|
2950
|
+
const edgeTypes: Record<GraphLinkType, number> = {
|
|
2951
|
+
wiki: 0,
|
|
2952
|
+
markdown: 0,
|
|
2953
|
+
similar: 0,
|
|
2954
|
+
};
|
|
2955
|
+
for (const edge of allEdges) {
|
|
2956
|
+
edgeTypes[edge.type] += 1;
|
|
2957
|
+
}
|
|
2958
|
+
const edgeConfidence: Record<GraphEdgeConfidence, number> = {
|
|
2959
|
+
explicit: 0,
|
|
2960
|
+
inferred: 0,
|
|
2961
|
+
ambiguous: 0,
|
|
2962
|
+
similarity: 0,
|
|
2963
|
+
};
|
|
2964
|
+
for (const edge of allEdges) {
|
|
2965
|
+
edgeConfidence[edge.confidence] += 1;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
const communityAnalysis = analyzeGraphCommunities(nodes, allEdges);
|
|
2969
|
+
warnings.push(...communityAnalysis.warnings);
|
|
2970
|
+
const nodesWithCommunities = nodes.map((node) => {
|
|
2971
|
+
const communityId = communityAnalysis.assignments[node.id];
|
|
2972
|
+
return communityId ? { ...node, communityId } : node;
|
|
2973
|
+
});
|
|
2974
|
+
const communityByNodeId = new Map(
|
|
2975
|
+
Object.entries(communityAnalysis.assignments)
|
|
2976
|
+
);
|
|
2977
|
+
|
|
2693
2978
|
const truncatedEdges = allEdges.length > limitEdges;
|
|
2694
2979
|
const links = allEdges.slice(0, limitEdges);
|
|
2695
2980
|
|
|
@@ -2702,8 +2987,50 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2702
2987
|
}
|
|
2703
2988
|
|
|
2704
2989
|
return ok({
|
|
2705
|
-
nodes,
|
|
2990
|
+
nodes: nodesWithCommunities,
|
|
2706
2991
|
links,
|
|
2992
|
+
report: {
|
|
2993
|
+
hubs: connectedNodes.slice(0, 10).map((node) => ({
|
|
2994
|
+
...toReportNode(node),
|
|
2995
|
+
communityId: communityByNodeId.get(node.docid),
|
|
2996
|
+
})),
|
|
2997
|
+
bridgeCandidates: connectedNodes
|
|
2998
|
+
.filter(
|
|
2999
|
+
(row) =>
|
|
3000
|
+
(inNeighbors.get(row.id)?.size ?? 0) > 0 &&
|
|
3001
|
+
(outNeighbors.get(row.id)?.size ?? 0) > 0
|
|
3002
|
+
)
|
|
3003
|
+
.slice(0, 10)
|
|
3004
|
+
.map((node) => ({
|
|
3005
|
+
...toReportNode(node),
|
|
3006
|
+
communityId: communityByNodeId.get(node.docid),
|
|
3007
|
+
})),
|
|
3008
|
+
isolated: {
|
|
3009
|
+
total: isolatedTotal,
|
|
3010
|
+
examples: isolatedExampleRows.map((node) => ({
|
|
3011
|
+
...toReportNode(node),
|
|
3012
|
+
communityId: communityByNodeId.get(node.docid),
|
|
3013
|
+
})),
|
|
3014
|
+
},
|
|
3015
|
+
unresolvedLinks: {
|
|
3016
|
+
total: totalEdgesUnresolved,
|
|
3017
|
+
byType: unresolvedByType,
|
|
3018
|
+
},
|
|
3019
|
+
edgeTypes,
|
|
3020
|
+
edgeConfidence,
|
|
3021
|
+
audit: {
|
|
3022
|
+
inferredEdges: edgeConfidence.inferred,
|
|
3023
|
+
ambiguousEdges: edgeConfidence.ambiguous,
|
|
3024
|
+
similarityEdges: edgeConfidence.similarity,
|
|
3025
|
+
},
|
|
3026
|
+
communities: {
|
|
3027
|
+
total: communityAnalysis.total,
|
|
3028
|
+
algorithm: communityAnalysis.algorithm,
|
|
3029
|
+
skipped: communityAnalysis.skipped,
|
|
3030
|
+
assignments: communityAnalysis.assignments,
|
|
3031
|
+
top: communityAnalysis.communities,
|
|
3032
|
+
},
|
|
3033
|
+
},
|
|
2707
3034
|
meta: {
|
|
2708
3035
|
collection,
|
|
2709
3036
|
nodeLimit: limitNodes,
|
|
@@ -2712,7 +3039,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2712
3039
|
// totalEdges = collapsed edge count within selected nodes (matches allEdges)
|
|
2713
3040
|
totalEdges: allEdges.length,
|
|
2714
3041
|
totalEdgesUnresolved,
|
|
2715
|
-
returnedNodes:
|
|
3042
|
+
returnedNodes: nodesWithCommunities.length,
|
|
2716
3043
|
returnedEdges: links.length,
|
|
2717
3044
|
truncated: truncatedNodes || truncatedEdges,
|
|
2718
3045
|
linkedOnly,
|
package/src/store/types.ts
CHANGED
|
@@ -418,6 +418,28 @@ export interface EmbeddingCleanupStats {
|
|
|
418
418
|
/** Graph link type (wiki, markdown, or similarity) */
|
|
419
419
|
export type GraphLinkType = "wiki" | "markdown" | "similar";
|
|
420
420
|
|
|
421
|
+
/** Trust classification for graph edges */
|
|
422
|
+
export type GraphEdgeConfidence =
|
|
423
|
+
| "explicit"
|
|
424
|
+
| "inferred"
|
|
425
|
+
| "ambiguous"
|
|
426
|
+
| "similarity";
|
|
427
|
+
|
|
428
|
+
/** Audit metadata explaining how an edge was derived */
|
|
429
|
+
export interface GraphEdgeAudit {
|
|
430
|
+
/** Resolution path used to create the edge */
|
|
431
|
+
resolution:
|
|
432
|
+
| "exact-title"
|
|
433
|
+
| "exact-path"
|
|
434
|
+
| "path-fallback"
|
|
435
|
+
| "ambiguous-fallback"
|
|
436
|
+
| "similarity";
|
|
437
|
+
/** Number of equally ranked target candidates, when applicable */
|
|
438
|
+
matchCount?: number;
|
|
439
|
+
/** Similarity score copied from weight for similarity edges */
|
|
440
|
+
score?: number;
|
|
441
|
+
}
|
|
442
|
+
|
|
421
443
|
/** Graph node representing a document */
|
|
422
444
|
export interface GraphNode {
|
|
423
445
|
/** Document ID (#hex) - primary identifier */
|
|
@@ -432,6 +454,42 @@ export interface GraphNode {
|
|
|
432
454
|
relPath: string;
|
|
433
455
|
/** Total degree (in + out unique neighbors) */
|
|
434
456
|
degree: number;
|
|
457
|
+
/** Optional deterministic community id from graph analysis */
|
|
458
|
+
communityId?: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Compact graph report node summary */
|
|
462
|
+
export interface GraphReportNode {
|
|
463
|
+
/** Document ID (#hex) */
|
|
464
|
+
id: string;
|
|
465
|
+
/** Document URI (gno://collection/path) */
|
|
466
|
+
uri: string;
|
|
467
|
+
/** Document title */
|
|
468
|
+
title: string | null;
|
|
469
|
+
/** Collection name */
|
|
470
|
+
collection: string;
|
|
471
|
+
/** Relative path within collection */
|
|
472
|
+
relPath: string;
|
|
473
|
+
/** Total degree (in + out unique neighbors) */
|
|
474
|
+
degree: number;
|
|
475
|
+
/** Optional deterministic community id from graph analysis */
|
|
476
|
+
communityId?: string;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Deterministic graph community summary */
|
|
480
|
+
export interface GraphCommunity {
|
|
481
|
+
/** Stable community id within this graph response */
|
|
482
|
+
id: string;
|
|
483
|
+
/** Human-readable label derived from the highest-degree member */
|
|
484
|
+
label: string;
|
|
485
|
+
/** Number of returned nodes assigned to this community */
|
|
486
|
+
size: number;
|
|
487
|
+
/** Internal edge count before edge-limit truncation */
|
|
488
|
+
edgeCount: number;
|
|
489
|
+
/** Internal density, 0-1 */
|
|
490
|
+
density: number;
|
|
491
|
+
/** Highest-degree example nodes in the community */
|
|
492
|
+
topNodes: GraphReportNode[];
|
|
435
493
|
}
|
|
436
494
|
|
|
437
495
|
/** Graph link (edge) between two nodes */
|
|
@@ -444,6 +502,55 @@ export interface GraphLink {
|
|
|
444
502
|
type: GraphLinkType;
|
|
445
503
|
/** Edge weight (link count for wiki/md, similarity score for similar) */
|
|
446
504
|
weight: number;
|
|
505
|
+
/** Trust classification for retrieval and agent audit */
|
|
506
|
+
confidence: GraphEdgeConfidence;
|
|
507
|
+
/** Audit metadata for how this edge was resolved */
|
|
508
|
+
audit: GraphEdgeAudit;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Graph report summary over the current graph result */
|
|
512
|
+
export interface GraphReport {
|
|
513
|
+
/** Highest-degree documents */
|
|
514
|
+
hubs: GraphReportNode[];
|
|
515
|
+
/** Bridge-like documents with both incoming and outgoing links */
|
|
516
|
+
bridgeCandidates: GraphReportNode[];
|
|
517
|
+
/** Isolated documents with no resolved explicit graph links */
|
|
518
|
+
isolated: {
|
|
519
|
+
/** Total isolated active documents in scope */
|
|
520
|
+
total: number;
|
|
521
|
+
/** First isolated documents by stable document id order */
|
|
522
|
+
examples: GraphReportNode[];
|
|
523
|
+
};
|
|
524
|
+
/** Unresolved explicit links */
|
|
525
|
+
unresolvedLinks: {
|
|
526
|
+
/** Total unresolved wiki/markdown links in scope */
|
|
527
|
+
total: number;
|
|
528
|
+
/** Unresolved links by type */
|
|
529
|
+
byType: Record<Exclude<GraphLinkType, "similar">, number>;
|
|
530
|
+
};
|
|
531
|
+
/** Edge breakdown by type before edge-limit truncation */
|
|
532
|
+
edgeTypes: Record<GraphLinkType, number>;
|
|
533
|
+
/** Edge confidence breakdown before edge-limit truncation */
|
|
534
|
+
edgeConfidence: Record<GraphEdgeConfidence, number>;
|
|
535
|
+
/** Explicit links resolved through fallback or ambiguous matching */
|
|
536
|
+
audit: {
|
|
537
|
+
inferredEdges: number;
|
|
538
|
+
ambiguousEdges: number;
|
|
539
|
+
similarityEdges: number;
|
|
540
|
+
};
|
|
541
|
+
/** Optional deterministic community/cluster analysis over returned nodes */
|
|
542
|
+
communities: {
|
|
543
|
+
/** Number of detected communities */
|
|
544
|
+
total: number;
|
|
545
|
+
/** Algorithm used for deterministic cluster labels */
|
|
546
|
+
algorithm: "deterministic-label-propagation";
|
|
547
|
+
/** Whether community detection was skipped for graph size */
|
|
548
|
+
skipped: boolean;
|
|
549
|
+
/** Node docid to community id map */
|
|
550
|
+
assignments: Record<string, string>;
|
|
551
|
+
/** Top communities by size */
|
|
552
|
+
top: GraphCommunity[];
|
|
553
|
+
};
|
|
447
554
|
}
|
|
448
555
|
|
|
449
556
|
/** Graph metadata with truncation info */
|
|
@@ -484,6 +591,7 @@ export interface GraphMeta {
|
|
|
484
591
|
export interface GraphResult {
|
|
485
592
|
nodes: GraphNode[];
|
|
486
593
|
links: GraphLink[];
|
|
594
|
+
report: GraphReport;
|
|
487
595
|
meta: GraphMeta;
|
|
488
596
|
}
|
|
489
597
|
|
|
@@ -641,6 +749,18 @@ export interface StorePort {
|
|
|
641
749
|
}
|
|
642
750
|
): Promise<StoreResult<DocumentRow[]>>;
|
|
643
751
|
|
|
752
|
+
/**
|
|
753
|
+
* Fetch documents by docids in batch.
|
|
754
|
+
* Useful for graph traversal pipelines to avoid per-node document lookups.
|
|
755
|
+
*/
|
|
756
|
+
getDocumentsByDocids(
|
|
757
|
+
docids: string[],
|
|
758
|
+
options?: {
|
|
759
|
+
collection?: string;
|
|
760
|
+
activeOnly?: boolean;
|
|
761
|
+
}
|
|
762
|
+
): Promise<StoreResult<DocumentRow[]>>;
|
|
763
|
+
|
|
644
764
|
/**
|
|
645
765
|
* List documents with pagination support.
|
|
646
766
|
* Returns documents and total count for efficient browsing.
|