@gmickel/gno 1.4.1 → 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.
@@ -51,6 +51,26 @@ interface GraphNode {
51
51
  collection: string;
52
52
  relPath: string;
53
53
  degree: number;
54
+ communityId?: string;
55
+ }
56
+
57
+ interface GraphReportNode {
58
+ id: string;
59
+ uri: string;
60
+ title: string | null;
61
+ collection: string;
62
+ relPath: string;
63
+ degree: number;
64
+ communityId?: string;
65
+ }
66
+
67
+ interface GraphCommunity {
68
+ id: string;
69
+ label: string;
70
+ size: number;
71
+ edgeCount: number;
72
+ density: number;
73
+ topNodes: GraphReportNode[];
54
74
  }
55
75
 
56
76
  interface GraphLink {
@@ -58,6 +78,17 @@ interface GraphLink {
58
78
  target: string;
59
79
  type: "wiki" | "markdown" | "similar";
60
80
  weight: number;
81
+ confidence: "explicit" | "inferred" | "ambiguous" | "similarity";
82
+ audit: {
83
+ resolution:
84
+ | "exact-title"
85
+ | "exact-path"
86
+ | "path-fallback"
87
+ | "ambiguous-fallback"
88
+ | "similarity";
89
+ matchCount?: number;
90
+ score?: number;
91
+ };
61
92
  }
62
93
 
63
94
  interface GraphMeta {
@@ -78,9 +109,49 @@ interface GraphMeta {
78
109
  warnings: string[];
79
110
  }
80
111
 
112
+ interface GraphReport {
113
+ hubs: GraphReportNode[];
114
+ bridgeCandidates: GraphReportNode[];
115
+ isolated: {
116
+ total: number;
117
+ examples: GraphReportNode[];
118
+ };
119
+ unresolvedLinks: {
120
+ total: number;
121
+ byType: {
122
+ wiki: number;
123
+ markdown: number;
124
+ };
125
+ };
126
+ edgeTypes: {
127
+ wiki: number;
128
+ markdown: number;
129
+ similar: number;
130
+ };
131
+ edgeConfidence: {
132
+ explicit: number;
133
+ inferred: number;
134
+ ambiguous: number;
135
+ similarity: number;
136
+ };
137
+ audit: {
138
+ inferredEdges: number;
139
+ ambiguousEdges: number;
140
+ similarityEdges: number;
141
+ };
142
+ communities: {
143
+ total: number;
144
+ algorithm: "deterministic-label-propagation";
145
+ skipped: boolean;
146
+ assignments: Record<string, string>;
147
+ top: GraphCommunity[];
148
+ };
149
+ }
150
+
81
151
  interface GraphResponse {
82
152
  nodes: GraphNode[];
83
153
  links: GraphLink[];
154
+ report: GraphReport;
84
155
  meta: GraphMeta;
85
156
  }
86
157
 
@@ -111,6 +182,8 @@ const COLORS = {
111
182
  edgeWiki: "rgba(77, 184, 168, 0.4)", // Teal threads
112
183
  edgeMarkdown: "rgba(77, 184, 168, 0.25)", // Fainter teal
113
184
  edgeSimilar: "rgba(212, 160, 83, 0.3)", // Gold similarity
185
+ edgeInferred: "rgba(124, 158, 178, 0.35)",
186
+ edgeAmbiguous: "rgba(245, 215, 142, 0.55)",
114
187
 
115
188
  // Background
116
189
  canvas: "#050505", // Deep black - observatory darkness
@@ -136,6 +209,22 @@ function getCollectionColor(collection: string): string {
136
209
  return palette[Math.abs(hash) % palette.length]!;
137
210
  }
138
211
 
212
+ function getCommunityColor(communityId: string | undefined): string | null {
213
+ if (!communityId) return null;
214
+ const palette = [
215
+ "#4db8a8",
216
+ "#d4a053",
217
+ "#6ba3d6",
218
+ "#a8c686",
219
+ "#c9a7c7",
220
+ "#e2a76f",
221
+ "#f5d78e",
222
+ "#7c9eb2",
223
+ ];
224
+ const index = Math.max(0, Number.parseInt(communityId.slice(1), 10) - 1);
225
+ return palette[index % palette.length] ?? null;
226
+ }
227
+
139
228
  // ─────────────────────────────────────────────────────────────────────────────
140
229
  // Loading & Empty States
141
230
  // ─────────────────────────────────────────────────────────────────────────────
@@ -240,6 +329,7 @@ export default function GraphView({ navigate }: PageProps) {
240
329
  // Filter state
241
330
  const [selectedCollection, setSelectedCollection] = useState<string>("_all");
242
331
  const [includeSimilar, setIncludeSimilar] = useState(false);
332
+ const [selectedCommunity, setSelectedCommunity] = useState<string>("_all");
243
333
 
244
334
  // UI state
245
335
  const [showTruncationBanner, setShowTruncationBanner] = useState(true);
@@ -289,6 +379,7 @@ export default function GraphView({ navigate }: PageProps) {
289
379
  setLoading(true);
290
380
  setError(null);
291
381
  setShowTruncationBanner(true);
382
+ setSelectedCommunity("_all");
292
383
  clearZoomTimeouts();
293
384
 
294
385
  const params = new URLSearchParams();
@@ -369,24 +460,47 @@ export default function GraphView({ navigate }: PageProps) {
369
460
  const forceGraphData = useMemo(() => {
370
461
  if (!graphData) return { nodes: [], links: [] };
371
462
 
463
+ const visibleNodeIds = new Set(
464
+ graphData.nodes
465
+ .filter(
466
+ (node) =>
467
+ selectedCommunity === "_all" ||
468
+ node.communityId === selectedCommunity
469
+ )
470
+ .map((node) => node.id)
471
+ );
472
+
372
473
  return {
373
- nodes: graphData.nodes.map((n) => ({
374
- ...n,
375
- // Add computed properties for rendering
376
- color: getCollectionColor(n.collection),
377
- size: Math.max(4, Math.min(20, 4 + Math.sqrt(n.degree) * 2)),
378
- })),
379
- links: graphData.links.map((l) => ({
380
- ...l,
381
- color:
382
- l.type === "similar"
383
- ? COLORS.edgeSimilar
384
- : l.type === "wiki"
385
- ? COLORS.edgeWiki
386
- : COLORS.edgeMarkdown,
387
- })),
474
+ nodes: graphData.nodes
475
+ .filter((node) => visibleNodeIds.has(node.id))
476
+ .map((n) => ({
477
+ ...n,
478
+ // Add computed properties for rendering
479
+ color:
480
+ getCommunityColor(n.communityId) ??
481
+ getCollectionColor(n.collection),
482
+ size: Math.max(4, Math.min(20, 4 + Math.sqrt(n.degree) * 2)),
483
+ })),
484
+ links: graphData.links
485
+ .filter(
486
+ (link) =>
487
+ visibleNodeIds.has(link.source) && visibleNodeIds.has(link.target)
488
+ )
489
+ .map((l) => ({
490
+ ...l,
491
+ color:
492
+ l.confidence === "ambiguous"
493
+ ? COLORS.edgeAmbiguous
494
+ : l.confidence === "inferred"
495
+ ? COLORS.edgeInferred
496
+ : l.type === "similar"
497
+ ? COLORS.edgeSimilar
498
+ : l.type === "wiki"
499
+ ? COLORS.edgeWiki
500
+ : COLORS.edgeMarkdown,
501
+ })),
388
502
  };
389
- }, [graphData]);
503
+ }, [graphData, selectedCommunity]);
390
504
 
391
505
  // Node canvas rendering for custom aesthetics
392
506
  // biome-ignore lint: dynamic import typing
@@ -494,6 +608,27 @@ export default function GraphView({ navigate }: PageProps) {
494
608
  <span className="hidden sm:inline">Dashboard</span>
495
609
  </Button>
496
610
 
611
+ {graphData &&
612
+ !graphData.report.communities.skipped &&
613
+ graphData.report.communities.top.length > 0 && (
614
+ <Select
615
+ onValueChange={setSelectedCommunity}
616
+ value={selectedCommunity}
617
+ >
618
+ <SelectTrigger className="h-8 w-[170px] border-border/50 bg-background/50 text-xs">
619
+ <SelectValue placeholder="All communities" />
620
+ </SelectTrigger>
621
+ <SelectContent>
622
+ <SelectItem value="_all">All communities</SelectItem>
623
+ {graphData.report.communities.top.map((community) => (
624
+ <SelectItem key={community.id} value={community.id}>
625
+ {community.id} · {community.size} docs
626
+ </SelectItem>
627
+ ))}
628
+ </SelectContent>
629
+ </Select>
630
+ )}
631
+
497
632
  <div className="h-4 w-px bg-border/50" />
498
633
 
499
634
  <h1 className="font-serif text-lg text-[#4db8a8]">Knowledge Graph</h1>
@@ -578,6 +713,36 @@ export default function GraphView({ navigate }: PageProps) {
578
713
  {graphData.links.length.toLocaleString()}
579
714
  </span>{" "}
580
715
  edges
716
+ <span className="text-border">•</span>
717
+ <span className="text-[#f5d78e]">
718
+ {graphData.report.hubs[0]?.degree ?? 0}
719
+ </span>{" "}
720
+ top degree
721
+ <span className="text-border">•</span>
722
+ <span className="text-[#d4a053]">
723
+ {graphData.report.unresolvedLinks.total.toLocaleString()}
724
+ </span>{" "}
725
+ unresolved
726
+ <span className="text-border">•</span>
727
+ <span className="text-[#4db8a8]">
728
+ {graphData.report.isolated.total.toLocaleString()}
729
+ </span>{" "}
730
+ isolated
731
+ <span className="text-border">•</span>
732
+ <span className="text-[#7c9eb2]">
733
+ {graphData.report.audit.inferredEdges.toLocaleString()}
734
+ </span>{" "}
735
+ inferred
736
+ <span className="text-border">•</span>
737
+ <span className="text-[#f5d78e]">
738
+ {graphData.report.audit.ambiguousEdges.toLocaleString()}
739
+ </span>{" "}
740
+ ambiguous
741
+ <span className="text-border">•</span>
742
+ <span className="text-[#6ba3d6]">
743
+ {graphData.report.communities.total.toLocaleString()}
744
+ </span>{" "}
745
+ communities
581
746
  </div>
582
747
  )}
583
748
 
@@ -611,6 +776,48 @@ export default function GraphView({ navigate }: PageProps) {
611
776
  />
612
777
  )}
613
778
 
779
+ {graphData &&
780
+ !loading &&
781
+ !graphData.report.communities.skipped &&
782
+ graphData.report.communities.top.length > 0 && (
783
+ <div className="absolute right-4 bottom-4 z-20 hidden max-w-xs rounded-lg border border-border/30 bg-[#0f1115]/85 p-3 shadow-lg backdrop-blur-md md:block">
784
+ <p className="mb-2 font-mono text-[#4db8a8] text-[10px] uppercase tracking-[0.12em]">
785
+ Communities
786
+ </p>
787
+ <div className="space-y-1.5">
788
+ {graphData.report.communities.top.slice(0, 6).map((community) => (
789
+ <button
790
+ className={cn(
791
+ "flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-xs transition-colors hover:bg-white/5",
792
+ selectedCommunity === community.id && "bg-white/10"
793
+ )}
794
+ key={community.id}
795
+ onClick={() =>
796
+ setSelectedCommunity((current) =>
797
+ current === community.id ? "_all" : community.id
798
+ )
799
+ }
800
+ type="button"
801
+ >
802
+ <span
803
+ className="size-2.5 shrink-0 rounded-full"
804
+ style={{
805
+ backgroundColor:
806
+ getCommunityColor(community.id) ?? COLORS.nodeDefault,
807
+ }}
808
+ />
809
+ <span className="min-w-0 flex-1 truncate text-muted-foreground">
810
+ {community.label}
811
+ </span>
812
+ <span className="font-mono text-[#d4a053]">
813
+ {community.size}
814
+ </span>
815
+ </button>
816
+ ))}
817
+ </div>
818
+ </div>
819
+ )}
820
+
614
821
  {/* Graph canvas */}
615
822
  <div className="h-full pt-12">
616
823
  {loading ? (
@@ -718,6 +925,20 @@ export default function GraphView({ navigate }: PageProps) {
718
925
  <span className="text-muted-foreground">Similarity</span>
719
926
  </div>
720
927
  )}
928
+ <div className="flex items-center gap-2">
929
+ <span
930
+ className="size-2 rounded-full"
931
+ style={{ backgroundColor: COLORS.edgeInferred }}
932
+ />
933
+ <span className="text-muted-foreground">Inferred</span>
934
+ </div>
935
+ <div className="flex items-center gap-2">
936
+ <span
937
+ className="size-2 rounded-full"
938
+ style={{ backgroundColor: COLORS.edgeAmbiguous }}
939
+ />
940
+ <span className="text-muted-foreground">Ambiguous</span>
941
+ </div>
721
942
  </div>
722
943
  </div>
723
944
  </div>
@@ -388,6 +388,7 @@ export default function Search({ navigate }: PageProps) {
388
388
  if (!useBm25) {
389
389
  body.noExpand = depthPolicy.noExpand;
390
390
  body.noRerank = depthPolicy.noRerank;
391
+ body.noGraph = thoroughness === "fast";
391
392
  if (queryModes.length > 0) {
392
393
  body.queryModes = queryModes;
393
394
  }
@@ -172,6 +172,7 @@ export interface QueryRequestBody {
172
172
  queryModes?: QueryModeInput[];
173
173
  noExpand?: boolean;
174
174
  noRerank?: boolean;
175
+ noGraph?: boolean;
175
176
  /** Comma-separated tags - filter to docs having ALL (AND) */
176
177
  tagsAll?: string;
177
178
  /** Comma-separated tags - filter to docs having ANY (OR) */
@@ -3310,6 +3311,7 @@ export async function handleQuery(
3310
3311
  queryModes: normalizedQueryModes,
3311
3312
  noExpand: body.noExpand,
3312
3313
  noRerank: body.noRerank,
3314
+ noGraph: body.noGraph,
3313
3315
  tagsAll,
3314
3316
  tagsAny,
3315
3317
  since: body.since,