skillwiki 0.6.0 → 0.6.1-beta.1

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
@@ -2780,7 +2780,7 @@ function extractSourceEntries(rawFm) {
2780
2780
  return entries;
2781
2781
  }
2782
2782
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
2783
- var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "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"];
2783
+ 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"];
2784
2784
  var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
2785
2785
  async function runLint(input) {
2786
2786
  const buckets = {};
@@ -2865,6 +2865,16 @@ async function runLint(input) {
2865
2865
  if (subDirDupes.length > 0) {
2866
2866
  buckets.raw_subdirectory_duplicate = subDirDupes;
2867
2867
  }
2868
+ const fileSourceUrlFlags = [];
2869
+ for (const raw of scan.data.raw) {
2870
+ const text = await readPage(raw);
2871
+ const split = splitFrontmatter(text);
2872
+ if (!split.ok) continue;
2873
+ if (/^source_url:\s*file:\/\//m.test(split.data.rawFrontmatter)) {
2874
+ fileSourceUrlFlags.push(raw.relPath);
2875
+ }
2876
+ }
2877
+ if (fileSourceUrlFlags.length > 0) buckets.file_source_url = fileSourceUrlFlags;
2868
2878
  const legacyPages = [];
2869
2879
  const orphanedPages = [];
2870
2880
  const structFlags = [];
@@ -3304,6 +3314,45 @@ ${newBody}`;
3304
3314
  else delete buckets.wikilink_citation;
3305
3315
  }
3306
3316
  }
3317
+ if (input.fix && fileSourceUrlFlags.length > 0) {
3318
+ const FILE_FIXED = [];
3319
+ for (const relPath of fileSourceUrlFlags) {
3320
+ try {
3321
+ const absPath = `${input.vault}/${relPath}`;
3322
+ const raw = await readFile15(absPath, "utf8");
3323
+ const parts = raw.split("---", 3);
3324
+ if (parts.length < 3) {
3325
+ unresolved.push(relPath);
3326
+ continue;
3327
+ }
3328
+ const rawFm = parts[1];
3329
+ const rest = parts[2];
3330
+ const sourceMatch = rest.match(/^source:\s*"?(https?:\/\/[^\s\n"]+)"?\s*$/m);
3331
+ if (!sourceMatch) {
3332
+ unresolved.push(relPath);
3333
+ continue;
3334
+ }
3335
+ const realUrl = sourceMatch[1];
3336
+ const newRawFm = rawFm.replace(/^source_url:\s*file:\/\/[^\n]+/m, `source_url: ${realUrl}`);
3337
+ const newContent = `---${newRawFm}---${rest}`;
3338
+ const w = await safeWritePage(absPath, newContent);
3339
+ if (!w.ok) {
3340
+ unresolved.push(relPath);
3341
+ continue;
3342
+ }
3343
+ FILE_FIXED.push(relPath);
3344
+ } catch {
3345
+ unresolved.push(relPath);
3346
+ }
3347
+ }
3348
+ fixed.push(...FILE_FIXED);
3349
+ if (FILE_FIXED.length > 0) {
3350
+ const fixedSet = new Set(FILE_FIXED);
3351
+ const remaining = fileSourceUrlFlags.filter((p) => !fixedSet.has(p));
3352
+ if (remaining.length > 0) buckets.file_source_url = remaining;
3353
+ else delete buckets.file_source_url;
3354
+ }
3355
+ }
3307
3356
  }
3308
3357
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3309
3358
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -3681,7 +3730,7 @@ function detectFuseMount(vaultPath) {
3681
3730
  best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
3682
3731
  }
3683
3732
  }
3684
- if (best) return best;
3733
+ if (best) return { mountPoint: best.point, fsType: best.fsType };
3685
3734
  }
3686
3735
  } catch {
3687
3736
  }
@@ -4363,6 +4412,17 @@ async function runDoctor(input) {
4363
4412
  // src/commands/archive.ts
4364
4413
  import { rename as rename5, mkdir as mkdir8, readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
4365
4414
  import { join as join25, dirname as dirname9 } from "path";
4415
+ function countWikilinks(body, slug) {
4416
+ const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4417
+ const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
4418
+ const m = body.match(re);
4419
+ return m ? m.length : 0;
4420
+ }
4421
+ function arraysEqual(a, b) {
4422
+ if (a.length !== b.length) return false;
4423
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
4424
+ return true;
4425
+ }
4366
4426
  async function runArchive(input) {
4367
4427
  const scan = await scanVault(input.vault);
4368
4428
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -4378,14 +4438,81 @@ async function runArchive(input) {
4378
4438
  }
4379
4439
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
4380
4440
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
4441
+ const slug = relPath.replace(/\.md$/, "").split("/").pop();
4381
4442
  const archivePath = join25("_archive", relPath).replace(/\\/g, "/");
4443
+ let cascade;
4444
+ if (input.cascade) {
4445
+ const wikilinkRefs = [];
4446
+ const sourceArrayRefs = [];
4447
+ for (const page of scan.data.typedKnowledge) {
4448
+ if (page.relPath === relPath) continue;
4449
+ const text = await readPage(page);
4450
+ const split = splitFrontmatter(text);
4451
+ if (!split.ok) continue;
4452
+ const wl = countWikilinks(split.data.body, slug);
4453
+ if (wl > 0) wikilinkRefs.push({ page: page.relPath, count: wl });
4454
+ const fm = extractFrontmatter(text);
4455
+ if (!fm.ok) continue;
4456
+ const sources = fm.data.sources;
4457
+ if (Array.isArray(sources) && sources.includes(relPath)) {
4458
+ const before = sources.filter((s) => typeof s === "string");
4459
+ const after = before.filter((s) => s !== relPath);
4460
+ sourceArrayRefs.push({ page: page.relPath, sources_before: before, sources_after: after });
4461
+ }
4462
+ }
4463
+ const indexRefs = [];
4464
+ if (!isRaw) {
4465
+ try {
4466
+ const idx = await readFile18(join25(input.vault, "index.md"), "utf8");
4467
+ idx.split("\n").forEach((line, i) => {
4468
+ if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
4469
+ });
4470
+ } catch (e) {
4471
+ if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
4472
+ }
4473
+ }
4474
+ cascade = { wikilink_refs: wikilinkRefs, index_refs: indexRefs, source_array_refs: sourceArrayRefs };
4475
+ }
4476
+ if (input.cascade && !input.apply) {
4477
+ const summary = `DRY-RUN \u2014 would archive ${relPath}; ${cascade.wikilink_refs.length} wikilink ref(s), ${cascade.index_refs.length} index ref(s), ${cascade.source_array_refs.length} source array ref(s).`;
4478
+ return {
4479
+ exitCode: ExitCode.OK,
4480
+ result: ok({
4481
+ archived_from: relPath,
4482
+ archived_to: archivePath,
4483
+ index_updated: false,
4484
+ applied: false,
4485
+ cascade,
4486
+ humanHint: summary
4487
+ })
4488
+ };
4489
+ }
4490
+ if (input.cascade && input.apply && cascade) {
4491
+ for (const ref of cascade.source_array_refs) {
4492
+ const absPath = join25(input.vault, ref.page);
4493
+ const text = await readFile18(absPath, "utf8");
4494
+ const split = splitFrontmatter(text);
4495
+ if (!split.ok) continue;
4496
+ const before = split.data.rawFrontmatter;
4497
+ const newSourcesYaml = ref.sources_after.length === 0 ? "sources: []" : "sources:\n" + ref.sources_after.map((s) => ` - ${s}`).join("\n");
4498
+ const fmRewritten = before.replace(
4499
+ /^sources:\s*(?:\[[^\]]*\]|(?:\r?\n(?:\s*-\s.*))+)/m,
4500
+ newSourcesYaml
4501
+ );
4502
+ if (fmRewritten === before) continue;
4503
+ if (!arraysEqual(ref.sources_after, ref.sources_before)) {
4504
+ await writeFile9(absPath, `---
4505
+ ${fmRewritten}
4506
+ ---${split.data.body}`, "utf8");
4507
+ }
4508
+ }
4509
+ }
4382
4510
  await mkdir8(dirname9(join25(input.vault, archivePath)), { recursive: true });
4383
4511
  let indexUpdated = false;
4384
4512
  if (!isRaw) {
4385
4513
  const indexPath = join25(input.vault, "index.md");
4386
4514
  try {
4387
4515
  const idx = await readFile18(indexPath, "utf8");
4388
- const slug = relPath.replace(/\.md$/, "").split("/").pop();
4389
4516
  const originalLines = idx.split("\n");
4390
4517
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
4391
4518
  if (filtered.length !== originalLines.length) {
@@ -4398,12 +4525,24 @@ async function runArchive(input) {
4398
4525
  }
4399
4526
  await rename5(join25(input.vault, relPath), join25(input.vault, archivePath));
4400
4527
  appendLastOp(input.vault, {
4401
- operation: "archive",
4402
- summary: `moved ${relPath} to ${archivePath}`,
4528
+ operation: input.cascade ? "archive-cascade" : "archive",
4529
+ summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
4403
4530
  files: [relPath],
4404
4531
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4405
4532
  });
4406
- return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
4533
+ const applied = input.cascade ? true : void 0;
4534
+ const cascadeNote = input.cascade ? ` (cascade: ${cascade.source_array_refs.length} src arrays updated, ${cascade.wikilink_refs.length} wikilinks reported)` : "";
4535
+ return {
4536
+ exitCode: ExitCode.OK,
4537
+ result: ok({
4538
+ archived_from: relPath,
4539
+ archived_to: archivePath,
4540
+ index_updated: indexUpdated,
4541
+ ...applied !== void 0 ? { applied } : {},
4542
+ ...cascade ? { cascade } : {},
4543
+ humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}${cascadeNote}`
4544
+ })
4545
+ };
4407
4546
  }
4408
4547
 
4409
4548
  // src/commands/drift.ts
@@ -7310,10 +7449,10 @@ program.command("status [vault]").description("output vault diagnostics").option
7310
7449
  langEnvValue: process.env.WIKI_LANG
7311
7450
  }), v.vault);
7312
7451
  });
7313
- program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
7452
+ program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").option("--cascade", "scan vault for references (wikilinks + sources arrays); preview by default", false).option("--apply", "with --cascade: mutate sources arrays and archive (without --apply, --cascade is preview-only)", false).action(async (page, vault, opts) => {
7314
7453
  const v = await resolveVaultArg(vault, opts.wiki);
7315
7454
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7316
- else emit(await runArchive({ vault: v.vault, page }), v.vault);
7455
+ else emit(await runArchive({ vault: v.vault, page, cascade: !!opts.cascade, apply: !!opts.apply }), v.vault);
7317
7456
  });
7318
7457
  program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7319
7458
  const v = await resolveVaultArg(vault, opts.wiki);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-beta.1",
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.6.0",
3
+ "version": "0.6.1-beta.1",
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.6.0",
3
+ "version": "0.6.1-beta.1",
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.6.0",
3
+ "version": "0.6.1-beta.1",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -1,7 +1,7 @@
1
1
  ---
2
- version: 0.3.0
2
+ version: 0.6.1
3
3
  name: wiki-sync
4
- description: Safely sync the vault git repository — multi-session safe via advisory lockfile. Runs skillwiki sync status, then guides push or pull with lint guards and conflict resolution.
4
+ description: Safely sync the vault git repository — multi-session safe via advisory lockfile. Handles rebase conflict storms from archive-commit × snapshot-stream patterns. Runs skillwiki sync status, then guides push or pull with lint guards and conflict resolution.
5
5
  ---
6
6
  # wiki-sync
7
7
  ## When This Skill Activates
@@ -143,6 +143,31 @@ When `git pull --rebase` reports `CONFLICT (modify/delete)`:
143
143
  - `git -C "$VAULT" add <path>` — keep the local restoration (rebase continues)
144
144
  4. `git -C "$VAULT" rebase --continue`.
145
145
 
146
+ ### Rebase conflict storm (archive commits × snapshot stream)
147
+
148
+ When many local archive-only commits (e.g., `archive: moved X to _archive/`) are rebased over an origin/main that receives frequent snapshot commits (e.g., sg01 `Snapshot YYYYMMDD_HHMMSS`), every archive commit re-triggers the same content conflicts on shared files (`log.md`, `knowledge.md`, `spec.md`). This is predictable and can be resolved systematically.
149
+
150
+ **Detection**: 3+ consecutive rebase stops on commits whose message matches `^archive: moved`.
151
+
152
+ **Resolution**: For each archive commit during the storm:
153
+
154
+ ```bash
155
+ # Apply --ours to all conflicting files (keep HEAD = origin/main + snapshots)
156
+ for f in $(git -C "$VAULT" diff --name-only --diff-filter=U); do
157
+ git -C "$VAULT" checkout --ours "$f" && git -C "$VAULT" add "$f"
158
+ done
159
+ git -C "$VAULT" rebase --continue
160
+ ```
161
+
162
+ **After the storm passes** (non-archive commits or clean rebase), pop the stash and handle any remaining conflicts per the normal Conflict Resolution sections above.
163
+
164
+ **Prevention**:
165
+ - Sync more frequently — don't let local fall >5 commits behind origin/main
166
+ - Bundle archive commits — `skillwiki archive --batch` groups 5-10 transcript archives into one commit, reducing rebase surface
167
+ - For vaults with snapshot cron, prefer smaller, more frequent syncs over large batch rebases
168
+
169
+ See `concepts/wiki-sync-rebase-conflict-storm-pattern.md` for detailed analysis.
170
+
146
171
  ## Multi-device coordination
147
172
  When the user mentions editing from Obsidian desktop and Claude Code on a server (or any two-device setup):
148
173
  - Recommend pulling before every editing session on each device.