skillwiki 0.2.0-beta.13 → 0.2.0-beta.15

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
@@ -67,7 +67,8 @@ var ExitCode = {
67
67
  ARCHIVE_TARGET_NOT_FOUND: 30,
68
68
  ARCHIVE_ALREADY_ARCHIVED: 31,
69
69
  DRIFT_DETECTED: 32,
70
- RAW_DEDUP_DETECTED: 33
70
+ RAW_DEDUP_DETECTED: 33,
71
+ MIGRATION_APPLIED: 34
71
72
  };
72
73
 
73
74
  // ../shared/src/json-output.ts
@@ -107,10 +108,10 @@ var TypedKnowledgeSchema = z.object({
107
108
  });
108
109
  var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
109
110
  var RawSourceSchema = z.object({
110
- title: z.string().min(1),
111
- source_url: z.string().url().nullable(),
111
+ title: z.string().min(1).optional(),
112
+ source_url: z.string().nullable(),
112
113
  ingested: isoDate,
113
- ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
114
+ ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
114
115
  sha256: sha256Hex,
115
116
  project: wikilink.optional(),
116
117
  work_item: wikilink.optional(),
@@ -704,16 +705,46 @@ import { dirname as dirname3, resolve, join as join3 } from "path";
704
705
 
705
706
  // src/parsers/citations.ts
706
707
  var FENCE2 = /```[\s\S]*?```/g;
708
+ var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
709
+ function stripFences(body) {
710
+ return body.replace(FENCE2, "");
711
+ }
707
712
  function extractCitationMarkers(body) {
708
- const stripped = body.replace(FENCE2, "");
713
+ const stripped = stripFences(body);
709
714
  const out = [];
710
- const re = /\^\[(raw\/[^\]]+)\]/g;
711
715
  let m;
712
- while ((m = re.exec(stripped)) !== null) {
716
+ while ((m = MARKER_RE.exec(stripped)) !== null) {
713
717
  out.push({ marker: m[0], target: m[1] });
714
718
  }
715
719
  return out;
716
720
  }
721
+ function hasSourcesFooter(body) {
722
+ return /^## Sources\s*$/m.test(stripFences(body));
723
+ }
724
+ function isLegacyCitationStyle(body) {
725
+ const markers = extractCitationMarkers(body);
726
+ if (markers.length === 0) return false;
727
+ if (!hasSourcesFooter(body)) return true;
728
+ const lines = stripFences(body).split("\n");
729
+ let inSources = false;
730
+ for (const line of lines) {
731
+ if (/^## Sources\b/.test(line.trim())) {
732
+ inSources = true;
733
+ continue;
734
+ }
735
+ if (inSources) continue;
736
+ const markerOnly = line.replace(MARKER_RE, "").trim();
737
+ if (markerOnly.length === 0 && /\^\[raw\//.test(line)) return true;
738
+ const lastMarkerIdx = line.lastIndexOf("^[raw/");
739
+ if (lastMarkerIdx >= 0) {
740
+ const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE, "").trim();
741
+ if (afterLast.length > 0) return true;
742
+ const beforeFirst = line.slice(0, line.indexOf("^[raw/")).trim();
743
+ if (beforeFirst.length > 0 && !/[.!?]\s*$/.test(beforeFirst)) return true;
744
+ }
745
+ }
746
+ return false;
747
+ }
717
748
 
718
749
  // src/commands/audit.ts
719
750
  async function runAudit(input) {
@@ -743,19 +774,35 @@ async function runAudit(input) {
743
774
  const unused_sources = sources.filter((s) => !referenced.has(s));
744
775
  const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
745
776
  const broken = resolved.filter((m) => !m.resolved);
777
+ const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
778
+ let footer_consistency;
779
+ if (footerMatch) {
780
+ const footerTargets = /* @__PURE__ */ new Set();
781
+ const footerRe = /\^\[(raw\/[^\]]+)\]/g;
782
+ let mm;
783
+ while ((mm = footerRe.exec(footerMatch[1])) !== null) footerTargets.add(mm[1]);
784
+ const bodyTargets = new Set(resolved.map((m) => m.target));
785
+ const missing_from_footer = [...bodyTargets].filter((t) => !footerTargets.has(t));
786
+ const extra_in_footer = [...footerTargets].filter((t) => !bodyTargets.has(t));
787
+ footer_consistency = { missing_from_footer, extra_in_footer };
788
+ }
746
789
  const hintLines = [];
747
790
  hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
748
791
  if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
749
792
  if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
793
+ if (footer_consistency) {
794
+ if (footer_consistency.missing_from_footer.length > 0) hintLines.push(`missing_from_footer: ${footer_consistency.missing_from_footer.length}`);
795
+ if (footer_consistency.extra_in_footer.length > 0) hintLines.push(`extra_in_footer: ${footer_consistency.extra_in_footer.length}`);
796
+ }
750
797
  if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
751
798
  const humanHint = hintLines.join("\n");
752
799
  if (resolved.some((m) => !m.resolved)) {
753
- return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
800
+ return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
754
801
  }
755
802
  if (unused_sources.length > 0 || missing_from_sources.length > 0) {
756
- return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
803
+ return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
757
804
  }
758
- return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
805
+ return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
759
806
  }
760
807
  async function findVaultRoot(start) {
761
808
  let cur = start;
@@ -1429,7 +1476,7 @@ async function runDedup(input) {
1429
1476
 
1430
1477
  // src/commands/lint.ts
1431
1478
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
1432
- var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
1479
+ var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans", "legacy_citation_style"];
1433
1480
  var INFO_ORDER = ["bridges", "low_confidence_single_source", "topic_map_recommended"];
1434
1481
  async function runLint(input) {
1435
1482
  const buckets = {};
@@ -1473,6 +1520,17 @@ async function runLint(input) {
1473
1520
  }
1474
1521
  const dedup = await runDedup({ vault: input.vault });
1475
1522
  if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
1523
+ const scan = await scanVault(input.vault);
1524
+ if (scan.ok) {
1525
+ const legacyPages = [];
1526
+ for (const page of scan.data.typedKnowledge) {
1527
+ const text = await readPage(page);
1528
+ const split = splitFrontmatter(text);
1529
+ if (!split.ok) continue;
1530
+ if (isLegacyCitationStyle(split.data.body)) legacyPages.push(page.relPath);
1531
+ }
1532
+ if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
1533
+ }
1476
1534
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1477
1535
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1478
1536
  const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -1796,6 +1854,151 @@ async function runDrift(input) {
1796
1854
  };
1797
1855
  }
1798
1856
 
1857
+ // src/commands/migrate-citations.ts
1858
+ import { writeFile as writeFile7 } from "fs/promises";
1859
+ var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
1860
+ function moveMarkersToParagraphEnd(body) {
1861
+ const lines = body.split("\n");
1862
+ const result = [];
1863
+ for (let i = 0; i < lines.length; i++) {
1864
+ const line = lines[i];
1865
+ if (line.trimStart().startsWith("```")) {
1866
+ result.push(line);
1867
+ if (!line.trimEnd().endsWith("```") || line.trim() === "```") {
1868
+ for (let j = i + 1; j < lines.length; j++) {
1869
+ result.push(lines[j]);
1870
+ if (lines[j].trimStart().startsWith("```")) {
1871
+ i = j;
1872
+ break;
1873
+ }
1874
+ }
1875
+ }
1876
+ continue;
1877
+ }
1878
+ if (/^## Sources\b/.test(line.trim())) {
1879
+ result.push(line);
1880
+ continue;
1881
+ }
1882
+ const markers = [...line.matchAll(MARKER_RE2)];
1883
+ if (markers.length === 0) {
1884
+ result.push(line);
1885
+ continue;
1886
+ }
1887
+ const proseOnly = line.replace(MARKER_RE2, "").trim();
1888
+ if (proseOnly.length === 0) {
1889
+ const markerStr = " " + markers.map((m) => m[0]).join(" ");
1890
+ let merged = false;
1891
+ for (let k = result.length - 1; k >= 0; k--) {
1892
+ if (result[k].trim().length > 0) {
1893
+ result[k] = result[k].trimEnd() + markerStr;
1894
+ merged = true;
1895
+ break;
1896
+ }
1897
+ }
1898
+ if (!merged) result.push(line);
1899
+ continue;
1900
+ }
1901
+ const lastMarkerIdx = line.lastIndexOf("^[raw/");
1902
+ const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE2, "").trim();
1903
+ const firstMarkerIdx = line.indexOf("^[raw/");
1904
+ const beforeFirst = line.slice(0, firstMarkerIdx).trim();
1905
+ const alreadyAtEnd = afterLast.length === 0 && (beforeFirst.length === 0 || /[.!?]\s*$/.test(beforeFirst));
1906
+ if (alreadyAtEnd) {
1907
+ result.push(line);
1908
+ continue;
1909
+ }
1910
+ let cleaned = line.replace(/\s*\^\[raw\/[^\]]+\]\s*/g, " ").trimEnd();
1911
+ cleaned = cleaned.replace(/ +/g, " ").trimEnd();
1912
+ const markerStrings = markers.map((m) => m[0]);
1913
+ if (cleaned.length > 0 && /[.!?]$/.test(cleaned)) {
1914
+ cleaned += " " + markerStrings.join(" ");
1915
+ } else if (cleaned.length > 0) {
1916
+ cleaned += ". " + markerStrings.join(" ");
1917
+ } else {
1918
+ cleaned = markerStrings.join(" ");
1919
+ }
1920
+ result.push(cleaned);
1921
+ }
1922
+ return result.join("\n");
1923
+ }
1924
+ function buildSourcesFooter(targets) {
1925
+ return "\n## Sources\n" + targets.map((t) => `- ^[${t}]`).join("\n") + "\n";
1926
+ }
1927
+ function reorderSourcesFm(rawFm, targets) {
1928
+ const sourcesLineRe = /^sources:\s*\[([^\]]*)\]\s*$/m;
1929
+ const match = rawFm.match(sourcesLineRe);
1930
+ if (!match) return rawFm;
1931
+ const existing = match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
1932
+ const targetSet = new Set(targets);
1933
+ const reordered = [
1934
+ ...targets,
1935
+ ...existing.filter((s) => !targetSet.has(s))
1936
+ ];
1937
+ const newLine = `sources: [${reordered.join(", ")}]`;
1938
+ return rawFm.replace(sourcesLineRe, newLine);
1939
+ }
1940
+ function removeExistingFooter(body) {
1941
+ const footerRe = /\n## Sources\n[\s\S]*$/;
1942
+ return body.replace(footerRe, "");
1943
+ }
1944
+ async function runMigrateCitations(input) {
1945
+ const scan = await scanVault(input.vault);
1946
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1947
+ const migrated = [];
1948
+ const skipped = [];
1949
+ let unchanged = 0;
1950
+ for (const page of scan.data.typedKnowledge) {
1951
+ const text = await readPage(page);
1952
+ const split = splitFrontmatter(text);
1953
+ if (!split.ok) continue;
1954
+ const { rawFrontmatter, body } = split.data;
1955
+ const markers = extractCitationMarkers(body);
1956
+ if (markers.length === 0) {
1957
+ unchanged++;
1958
+ continue;
1959
+ }
1960
+ const seen = /* @__PURE__ */ new Set();
1961
+ const uniqueTargets = [];
1962
+ for (const m of markers) {
1963
+ if (!seen.has(m.target)) {
1964
+ seen.add(m.target);
1965
+ uniqueTargets.push(m.target);
1966
+ }
1967
+ }
1968
+ const bodyWithoutFooter = removeExistingFooter(body);
1969
+ const migratedBody = moveMarkersToParagraphEnd(bodyWithoutFooter);
1970
+ const newFooter = buildSourcesFooter(uniqueTargets);
1971
+ const newFm = reorderSourcesFm(rawFrontmatter, uniqueTargets);
1972
+ const newText = `---
1973
+ ${newFm}
1974
+ ---
1975
+ ${migratedBody}${newFooter}`;
1976
+ if (newText === text) {
1977
+ skipped.push(page.relPath);
1978
+ continue;
1979
+ }
1980
+ if (!input.dryRun) {
1981
+ await writeFile7(page.absPath, newText, "utf8");
1982
+ }
1983
+ migrated.push(page.relPath);
1984
+ }
1985
+ const exitCode = migrated.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
1986
+ const hintLines = [`scanned: ${migrated.length + skipped.length + unchanged}`];
1987
+ if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
1988
+ if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
1989
+ if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
1990
+ return {
1991
+ exitCode,
1992
+ result: ok({
1993
+ scanned: migrated.length + skipped.length + unchanged,
1994
+ migrated,
1995
+ skipped,
1996
+ unchanged,
1997
+ humanHint: hintLines.join("\n")
1998
+ })
1999
+ };
2000
+ }
2001
+
1799
2002
  // src/cli.ts
1800
2003
  var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
1801
2004
  var program = new Command();
@@ -1931,6 +2134,11 @@ program.command("dedup [vault]").description("detect duplicate raw sources by sh
1931
2134
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1932
2135
  else emit(await runDedup({ vault: v.vault }));
1933
2136
  });
2137
+ program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).action(async (vault, opts) => {
2138
+ const v = await resolveVaultArg(vault);
2139
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2140
+ else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
2141
+ });
1934
2142
  program.parseAsync(process.argv).catch((e) => {
1935
2143
  process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
1936
2144
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.2.0-beta.13",
3
+ "version": "0.2.0-beta.15",
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.2.0-beta.13",
3
+ "version": "0.2.0-beta.15",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 11 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI (8 subcommands, JSON-by-default).",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.2.0-beta.13",
3
+ "version": "0.2.0-beta.15",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -49,7 +49,8 @@ Rule: every tag on every page MUST appear in this taxonomy. Add new tags here fi
49
49
 
50
50
  - File names: lowercase-hyphenated, no spaces.
51
51
  - Wikilinks in YAML: quoted, `"[[name]]"`. Body wikilinks: unquoted `[[name]]`.
52
- - Citations in body: `^[raw/...]` markers; every entry in `sources:` MUST appear in body.
52
+ - Citations in body: `^[raw/...]` markers at paragraph-end; every entry in `sources:` MUST appear in body and in `## Sources` footer.
53
+ - Legacy inline `^[raw/...]` markers remain valid; `migrate-citations` converts them.
53
54
  - sha256 in `raw/` frontmatter is computed by `skillwiki hash` over body bytes after closing `---`.
54
55
 
55
56
  ## Obsidian Integration