2026-01-18 14:50:17 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Command: Inspect Chains (Current Section)
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import type { App, Editor } from "obsidian";
|
|
|
|
|
|
import { resolveSectionContext } from "../analysis/sectionContext";
|
|
|
|
|
|
import { inspectChains } from "../analysis/chainInspector";
|
2026-01-18 21:10:33 +01:00
|
|
|
|
import type { ChainRolesConfig, ChainTemplatesConfig, DictionaryLoadResult } from "../dictionary/types";
|
|
|
|
|
|
import type { MindnetSettings } from "../settings";
|
2026-01-18 14:50:17 +01:00
|
|
|
|
|
|
|
|
|
|
export interface InspectChainsOptions {
|
|
|
|
|
|
includeNoteLinks?: boolean;
|
|
|
|
|
|
includeCandidates?: boolean;
|
|
|
|
|
|
maxDepth?: number;
|
|
|
|
|
|
direction?: "forward" | "backward" | "both";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Format report as pretty-printed string.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatReport(report: Awaited<ReturnType<typeof inspectChains>>): string {
|
|
|
|
|
|
const lines: string[] = [];
|
|
|
|
|
|
lines.push("=== Chain Inspector Report ===");
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
lines.push(`Context: ${report.context.file}`);
|
|
|
|
|
|
if (report.context.heading) {
|
|
|
|
|
|
lines.push(`Section: ${report.context.heading}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push(`Zone: ${report.context.zoneKind}`);
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
lines.push("Settings:");
|
|
|
|
|
|
lines.push(` - Include Note Links: ${report.settings.includeNoteLinks}`);
|
|
|
|
|
|
lines.push(` - Include Candidates: ${report.settings.includeCandidates}`);
|
|
|
|
|
|
lines.push(` - Max Depth: ${report.settings.maxDepth}`);
|
|
|
|
|
|
lines.push(` - Direction: ${report.settings.direction}`);
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(`Neighbors:`);
|
|
|
|
|
|
lines.push(` Incoming: ${report.neighbors.incoming.length}`);
|
|
|
|
|
|
for (const edge of report.neighbors.incoming.slice(0, 5)) {
|
|
|
|
|
|
const targetStr = edge.target.heading
|
|
|
|
|
|
? `${edge.target.file}#${edge.target.heading}`
|
|
|
|
|
|
: edge.target.file;
|
|
|
|
|
|
lines.push(` - ${edge.rawEdgeType} -> ${targetStr} [${edge.scope}]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (report.neighbors.incoming.length > 5) {
|
|
|
|
|
|
lines.push(` ... and ${report.neighbors.incoming.length - 5} more`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(` Outgoing: ${report.neighbors.outgoing.length}`);
|
|
|
|
|
|
for (const edge of report.neighbors.outgoing.slice(0, 5)) {
|
|
|
|
|
|
const targetStr = edge.target.heading
|
|
|
|
|
|
? `${edge.target.file}#${edge.target.heading}`
|
|
|
|
|
|
: edge.target.file;
|
|
|
|
|
|
lines.push(` - ${edge.rawEdgeType} -> ${targetStr} [${edge.scope}]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (report.neighbors.outgoing.length > 5) {
|
|
|
|
|
|
lines.push(` ... and ${report.neighbors.outgoing.length - 5} more`);
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(`Paths:`);
|
|
|
|
|
|
lines.push(` Forward: ${report.paths.forward.length}`);
|
|
|
|
|
|
for (const path of report.paths.forward.slice(0, 3)) {
|
|
|
|
|
|
const pathStr = path.nodes
|
|
|
|
|
|
.map((n) => (n.heading ? `${n.file}#${n.heading}` : n.file))
|
|
|
|
|
|
.join(" -> ");
|
|
|
|
|
|
lines.push(` - ${pathStr} (${path.edges.length} edges)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (report.paths.forward.length > 3) {
|
|
|
|
|
|
lines.push(` ... and ${report.paths.forward.length - 3} more`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(` Backward: ${report.paths.backward.length}`);
|
|
|
|
|
|
for (const path of report.paths.backward.slice(0, 3)) {
|
|
|
|
|
|
const pathStr = path.nodes
|
|
|
|
|
|
.map((n) => (n.heading ? `${n.file}#${n.heading}` : n.file))
|
|
|
|
|
|
.join(" -> ");
|
|
|
|
|
|
lines.push(` - ${pathStr} (${path.edges.length} edges)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (report.paths.backward.length > 3) {
|
|
|
|
|
|
lines.push(` ... and ${report.paths.backward.length - 3} more`);
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
lines.push(`Gap Heuristics (Findings): ${report.findings.length}`);
|
|
|
|
|
|
if (report.findings.length === 0) {
|
|
|
|
|
|
lines.push(` ✓ No issues detected`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (const finding of report.findings) {
|
|
|
|
|
|
const severityIcon =
|
|
|
|
|
|
finding.severity === "error"
|
|
|
|
|
|
? "❌"
|
|
|
|
|
|
: finding.severity === "warn"
|
|
|
|
|
|
? "⚠️"
|
|
|
|
|
|
: "ℹ️";
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
` ${severityIcon} [${finding.severity.toUpperCase()}] ${finding.code}: ${finding.message}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-18 21:10:33 +01:00
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
// Add analysisMeta section
|
|
|
|
|
|
if (report.analysisMeta) {
|
|
|
|
|
|
lines.push("Analysis Meta:");
|
|
|
|
|
|
lines.push(` - Edges Total: ${report.analysisMeta.edgesTotal}`);
|
|
|
|
|
|
lines.push(` - Edges With Canonical: ${report.analysisMeta.edgesWithCanonical}`);
|
|
|
|
|
|
lines.push(` - Edges Unmapped: ${report.analysisMeta.edgesUnmapped}`);
|
|
|
|
|
|
if (Object.keys(report.analysisMeta.roleMatches).length > 0) {
|
|
|
|
|
|
lines.push(` - Role Matches:`);
|
|
|
|
|
|
for (const [roleName, count] of Object.entries(report.analysisMeta.roleMatches)) {
|
|
|
|
|
|
lines.push(` - ${roleName}: ${count}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lines.push(` - Role Matches: (none)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
|
|
|
|
|
|
// Add template matches section
|
|
|
|
|
|
if (report.templateMatches && report.templateMatches.length > 0) {
|
|
|
|
|
|
lines.push("Template Matches:");
|
|
|
|
|
|
// Sort by score desc, then name asc (already sorted in matching)
|
|
|
|
|
|
const topMatches = report.templateMatches.slice(0, 3);
|
|
|
|
|
|
for (const match of topMatches) {
|
|
|
|
|
|
lines.push(` - ${match.templateName} (score: ${match.score})`);
|
|
|
|
|
|
if (Object.keys(match.slotAssignments).length > 0) {
|
|
|
|
|
|
lines.push(` Slots:`);
|
|
|
|
|
|
const sortedSlots = Object.keys(match.slotAssignments).sort();
|
|
|
|
|
|
for (const slotId of sortedSlots) {
|
|
|
|
|
|
const assignment = match.slotAssignments[slotId];
|
|
|
|
|
|
if (assignment) {
|
|
|
|
|
|
const nodeStr = assignment.heading
|
|
|
|
|
|
? `${assignment.file}#${assignment.heading}`
|
|
|
|
|
|
: assignment.file;
|
|
|
|
|
|
lines.push(` ${slotId}: ${nodeStr} [${assignment.noteType}]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (match.missingSlots.length > 0) {
|
|
|
|
|
|
lines.push(` Missing slots: ${match.missingSlots.join(", ")}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (match.roleEvidence && match.roleEvidence.length > 0) {
|
|
|
|
|
|
lines.push(` Links: ${match.satisfiedLinks}/${match.requiredLinks} satisfied`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (report.templateMatches.length > 3) {
|
|
|
|
|
|
lines.push(` ... and ${report.templateMatches.length - 3} more`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (report.templatesSource) {
|
|
|
|
|
|
lines.push("Template Matches: (none)");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add templates source info
|
|
|
|
|
|
if (report.templatesSource) {
|
|
|
|
|
|
lines.push("");
|
|
|
|
|
|
lines.push("Templates Source:");
|
|
|
|
|
|
lines.push(` - Path: ${report.templatesSource.path}`);
|
|
|
|
|
|
lines.push(` - Status: ${report.templatesSource.status}`);
|
|
|
|
|
|
if (report.templatesSource.loadedAt) {
|
|
|
|
|
|
const date = new Date(report.templatesSource.loadedAt);
|
|
|
|
|
|
lines.push(` - Loaded: ${date.toISOString()}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push(` - Templates: ${report.templatesSource.templateCount}`);
|
|
|
|
|
|
}
|
2026-01-18 14:50:17 +01:00
|
|
|
|
|
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Execute inspect chains command.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function executeInspectChains(
|
|
|
|
|
|
app: App,
|
|
|
|
|
|
editor: Editor,
|
|
|
|
|
|
filePath: string,
|
|
|
|
|
|
chainRoles: ChainRolesConfig | null,
|
2026-01-18 21:10:33 +01:00
|
|
|
|
settings: MindnetSettings,
|
|
|
|
|
|
options: InspectChainsOptions = {},
|
|
|
|
|
|
chainTemplates?: ChainTemplatesConfig | null,
|
|
|
|
|
|
templatesLoadResult?: DictionaryLoadResult<ChainTemplatesConfig>
|
2026-01-18 14:50:17 +01:00
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
// Resolve section context
|
|
|
|
|
|
const context = resolveSectionContext(editor, filePath);
|
|
|
|
|
|
|
|
|
|
|
|
// Build options with defaults
|
|
|
|
|
|
const inspectorOptions = {
|
|
|
|
|
|
includeNoteLinks: options.includeNoteLinks ?? true,
|
|
|
|
|
|
includeCandidates: options.includeCandidates ?? false,
|
|
|
|
|
|
maxDepth: options.maxDepth ?? 3,
|
|
|
|
|
|
direction: options.direction ?? "both",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-18 21:10:33 +01:00
|
|
|
|
// Prepare templates source info
|
|
|
|
|
|
let templatesSourceInfo: { path: string; status: string; loadedAt: number | null; templateCount: number } | undefined;
|
|
|
|
|
|
if (templatesLoadResult) {
|
|
|
|
|
|
templatesSourceInfo = {
|
|
|
|
|
|
path: templatesLoadResult.resolvedPath,
|
|
|
|
|
|
status: templatesLoadResult.status,
|
|
|
|
|
|
loadedAt: templatesLoadResult.loadedAt,
|
|
|
|
|
|
templateCount: chainTemplates?.templates?.length || 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 14:50:17 +01:00
|
|
|
|
// Inspect chains
|
2026-01-18 21:10:33 +01:00
|
|
|
|
const report = await inspectChains(
|
|
|
|
|
|
app,
|
|
|
|
|
|
context,
|
|
|
|
|
|
inspectorOptions,
|
|
|
|
|
|
chainRoles,
|
|
|
|
|
|
settings.edgeVocabularyPath,
|
|
|
|
|
|
chainTemplates,
|
|
|
|
|
|
templatesSourceInfo
|
|
|
|
|
|
);
|
2026-01-18 14:50:17 +01:00
|
|
|
|
|
|
|
|
|
|
// Log report as JSON
|
|
|
|
|
|
console.log("=== Chain Inspector Report (JSON) ===");
|
|
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
|
|
|
|
|
|
|
|
|
|
// Log pretty-printed summary
|
|
|
|
|
|
console.log("\n=== Chain Inspector Report (Summary) ===");
|
|
|
|
|
|
console.log(formatReport(report));
|
|
|
|
|
|
}
|