skillwiki 0.8.1-beta.1 → 0.8.1-beta.3

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) {
@@ -2791,6 +2918,7 @@ function buildCliSurface() {
2791
2918
  program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
2792
2919
  program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
2793
2920
  program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
2921
+ program2.command("log-append").requiredOption("--content <text>").option("--wiki <name>");
2794
2922
  program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
2795
2923
  program2.command("config");
2796
2924
  program2.command("doctor");
@@ -2940,7 +3068,7 @@ function extractSourceEntries(rawFm) {
2940
3068
  }
2941
3069
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
2942
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"];
2943
- 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"];
2944
3072
  async function runLint(input) {
2945
3073
  const buckets = {};
2946
3074
  const fixed = [];
@@ -2983,6 +3111,10 @@ async function runLint(input) {
2983
3111
  if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
2984
3112
  if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
2985
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
+ }
2986
3118
  const topicMap = await runTopicMapCheck({ vault: input.vault });
2987
3119
  if (topicMap.result.ok && topicMap.result.data.recommended) {
2988
3120
  buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.8.1-beta.1",
3
+ "version": "0.8.1-beta.3",
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.1",
3
+ "version": "0.8.1-beta.3",
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.1",
3
+ "version": "0.8.1-beta.3",
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.1",
3
+ "version": "0.8.1-beta.3",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",