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/commands/graph.ts
552
- async function runGraphBuild(input) {
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 scan.data.typedKnowledge) {
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 scan.data.typedKnowledge) {
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.2",
3
+ "version": "0.8.1-beta.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.8.1-beta.2",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.8.1-beta.2",
3
+ "version": "0.8.1-beta.4",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.8.1-beta.2",
3
+ "version": "0.8.1-beta.4",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",