Skip to content

Commit c3be9b5

Browse files
committed
refactor: Reduce whisperer blocks
This removes some redundant mapping and clustering from the whisperer methods and we finally have a real-world, portable working Mermaid workflow thanks to the backoff procedure neatly reducing large vaults along a sublinear curve.
1 parent f1a7757 commit c3be9b5

1 file changed

Lines changed: 92 additions & 136 deletions

File tree

whisperer.ts

Lines changed: 92 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ interface VaultEnv {
2222
nodes: Map<string, GraphNode>
2323
relationships: NodeRelationship[]
2424
settings: EGVSettings
25-
getN(): number // Total number of notes
26-
getT(): number // Total number of unique tags
27-
getK(): number // Average tags per note
28-
getOptimalN(E_max?: number): number // Calculate optimal note count based on formula
29-
isPastSingularity(E_max?: number): boolean // Check if we're hitting Mermaid limits
25+
getN(): number
26+
getT(): number
27+
getK(): number
28+
getOptimalN(E_max?: number): number
29+
isTearing(E_max?: number): boolean
3030
}
3131

3232
export class VaultWhisperer {
@@ -108,7 +108,7 @@ export class VaultWhisperer {
108108
const k = this.getK()
109109
return Math.floor(Math.sqrt((2 * E_max * t) / Math.max(1, k)))
110110
},
111-
isPastSingularity(E_max: number = 150): boolean {
111+
isTearing(E_max: number = 150): boolean {
112112
const N = this.getN()
113113
const T = this.getT()
114114
const K = this.getK()
@@ -353,7 +353,7 @@ export class VaultWhisperer {
353353
relationships: [...e],
354354
}
355355

356-
if (tempEnv.isPastSingularity()) {
356+
if (tempEnv.isTearing()) {
357357
console.log('Singularity detected - applying backoff strategy')
358358
this.backoffSingularity(vAndW, e, vaultEnv)
359359
} else {
@@ -446,136 +446,107 @@ export class VaultWhisperer {
446446
vaultEnv.relationships = e
447447
}
448448

449-
// Mermaid specific to not tear the mermaid chart
449+
// Mermaid-specific task when the initial mermaid graph object is tearing
450+
// Creates a reduced graph G'(V',E') where |V'| + |E'| ≤ MAX_TOTAL_ELEMENTS - BRIDGING PATTERN
450451
private backoffSingularity(
451452
nodes: Map<string, GraphNode>,
452453
relationships: NodeRelationship[],
453454
vaultEnv: VaultEnv
454455
): void {
455-
// Strict limits for Mermaid rendering
456-
const MAX_TOTAL_ELEMENTS = 100 // Total nodes + relationships
457-
const MAX_NODES = 40 // Maximum number of nodes
458-
const MAX_RELATIONSHIPS = 60 // Maximum number of relationships
456+
const MAX_NODES = 40
457+
const MAX_RELATIONSHIPS = 60
458+
const MAX_TAGS = 10
459459

460-
console.log(
461-
'Performing aggressive graph reduction for Mermaid compatibility'
462-
)
460+
console.log('Starting backoff task')
463461

464-
// Step 1: Build tag importance metrics
465-
const tagClusters = new Map<string, string[]>()
466-
const tagImportance = new Map<string, number>()
462+
// Build the count of {E(v,w)}
463+
const optimalVAndW = new Map<string, string[]>()
467464

468-
// Group notes by tag
469465
relationships.forEach((rel) => {
470-
const targetNode = nodes.get(rel.target)
471-
if (targetNode?.type === 'tag') {
472-
if (!tagClusters.has(rel.target)) {
473-
tagClusters.set(rel.target, [])
466+
const target = vaultEnv.nodes.get(rel.target)
467+
if (target?.type === 'tag') {
468+
if (!optimalVAndW.has(rel.target)) {
469+
optimalVAndW.set(rel.target, [])
474470
}
475-
tagClusters.get(rel.target)!.push(rel.source)
471+
optimalVAndW.get(rel.target)!.push(rel.source)
476472
}
477473
})
478474

479-
// Calculate tag importance: weighted by cluster size and connectivity potential
480-
tagClusters.forEach((notes, tagId) => {
481-
const clusterSize = notes.length
482-
// Tags with moderate-sized clusters are most valuable (not too small, not too large)
483-
const connectivityValue = Math.min(clusterSize, 10) // Cap value to avoid huge clusters dominating
484-
const importanceScore =
485-
connectivityValue * Math.log(clusterSize + 1)
486-
tagImportance.set(tagId, importanceScore)
487-
})
488-
489-
// Step 2: Select optimal number of tags based on our formula
490-
const targetTagCount = Math.min(10, Math.ceil(MAX_NODES / 4))
491-
492-
const selectedTags = Array.from(tagClusters.entries())
493-
.sort(
494-
(a, b) =>
495-
(tagImportance.get(b[0]) || 0) -
496-
(tagImportance.get(a[0]) || 0)
497-
)
498-
.slice(0, targetTagCount)
475+
// Select top tags by the amount of notes pointing to them - most important tags at the front of the pack
476+
const survivingTags = Array.from(optimalVAndW.entries())
477+
.sort((a, b) => b[1].length - a[1].length)
478+
.slice(0, MAX_TAGS)
499479
.map(([tagId]) => tagId)
500480

501-
// Step 3: Select most important notes per tag
502-
const selectedNotes = new Set<string>()
503-
const processedRelationships: NodeRelationship[] = []
504-
505-
// Distribute note quota among selected tags
506-
const notesPerTag =
507-
Math.floor(MAX_NODES - selectedTags.length) / selectedTags.length
508-
509-
selectedTags.forEach((tagId) => {
510-
const notesForTag = tagClusters.get(tagId) || []
511-
512-
// Get most connected notes (those with most tags/connections)
513-
const noteConnectionCounts = new Map<string, number>()
514-
515-
notesForTag.forEach((noteId) => {
516-
relationships.forEach((rel) => {
517-
if (rel.source === noteId) {
518-
noteConnectionCounts.set(
519-
noteId,
520-
(noteConnectionCounts.get(noteId) || 0) + 1
521-
)
522-
}
523-
})
481+
// Score the E where E = (v_important, w) w being the notes - if we're looking for a bridging pattern in the overall mermaid chart
482+
const noteScores = new Map<string, number>()
483+
survivingTags.forEach((tagId) => {
484+
optimalVAndW.get(tagId)?.forEach((noteId) => {
485+
noteScores.set(noteId, (noteScores.get(noteId) || 0) + 1)
524486
})
525-
526-
// Select top notes for this tag
527-
const topNotesForTag = Array.from(notesForTag)
528-
.sort(
529-
(a, b) =>
530-
(noteConnectionCounts.get(b) || 0) -
531-
(noteConnectionCounts.get(a) || 0)
532-
)
533-
.slice(0, Math.max(2, Math.ceil(notesPerTag)))
534-
535-
// Add to selected notes
536-
topNotesForTag.forEach((noteId) => selectedNotes.add(noteId))
537487
})
538488

539-
// Step 4: Build final graph with strict limits
540-
const finalNodes = new Map<string, GraphNode>()
541-
const finalRelationships: NodeRelationship[] = []
489+
// Select top notes based on E(v_important, w_most)
490+
const survivingNotes = new Set(
491+
Array.from(noteScores.entries())
492+
.sort((a, b) => b[1] - a[1])
493+
.slice(0, MAX_NODES - survivingTags.length)
494+
.map(([noteId]) => noteId)
495+
)
542496

543-
// Add selected tag nodes
544-
selectedTags.forEach((tagId) => {
545-
const tagNode = nodes.get(tagId)
546-
if (tagNode) finalNodes.set(tagId, tagNode)
497+
// Rebuild graph
498+
nodes.clear()
499+
relationships.length = 0
500+
501+
survivingTags.forEach((tagId) => {
502+
const v = vaultEnv.nodes.get(tagId)
503+
if (v) nodes.set(tagId, v)
547504
})
548505

549-
// Add selected note nodes
550-
selectedNotes.forEach((noteId) => {
551-
const noteNode = nodes.get(noteId)
552-
if (noteNode) finalNodes.set(noteId, noteNode)
506+
survivingNotes.forEach((noteId) => {
507+
const w = vaultEnv.nodes.get(noteId)
508+
if (w) nodes.set(noteId, w)
553509
})
554510

555-
// Add relationships, but only between selected nodes
556-
relationships.forEach((rel) => {
557-
if (finalNodes.has(rel.source) && finalNodes.has(rel.target)) {
558-
finalRelationships.push(rel)
511+
// Rebuild relationships for graph
512+
let E = new Set<string>()
559513

560-
// If we exceed relationship limit, stop adding
561-
if (finalRelationships.length >= MAX_RELATIONSHIPS) {
562-
return
514+
// First pass: tag-note connections
515+
vaultEnv.relationships.forEach((rel) => {
516+
if (
517+
nodes.has(rel.source) &&
518+
nodes.has(rel.target) &&
519+
E.size < MAX_RELATIONSHIPS
520+
) {
521+
const sourceIsNote =
522+
vaultEnv.nodes.get(rel.source)?.type !== 'tag'
523+
const targetIsTag =
524+
vaultEnv.nodes.get(rel.target)?.type === 'tag'
525+
526+
if (sourceIsNote && targetIsTag) {
527+
relationships.push(rel)
528+
E.add(`${rel.source}-${rel.target}`)
563529
}
564530
}
565531
})
566532

567-
// Final verification
568-
const totalElements = finalNodes.size + finalRelationships.length
533+
// Second pass: remaining connections
534+
vaultEnv.relationships.forEach((rel) => {
535+
const relId = `${rel.source}-${rel.target}`
536+
if (
537+
nodes.has(rel.source) &&
538+
nodes.has(rel.target) &&
539+
!E.has(relId) &&
540+
E.size < MAX_RELATIONSHIPS
541+
) {
542+
relationships.push(rel)
543+
E.add(relId)
544+
}
545+
})
546+
569547
console.log(
570-
`Final reduced graph: ${finalNodes.size} nodes, ${finalRelationships.length} relationships (${totalElements} total elements)`
548+
`Final reduced graph: ${nodes.size} nodes, ${relationships.length} relationships (${nodes.size + relationships.length} total elements)`
571549
)
572-
573-
// Apply our changes
574-
nodes.clear()
575-
finalNodes.forEach((node, key) => nodes.set(key, node))
576-
577-
relationships.length = 0
578-
relationships.push(...finalRelationships)
579550
}
580551

581552
private prune(
@@ -600,7 +571,7 @@ export class VaultWhisperer {
600571
filteredNodes.forEach((node, key) => nodes.set(key, node))
601572
}
602573

603-
// Applies DOT-specific features
574+
// Some f(Weight(v,w), Format = .dot)
604575
private runDotSettings(graph: Graph): void {
605576
// Respect relationship weight setting
606577
if (this.settings.includeWeights && this.settings.weightThreshold) {
@@ -622,44 +593,29 @@ export class VaultWhisperer {
622593
})
623594
}
624595

625-
// Applies Mermaid-specific features
596+
// Some f(Weight(v,w), Format = .mmd)
626597
private runMMDSettings(graph: Graph): void {
627598
// Respect max-relationships-per-note setting
628599
if (this.settings.maxEPerV) {
629-
const outgoingArrows = new Map<string, number>()
630-
631-
// Count outgoing edges per node
632-
graph.relationships.forEach((rel) => {
633-
outgoingArrows.set(
634-
rel.source,
635-
(outgoingArrows.get(rel.source) || 0) + 1
636-
)
637-
})
600+
const survivors: NodeRelationship[] = []
601+
const cutoff = new Map<string, number>()
638602

639-
// Filter relationships for notes with too many connections
640-
const filteredArrows: NodeRelationship[] = []
641-
const processedArrows = new Map<string, number>()
642-
643-
// Sort by weight first to keep the most important relationships
644-
const sortedRelationships = [...graph.relationships].sort(
603+
// Most weighted notes at the front of the pack to survive any pruning
604+
const survivingECandidates = [...graph.relationships].sort(
645605
(a, b) => b.weight - a.weight
646606
)
647607

648-
for (const rel of sortedRelationships) {
649-
const sourceKey = rel.source
650-
const currentCount = processedArrows.get(sourceKey) || 0
651-
652-
if (
653-
currentCount < this.settings.maxEPerV ||
654-
(outgoingArrows.get(sourceKey) || 0) <=
655-
this.settings.maxEPerV
656-
) {
657-
filteredArrows.push(rel)
658-
processedArrows.set(sourceKey, currentCount + 1)
608+
// Add important edges until we reach the mavEPerV cutoff point
609+
for (const e of survivingECandidates) {
610+
const eCount = cutoff.get(e.source) || 0
611+
612+
if (eCount < this.settings.maxEPerV) {
613+
survivors.push(e)
614+
cutoff.set(e.source, eCount + 1)
659615
}
660616
}
661617

662-
graph.relationships = filteredArrows
618+
graph.relationships = survivors
663619
}
664620
}
665621

0 commit comments

Comments
 (0)