skillwiki 0.8.1-beta.2 → 0.8.1-beta.4
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/dist/cli.js
CHANGED
|
@@ -548,23 +548,137 @@ function extractBodyWikilinks(body) {
|
|
|
548
548
|
return out;
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
-
// src/
|
|
552
|
-
async function
|
|
553
|
-
const scan = await scanVault(input.vault);
|
|
554
|
-
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
551
|
+
// src/utils/community.ts
|
|
552
|
+
async function buildWikilinkAdjacency(typedKnowledge) {
|
|
555
553
|
const adjacency = {};
|
|
556
554
|
const slugToPath = {};
|
|
557
|
-
for (const p of
|
|
555
|
+
for (const p of typedKnowledge) {
|
|
558
556
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
559
557
|
slugToPath[slug] = p.relPath;
|
|
560
558
|
}
|
|
561
|
-
for (const p of
|
|
559
|
+
for (const p of typedKnowledge) {
|
|
562
560
|
const text = await readPage(p);
|
|
563
561
|
const split = splitFrontmatter(text);
|
|
564
562
|
const body = split.ok ? split.data.body : text;
|
|
565
563
|
const links = extractBodyWikilinks(body);
|
|
566
564
|
adjacency[p.relPath] = links.map((slug) => slugToPath[slug.split("/").pop()]).filter((x) => Boolean(x));
|
|
567
565
|
}
|
|
566
|
+
return adjacency;
|
|
567
|
+
}
|
|
568
|
+
function toUndirectedWeighted(adj) {
|
|
569
|
+
const g = /* @__PURE__ */ new Map();
|
|
570
|
+
const ensure = (n) => {
|
|
571
|
+
let m = g.get(n);
|
|
572
|
+
if (!m) {
|
|
573
|
+
m = /* @__PURE__ */ new Map();
|
|
574
|
+
g.set(n, m);
|
|
575
|
+
}
|
|
576
|
+
return m;
|
|
577
|
+
};
|
|
578
|
+
for (const node of Object.keys(adj)) ensure(node);
|
|
579
|
+
for (const [a, nbrs] of Object.entries(adj)) {
|
|
580
|
+
for (const b of nbrs) {
|
|
581
|
+
if (a === b) continue;
|
|
582
|
+
ensure(a).set(b, 1);
|
|
583
|
+
ensure(b).set(a, 1);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return g;
|
|
587
|
+
}
|
|
588
|
+
function louvain(g) {
|
|
589
|
+
const nodes = [...g.keys()].sort();
|
|
590
|
+
const comm = /* @__PURE__ */ new Map();
|
|
591
|
+
nodes.forEach((n, i) => comm.set(n, i));
|
|
592
|
+
const k = /* @__PURE__ */ new Map();
|
|
593
|
+
let m2 = 0;
|
|
594
|
+
for (const n of nodes) {
|
|
595
|
+
let deg = 0;
|
|
596
|
+
for (const w of g.get(n).values()) deg += w;
|
|
597
|
+
k.set(n, deg);
|
|
598
|
+
m2 += deg;
|
|
599
|
+
}
|
|
600
|
+
if (m2 === 0) return comm;
|
|
601
|
+
const sumTot = /* @__PURE__ */ new Map();
|
|
602
|
+
for (const n of nodes) {
|
|
603
|
+
const c = comm.get(n);
|
|
604
|
+
sumTot.set(c, (sumTot.get(c) ?? 0) + k.get(n));
|
|
605
|
+
}
|
|
606
|
+
let improved = true;
|
|
607
|
+
while (improved) {
|
|
608
|
+
improved = false;
|
|
609
|
+
for (const n of nodes) {
|
|
610
|
+
const cur = comm.get(n);
|
|
611
|
+
const kn = k.get(n);
|
|
612
|
+
sumTot.set(cur, sumTot.get(cur) - kn);
|
|
613
|
+
const wToComm = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const [nb, w] of g.get(n)) {
|
|
615
|
+
if (nb === n) continue;
|
|
616
|
+
const c = comm.get(nb);
|
|
617
|
+
wToComm.set(c, (wToComm.get(c) ?? 0) + w);
|
|
618
|
+
}
|
|
619
|
+
const gainFor = (c) => (wToComm.get(c) ?? 0) - (sumTot.get(c) ?? 0) * kn / m2;
|
|
620
|
+
const curGain = gainFor(cur);
|
|
621
|
+
let bestComm = cur;
|
|
622
|
+
let bestDelta = 0;
|
|
623
|
+
for (const c of wToComm.keys()) {
|
|
624
|
+
const delta = gainFor(c) - curGain;
|
|
625
|
+
if (delta > bestDelta) {
|
|
626
|
+
bestDelta = delta;
|
|
627
|
+
bestComm = c;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
comm.set(n, bestComm);
|
|
631
|
+
sumTot.set(bestComm, (sumTot.get(bestComm) ?? 0) + kn);
|
|
632
|
+
if (bestComm !== cur) improved = true;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return comm;
|
|
636
|
+
}
|
|
637
|
+
function communityCohesion(members, g) {
|
|
638
|
+
const n = members.length;
|
|
639
|
+
if (n < 2) return 1;
|
|
640
|
+
const set = new Set(members);
|
|
641
|
+
let internal = 0;
|
|
642
|
+
for (const a of members) {
|
|
643
|
+
for (const [b, w] of g.get(a) ?? /* @__PURE__ */ new Map()) {
|
|
644
|
+
if (a < b && set.has(b)) internal += w;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return internal / (n * (n - 1) / 2);
|
|
648
|
+
}
|
|
649
|
+
function findSparseCommunities(adj, opts = {}) {
|
|
650
|
+
const minSize = opts.minSize ?? 3;
|
|
651
|
+
const maxCohesion = opts.maxCohesion ?? 0.15;
|
|
652
|
+
const g = toUndirectedWeighted(adj);
|
|
653
|
+
const comm = louvain(g);
|
|
654
|
+
const groups = /* @__PURE__ */ new Map();
|
|
655
|
+
for (const [node, c] of comm) {
|
|
656
|
+
const arr = groups.get(c);
|
|
657
|
+
if (arr) arr.push(node);
|
|
658
|
+
else groups.set(c, [node]);
|
|
659
|
+
}
|
|
660
|
+
const out = [];
|
|
661
|
+
for (const members of groups.values()) {
|
|
662
|
+
if (members.length < minSize) continue;
|
|
663
|
+
const cohesion = communityCohesion(members, g);
|
|
664
|
+
if (cohesion < maxCohesion) {
|
|
665
|
+
out.push({
|
|
666
|
+
members: [...members].sort(),
|
|
667
|
+
size: members.length,
|
|
668
|
+
cohesion: Math.round(cohesion * 1e3) / 1e3,
|
|
669
|
+
action: members.length <= 5 ? "merge into adjacent community" : "split into smaller topics"
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
out.sort((a, b) => a.cohesion - b.cohesion);
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/commands/graph.ts
|
|
678
|
+
async function runGraphBuild(input) {
|
|
679
|
+
const scan = await scanVault(input.vault);
|
|
680
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
681
|
+
const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
|
|
568
682
|
const adamicAdar = computeAdamicAdar(adjacency);
|
|
569
683
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
570
684
|
try {
|
|
@@ -2469,6 +2583,19 @@ import { existsSync as existsSync4 } from "fs";
|
|
|
2469
2583
|
import { readFile as readFile16, rename as rename6 } from "fs/promises";
|
|
2470
2584
|
import { join as join21 } from "path";
|
|
2471
2585
|
|
|
2586
|
+
// src/commands/sparse-community.ts
|
|
2587
|
+
async function runSparseCommunity(input) {
|
|
2588
|
+
const scan = await scanVault(input.vault);
|
|
2589
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2590
|
+
const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
|
|
2591
|
+
const communities = findSparseCommunities(adjacency, {
|
|
2592
|
+
minSize: input.minSize,
|
|
2593
|
+
maxCohesion: input.maxCohesion
|
|
2594
|
+
});
|
|
2595
|
+
const humanHint = communities.length === 0 ? "no sparse communities" : communities.map((c) => ` cohesion ${c.cohesion} (${c.size} pages): ${c.action}`).join("\n");
|
|
2596
|
+
return { exitCode: ExitCode.OK, result: ok({ communities, humanHint }) };
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2472
2599
|
// src/commands/topic-map-check.ts
|
|
2473
2600
|
var DEFAULT_THRESHOLD = 200;
|
|
2474
2601
|
async function runTopicMapCheck(input) {
|
|
@@ -2941,7 +3068,7 @@ function extractSourceEntries(rawFm) {
|
|
|
2941
3068
|
}
|
|
2942
3069
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
|
|
2943
3070
|
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
|
|
2944
|
-
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
3071
|
+
var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
2945
3072
|
async function runLint(input) {
|
|
2946
3073
|
const buckets = {};
|
|
2947
3074
|
const fixed = [];
|
|
@@ -2984,6 +3111,10 @@ async function runLint(input) {
|
|
|
2984
3111
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
2985
3112
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
2986
3113
|
}
|
|
3114
|
+
const sparse = await runSparseCommunity({ vault: input.vault });
|
|
3115
|
+
if (sparse.result.ok && sparse.result.data.communities.length > 0) {
|
|
3116
|
+
buckets.sparse_community = sparse.result.data.communities;
|
|
3117
|
+
}
|
|
2987
3118
|
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
2988
3119
|
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
2989
3120
|
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
@@ -4855,6 +4986,55 @@ function findSkillNames(dir) {
|
|
|
4855
4986
|
}
|
|
4856
4987
|
return results;
|
|
4857
4988
|
}
|
|
4989
|
+
var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
4990
|
+
async function vaultMetrics(resolvedPath) {
|
|
4991
|
+
const ids = [
|
|
4992
|
+
["vault_metric_pages", "Vault pages by type"],
|
|
4993
|
+
["vault_metric_orphans", "Vault orphan rate"],
|
|
4994
|
+
["vault_metric_bridges", "Vault bridge count"],
|
|
4995
|
+
["vault_metric_cohesion", "Mean community cohesion"],
|
|
4996
|
+
["vault_metric_log_size", "Vault log size"]
|
|
4997
|
+
];
|
|
4998
|
+
const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
|
|
4999
|
+
if (!resolvedPath) return noVault();
|
|
5000
|
+
const scan = await scanVault(resolvedPath);
|
|
5001
|
+
if (!scan.ok) return noVault();
|
|
5002
|
+
const tk = scan.data.typedKnowledge;
|
|
5003
|
+
const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
|
|
5004
|
+
const adj = await buildWikilinkAdjacency(tk);
|
|
5005
|
+
const g = toUndirectedWeighted(adj);
|
|
5006
|
+
const nodes = [...g.keys()];
|
|
5007
|
+
const total = nodes.length;
|
|
5008
|
+
const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
|
|
5009
|
+
const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
|
|
5010
|
+
const comm = louvain(g);
|
|
5011
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5012
|
+
for (const [node, c] of comm) {
|
|
5013
|
+
const arr = groups.get(c);
|
|
5014
|
+
if (arr) arr.push(node);
|
|
5015
|
+
else groups.set(c, [node]);
|
|
5016
|
+
}
|
|
5017
|
+
const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
|
|
5018
|
+
const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
|
|
5019
|
+
let bridges = 0;
|
|
5020
|
+
for (const n of nodes) {
|
|
5021
|
+
const nbrComms = /* @__PURE__ */ new Set();
|
|
5022
|
+
for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
|
|
5023
|
+
if (nbrComms.size >= 3) bridges++;
|
|
5024
|
+
}
|
|
5025
|
+
let logLines = 0;
|
|
5026
|
+
try {
|
|
5027
|
+
logLines = readFileSync7(join26(resolvedPath, "log.md"), "utf8").split("\n").length;
|
|
5028
|
+
} catch {
|
|
5029
|
+
}
|
|
5030
|
+
return [
|
|
5031
|
+
check("info", "vault_metric_pages", "Vault pages by type", `${total} typed (${perType})`),
|
|
5032
|
+
check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
|
|
5033
|
+
check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
|
|
5034
|
+
check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
|
|
5035
|
+
check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
|
|
5036
|
+
];
|
|
5037
|
+
}
|
|
4858
5038
|
async function runDoctor(input) {
|
|
4859
5039
|
const checks = [];
|
|
4860
5040
|
const vsConfig = readVaultSyncConfig(input.home);
|
|
@@ -4890,6 +5070,7 @@ async function runDoctor(input) {
|
|
|
4890
5070
|
vaultSyncInstalled: vsConfig.installed,
|
|
4891
5071
|
vaultSyncRole: vsConfig.role
|
|
4892
5072
|
}));
|
|
5073
|
+
checks.push(...await vaultMetrics(resolvedPath));
|
|
4893
5074
|
const summary = {
|
|
4894
5075
|
pass: checks.filter((c) => c.status === "pass").length,
|
|
4895
5076
|
info: checks.filter((c) => c.status === "info").length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.8.1-beta.
|
|
3
|
+
"version": "0.8.1-beta.4",
|
|
4
4
|
"skills": "./",
|
|
5
5
|
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
|
|
6
6
|
"author": {
|