skillwiki 0.10.49 → 0.10.50

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.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  atomicWriteText,
4
4
  buildRootIndexUniverse
5
- } from "./chunk-UHZBOOMK.js";
5
+ } from "./chunk-74OSLCXE.js";
6
6
  import {
7
7
  authorizeRawOperation,
8
8
  buildSourceReferenceIndex,
@@ -22,7 +22,7 @@ import {
22
22
  resolveExistingRegularFileInsideVault,
23
23
  stripFencedBlocks,
24
24
  writeLogEvent
25
- } from "./chunk-PLUHIOCQ.js";
25
+ } from "./chunk-GAHMWLWU.js";
26
26
  import {
27
27
  ExitCode,
28
28
  FleetManifestSchema,
@@ -40,11 +40,11 @@ import {
40
40
  scanVault,
41
41
  splitFrontmatter,
42
42
  vaultIoConcurrency
43
- } from "./chunk-EVQASYII.js";
43
+ } from "./chunk-IJ7DD7QZ.js";
44
44
 
45
45
  // src/utils/managed-write-preflight.ts
46
46
  import { existsSync as existsSync11 } from "fs";
47
- import { join as join20, resolve as resolve6 } from "path";
47
+ import { join as join21, resolve as resolve6 } from "path";
48
48
 
49
49
  // src/commands/fleet.ts
50
50
  import { readFile as readFile2 } from "fs/promises";
@@ -606,13 +606,332 @@ function safeUserName() {
606
606
 
607
607
  // src/commands/sync.ts
608
608
  import { existsSync as existsSync8 } from "fs";
609
- import { join as join16 } from "path";
609
+ import { join as join17 } from "path";
610
610
  import { execFileSync as execFileSync3 } from "child_process";
611
611
 
612
- // src/commands/lint.ts
612
+ // src/utils/last-op.ts
613
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
614
+ import { join as join2 } from "path";
615
+ var LAST_OP_DIR = ".skillwiki";
616
+ var LAST_OP_FILE = "last-op.json";
617
+ function lastOpPath(vault) {
618
+ return join2(vault, LAST_OP_DIR, LAST_OP_FILE);
619
+ }
620
+ function readLastOp(vault) {
621
+ const p = lastOpPath(vault);
622
+ if (!existsSync(p)) return [];
623
+ try {
624
+ const raw = readFileSync(p, "utf8");
625
+ const parsed = JSON.parse(raw);
626
+ if (!Array.isArray(parsed)) {
627
+ unlinkSync(p);
628
+ return [];
629
+ }
630
+ return parsed;
631
+ } catch {
632
+ try {
633
+ unlinkSync(p);
634
+ } catch (_e) {
635
+ }
636
+ return [];
637
+ }
638
+ }
639
+ function appendLastOp(vault, entry) {
640
+ const existing = readLastOp(vault);
641
+ existing.push(entry);
642
+ const dir = join2(vault, LAST_OP_DIR);
643
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
644
+ writeFileSync(lastOpPath(vault), JSON.stringify(existing, null, 2), "utf8");
645
+ }
646
+ function clearLastOp(vault) {
647
+ const p = lastOpPath(vault);
648
+ try {
649
+ unlinkSync(p);
650
+ } catch (_e) {
651
+ }
652
+ }
653
+
654
+ // src/lint/helpers.ts
655
+ import { readdir } from "fs/promises";
656
+ import { join as join3, relative, sep } from "path";
657
+ var ERROR_ORDER = [
658
+ "sensitive_content",
659
+ "conflict_markers",
660
+ "broken_wikilinks",
661
+ "invalid_frontmatter",
662
+ "raw_source_identity_conflict",
663
+ "raw_dedup",
664
+ "broken_sources",
665
+ "tag_not_in_taxonomy",
666
+ "path_too_long"
667
+ ];
668
+ var WARNING_ORDER = [
669
+ "raw_body_duplicate",
670
+ "raw_subdirectory_duplicate",
671
+ "file_source_url",
672
+ "index_incomplete",
673
+ "index_link_format",
674
+ "stale_page",
675
+ "page_too_large",
676
+ "log_rotate_needed",
677
+ "orphans",
678
+ "compound_refs",
679
+ "legacy_citation_style",
680
+ "orphaned_citations",
681
+ "duplicate_frontmatter",
682
+ "frontmatter_yaml_invalid",
683
+ "work_item_health",
684
+ "orphaned_project_pages",
685
+ "missing_overview",
686
+ "missing_diagram",
687
+ "cycle_traps"
688
+ ];
689
+ var INFO_ORDER = [
690
+ "bridges",
691
+ "sparse_community",
692
+ "page_structure",
693
+ "topic_map_recommended",
694
+ "frontmatter_wikilink",
695
+ "wikilink_citation",
696
+ "missing_tldr",
697
+ "stale_sections",
698
+ "cli_refs"
699
+ ];
700
+ var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
701
+ var CLI_REFS_TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
702
+ var STRUCT_MIN_BODY_LINES = 60;
703
+ var STRUCT_MIN_SECTIONS = 3;
704
+ var CANONICAL_LOCAL_SOURCE_LABEL = /^\s*(?:>\s*)?(?:[-*+]\s*)?Source (?:file|inspected):/i;
705
+ var LOCAL_ABSOLUTE_SOURCE_REF = /(?:file:\/\/(?:\/)?(?:Users|home)\/|\/(?:Users|home)\/)/;
706
+ function hasDuplicateFrontmatter(body) {
707
+ if (/^---\r?\n/.test(body)) return true;
708
+ const lines = body.split(/\r?\n/);
709
+ const limit = Math.min(lines.length, 20);
710
+ let seenYamlKey = false;
711
+ for (let i = 0; i < limit; i++) {
712
+ if (/^\w[\w-]*:/.test(lines[i].trim())) seenYamlKey = true;
713
+ if (seenYamlKey && lines[i].trim() === "---") return true;
714
+ }
715
+ return false;
716
+ }
717
+ function hasCanonicalLocalSourceAssertion(body) {
718
+ const visibleBody = stripFencedBlocks(body);
719
+ return visibleBody.split(/\r?\n/).some(
720
+ (line) => CANONICAL_LOCAL_SOURCE_LABEL.test(line) && LOCAL_ABSOLUTE_SOURCE_REF.test(line)
721
+ );
722
+ }
723
+ function shouldCheckCanonicalLocalSourceAssertion(page) {
724
+ if (page.relPath.startsWith("raw/transcripts/")) return false;
725
+ if (/^projects\/[^/]+\/work\/[^/]+\/log\.md$/.test(page.relPath)) return false;
726
+ if (page.relPath.startsWith("raw/")) return true;
727
+ if (/^(entities|concepts|comparisons|queries|meta)\//.test(page.relPath)) return true;
728
+ if (/^projects\/[^/]+\/compound\//.test(page.relPath)) return true;
729
+ if (/^projects\/[^/]+\/work\/[^/]+\/(spec|plan)\.md$/.test(page.relPath)) return true;
730
+ return false;
731
+ }
732
+ function extractSourceEntries(rawFm) {
733
+ const lines = rawFm.split(/\r?\n/);
734
+ const sourcesLineIdx = lines.findIndex((l) => /^sources:/.test(l));
735
+ if (sourcesLineIdx === -1) return [];
736
+ const sourcesLine = lines[sourcesLineIdx].trim();
737
+ const inlineMatch = sourcesLine.match(/^sources:\s*\[(.+)]\s*$/);
738
+ if (inlineMatch) {
739
+ return [...inlineMatch[1].matchAll(/"[^"]*"|'[^']*'|[^,\s]\S*/g)].map(
740
+ (m) => m[0].replace(/,\s*$/, "")
741
+ );
742
+ }
743
+ const entries = [];
744
+ for (let i = sourcesLineIdx + 1; i < lines.length; i++) {
745
+ const line = lines[i];
746
+ if (!/^\s+- /.test(line)) break;
747
+ entries.push(line.replace(/^\s+- /, "").trim());
748
+ }
749
+ return entries;
750
+ }
751
+ function shellQuote(value) {
752
+ return `'${value.replace(/'/g, `'\\''`)}'`;
753
+ }
754
+ function formatExample(item) {
755
+ if (typeof item === "string") return item;
756
+ if (item === null || item === void 0) return String(item);
757
+ try {
758
+ return JSON.stringify(item);
759
+ } catch {
760
+ return String(item);
761
+ }
762
+ }
763
+ function summarizeBucket(bucket, severity, vaultPath, examplesLimit) {
764
+ const safeLimit = Math.max(0, Math.min(examplesLimit, 10));
765
+ const examples = bucket.items.slice(0, safeLimit).map(formatExample);
766
+ return {
767
+ kind: bucket.kind,
768
+ severity,
769
+ count: bucket.items.length,
770
+ examples,
771
+ examples_limit: safeLimit,
772
+ sample_truncated: bucket.items.length > examples.length,
773
+ details_command: `skillwiki lint ${shellQuote(vaultPath)} --only ${bucket.kind}`
774
+ };
775
+ }
776
+ function severityForBucket(kind) {
777
+ if (ERROR_ORDER.includes(kind)) return "error";
778
+ if (WARNING_ORDER.includes(kind)) return "warning";
779
+ return "info";
780
+ }
781
+ function lintReadVault(input) {
782
+ if (input.fix) {
783
+ return { readPath: input.vault, readMirror: false };
784
+ }
785
+ const resolved = resolveReadOnlyVaultRoot(input.vault);
786
+ return { readPath: resolved.root, readMirror: resolved.mirrored };
787
+ }
788
+ function lintVaultOutput(input, readVault) {
789
+ return {
790
+ path: input.vault,
791
+ source: input.source ?? "resolved",
792
+ read_path: readVault.readPath,
793
+ read_mirror: readVault.readMirror
794
+ };
795
+ }
796
+ function readMirrorHintLines(vault) {
797
+ if (!vault.read_mirror) return [];
798
+ return [
799
+ `read mirror: ${vault.read_path}`,
800
+ `requested vault: ${vault.path}`,
801
+ "if results look stale, refresh the read mirror or rerun with SKILLWIKI_DISABLE_VAULT_READ_MIRROR=1 for a live scan; live scans may be slower"
802
+ ];
803
+ }
804
+ function appendLintFixLastOp(vault, fixed) {
805
+ if (fixed.length === 0) return;
806
+ appendLastOp(vault, {
807
+ operation: "lint-fix",
808
+ summary: `fixed ${fixed.length} page(s)`,
809
+ files: fixed,
810
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
811
+ });
812
+ }
813
+ function summarizeLintOutput(output, examplesLimit = 3) {
814
+ const buckets = [
815
+ ...output.by_severity.error.map(
816
+ (bucket) => summarizeBucket(bucket, "error", output.vault.path, examplesLimit)
817
+ ),
818
+ ...output.by_severity.warning.map(
819
+ (bucket) => summarizeBucket(bucket, "warning", output.vault.path, examplesLimit)
820
+ ),
821
+ ...output.by_severity.info.map(
822
+ (bucket) => summarizeBucket(bucket, "info", output.vault.path, examplesLimit)
823
+ )
824
+ ];
825
+ const lines = [];
826
+ lines.push(...readMirrorHintLines(output.vault));
827
+ lines.push(`errors: ${output.summary.errors}`);
828
+ lines.push(`warnings: ${output.summary.warnings}`);
829
+ lines.push(`info: ${output.summary.info}`);
830
+ for (const bucket of buckets) {
831
+ lines.push(` ${bucket.kind}: ${bucket.count}`);
832
+ if (bucket.examples.length > 0) {
833
+ lines.push(` e.g. ${bucket.examples[0]}`);
834
+ }
835
+ }
836
+ return {
837
+ vault: output.vault,
838
+ summary: output.summary,
839
+ buckets,
840
+ details_included: false,
841
+ truncated: false,
842
+ fixed: output.fixed,
843
+ unresolved: output.unresolved,
844
+ humanHint: lines.join("\n")
845
+ };
846
+ }
847
+ function outputForOnlyBucket(input, match, fixed, unresolved, readVault = lintReadVault(input)) {
848
+ const severity = severityForBucket(input.only);
849
+ const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
850
+ const summary = {
851
+ errors: filtered.error.reduce((n, b) => n + b.items.length, 0),
852
+ warnings: filtered.warning.reduce((n, b) => n + b.items.length, 0),
853
+ info: filtered.info.reduce((n, b) => n + b.items.length, 0)
854
+ };
855
+ let exitCode = ExitCode.OK;
856
+ if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
857
+ else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
858
+ const vault = lintVaultOutput(input, readVault);
859
+ const hintLines = [
860
+ ...readMirrorHintLines(vault),
861
+ `--only ${input.only}`,
862
+ match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")
863
+ ];
864
+ const output = {
865
+ vault,
866
+ summary,
867
+ by_severity: filtered,
868
+ fixed,
869
+ unresolved,
870
+ humanHint: hintLines.join("\n")
871
+ };
872
+ return {
873
+ exitCode,
874
+ result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
875
+ };
876
+ }
877
+ async function walkMarkdownFiles(absDir, vaultRoot) {
878
+ const entries = await readdir(absDir, { withFileTypes: true });
879
+ const pages = [];
880
+ for (const entry of entries) {
881
+ const absPath = join3(absDir, entry.name);
882
+ if (entry.isDirectory()) {
883
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
884
+ pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
885
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
886
+ pages.push({ absPath, relPath: relative(vaultRoot, absPath).split(sep).join("/") });
887
+ }
888
+ }
889
+ return pages;
890
+ }
891
+
892
+ // src/lint/fingerprints.ts
893
+ function lintIssueFingerprint(bucket, item) {
894
+ const page = extractIssuePage(item);
895
+ const detail = normalizeIssueDetail(item);
896
+ return `${bucket}\0${page}\0${detail}`;
897
+ }
898
+ function extractIssuePage(item) {
899
+ if (typeof item === "string") {
900
+ const m = item.match(/^([^:]+?)(?::\s|$)/);
901
+ return (m?.[1] ?? item).trim();
902
+ }
903
+ if (item && typeof item === "object") {
904
+ const obj = item;
905
+ for (const key of ["path", "file", "page", "relPath"]) {
906
+ if (typeof obj[key] === "string") return obj[key];
907
+ }
908
+ }
909
+ return "";
910
+ }
911
+ function normalizeIssueDetail(item) {
912
+ if (typeof item === "string") {
913
+ return item.replace(/\s+/g, " ").trim();
914
+ }
915
+ try {
916
+ return JSON.stringify(item, Object.keys(item).sort());
917
+ } catch {
918
+ return String(item);
919
+ }
920
+ }
921
+ function collectLintErrorFingerprints(output) {
922
+ const fps = /* @__PURE__ */ new Set();
923
+ for (const bucket of output.by_severity.error) {
924
+ for (const item of bucket.items) {
925
+ fps.add(lintIssueFingerprint(bucket.kind, item));
926
+ }
927
+ }
928
+ return fps;
929
+ }
930
+
931
+ // src/lint/rules.ts
613
932
  import { existsSync as existsSync4 } from "fs";
614
- import { readFile as readFile13, readdir as readdir2 } from "fs/promises";
615
- import { join as join12, relative, sep } from "path";
933
+ import { readFile as readFile13 } from "fs/promises";
934
+ import { join as join13 } from "path";
616
935
 
617
936
  // src/parsers/wikilinks.ts
618
937
  var FENCE = /```[\s\S]*?```|`[^`\n]*`/g;
@@ -720,7 +1039,7 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
720
1039
 
721
1040
  // src/commands/tag-audit.ts
722
1041
  import { readFile as readFile3 } from "fs/promises";
723
- import { join as join2 } from "path";
1042
+ import { join as join4 } from "path";
724
1043
 
725
1044
  // src/parsers/taxonomy.ts
726
1045
  import yaml2 from "js-yaml";
@@ -873,7 +1192,7 @@ async function runTagAudit(input) {
873
1192
  const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
874
1193
  if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
875
1194
  const scan = scanResult.data;
876
- const schemaText = await readFile3(join2(input.vault, "SCHEMA.md"), "utf8");
1195
+ const schemaText = await readFile3(join4(input.vault, "SCHEMA.md"), "utf8");
877
1196
  const tax = extractTaxonomy(schemaText);
878
1197
  if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
879
1198
  const allowed = new Set(tax.data);
@@ -906,7 +1225,7 @@ async function runTagAudit(input) {
906
1225
 
907
1226
  // src/commands/index-check.ts
908
1227
  import { readFile as readFile4 } from "fs/promises";
909
- import { join as join3 } from "path";
1228
+ import { join as join5 } from "path";
910
1229
  function normalizeIndexTarget(raw) {
911
1230
  return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
912
1231
  }
@@ -918,7 +1237,7 @@ async function runIndexCheck(input) {
918
1237
  }
919
1238
  let indexText = "";
920
1239
  try {
921
- indexText = await readFile4(join3(input.vault, "index.md"), "utf8");
1240
+ indexText = await readFile4(join5(input.vault, "index.md"), "utf8");
922
1241
  } catch {
923
1242
  }
924
1243
  const indexTargets = /* @__PURE__ */ new Set();
@@ -971,9 +1290,28 @@ async function runIndexCheck(input) {
971
1290
  return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
972
1291
  }
973
1292
 
1293
+ // src/commands/index-link-format.ts
1294
+ import { readFile as readFile5 } from "fs/promises";
1295
+ import { join as join6 } from "path";
1296
+ var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
1297
+ async function runIndexLinkFormat(input) {
1298
+ let text = "";
1299
+ try {
1300
+ text = await readFile5(join6(input.vault, "index.md"), "utf8");
1301
+ } catch {
1302
+ }
1303
+ const markdown_links = [];
1304
+ for (const [i, line] of text.split("\n").entries()) {
1305
+ if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
1306
+ }
1307
+ const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
1308
+ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
1309
+ return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
1310
+ }
1311
+
974
1312
  // src/commands/stale.ts
975
- import { readdir, rename, mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
976
- import { join as join5 } from "path";
1313
+ import { readdir as readdir2, rename, mkdir as mkdir3, readFile as readFile7 } from "fs/promises";
1314
+ import { join as join7 } from "path";
977
1315
 
978
1316
  // src/parsers/expiry-annotations.ts
979
1317
  var HEADING_RE = /^#{1,6}\s+(.+)$/;
@@ -1009,51 +1347,9 @@ function parseExpiryAnnotations(content, pagePath) {
1009
1347
  return annotations;
1010
1348
  }
1011
1349
 
1012
- // src/utils/last-op.ts
1013
- import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
1014
- import { join as join4 } from "path";
1015
- var LAST_OP_DIR = ".skillwiki";
1016
- var LAST_OP_FILE = "last-op.json";
1017
- function lastOpPath(vault) {
1018
- return join4(vault, LAST_OP_DIR, LAST_OP_FILE);
1019
- }
1020
- function readLastOp(vault) {
1021
- const p = lastOpPath(vault);
1022
- if (!existsSync(p)) return [];
1023
- try {
1024
- const raw = readFileSync(p, "utf8");
1025
- const parsed = JSON.parse(raw);
1026
- if (!Array.isArray(parsed)) {
1027
- unlinkSync(p);
1028
- return [];
1029
- }
1030
- return parsed;
1031
- } catch {
1032
- try {
1033
- unlinkSync(p);
1034
- } catch (_e) {
1035
- }
1036
- return [];
1037
- }
1038
- }
1039
- function appendLastOp(vault, entry) {
1040
- const existing = readLastOp(vault);
1041
- existing.push(entry);
1042
- const dir = join4(vault, LAST_OP_DIR);
1043
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1044
- writeFileSync(lastOpPath(vault), JSON.stringify(existing, null, 2), "utf8");
1045
- }
1046
- function clearLastOp(vault) {
1047
- const p = lastOpPath(vault);
1048
- try {
1049
- unlinkSync(p);
1050
- } catch (_e) {
1051
- }
1052
- }
1053
-
1054
1350
  // src/utils/raw-structural-transaction.ts
1055
1351
  import { createHash } from "crypto";
1056
- import { copyFile, mkdir as mkdir2, readFile as readFile5, unlink } from "fs/promises";
1352
+ import { copyFile, mkdir as mkdir2, readFile as readFile6, unlink } from "fs/promises";
1057
1353
  import { constants } from "fs";
1058
1354
  import { dirname as dirname2 } from "path";
1059
1355
  function sha256(bytes) {
@@ -1109,7 +1405,7 @@ async function planRawStructuralMove(input) {
1109
1405
  if (!destinationResolved.ok) {
1110
1406
  return destinationResolved.error === "RAW_DESTINATION_EXISTS" ? destinationResolved : err("RAW_DESTINATION_UNSAFE", { path: input.destination, cause: destinationResolved });
1111
1407
  }
1112
- const sourceBytes = await readFile5(sourceResolved.data);
1408
+ const sourceBytes = await readFile6(sourceResolved.data);
1113
1409
  const sourceSha = sha256(sourceBytes);
1114
1410
  const operation_id = operationId("raw-structural", [input.operation, input.source, input.destination, sourceSha]);
1115
1411
  const base = {
@@ -1142,7 +1438,7 @@ async function applyRawStructuralMove(input) {
1142
1438
  } catch (error) {
1143
1439
  return err("RAW_MOVE_COPY_FAILED", { source: input.source, destination: input.destination, message: String(error) });
1144
1440
  }
1145
- const destinationBytes = await readFile5(destinationAbs);
1441
+ const destinationBytes = await readFile6(destinationAbs);
1146
1442
  const destinationSha = sha256(destinationBytes);
1147
1443
  if (destinationSha !== planned.data.source_sha256) {
1148
1444
  return err("RAW_MOVE_HASH_MISMATCH", { source: input.source, destination: input.destination, source_sha256: planned.data.source_sha256, destination_sha256: destinationSha });
@@ -1288,10 +1584,10 @@ async function runStale(input) {
1288
1584
  const incompleteWorkItems = [];
1289
1585
  const archived = [];
1290
1586
  const workDirs = /* @__PURE__ */ new Map();
1291
- const projectsDir = join5(input.vault, "projects");
1587
+ const projectsDir = join7(input.vault, "projects");
1292
1588
  let projectSlugs = [];
1293
1589
  try {
1294
- projectSlugs = (await readdir(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
1590
+ projectSlugs = (await readdir2(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
1295
1591
  } catch {
1296
1592
  }
1297
1593
  if (input.project) {
@@ -1301,21 +1597,21 @@ async function runStale(input) {
1301
1597
  projectSlugs = [input.project];
1302
1598
  }
1303
1599
  for (const slug of projectSlugs) {
1304
- const workPath = join5(projectsDir, slug, "work");
1600
+ const workPath = join7(projectsDir, slug, "work");
1305
1601
  let entries;
1306
1602
  try {
1307
- entries = await readdir(workPath, { withFileTypes: true });
1603
+ entries = await readdir2(workPath, { withFileTypes: true });
1308
1604
  } catch {
1309
1605
  continue;
1310
1606
  }
1311
1607
  for (const e of entries) {
1312
1608
  if (!e.isDirectory()) continue;
1313
1609
  const relDir = `projects/${slug}/work/${e.name}`;
1314
- const absDir = join5(workPath, e.name);
1610
+ const absDir = join7(workPath, e.name);
1315
1611
  let status = "";
1316
1612
  let files;
1317
1613
  try {
1318
- files = await readdir(absDir);
1614
+ files = await readdir2(absDir);
1319
1615
  } catch {
1320
1616
  workDirs.set(relDir, "");
1321
1617
  continue;
@@ -1323,7 +1619,7 @@ async function runStale(input) {
1323
1619
  for (const f of files) {
1324
1620
  if (!f.endsWith(".md")) continue;
1325
1621
  try {
1326
- const fm = extractFrontmatter(await readFile6(join5(absDir, f), "utf8"));
1622
+ const fm = extractFrontmatter(await readFile7(join7(absDir, f), "utf8"));
1327
1623
  if (fm.ok && typeof fm.data.status === "string") {
1328
1624
  status = fm.data.status;
1329
1625
  break;
@@ -1383,9 +1679,9 @@ async function runStale(input) {
1383
1679
  if (entry) transcriptMeta.set(entry[0], entry[1]);
1384
1680
  }
1385
1681
  const claimSources = await mapWithConcurrency([...workDirs.keys()], vaultIoConcurrency(), async (relDir) => {
1386
- const specPath = join5(input.vault, relDir, "spec.md");
1682
+ const specPath = join7(input.vault, relDir, "spec.md");
1387
1683
  try {
1388
- const specContent = await readFile6(specPath, "utf8");
1684
+ const specContent = await readFile7(specPath, "utf8");
1389
1685
  const specFm = extractFrontmatter(specContent);
1390
1686
  if (!specFm.ok) return null;
1391
1687
  return {
@@ -1427,7 +1723,7 @@ async function runStale(input) {
1427
1723
  if (daysSince(dateStr) < input.days) continue;
1428
1724
  let files;
1429
1725
  try {
1430
- files = await readdir(join5(input.vault, relDir));
1726
+ files = await readdir2(join7(input.vault, relDir));
1431
1727
  } catch {
1432
1728
  continue;
1433
1729
  }
@@ -1497,7 +1793,7 @@ async function runStale(input) {
1497
1793
  staleSections.push(...staleSectionResults.flat());
1498
1794
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1499
1795
  if (input.archive) {
1500
- const archiveDir = join5(input.vault, "_archive", today);
1796
+ const archiveDir = join7(input.vault, "_archive", today);
1501
1797
  const relocations = await readSourceRelocations(input.vault);
1502
1798
  if (!relocations.ok) return { exitCode: ExitCode.WRITE_FAILED, result: relocations };
1503
1799
  const typedPaths = new Set(scan.typedKnowledge.map((page) => page.relPath));
@@ -1566,19 +1862,19 @@ async function runStale(input) {
1566
1862
  const active = parseActiveWorkPath(w.path);
1567
1863
  if (active) {
1568
1864
  const { project: slug, item: itemName } = active;
1569
- const histDir = join5(input.vault, "projects", slug, "history", "archived-work");
1865
+ const histDir = join7(input.vault, "projects", slug, "history", "archived-work");
1570
1866
  await mkdir3(histDir, { recursive: true });
1571
- const dest = join5(histDir, itemName);
1867
+ const dest = join7(histDir, itemName);
1572
1868
  try {
1573
- await rename(join5(input.vault, w.path), dest);
1869
+ await rename(join7(input.vault, w.path), dest);
1574
1870
  archived.push(w.path);
1575
1871
  } catch (error) {
1576
1872
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: w.path, destination: dest, message: String(error) }) };
1577
1873
  }
1578
1874
  } else {
1579
- const dest = join5(archiveDir, w.path.replace(/\//g, "_"));
1875
+ const dest = join7(archiveDir, w.path.replace(/\//g, "_"));
1580
1876
  try {
1581
- await rename(join5(input.vault, w.path), dest);
1877
+ await rename(join7(input.vault, w.path), dest);
1582
1878
  archived.push(w.path);
1583
1879
  } catch (error) {
1584
1880
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: w.path, destination: dest, message: String(error) }) };
@@ -1634,8 +1930,8 @@ async function runPagesize(input) {
1634
1930
  }
1635
1931
 
1636
1932
  // src/commands/log-rotate.ts
1637
- import { access, readFile as readFile7, rename as rename2, writeFile as writeFile2, stat } from "fs/promises";
1638
- import { join as join6 } from "path";
1933
+ import { access, readFile as readFile8, rename as rename2, writeFile as writeFile2, stat } from "fs/promises";
1934
+ import { join as join8 } from "path";
1639
1935
  var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
1640
1936
  var FULL_DATE_RE = /^## \[(\d{4}-\d{2}-\d{2})\]/gm;
1641
1937
  async function pathExists(p) {
@@ -1658,27 +1954,27 @@ function archiveNameForExistingYearLog(existingText, fallbackYear) {
1658
1954
  }
1659
1955
  async function sidelineExistingYearLog(vault, rotatedPath, fallbackYear) {
1660
1956
  if (!await pathExists(rotatedPath)) return void 0;
1661
- const existingText = await readFile7(rotatedPath, "utf8");
1957
+ const existingText = await readFile8(rotatedPath, "utf8");
1662
1958
  let archiveName = archiveNameForExistingYearLog(existingText, fallbackYear);
1663
- let archivePath = join6(vault, archiveName);
1959
+ let archivePath = join8(vault, archiveName);
1664
1960
  if (await pathExists(archivePath)) {
1665
1961
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1666
1962
  archiveName = archiveName.replace(/\.md$/, `-${stamp}.md`);
1667
- archivePath = join6(vault, archiveName);
1963
+ archivePath = join8(vault, archiveName);
1668
1964
  }
1669
1965
  await rename2(rotatedPath, archivePath);
1670
1966
  return archiveName;
1671
1967
  }
1672
1968
  async function runLogRotate(input) {
1673
1969
  try {
1674
- await stat(join6(input.vault, "SCHEMA.md"));
1970
+ await stat(join8(input.vault, "SCHEMA.md"));
1675
1971
  } catch {
1676
1972
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
1677
1973
  }
1678
- const logPath = join6(input.vault, "log.md");
1974
+ const logPath = join8(input.vault, "log.md");
1679
1975
  let logText;
1680
1976
  try {
1681
- logText = await readFile7(logPath, "utf8");
1977
+ logText = await readFile8(logPath, "utf8");
1682
1978
  } catch {
1683
1979
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
1684
1980
  }
@@ -1689,7 +1985,7 @@ async function runLogRotate(input) {
1689
1985
  }
1690
1986
  const newestYear = matches[matches.length - 1][1];
1691
1987
  const rotatedName = `log-${newestYear}.md`;
1692
- const rotatedPath = join6(input.vault, rotatedName);
1988
+ const rotatedPath = join8(input.vault, rotatedName);
1693
1989
  const yearExists = await pathExists(rotatedPath);
1694
1990
  if (!input.apply) {
1695
1991
  const hint = yearExists ? `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply); existing ${rotatedName} will be sidelined to log-archive-* first` : `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)`;
@@ -1740,7 +2036,7 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
1740
2036
  }
1741
2037
 
1742
2038
  // src/utils/wiki-path.ts
1743
- import { join as join7 } from "path";
2039
+ import { join as join9 } from "path";
1744
2040
  async function resolveInitTimePath(input) {
1745
2041
  const chain = [];
1746
2042
  if (input.flag !== void 0 && input.flag.length > 0) {
@@ -1753,27 +2049,27 @@ async function resolveInitTimePath(input) {
1753
2049
  return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
1754
2050
  }
1755
2051
  if (input.explain) chain.push({ source: "env", matched: false });
1756
- const sw = await parseDotenvFile(join7(input.home, ".skillwiki", ".env"));
2052
+ const sw = await parseDotenvFile(join9(input.home, ".skillwiki", ".env"));
1757
2053
  if (sw.WIKI_PATH !== void 0) {
1758
2054
  if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
1759
2055
  return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
1760
2056
  }
1761
2057
  if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
1762
- const hermes = await parseDotenvFile(join7(input.home, ".hermes", ".env"));
2058
+ const hermes = await parseDotenvFile(join9(input.home, ".hermes", ".env"));
1763
2059
  if (hermes.WIKI_PATH !== void 0) {
1764
2060
  if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
1765
2061
  return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
1766
2062
  }
1767
2063
  if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
1768
2064
  if (input.cwd) {
1769
- const projCfg = await parseDotenvFile(join7(input.cwd, ".skillwiki", ".env"));
2065
+ const projCfg = await parseDotenvFile(join9(input.cwd, ".skillwiki", ".env"));
1770
2066
  if (projCfg.WIKI_PATH !== void 0) {
1771
2067
  if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
1772
2068
  return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
1773
2069
  }
1774
2070
  }
1775
2071
  if (input.explain) chain.push({ source: "project-dotenv", matched: false });
1776
- const fallback = join7(input.home, "wiki");
2072
+ const fallback = join9(input.home, "wiki");
1777
2073
  if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
1778
2074
  return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
1779
2075
  }
@@ -1784,7 +2080,7 @@ async function resolveRuntimePath(input) {
1784
2080
  return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
1785
2081
  }
1786
2082
  if (input.explain) chain.push({ source: "flag", matched: false });
1787
- const swGlobal = await parseDotenvFile(join7(input.home, ".skillwiki", ".env"));
2083
+ const swGlobal = await parseDotenvFile(join9(input.home, ".skillwiki", ".env"));
1788
2084
  const wikiName = input.wiki;
1789
2085
  if (wikiName !== void 0 && wikiName.length > 0) {
1790
2086
  if (wikiName.toLowerCase() === "default") {
@@ -1828,7 +2124,7 @@ async function resolveRuntimePath(input) {
1828
2124
  }
1829
2125
  if (input.explain) chain.push({ source: "env", matched: false });
1830
2126
  if (input.cwd) {
1831
- const projCfg = await parseDotenvFile(join7(input.cwd, ".skillwiki", ".env"));
2127
+ const projCfg = await parseDotenvFile(join9(input.cwd, ".skillwiki", ".env"));
1832
2128
  if (projCfg.WIKI_PATH !== void 0) {
1833
2129
  if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
1834
2130
  return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
@@ -2107,29 +2403,10 @@ async function runTopicMapCheck(input) {
2107
2403
  };
2108
2404
  }
2109
2405
 
2110
- // src/commands/index-link-format.ts
2111
- import { readFile as readFile8 } from "fs/promises";
2112
- import { join as join8 } from "path";
2113
- var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
2114
- async function runIndexLinkFormat(input) {
2115
- let text = "";
2116
- try {
2117
- text = await readFile8(join8(input.vault, "index.md"), "utf8");
2118
- } catch {
2119
- }
2120
- const markdown_links = [];
2121
- for (const [i, line] of text.split("\n").entries()) {
2122
- if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
2123
- }
2124
- const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
2125
- ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
2126
- return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
2127
- }
2128
-
2129
2406
  // src/commands/dedup.ts
2130
2407
  import { createHash as createHash3 } from "crypto";
2131
2408
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2132
- import { dirname as dirname3, join as join9, posix, resolve } from "path";
2409
+ import { dirname as dirname3, join as join10, posix, resolve } from "path";
2133
2410
 
2134
2411
  // src/utils/rclone.ts
2135
2412
  import { execFile } from "child_process";
@@ -2374,7 +2651,7 @@ async function runDedup(input) {
2374
2651
  const structuralPlans = [];
2375
2652
  for (const entry of safeEntries) {
2376
2653
  for (const duplicate of entry.duplicates) {
2377
- if (!existsSync2(join9(input.vault, duplicate))) continue;
2654
+ if (!existsSync2(join10(input.vault, duplicate))) continue;
2378
2655
  const destination = dedupDestination(input.vault, duplicate);
2379
2656
  const plan = await planRawStructuralMove({ vault: input.vault, operation: "deduplicate", source: duplicate, destination });
2380
2657
  if (!plan.ok) return { exitCode: ExitCode.WRITE_FAILED, result: plan };
@@ -2403,7 +2680,7 @@ async function runDedup(input) {
2403
2680
  for (const plan of structuralPlans) replacements.set(plan.source, plan.canonical);
2404
2681
  const pendingWrites = [];
2405
2682
  for (const page of scan.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
2406
- const text = readFileSync2(join9(input.vault, page.relPath), "utf-8");
2683
+ const text = readFileSync2(join10(input.vault, page.relPath), "utf-8");
2407
2684
  let updated = text;
2408
2685
  let changed = false;
2409
2686
  for (const [oldPath, newPath] of replacements) {
@@ -2430,7 +2707,7 @@ async function runDedup(input) {
2430
2707
  relocated.push({ from: plan.source, to: plan.destination });
2431
2708
  }
2432
2709
  for (const pending of pendingWrites) {
2433
- const write = await safeWritePage(join9(input.vault, pending.page), pending.text);
2710
+ const write = await safeWritePage(join10(input.vault, pending.page), pending.text);
2434
2711
  if (!write.ok) return { exitCode: ExitCode.WRITE_FAILED, result: write };
2435
2712
  }
2436
2713
  }
@@ -2485,8 +2762,8 @@ async function runDedup(input) {
2485
2762
  function dedupDestination(vault, source) {
2486
2763
  const base = lifecycleDestination(source, "deduplicate");
2487
2764
  if (!base.ok) throw new Error(`unsupported raw dedup path: ${source}`);
2488
- if (!existsSync2(join9(vault, base.data))) return base.data;
2489
- const contentHash = createHash3("sha256").update(readFileSync2(join9(vault, source))).digest("hex").slice(0, 8);
2765
+ if (!existsSync2(join10(vault, base.data))) return base.data;
2766
+ const contentHash = createHash3("sha256").update(readFileSync2(join10(vault, source))).digest("hex").slice(0, 8);
2490
2767
  const ext = posix.extname(base.data);
2491
2768
  const stem = ext ? base.data.slice(0, -ext.length) : base.data;
2492
2769
  return `${stem}--${contentHash}${ext}`;
@@ -2538,7 +2815,7 @@ function buildSafeEntries(vault, duplicates, unsafe) {
2538
2815
  return entries;
2539
2816
  }
2540
2817
  function hashRawBody(vault, relPath) {
2541
- const text = readFileSync2(join9(vault, relPath), "utf-8");
2818
+ const text = readFileSync2(join10(vault, relPath), "utf-8");
2542
2819
  return hashRawBodyText(text);
2543
2820
  }
2544
2821
  function hashRawBodyText(text) {
@@ -2579,13 +2856,13 @@ function validateManifestLocalState(vault, manifest) {
2579
2856
  return err("APPROVAL_INVALID", { message: "dedup manifest contains an invalid active raw source path", path });
2580
2857
  }
2581
2858
  }
2582
- const canonicalExists = existsSync2(join9(vault, entry.canonical));
2583
- const liveDuplicates = entry.duplicates.filter((path) => existsSync2(join9(vault, path)));
2859
+ const canonicalExists = existsSync2(join10(vault, entry.canonical));
2860
+ const liveDuplicates = entry.duplicates.filter((path) => existsSync2(join10(vault, path)));
2584
2861
  if (!canonicalExists && liveDuplicates.length > 0) {
2585
2862
  return err("APPROVAL_INVALID", { message: "dedup manifest canonical is missing while a local duplicate exists", canonical: entry.canonical });
2586
2863
  }
2587
2864
  for (const path of canonicalExists ? [entry.canonical, ...liveDuplicates] : liveDuplicates) {
2588
- const text = readFileSync2(join9(vault, path), "utf8");
2865
+ const text = readFileSync2(join10(vault, path), "utf8");
2589
2866
  const fm = extractFrontmatter(text);
2590
2867
  const declaredSha = fm.ok && typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
2591
2868
  if (declaredSha !== entry.sha256 || hashRawBodyText(text) !== entry.bodyHash) {
@@ -2644,7 +2921,7 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
2644
2921
 
2645
2922
  // src/commands/audit.ts
2646
2923
  import { readFile as readFile11, stat as stat2 } from "fs/promises";
2647
- import { dirname as dirname4, resolve as resolve2, join as join10 } from "path";
2924
+ import { dirname as dirname4, resolve as resolve2, join as join11 } from "path";
2648
2925
  async function runAudit(input) {
2649
2926
  let text;
2650
2927
  try {
@@ -2701,7 +2978,7 @@ async function findVaultRoot(start) {
2701
2978
  let cur = start;
2702
2979
  for (let i = 0; i < 20; i++) {
2703
2980
  try {
2704
- await stat2(join10(cur, "SCHEMA.md"));
2981
+ await stat2(join11(cur, "SCHEMA.md"));
2705
2982
  return cur;
2706
2983
  } catch {
2707
2984
  }
@@ -2752,86 +3029,10 @@ async function validateCompoundReferences(vault, existingScan, pageTextCache) {
2752
3029
  return ok(findings);
2753
3030
  }
2754
3031
 
2755
- // src/commands/frontmatter-fix.ts
2756
- function isoToday() {
2757
- return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2758
- }
2759
- function fixFrontmatter(rawFm) {
2760
- const additions = [];
2761
- if (!/^created:/m.test(rawFm)) additions.push(`created: ${isoToday()}`);
2762
- if (!/^updated:/m.test(rawFm)) additions.push(`updated: ${isoToday()}`);
2763
- if (!/^tags:/m.test(rawFm)) additions.push("tags: []");
2764
- if (!/^sources:/m.test(rawFm)) additions.push("sources: []");
2765
- if (!/^provenance:/m.test(rawFm)) additions.push("provenance: research");
2766
- if (additions.length === 0) return rawFm;
2767
- return rawFm.trimEnd() + "\n" + additions.join("\n") + "\n";
2768
- }
2769
- function removeOrphanTagsLines(body) {
2770
- return body.split("\n").filter((line) => !/^tags:\s*\[/.test(line.trim())).join("\n");
2771
- }
2772
- async function runFrontmatterFix(input) {
2773
- const scan = await scanVault(input.vault);
2774
- if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2775
- const fixed = [];
2776
- const skipped = [];
2777
- let unchanged = 0;
2778
- for (const page of scan.data.typedKnowledge) {
2779
- const text = await readPage(page);
2780
- const split = splitFrontmatter(text);
2781
- if (!split.ok) {
2782
- skipped.push(page.relPath);
2783
- continue;
2784
- }
2785
- const { rawFrontmatter, body } = split.data;
2786
- const newFm = fixFrontmatter(rawFrontmatter);
2787
- const newBody = removeOrphanTagsLines(body);
2788
- const newText = `---
2789
- ${newFm}
2790
- ---
2791
- ${newBody}`;
2792
- if (newText === text) {
2793
- unchanged++;
2794
- continue;
2795
- }
2796
- if (!input.dryRun) {
2797
- const w = await safeWritePage(page.absPath, newText);
2798
- if (!w.ok) {
2799
- skipped.push(page.relPath);
2800
- continue;
2801
- }
2802
- }
2803
- fixed.push(page.relPath);
2804
- }
2805
- const exitCode = fixed.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
2806
- const hintLines = [`scanned: ${fixed.length + skipped.length + unchanged}`];
2807
- if (fixed.length > 0) hintLines.push(`fixed: ${fixed.length}`);
2808
- if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
2809
- if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
2810
- if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
2811
- if (!input.dryRun && fixed.length > 0) {
2812
- appendLastOp(input.vault, {
2813
- operation: "frontmatter-fix",
2814
- summary: `normalized frontmatter on ${fixed.length} page(s)`,
2815
- files: fixed,
2816
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2817
- });
2818
- }
2819
- return {
2820
- exitCode,
2821
- result: ok({
2822
- scanned: fixed.length + skipped.length + unchanged,
2823
- fixed,
2824
- skipped,
2825
- unchanged,
2826
- humanHint: hintLines.join("\n")
2827
- })
2828
- };
2829
- }
2830
-
2831
3032
  // src/commands/path-too-long.ts
2832
3033
  import { existsSync as existsSync3 } from "fs";
2833
3034
  import { mkdir as mkdir4, readFile as readFile12, rename as rename3, unlink as unlink2 } from "fs/promises";
2834
- import { dirname as dirname5, join as join11, posix as posix2, resolve as resolve3 } from "path";
3035
+ import { dirname as dirname5, join as join12, posix as posix2, resolve as resolve3 } from "path";
2835
3036
  var MAX_PATH_LENGTH = 240;
2836
3037
  var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
2837
3038
  async function runPathTooLong(input) {
@@ -2868,10 +3069,10 @@ async function fixPathTooLong(input) {
2868
3069
  }
2869
3070
  try {
2870
3071
  if (target.mode === "dedupe") {
2871
- await unlink2(join11(input.vault, violation.relPath));
3072
+ await unlink2(join12(input.vault, violation.relPath));
2872
3073
  } else {
2873
- await mkdir4(dirname5(join11(input.vault, target.relPath)), { recursive: true });
2874
- await rename3(join11(input.vault, violation.relPath), join11(input.vault, target.relPath));
3074
+ await mkdir4(dirname5(join12(input.vault, target.relPath)), { recursive: true });
3075
+ await rename3(join12(input.vault, violation.relPath), join12(input.vault, target.relPath));
2875
3076
  }
2876
3077
  fixed.push({ from: violation.relPath, to: target.relPath });
2877
3078
  } catch {
@@ -2950,9 +3151,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
2950
3151
  async function resolveFixTarget(vault, original, preferred, maxLength) {
2951
3152
  for (const candidate of candidateRelPaths(preferred, maxLength)) {
2952
3153
  if (candidate === original || candidate.length > maxLength) continue;
2953
- const candidatePath = join11(vault, candidate);
3154
+ const candidatePath = join12(vault, candidate);
2954
3155
  if (!existsSync3(candidatePath)) return { relPath: candidate, mode: "rename" };
2955
- if (await hasSameContent(join11(vault, original), candidatePath)) {
3156
+ if (await hasSameContent(join12(vault, original), candidatePath)) {
2956
3157
  return { relPath: candidate, mode: "dedupe" };
2957
3158
  }
2958
3159
  }
@@ -2996,15 +3197,91 @@ function replacePathReferences(content, oldRelPath, newRelPath) {
2996
3197
  const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
2997
3198
  updated = updated.replace(stemWikilinkRe, (_match, alias) => `[[${newStem}${alias ?? ""}]]`);
2998
3199
  }
2999
- return updated;
3000
- }
3001
- function computeShortHash(input) {
3002
- let hash = 2166136261;
3003
- for (let i = 0; i < input.length; i++) {
3004
- hash ^= input.charCodeAt(i);
3005
- hash = Math.imul(hash, 16777619);
3200
+ return updated;
3201
+ }
3202
+ function computeShortHash(input) {
3203
+ let hash = 2166136261;
3204
+ for (let i = 0; i < input.length; i++) {
3205
+ hash ^= input.charCodeAt(i);
3206
+ hash = Math.imul(hash, 16777619);
3207
+ }
3208
+ return (hash >>> 0).toString(16).padStart(8, "0").slice(0, 8);
3209
+ }
3210
+
3211
+ // src/commands/frontmatter-fix.ts
3212
+ function isoToday() {
3213
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3214
+ }
3215
+ function fixFrontmatter(rawFm) {
3216
+ const additions = [];
3217
+ if (!/^created:/m.test(rawFm)) additions.push(`created: ${isoToday()}`);
3218
+ if (!/^updated:/m.test(rawFm)) additions.push(`updated: ${isoToday()}`);
3219
+ if (!/^tags:/m.test(rawFm)) additions.push("tags: []");
3220
+ if (!/^sources:/m.test(rawFm)) additions.push("sources: []");
3221
+ if (!/^provenance:/m.test(rawFm)) additions.push("provenance: research");
3222
+ if (additions.length === 0) return rawFm;
3223
+ return rawFm.trimEnd() + "\n" + additions.join("\n") + "\n";
3224
+ }
3225
+ function removeOrphanTagsLines(body) {
3226
+ return body.split("\n").filter((line) => !/^tags:\s*\[/.test(line.trim())).join("\n");
3227
+ }
3228
+ async function runFrontmatterFix(input) {
3229
+ const scan = await scanVault(input.vault);
3230
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
3231
+ const fixed = [];
3232
+ const skipped = [];
3233
+ let unchanged = 0;
3234
+ for (const page of scan.data.typedKnowledge) {
3235
+ const text = await readPage(page);
3236
+ const split = splitFrontmatter(text);
3237
+ if (!split.ok) {
3238
+ skipped.push(page.relPath);
3239
+ continue;
3240
+ }
3241
+ const { rawFrontmatter, body } = split.data;
3242
+ const newFm = fixFrontmatter(rawFrontmatter);
3243
+ const newBody = removeOrphanTagsLines(body);
3244
+ const newText = `---
3245
+ ${newFm}
3246
+ ---
3247
+ ${newBody}`;
3248
+ if (newText === text) {
3249
+ unchanged++;
3250
+ continue;
3251
+ }
3252
+ if (!input.dryRun) {
3253
+ const w = await safeWritePage(page.absPath, newText);
3254
+ if (!w.ok) {
3255
+ skipped.push(page.relPath);
3256
+ continue;
3257
+ }
3258
+ }
3259
+ fixed.push(page.relPath);
3260
+ }
3261
+ const exitCode = fixed.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
3262
+ const hintLines = [`scanned: ${fixed.length + skipped.length + unchanged}`];
3263
+ if (fixed.length > 0) hintLines.push(`fixed: ${fixed.length}`);
3264
+ if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
3265
+ if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
3266
+ if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
3267
+ if (!input.dryRun && fixed.length > 0) {
3268
+ appendLastOp(input.vault, {
3269
+ operation: "frontmatter-fix",
3270
+ summary: `normalized frontmatter on ${fixed.length} page(s)`,
3271
+ files: fixed,
3272
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3273
+ });
3006
3274
  }
3007
- return (hash >>> 0).toString(16).padStart(8, "0").slice(0, 8);
3275
+ return {
3276
+ exitCode,
3277
+ result: ok({
3278
+ scanned: fixed.length + skipped.length + unchanged,
3279
+ fixed,
3280
+ skipped,
3281
+ unchanged,
3282
+ humanHint: hintLines.join("\n")
3283
+ })
3284
+ };
3008
3285
  }
3009
3286
 
3010
3287
  // src/utils/cli-surface.ts
@@ -3018,8 +3295,10 @@ function buildCliSurface() {
3018
3295
  program.command("validate").option("--apply").option("--vault <dir>").option("--wiki <name>");
3019
3296
  program.command("graph");
3020
3297
  program.command("canvas");
3298
+ program.command("eval").option("--base <git-ref>").option("--top <n>").option("--wiki <name>");
3021
3299
  program.command("overlap").option("--wiki <name>");
3022
- program.command("query").option("--limit <n>").option("--include-pending").option("--wiki <name>");
3300
+ program.command("query").option("--limit <n>").option("--include-pending").option("--hybrid").option("--wiki <name>");
3301
+ program.command("vectors");
3023
3302
  program.command("sources");
3024
3303
  program.command("orphans").option("--wiki <name>");
3025
3304
  program.command("audit");
@@ -3049,7 +3328,7 @@ function buildCliSurface() {
3049
3328
  program.command("status").option("--wiki <name>");
3050
3329
  program.command("archive").option("--wiki <name>").option("--cascade").option("--apply").option("--approve <token>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>");
3051
3330
  program.command("remove").option("--wiki <name>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--reason <text>");
3052
- program.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
3331
+ program.command("drift").option("--apply").option("--affected-pages").option("--new <date>").option("--wiki <name>");
3053
3332
  program.command("dedup").option("--apply").option("--approve <token>").option("--canonical-policy <policy>").option("--manifest-out <path>").option("--manifest-in <path>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--wiki <name>");
3054
3333
  program.command("migrate-citations").option("--dry-run").option("--wiki <name>");
3055
3334
  program.command("frontmatter-fix").option("--dry-run").option("--wiki <name>");
@@ -3076,9 +3355,22 @@ function buildCliSurface() {
3076
3355
  program.command("mcp");
3077
3356
  const graphCmd = program.commands.find((c) => c.name() === "graph");
3078
3357
  graphCmd.command("build").option("--out <path>").option("--wiki <name>");
3358
+ const vectorsCmd = program.commands.find((c) => c.name() === "vectors");
3359
+ vectorsCmd.command("rebuild").option("--wiki <name>");
3360
+ vectorsCmd.command("status").option("--wiki <name>");
3361
+ vectorsCmd.command("reindex-page").option("--wiki <name>");
3362
+ vectorsCmd.command("prune-page").option("--dry-run").option("--wiki <name>");
3079
3363
  const sourcesCmd = program.commands.find((c) => c.name() === "sources");
3080
3364
  sourcesCmd.command("pending").option("--since <date>").option("--older-than <days>").option("--match <text>").option("--ingested-by <channel>").option("--scope <scope>").option("--sort <order>").option("--limit <n>").option("--all").option("--include-integrated").option("--include-archived").option("--include-duplicates").option("--include-legacy-archived").option("--wiki <name>");
3365
+ sourcesCmd.command("skipped").option("--wiki <name>");
3081
3366
  sourcesCmd.command("disposition").requiredOption("--status <status>").requiredOption("--reason <text>").option("--review-after <date>").option("--duplicate-of <raw-path>").option("--write").option("--approve <token>").option("--wiki <name>");
3367
+ const sourcesCompileCmd = sourcesCmd.command("compile");
3368
+ sourcesCompileCmd.command("claim").requiredOption("--reason <text>").option("--write").option("--approve <token>").option("--wiki <name>");
3369
+ sourcesCompileCmd.command("release").requiredOption("--reason <text>").option("--write").option("--approve <token>").option("--wiki <name>");
3370
+ sourcesCompileCmd.command("published").requiredOption("--pages <paths>").requiredOption("--reason <text>").option("--write").option("--approve <token>").option("--wiki <name>");
3371
+ sourcesCompileCmd.command("status").option("--wiki <name>");
3372
+ sourcesCmd.command("review").requiredOption("--status <status>").requiredOption("--reason <text>").option("--write").option("--approve <token>").option("--wiki <name>");
3373
+ sourcesCmd.command("reviews").option("--wiki <name>");
3082
3374
  sourcesCmd.command("dispose").requiredOption("--reason <text>").option("--write").option("--approve <token>").option("--wiki <name>");
3083
3375
  const canvasCmd = program.commands.find((c) => c.name() === "canvas");
3084
3376
  canvasCmd.command("generate").option("--graph-path <path>").option("--wiki <name>");
@@ -3254,275 +3546,50 @@ function compatible(left, right) {
3254
3546
  }
3255
3547
  function hasAnyIncompatibleSignals(leftSignals, rightSignals) {
3256
3548
  if (leftSignals.length === 0 || rightSignals.length === 0) return false;
3257
- return leftSignals.some((left) => rightSignals.some((right) => !compatible(left, right)));
3258
- }
3259
- function hasAnyCompatibleSignals(leftSignals, rightSignals) {
3260
- return leftSignals.some((left) => rightSignals.some((right) => compatible(left, right)));
3261
- }
3262
- function assessSourceIdentity(input) {
3263
- const pathSignals = collectSignals(input.rawPath);
3264
- const sourceSignals = collectSignals(input.sourceUrl ?? "");
3265
- const bodySignals = collectSignals(firstBodyWindow(input.body));
3266
- const reasons = [];
3267
- if (hasAnyIncompatibleSignals(pathSignals, sourceSignals)) {
3268
- reasons.push(`filename/path signals [${pathSignals.join(", ")}] but source_url signals [${sourceSignals.join(", ")}]`);
3269
- }
3270
- if (pathSignals.length > 0 && bodySignals.length > 0 && !hasAnyCompatibleSignals(pathSignals, bodySignals)) {
3271
- reasons.push(`filename/path signals [${pathSignals.join(", ")}] but body signals [${bodySignals.join(", ")}]`);
3272
- }
3273
- if (reasons.length > 0) {
3274
- return { status: "conflict", pathSignals, sourceSignals, bodySignals, reasons };
3275
- }
3276
- if (pathSignals.length === 0 && sourceSignals.length > 0 && bodySignals.length > 0 && !hasAnyCompatibleSignals(sourceSignals, bodySignals)) {
3277
- return {
3278
- status: "suspicious",
3279
- pathSignals,
3280
- sourceSignals,
3281
- bodySignals,
3282
- reasons: [`source_url signals [${sourceSignals.join(", ")}] but body signals [${bodySignals.join(", ")}]`]
3283
- };
3284
- }
3285
- return { status: "ok", pathSignals, sourceSignals, bodySignals, reasons };
3286
- }
3287
-
3288
- // src/commands/lint.ts
3289
- var STRUCT_MIN_BODY_LINES = 60;
3290
- var STRUCT_MIN_SECTIONS = 3;
3291
- function hasDuplicateFrontmatter(body) {
3292
- if (/^---\r?\n/.test(body)) return true;
3293
- const lines = body.split(/\r?\n/);
3294
- const limit = Math.min(lines.length, 20);
3295
- let seenYamlKey = false;
3296
- for (let i = 0; i < limit; i++) {
3297
- if (/^\w[\w-]*:/.test(lines[i].trim())) seenYamlKey = true;
3298
- if (seenYamlKey && lines[i].trim() === "---") return true;
3299
- }
3300
- return false;
3301
- }
3302
- var CANONICAL_LOCAL_SOURCE_LABEL = /^\s*(?:>\s*)?(?:[-*+]\s*)?Source (?:file|inspected):/i;
3303
- var LOCAL_ABSOLUTE_SOURCE_REF = /(?:file:\/\/(?:\/)?(?:Users|home)\/|\/(?:Users|home)\/)/;
3304
- function hasCanonicalLocalSourceAssertion(body) {
3305
- const visibleBody = stripFencedBlocks(body);
3306
- return visibleBody.split(/\r?\n/).some(
3307
- (line) => CANONICAL_LOCAL_SOURCE_LABEL.test(line) && LOCAL_ABSOLUTE_SOURCE_REF.test(line)
3308
- );
3309
- }
3310
- function shouldCheckCanonicalLocalSourceAssertion(page) {
3311
- if (page.relPath.startsWith("raw/transcripts/")) return false;
3312
- if (/^projects\/[^/]+\/work\/[^/]+\/log\.md$/.test(page.relPath)) return false;
3313
- if (page.relPath.startsWith("raw/")) return true;
3314
- if (/^(entities|concepts|comparisons|queries|meta)\//.test(page.relPath)) return true;
3315
- if (/^projects\/[^/]+\/compound\//.test(page.relPath)) return true;
3316
- if (/^projects\/[^/]+\/work\/[^/]+\/(spec|plan)\.md$/.test(page.relPath)) return true;
3317
- return false;
3318
- }
3319
- function extractSourceEntries(rawFm) {
3320
- const lines = rawFm.split(/\r?\n/);
3321
- const sourcesLineIdx = lines.findIndex((l) => /^sources:/.test(l));
3322
- if (sourcesLineIdx === -1) return [];
3323
- const sourcesLine = lines[sourcesLineIdx].trim();
3324
- const inlineMatch = sourcesLine.match(/^sources:\s*\[(.+)]\s*$/);
3325
- if (inlineMatch) {
3326
- return [...inlineMatch[1].matchAll(/"[^"]*"|'[^']*'|[^,\s]\S*/g)].map((m) => m[0].replace(/,\s*$/, ""));
3327
- }
3328
- const entries = [];
3329
- for (let i = sourcesLineIdx + 1; i < lines.length; i++) {
3330
- const line = lines[i];
3331
- if (!/^\s+- /.test(line)) break;
3332
- entries.push(line.replace(/^\s+- /, "").trim());
3333
- }
3334
- return entries;
3335
- }
3336
- var ERROR_ORDER = ["sensitive_content", "conflict_markers", "broken_wikilinks", "invalid_frontmatter", "raw_source_identity_conflict", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
3337
- 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", "frontmatter_yaml_invalid", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
3338
- var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
3339
- var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
3340
- var CLI_REFS_TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
3341
- function shellQuote(value) {
3342
- return `'${value.replace(/'/g, `'\\''`)}'`;
3343
- }
3344
- function formatExample(item) {
3345
- if (typeof item === "string") return item;
3346
- if (item === null || item === void 0) return String(item);
3347
- try {
3348
- return JSON.stringify(item);
3349
- } catch {
3350
- return String(item);
3351
- }
3352
- }
3353
- function summarizeBucket(bucket, severity, vaultPath, examplesLimit) {
3354
- const safeLimit = Math.max(0, Math.min(examplesLimit, 10));
3355
- const examples = bucket.items.slice(0, safeLimit).map(formatExample);
3356
- return {
3357
- kind: bucket.kind,
3358
- severity,
3359
- count: bucket.items.length,
3360
- examples,
3361
- examples_limit: safeLimit,
3362
- sample_truncated: bucket.items.length > examples.length,
3363
- details_command: `skillwiki lint ${shellQuote(vaultPath)} --only ${bucket.kind}`
3364
- };
3365
- }
3366
- function severityForBucket(kind) {
3367
- if (ERROR_ORDER.includes(kind)) return "error";
3368
- if (WARNING_ORDER.includes(kind)) return "warning";
3369
- return "info";
3370
- }
3371
- function outputForOnlyBucket(input, match, fixed, unresolved, readVault = lintReadVault(input)) {
3372
- const severity = severityForBucket(input.only);
3373
- const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
3374
- const summary = {
3375
- errors: filtered.error.reduce((n, b) => n + b.items.length, 0),
3376
- warnings: filtered.warning.reduce((n, b) => n + b.items.length, 0),
3377
- info: filtered.info.reduce((n, b) => n + b.items.length, 0)
3378
- };
3379
- let exitCode = ExitCode.OK;
3380
- if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
3381
- else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
3382
- const vault = lintVaultOutput(input, readVault);
3383
- const hintLines = [
3384
- ...readMirrorHintLines(vault),
3385
- `--only ${input.only}`,
3386
- match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")
3387
- ];
3388
- const output = {
3389
- vault,
3390
- summary,
3391
- by_severity: filtered,
3392
- fixed,
3393
- unresolved,
3394
- humanHint: hintLines.join("\n")
3395
- };
3396
- if (input.fix) appendLintFixLastOp(input.vault, fixed);
3397
- return {
3398
- exitCode,
3399
- result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
3400
- };
3401
- }
3402
- function lintReadVault(input) {
3403
- if (input.fix) {
3404
- return { readPath: input.vault, readMirror: false };
3405
- }
3406
- const resolved = resolveReadOnlyVaultRoot(input.vault);
3407
- return { readPath: resolved.root, readMirror: resolved.mirrored };
3408
- }
3409
- function lintVaultOutput(input, readVault) {
3410
- return {
3411
- path: input.vault,
3412
- source: input.source ?? "resolved",
3413
- read_path: readVault.readPath,
3414
- read_mirror: readVault.readMirror
3415
- };
3416
- }
3417
- function readMirrorHintLines(vault) {
3418
- if (!vault.read_mirror) return [];
3419
- return [
3420
- `read mirror: ${vault.read_path}`,
3421
- `requested vault: ${vault.path}`,
3422
- "if results look stale, refresh the read mirror or rerun with SKILLWIKI_DISABLE_VAULT_READ_MIRROR=1 for a live scan; live scans may be slower"
3423
- ];
3424
- }
3425
- function appendLintFixLastOp(vault, fixed) {
3426
- if (fixed.length === 0) return;
3427
- appendLastOp(vault, {
3428
- operation: "lint-fix",
3429
- summary: `fixed ${fixed.length} page(s)`,
3430
- files: fixed,
3431
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
3432
- });
3433
- }
3434
- function summarizeLintOutput(output, examplesLimit = 3) {
3435
- const buckets = [
3436
- ...output.by_severity.error.map((bucket) => summarizeBucket(bucket, "error", output.vault.path, examplesLimit)),
3437
- ...output.by_severity.warning.map((bucket) => summarizeBucket(bucket, "warning", output.vault.path, examplesLimit)),
3438
- ...output.by_severity.info.map((bucket) => summarizeBucket(bucket, "info", output.vault.path, examplesLimit))
3439
- ];
3440
- const lines = [];
3441
- lines.push(...readMirrorHintLines(output.vault));
3442
- lines.push(`errors: ${output.summary.errors}`);
3443
- lines.push(`warnings: ${output.summary.warnings}`);
3444
- lines.push(`info: ${output.summary.info}`);
3445
- for (const bucket of buckets) {
3446
- lines.push(` ${bucket.kind}: ${bucket.count}`);
3447
- if (bucket.examples.length > 0) {
3448
- lines.push(` e.g. ${bucket.examples[0]}`);
3449
- }
3450
- }
3451
- return {
3452
- vault: output.vault,
3453
- summary: output.summary,
3454
- buckets,
3455
- details_included: false,
3456
- truncated: false,
3457
- fixed: output.fixed,
3458
- unresolved: output.unresolved,
3459
- humanHint: lines.join("\n")
3460
- };
3549
+ return leftSignals.some((left) => rightSignals.some((right) => !compatible(left, right)));
3461
3550
  }
3462
- async function walkMarkdownFiles(absDir, vaultRoot) {
3463
- const entries = await readdir2(absDir, { withFileTypes: true });
3464
- const pages = [];
3465
- for (const entry of entries) {
3466
- const absPath = join12(absDir, entry.name);
3467
- if (entry.isDirectory()) {
3468
- if (entry.name === ".git" || entry.name === "node_modules") continue;
3469
- pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
3470
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
3471
- pages.push({ absPath, relPath: relative(vaultRoot, absPath).split(sep).join("/") });
3472
- }
3551
+ function hasAnyCompatibleSignals(leftSignals, rightSignals) {
3552
+ return leftSignals.some((left) => rightSignals.some((right) => compatible(left, right)));
3553
+ }
3554
+ function assessSourceIdentity(input) {
3555
+ const pathSignals = collectSignals(input.rawPath);
3556
+ const sourceSignals = collectSignals(input.sourceUrl ?? "");
3557
+ const bodySignals = collectSignals(firstBodyWindow(input.body));
3558
+ const reasons = [];
3559
+ if (hasAnyIncompatibleSignals(pathSignals, sourceSignals)) {
3560
+ reasons.push(`filename/path signals [${pathSignals.join(", ")}] but source_url signals [${sourceSignals.join(", ")}]`);
3473
3561
  }
3474
- return pages;
3562
+ if (pathSignals.length > 0 && bodySignals.length > 0 && !hasAnyCompatibleSignals(pathSignals, bodySignals)) {
3563
+ reasons.push(`filename/path signals [${pathSignals.join(", ")}] but body signals [${bodySignals.join(", ")}]`);
3564
+ }
3565
+ if (reasons.length > 0) {
3566
+ return { status: "conflict", pathSignals, sourceSignals, bodySignals, reasons };
3567
+ }
3568
+ if (pathSignals.length === 0 && sourceSignals.length > 0 && bodySignals.length > 0 && !hasAnyCompatibleSignals(sourceSignals, bodySignals)) {
3569
+ return {
3570
+ status: "suspicious",
3571
+ pathSignals,
3572
+ sourceSignals,
3573
+ bodySignals,
3574
+ reasons: [`source_url signals [${sourceSignals.join(", ")}] but body signals [${bodySignals.join(", ")}]`]
3575
+ };
3576
+ }
3577
+ return { status: "ok", pathSignals, sourceSignals, bodySignals, reasons };
3475
3578
  }
3579
+
3580
+ // src/lint/rules.ts
3476
3581
  async function collectCliRefsPages(vault) {
3477
- if (!existsSync4(join12(vault, "SCHEMA.md"))) {
3582
+ if (!existsSync4(join13(vault, "SCHEMA.md"))) {
3478
3583
  return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
3479
3584
  }
3480
3585
  const pages = [];
3481
3586
  for (const dir of CLI_REFS_TYPED_DIRS) {
3482
- const absDir = join12(vault, dir);
3587
+ const absDir = join13(vault, dir);
3483
3588
  if (!existsSync4(absDir)) continue;
3484
3589
  pages.push(...await walkMarkdownFiles(absDir, vault));
3485
3590
  }
3486
3591
  return ok(pages);
3487
3592
  }
3488
- async function runCliRefsOnly(input) {
3489
- const readVault = lintReadVault(input);
3490
- const lintVault = readVault.readPath;
3491
- const pages = await collectCliRefsPages(lintVault);
3492
- if (!pages.ok) {
3493
- return { exitCode: ExitCode.VAULT_PATH_INVALID, result: pages };
3494
- }
3495
- const cliRefFlags = [];
3496
- const cliSurface = buildCliSurface();
3497
- for (const page of pages.data) {
3498
- const text = await readPageCached(page);
3499
- const violations = validateCliRefs(text, page.relPath, cliSurface);
3500
- for (const v of violations) {
3501
- cliRefFlags.push(`${v.page}: ${v.ref} (${v.reason})`);
3502
- }
3503
- }
3504
- const infoOut = cliRefFlags.length > 0 ? [{ kind: "cli_refs", items: cliRefFlags }] : [];
3505
- const summary = { errors: 0, warnings: 0, info: cliRefFlags.length };
3506
- const exitCode = cliRefFlags.length > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
3507
- const vault = lintVaultOutput(input, readVault);
3508
- const hintLines = [
3509
- ...readMirrorHintLines(vault),
3510
- `--only cli_refs`,
3511
- cliRefFlags.length === 0 ? "0 violations" : ` cli_refs: ${cliRefFlags.length}`
3512
- ];
3513
- const output = {
3514
- vault,
3515
- summary,
3516
- by_severity: { error: [], warning: [], info: infoOut },
3517
- fixed: [],
3518
- unresolved: [],
3519
- humanHint: hintLines.join("\n")
3520
- };
3521
- return {
3522
- exitCode,
3523
- result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
3524
- };
3525
- }
3526
3593
  async function collectFileSourceUrlFindings(scan, pageTextCache, options) {
3527
3594
  const fileSourceUrlFlags = /* @__PURE__ */ new Set();
3528
3595
  const fileSourceUrlFrontmatterFlags = /* @__PURE__ */ new Set();
@@ -3654,884 +3721,1466 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
3654
3721
  }
3655
3722
  return remaining;
3656
3723
  }
3657
- async function runFileSourceUrlOnly(input) {
3658
- const readVault = lintReadVault(input);
3659
- const lintVault = readVault.readPath;
3660
- const scanResult = await scanVault(lintVault);
3661
- if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
3662
- const pageTextCache = /* @__PURE__ */ new Map();
3663
- const fixed = [];
3664
- const unresolved = [];
3665
- const findings = await collectFileSourceUrlFindings(scanResult.data, pageTextCache, { includeRawIdentityConflicts: false });
3666
- const remaining = await applyFileSourceUrlFix(
3667
- input,
3668
- scanResult.data,
3669
- findings.fileSourceUrlFlags,
3670
- findings.fileSourceUrlFrontmatterFlags,
3671
- fixed,
3672
- unresolved
3673
- );
3674
- const match = remaining.size > 0 ? [{ kind: "file_source_url", items: [...remaining] }] : [];
3675
- return outputForOnlyBucket(input, match, fixed, unresolved, readVault);
3676
- }
3677
- function scanConflictMarkerBlocks(path, text) {
3678
- const findings = [];
3679
- const lines = text.split(/\r?\n/);
3680
- let inFence = false;
3681
- let openLine = 0;
3682
- let sawSeparator = false;
3683
- for (let i = 0; i < lines.length; i += 1) {
3684
- const line = lines[i];
3685
- if (line.startsWith("```") || line.startsWith("~~~")) {
3686
- inFence = !inFence;
3687
- continue;
3724
+ var brokenWikilinksRule = {
3725
+ id: "broken_wikilinks",
3726
+ severity: "error",
3727
+ producedBuckets: ["broken_wikilinks", "invalid_frontmatter"],
3728
+ async run(ctx) {
3729
+ const buckets = {};
3730
+ const links = await runLinks({ vault: ctx.vault, scan: ctx.scan, pageTextCache: ctx.pageTextCache });
3731
+ if (links.result.ok && links.result.data.broken.length > 0) {
3732
+ buckets.broken_wikilinks = links.result.data.broken;
3688
3733
  }
3689
- if (inFence) continue;
3690
- if (line.startsWith("<<<<<<< ")) {
3691
- openLine = i + 1;
3692
- sawSeparator = false;
3693
- continue;
3734
+ if (!links.result.ok && links.result.error === "INVALID_FRONTMATTER") {
3735
+ buckets.invalid_frontmatter = [links.result.detail ?? {}];
3694
3736
  }
3695
- if (line === "=======" && openLine > 0) {
3696
- sawSeparator = true;
3697
- continue;
3737
+ return { buckets };
3738
+ }
3739
+ };
3740
+ var tagNotInTaxonomyRule = {
3741
+ id: "tag_not_in_taxonomy",
3742
+ severity: "error",
3743
+ producedBuckets: ["tag_not_in_taxonomy", "invalid_frontmatter"],
3744
+ async run(ctx) {
3745
+ const buckets = {};
3746
+ const tags = await runTagAudit({ vault: ctx.vault, scan: ctx.scan, pageTextCache: ctx.pageTextCache });
3747
+ if (tags.result.ok && tags.result.data.violations.length > 0) {
3748
+ buckets.tag_not_in_taxonomy = tags.result.data.violations;
3749
+ }
3750
+ if (!tags.result.ok && tags.result.error === "INVALID_FRONTMATTER") {
3751
+ buckets.invalid_frontmatter = [tags.result.detail ?? {}];
3752
+ }
3753
+ return { buckets };
3754
+ }
3755
+ };
3756
+ var indexIncompleteRule = {
3757
+ id: "index_incomplete",
3758
+ severity: "warning",
3759
+ async run(ctx) {
3760
+ const buckets = {};
3761
+ const idx = await runIndexCheck({ vault: ctx.vault, scan: ctx.scan });
3762
+ if (idx.result.ok && (idx.result.data.missing_from_index.length > 0 || idx.result.data.ghost_entries.length > 0)) {
3763
+ buckets.index_incomplete = [
3764
+ {
3765
+ missing_from_index: idx.result.data.missing_from_index,
3766
+ ghost_entries: idx.result.data.ghost_entries
3767
+ }
3768
+ ];
3769
+ }
3770
+ return { buckets };
3771
+ }
3772
+ };
3773
+ var indexLinkFormatRule = {
3774
+ id: "index_link_format",
3775
+ severity: "warning",
3776
+ async run(ctx) {
3777
+ const buckets = {};
3778
+ const linkFmt = await runIndexLinkFormat({ vault: ctx.vault });
3779
+ if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
3780
+ buckets.index_link_format = linkFmt.result.data.markdown_links;
3781
+ }
3782
+ return { buckets };
3783
+ }
3784
+ };
3785
+ var stalePageRule = {
3786
+ id: "stale_page",
3787
+ severity: "warning",
3788
+ async run(ctx) {
3789
+ const buckets = {};
3790
+ const staleResult = await runStale({
3791
+ vault: ctx.vault,
3792
+ days: ctx.days,
3793
+ scan: ctx.scan,
3794
+ pageTextCache: ctx.pageTextCache
3795
+ });
3796
+ if (staleResult.result.ok) {
3797
+ const st = staleResult.result.data;
3798
+ const staleList = [
3799
+ ...st.stale_transcripts.map((t) => t.path),
3800
+ ...(st.unclaimed_transcripts ?? []).map((t) => t.path),
3801
+ ...st.incomplete_work_items.map((w) => w.path),
3802
+ ...(st.done_work_items ?? []).map((w) => w.path)
3803
+ ];
3804
+ if (staleList.length > 0) buckets.stale_page = staleList;
3805
+ }
3806
+ return { buckets };
3807
+ }
3808
+ };
3809
+ var pageTooLargeRule = {
3810
+ id: "page_too_large",
3811
+ severity: "warning",
3812
+ async run(ctx) {
3813
+ const buckets = {};
3814
+ const pagesize = await runPagesize({
3815
+ vault: ctx.vault,
3816
+ lines: ctx.lines,
3817
+ scan: ctx.scan,
3818
+ pageTextCache: ctx.pageTextCache
3819
+ });
3820
+ if (pagesize.result.ok && pagesize.result.data.oversized.length > 0) {
3821
+ buckets.page_too_large = pagesize.result.data.oversized;
3822
+ }
3823
+ return { buckets };
3824
+ }
3825
+ };
3826
+ var logRotateNeededRule = {
3827
+ id: "log_rotate_needed",
3828
+ severity: "warning",
3829
+ async run(ctx) {
3830
+ const buckets = {};
3831
+ const rotate = await runLogRotate({ vault: ctx.vault, threshold: ctx.logThreshold, apply: false });
3832
+ if (rotate.result.ok && rotate.exitCode === ExitCode.LOG_ROTATE_NEEDED) {
3833
+ buckets.log_rotate_needed = [{ entries: rotate.result.data.entries, threshold: rotate.result.data.threshold }];
3834
+ }
3835
+ return { buckets };
3836
+ }
3837
+ };
3838
+ var orphansRule = {
3839
+ id: "orphans",
3840
+ severity: "warning",
3841
+ producedBuckets: ["orphans", "bridges"],
3842
+ async run(ctx) {
3843
+ const buckets = {};
3844
+ const orphans = await runOrphans({ vault: ctx.vault, scan: ctx.scan, pageTextCache: ctx.pageTextCache });
3845
+ if (orphans.result.ok) {
3846
+ if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
3847
+ if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
3848
+ }
3849
+ return { buckets };
3850
+ }
3851
+ };
3852
+ var sparseCommunityRule = {
3853
+ id: "sparse_community",
3854
+ severity: "info",
3855
+ async run(ctx) {
3856
+ const buckets = {};
3857
+ const sparse = await runSparseCommunity({ vault: ctx.vault, scan: ctx.scan, pageTextCache: ctx.pageTextCache });
3858
+ if (sparse.result.ok && sparse.result.data.communities.length > 0) {
3859
+ buckets.sparse_community = sparse.result.data.communities;
3860
+ }
3861
+ return { buckets };
3862
+ }
3863
+ };
3864
+ var topicMapRecommendedRule = {
3865
+ id: "topic_map_recommended",
3866
+ severity: "info",
3867
+ async run(ctx) {
3868
+ const buckets = {};
3869
+ const topicMap = await runTopicMapCheck({ vault: ctx.vault, scan: ctx.scan });
3870
+ if (topicMap.result.ok && topicMap.result.data.recommended) {
3871
+ buckets.topic_map_recommended = [
3872
+ { page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }
3873
+ ];
3874
+ }
3875
+ return { buckets };
3876
+ }
3877
+ };
3878
+ var rawDedupRule = {
3879
+ id: "raw_dedup",
3880
+ severity: "error",
3881
+ async run(ctx) {
3882
+ const buckets = {};
3883
+ const dedup = await runDedup({ vault: ctx.vault, scan: ctx.scan, pageTextCache: ctx.pageTextCache });
3884
+ if (dedup.result.ok && dedup.result.data.duplicates.length > 0) {
3885
+ buckets.raw_dedup = dedup.result.data.duplicates;
3886
+ }
3887
+ return { buckets };
3888
+ }
3889
+ };
3890
+ var rawBodyDuplicateRule = {
3891
+ id: "raw_body_duplicate",
3892
+ severity: "warning",
3893
+ async run(ctx) {
3894
+ const buckets = {};
3895
+ const bodyDedup = await runRawBodyDedup(ctx.vault, ctx.scan, ctx.pageTextCache);
3896
+ if (bodyDedup.result.ok && bodyDedup.result.data.duplicates.length > 0) {
3897
+ buckets.raw_body_duplicate = bodyDedup.result.data.duplicates.map((d) => ({
3898
+ body_hash: d.bodyHash.slice(0, 12),
3899
+ files: d.files.map((f) => `${f.relPath} (sha256: ${f.sha256 ?? "none"})`)
3900
+ }));
3901
+ }
3902
+ return { buckets };
3903
+ }
3904
+ };
3905
+ var compoundRefsRule = {
3906
+ id: "compound_refs",
3907
+ severity: "warning",
3908
+ async run(ctx) {
3909
+ const buckets = {};
3910
+ const compoundRefs = await validateCompoundReferences(ctx.vault, ctx.scan, ctx.pageTextCache);
3911
+ if (compoundRefs.ok && compoundRefs.data.length > 0) {
3912
+ buckets.compound_refs = compoundRefs.data;
3913
+ }
3914
+ return { buckets };
3915
+ }
3916
+ };
3917
+ var pathTooLongRule = {
3918
+ id: "path_too_long",
3919
+ severity: "error",
3920
+ async run(ctx) {
3921
+ const buckets = {};
3922
+ const pathCheck = await runPathTooLong({ vault: ctx.vault, scan: ctx.scan });
3923
+ if (pathCheck.result.ok && pathCheck.result.data.violations.length > 0) {
3924
+ buckets.path_too_long = pathCheck.result.data.violations;
3925
+ }
3926
+ return { buckets };
3927
+ },
3928
+ async fix(ctx, currentBucketItems) {
3929
+ const pathViolations = currentBucketItems;
3930
+ if (!pathViolations || pathViolations.length === 0) return currentBucketItems;
3931
+ const pathFix = await fixPathTooLong({ vault: ctx.input.vault });
3932
+ const pathFixed = pathFix.result.ok ? pathFix.result.data.fixed.map((f) => f.from) : [];
3933
+ if (pathFix.result.ok) ctx.unresolved.push(...pathFix.result.data.unresolved);
3934
+ else ctx.unresolved.push(...pathViolations.map((v) => v.relPath));
3935
+ ctx.fixed.push(...pathFixed);
3936
+ let remaining = pathViolations;
3937
+ if (pathFixed.length > 0) {
3938
+ const fixedSet = new Set(pathFixed);
3939
+ remaining = pathViolations.filter((v) => !fixedSet.has(v.relPath));
3940
+ }
3941
+ if (remaining && remaining.length > 0) {
3942
+ const rawRemaining = remaining.filter((v) => v.relPath.startsWith("raw/"));
3943
+ if (rawRemaining.length > 0) {
3944
+ ctx.unresolved.push(...rawRemaining.map((v) => v.relPath));
3945
+ const nonRaw = remaining.filter((v) => !v.relPath.startsWith("raw/"));
3946
+ return nonRaw.length > 0 ? nonRaw : void 0;
3947
+ }
3948
+ return remaining;
3949
+ }
3950
+ return void 0;
3951
+ }
3952
+ };
3953
+ var cliRefsRule = {
3954
+ id: "cli_refs",
3955
+ severity: "info",
3956
+ async run(ctx) {
3957
+ const cliRefFlags = [];
3958
+ const allScanPages = [...ctx.scan.typedKnowledge];
3959
+ const cliRefResults = await mapWithConcurrency(allScanPages, vaultIoConcurrency(), async (page) => {
3960
+ const flags = [];
3961
+ try {
3962
+ const text = await readPageCached(page, ctx.pageTextCache);
3963
+ const violations = validateCliRefs(text, page.relPath, ctx.cliSurface);
3964
+ for (const v of violations) {
3965
+ flags.push(`${v.page}: ${v.ref} (${v.reason})`);
3966
+ }
3967
+ } catch {
3968
+ }
3969
+ return flags;
3970
+ });
3971
+ cliRefFlags.push(...cliRefResults.flat());
3972
+ const buckets = {};
3973
+ if (cliRefFlags.length > 0) buckets.cli_refs = cliRefFlags;
3974
+ return { buckets };
3975
+ },
3976
+ async runFastPath(input) {
3977
+ const readVault = lintReadVault(input);
3978
+ const lintVault = readVault.readPath;
3979
+ const pages = await collectCliRefsPages(lintVault);
3980
+ if (!pages.ok) {
3981
+ return { exitCode: ExitCode.VAULT_PATH_INVALID, result: pages };
3982
+ }
3983
+ const cliRefFlags = [];
3984
+ const cliSurface = buildCliSurface();
3985
+ for (const page of pages.data) {
3986
+ const text = await readPageCached(page);
3987
+ const violations = validateCliRefs(text, page.relPath, cliSurface);
3988
+ for (const v of violations) {
3989
+ cliRefFlags.push(`${v.page}: ${v.ref} (${v.reason})`);
3990
+ }
3991
+ }
3992
+ const infoOut = cliRefFlags.length > 0 ? [{ kind: "cli_refs", items: cliRefFlags }] : [];
3993
+ const summary = { errors: 0, warnings: 0, info: cliRefFlags.length };
3994
+ const exitCode = cliRefFlags.length > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
3995
+ const vault = lintVaultOutput(input, readVault);
3996
+ const hintLines = [
3997
+ ...readMirrorHintLines(vault),
3998
+ `--only cli_refs`,
3999
+ cliRefFlags.length === 0 ? "0 violations" : ` cli_refs: ${cliRefFlags.length}`
4000
+ ];
4001
+ const output = {
4002
+ vault,
4003
+ summary,
4004
+ by_severity: { error: [], warning: [], info: infoOut },
4005
+ fixed: [],
4006
+ unresolved: [],
4007
+ humanHint: hintLines.join("\n")
4008
+ };
4009
+ return {
4010
+ exitCode,
4011
+ result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
4012
+ };
4013
+ }
4014
+ };
4015
+ var fileSourceUrlRule = {
4016
+ id: "file_source_url",
4017
+ severity: "warning",
4018
+ producedBuckets: ["file_source_url", "raw_source_identity_conflict"],
4019
+ async run(ctx) {
4020
+ if (!ctx.parsedPagesCache) ctx.parsedPagesCache = {};
4021
+ if (!ctx.parsedPagesCache.fileSourceUrlFindings) {
4022
+ ctx.parsedPagesCache.fileSourceUrlFindings = await collectFileSourceUrlFindings(ctx.scan, ctx.pageTextCache, {
4023
+ includeRawIdentityConflicts: true
4024
+ });
4025
+ }
4026
+ const findings = ctx.parsedPagesCache.fileSourceUrlFindings;
4027
+ const buckets = {};
4028
+ if (findings.fileSourceUrlFlags.size > 0) {
4029
+ buckets.file_source_url = [...findings.fileSourceUrlFlags];
4030
+ }
4031
+ if (findings.rawIdentityConflicts.length > 0) {
4032
+ buckets.raw_source_identity_conflict = findings.rawIdentityConflicts;
4033
+ }
4034
+ return { buckets };
4035
+ },
4036
+ async runFastPath(input) {
4037
+ const readVault = lintReadVault(input);
4038
+ const lintVault = readVault.readPath;
4039
+ const scanResult = await scanVault(lintVault);
4040
+ if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
4041
+ const pageTextCache = /* @__PURE__ */ new Map();
4042
+ const fixed = [];
4043
+ const unresolved = [];
4044
+ const findings = await collectFileSourceUrlFindings(scanResult.data, pageTextCache, {
4045
+ includeRawIdentityConflicts: false
4046
+ });
4047
+ const remaining = await applyFileSourceUrlFix(
4048
+ input,
4049
+ scanResult.data,
4050
+ findings.fileSourceUrlFlags,
4051
+ findings.fileSourceUrlFrontmatterFlags,
4052
+ fixed,
4053
+ unresolved
4054
+ );
4055
+ const match = remaining.size > 0 ? [{ kind: "file_source_url", items: [...remaining] }] : [];
4056
+ return outputForOnlyBucket(input, match, fixed, unresolved, readVault);
4057
+ },
4058
+ async fix(ctx, currentBucketItems) {
4059
+ const findings = ctx.pageTextCache ? await collectFileSourceUrlFindings(ctx.scan, ctx.pageTextCache, { includeRawIdentityConflicts: false }) : { fileSourceUrlFlags: /* @__PURE__ */ new Set(), fileSourceUrlFrontmatterFlags: /* @__PURE__ */ new Set(), rawIdentityConflicts: [] };
4060
+ const remaining = await applyFileSourceUrlFix(
4061
+ ctx.input,
4062
+ ctx.scan,
4063
+ findings.fileSourceUrlFlags,
4064
+ findings.fileSourceUrlFrontmatterFlags,
4065
+ ctx.fixed,
4066
+ ctx.unresolved
4067
+ );
4068
+ return remaining.size > 0 ? [...remaining] : void 0;
4069
+ }
4070
+ };
4071
+ var sensitiveContentRule = {
4072
+ id: "sensitive_content",
4073
+ severity: "error",
4074
+ async run(ctx) {
4075
+ const sensitiveFlags = [];
4076
+ await mapWithConcurrency(ctx.scan.allMarkdown, vaultIoConcurrency(), async (page) => {
4077
+ try {
4078
+ const text = await readPageCached(page, ctx.pageTextCache);
4079
+ sensitiveFlags.push(...scanSensitiveContent(text, { file: page.relPath }));
4080
+ } catch {
4081
+ }
4082
+ });
4083
+ const buckets = {};
4084
+ if (sensitiveFlags.length > 0) buckets.sensitive_content = sensitiveFlags;
4085
+ return { buckets };
4086
+ },
4087
+ async fix(ctx, currentBucketItems) {
4088
+ const sensitiveFixed = [];
4089
+ for (const page of ctx.scan.allMarkdown) {
4090
+ try {
4091
+ const raw = await readPage(page);
4092
+ const redacted = redactSensitiveContent(raw, { file: page.relPath });
4093
+ if (!redacted.changed) continue;
4094
+ if (page.relPath.startsWith("raw/")) {
4095
+ ctx.unresolved.push(page.relPath);
4096
+ continue;
4097
+ }
4098
+ const w = await safeWritePage(page.absPath, redacted.text, { minBodyRatio: null });
4099
+ if (!w.ok) {
4100
+ ctx.unresolved.push(page.relPath);
4101
+ continue;
4102
+ }
4103
+ sensitiveFixed.push(page.relPath);
4104
+ } catch {
4105
+ ctx.unresolved.push(page.relPath);
4106
+ }
4107
+ }
4108
+ ctx.fixed.push(...sensitiveFixed);
4109
+ const remainingSensitiveFlags = [];
4110
+ for (const page of ctx.scan.allMarkdown) {
4111
+ try {
4112
+ const text = await readPage(page);
4113
+ remainingSensitiveFlags.push(...scanSensitiveContent(text, { file: page.relPath }));
4114
+ } catch {
4115
+ }
4116
+ }
4117
+ return remainingSensitiveFlags.length > 0 ? remainingSensitiveFlags : void 0;
4118
+ }
4119
+ };
4120
+ var conflictMarkersRule = {
4121
+ id: "conflict_markers",
4122
+ severity: "error",
4123
+ async run(ctx) {
4124
+ const conflictMarkers = [];
4125
+ await mapWithConcurrency(ctx.scan.allMarkdown, vaultIoConcurrency(), async (page) => {
4126
+ try {
4127
+ const text = await readPageCached(page, ctx.pageTextCache);
4128
+ const lines = text.split(/\r?\n/);
4129
+ let inFence = false;
4130
+ let openLine = 0;
4131
+ let sawSeparator = false;
4132
+ for (let i = 0; i < lines.length; i += 1) {
4133
+ const line = lines[i];
4134
+ if (line.startsWith("```") || line.startsWith("~~~")) {
4135
+ inFence = !inFence;
4136
+ continue;
4137
+ }
4138
+ if (inFence) continue;
4139
+ if (line.startsWith("<<<<<<< ")) {
4140
+ openLine = i + 1;
4141
+ sawSeparator = false;
4142
+ continue;
4143
+ }
4144
+ if (line === "=======" && openLine > 0) {
4145
+ sawSeparator = true;
4146
+ continue;
4147
+ }
4148
+ if (line.startsWith(">>>>>>> ")) {
4149
+ if (openLine > 0 && sawSeparator) {
4150
+ conflictMarkers.push({ path: page.relPath, line: openLine, message: "complete Git conflict-marker block" });
4151
+ }
4152
+ openLine = 0;
4153
+ sawSeparator = false;
4154
+ }
4155
+ }
4156
+ } catch {
4157
+ }
4158
+ });
4159
+ const buckets = {};
4160
+ if (conflictMarkers.length > 0) buckets.conflict_markers = conflictMarkers;
4161
+ return { buckets };
4162
+ }
4163
+ };
4164
+ var frontmatterYamlInvalidRule = {
4165
+ id: "frontmatter_yaml_invalid",
4166
+ severity: "warning",
4167
+ async run(ctx) {
4168
+ const fmYamlInvalid = [];
4169
+ await mapWithConcurrency(ctx.scan.allMarkdown, vaultIoConcurrency(), async (page) => {
4170
+ try {
4171
+ const text = await readPageCached(page, ctx.pageTextCache);
4172
+ const fm = extractFrontmatter(text);
4173
+ if (!fm.ok && fm.error === "INVALID_FRONTMATTER") {
4174
+ const detail = fm.detail;
4175
+ const message = detail?.message ?? "invalid YAML";
4176
+ fmYamlInvalid.push({ path: page.relPath, message });
4177
+ }
4178
+ } catch {
4179
+ }
4180
+ });
4181
+ const buckets = {};
4182
+ if (fmYamlInvalid.length > 0) buckets.frontmatter_yaml_invalid = fmYamlInvalid;
4183
+ return { buckets };
4184
+ },
4185
+ async fix(ctx, currentBucketItems) {
4186
+ const invalidItems = currentBucketItems;
4187
+ if (!invalidItems) return currentBucketItems;
4188
+ const remaining = [];
4189
+ for (const item of invalidItems) {
4190
+ if (item.path.startsWith("raw/")) {
4191
+ ctx.unresolved.push(item.path);
4192
+ remaining.push(item);
4193
+ continue;
4194
+ }
4195
+ const page = ctx.scan.allMarkdown.find((p) => p.relPath === item.path);
4196
+ if (!page) {
4197
+ ctx.unresolved.push(item.path);
4198
+ remaining.push(item);
4199
+ continue;
4200
+ }
4201
+ try {
4202
+ const text = await readPage(page);
4203
+ const split = splitFrontmatter(text);
4204
+ if (!split.ok) {
4205
+ ctx.unresolved.push(item.path);
4206
+ remaining.push(item);
4207
+ continue;
4208
+ }
4209
+ const newFm = fixFrontmatter(split.data.rawFrontmatter);
4210
+ const newText = `---
4211
+ ${newFm}
4212
+ ---
4213
+ ${split.data.body}`;
4214
+ const recheck = extractFrontmatter(newText);
4215
+ if (!recheck.ok) {
4216
+ ctx.unresolved.push(item.path);
4217
+ remaining.push(item);
4218
+ continue;
4219
+ }
4220
+ const w = await safeWritePage(page.absPath, newText, { minBodyRatio: null });
4221
+ if (!w.ok) {
4222
+ ctx.unresolved.push(item.path);
4223
+ remaining.push(item);
4224
+ continue;
4225
+ }
4226
+ ctx.fixed.push(item.path);
4227
+ } catch {
4228
+ ctx.unresolved.push(item.path);
4229
+ remaining.push(item);
4230
+ }
4231
+ }
4232
+ return remaining.length > 0 ? remaining : void 0;
4233
+ }
4234
+ };
4235
+ var rawSubdirectoryDuplicateRule = {
4236
+ id: "raw_subdirectory_duplicate",
4237
+ severity: "warning",
4238
+ async run(ctx) {
4239
+ const subDirDupes = [];
4240
+ const flatStems = /* @__PURE__ */ new Map();
4241
+ const deepFiles = [];
4242
+ for (const raw of ctx.scan.raw) {
4243
+ const parts = raw.relPath.split("/");
4244
+ if (parts.length === 3) {
4245
+ const stem = parts[2].replace(/\.md$/, "");
4246
+ flatStems.set(`${parts[1]}/${stem}`, raw.relPath);
4247
+ } else if (parts.length > 3) {
4248
+ const stem = parts[parts.length - 1].replace(/\.md$/, "");
4249
+ deepFiles.push({ relPath: raw.relPath, stem, parentType: parts[1] });
4250
+ }
4251
+ }
4252
+ for (const df of deepFiles) {
4253
+ const flatPath = flatStems.get(`${df.parentType}/${df.stem}`);
4254
+ if (flatPath) {
4255
+ subDirDupes.push(`${df.relPath} -> duplicate of ${flatPath}`);
4256
+ }
3698
4257
  }
3699
- if (line.startsWith(">>>>>>> ")) {
3700
- if (openLine > 0 && sawSeparator) {
3701
- findings.push({ path, line: openLine, message: "complete Git conflict-marker block" });
4258
+ const buckets = {};
4259
+ if (subDirDupes.length > 0) buckets.raw_subdirectory_duplicate = subDirDupes;
4260
+ return { buckets };
4261
+ }
4262
+ };
4263
+ var legacyCitationStyleRule = {
4264
+ id: "legacy_citation_style",
4265
+ severity: "warning",
4266
+ async run(ctx) {
4267
+ const legacyPages = [];
4268
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4269
+ try {
4270
+ const text = await readPageCached(page, ctx.pageTextCache);
4271
+ const split = splitFrontmatter(text);
4272
+ if (!split.ok) return;
4273
+ if (isLegacyCitationStyle(split.data.body)) legacyPages.push(page.relPath);
4274
+ } catch {
4275
+ }
4276
+ });
4277
+ const buckets = {};
4278
+ if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
4279
+ return { buckets };
4280
+ },
4281
+ async fix(ctx, currentBucketItems) {
4282
+ const legacyPages = currentBucketItems;
4283
+ if (!legacyPages || legacyPages.length === 0) return currentBucketItems;
4284
+ const FENCE_RE = /```[\s\S]*?```/g;
4285
+ const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
4286
+ const fixedHere = [];
4287
+ for (const relPath of legacyPages) {
4288
+ try {
4289
+ const absPath = `${ctx.input.vault}/${relPath}`;
4290
+ const raw = await readFile13(absPath, "utf8");
4291
+ const split = splitFrontmatter(raw);
4292
+ if (!split.ok) {
4293
+ ctx.unresolved.push(relPath);
4294
+ continue;
4295
+ }
4296
+ const body = split.data.body;
4297
+ const rawFm = split.data.rawFrontmatter;
4298
+ const stripped = body.replace(FENCE_RE, "");
4299
+ const lines = stripped.split("\n");
4300
+ const inlineMarkers = [];
4301
+ let inSources = false;
4302
+ for (const line of lines) {
4303
+ if (/^## Sources\b/.test(line.trim())) {
4304
+ inSources = true;
4305
+ continue;
4306
+ }
4307
+ if (inSources) continue;
4308
+ for (const m of line.matchAll(INLINE_MARKER)) {
4309
+ inlineMarkers.push(m[0]);
4310
+ }
4311
+ }
4312
+ if (inlineMarkers.length === 0) {
4313
+ ctx.unresolved.push(relPath);
4314
+ continue;
4315
+ }
4316
+ const bodyLines = body.split("\n");
4317
+ let inSrc = false;
4318
+ const newBodyLines = [];
4319
+ for (const line of bodyLines) {
4320
+ if (/^## Sources\b/.test(line.trim())) {
4321
+ inSrc = true;
4322
+ newBodyLines.push(line);
4323
+ continue;
4324
+ }
4325
+ if (inSrc) {
4326
+ newBodyLines.push(line);
4327
+ continue;
4328
+ }
4329
+ INLINE_MARKER.lastIndex = 0;
4330
+ const lineWithoutMarkers = line.replace(INLINE_MARKER, "").trim();
4331
+ INLINE_MARKER.lastIndex = 0;
4332
+ if (lineWithoutMarkers.length === 0 && INLINE_MARKER.test(line)) {
4333
+ continue;
4334
+ }
4335
+ let cleaned = line;
4336
+ for (const marker of inlineMarkers) {
4337
+ const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4338
+ const trailingRe = new RegExp(`([.!?]\\s*)${escapedMarker}`);
4339
+ if (trailingRe.test(cleaned)) {
4340
+ cleaned = cleaned.replace(trailingRe, "$1");
4341
+ }
4342
+ const midRe = new RegExp(`${escapedMarker}\\s*`);
4343
+ if (midRe.test(cleaned)) {
4344
+ cleaned = cleaned.replace(midRe, "");
4345
+ }
4346
+ }
4347
+ newBodyLines.push(cleaned);
4348
+ }
4349
+ let newBody = newBodyLines.join("\n");
4350
+ const dedupedMarkers = [...new Set(inlineMarkers)];
4351
+ if (inSrc) {
4352
+ const existingSources = new Set(
4353
+ body.split("\n").filter((l) => /^- \^\[raw\//.test(l.trim())).map((l) => l.trim().replace(/^- /, ""))
4354
+ );
4355
+ const newMarkers = dedupedMarkers.filter((m) => !existingSources.has(m));
4356
+ const sourceLines = newMarkers.map((m) => `- ${m}`);
4357
+ if (sourceLines.length > 0) {
4358
+ newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
4359
+ }
4360
+ } else {
4361
+ const sourceLines = dedupedMarkers.map((m) => `- ${m}`);
4362
+ newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
4363
+ }
4364
+ const newContent = `---
4365
+ ${rawFm}
4366
+ ---
4367
+ ${newBody}`;
4368
+ const w = await safeWritePage(absPath, newContent);
4369
+ if (!w.ok) {
4370
+ ctx.unresolved.push(relPath);
4371
+ continue;
4372
+ }
4373
+ fixedHere.push(relPath);
4374
+ } catch {
4375
+ ctx.unresolved.push(relPath);
3702
4376
  }
3703
- openLine = 0;
3704
- sawSeparator = false;
3705
4377
  }
4378
+ ctx.fixed.push(...fixedHere);
4379
+ if (fixedHere.length > 0) {
4380
+ const fixedSet = new Set(fixedHere);
4381
+ const remaining = legacyPages.filter((p) => !fixedSet.has(p));
4382
+ return remaining.length > 0 ? remaining : void 0;
4383
+ }
4384
+ return currentBucketItems;
3706
4385
  }
3707
- return findings;
3708
- }
3709
- async function runLint(input) {
3710
- if (input.only && !KNOWN_BUCKETS.includes(input.only)) {
3711
- return {
3712
- exitCode: ExitCode.USAGE,
3713
- result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${KNOWN_BUCKETS.join(", ")}` }
3714
- };
3715
- }
3716
- if (input.only === "cli_refs") {
3717
- return runCliRefsOnly(input);
4386
+ };
4387
+ var orphanedCitationsRule = {
4388
+ id: "orphaned_citations",
4389
+ severity: "warning",
4390
+ async run(ctx) {
4391
+ const orphanedPages = [];
4392
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4393
+ try {
4394
+ const text = await readPageCached(page, ctx.pageTextCache);
4395
+ const split = splitFrontmatter(text);
4396
+ if (split.ok && hasOrphanedCitations(split.data.body)) orphanedPages.push(page.relPath);
4397
+ } catch {
4398
+ }
4399
+ });
4400
+ const buckets = {};
4401
+ if (orphanedPages.length > 0) buckets.orphaned_citations = orphanedPages;
4402
+ return { buckets };
3718
4403
  }
3719
- if (input.only === "file_source_url") {
3720
- return runFileSourceUrlOnly(input);
4404
+ };
4405
+ var pageStructureRule = {
4406
+ id: "page_structure",
4407
+ severity: "info",
4408
+ async run(ctx) {
4409
+ const structFlags = [];
4410
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4411
+ try {
4412
+ const text = await readPageCached(page, ctx.pageTextCache);
4413
+ const split = splitFrontmatter(text);
4414
+ if (!split.ok) return;
4415
+ const body = split.data.body;
4416
+ const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
4417
+ if (bodyLines < STRUCT_MIN_BODY_LINES) {
4418
+ const hasRelated = /^## (Related|Relationships)/m.test(body);
4419
+ const sectionCount = (body.match(/^## /gm) ?? []).length;
4420
+ if (!hasRelated || sectionCount < STRUCT_MIN_SECTIONS) {
4421
+ const reasons = [];
4422
+ if (!hasRelated) reasons.push("no Related or Relationships");
4423
+ if (sectionCount < STRUCT_MIN_SECTIONS) reasons.push(`only ${sectionCount} sections`);
4424
+ structFlags.push(`${page.relPath}: ${bodyLines} lines, ${reasons.join(", ")}`);
4425
+ }
4426
+ }
4427
+ } catch {
4428
+ }
4429
+ });
4430
+ const buckets = {};
4431
+ if (structFlags.length > 0) buckets.page_structure = structFlags;
4432
+ return { buckets };
3721
4433
  }
3722
- const shouldFix = (bucket) => !!input.fix && (!input.only || input.only === bucket);
3723
- const readVault = lintReadVault(input);
3724
- const lintVault = readVault.readPath;
3725
- const buckets = {};
3726
- const fixed = [];
3727
- const unresolved = [];
3728
- const scanResult = await scanVault(lintVault);
3729
- if (!scanResult.ok) {
3730
- return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
4434
+ };
4435
+ var duplicateFrontmatterRule = {
4436
+ id: "duplicate_frontmatter",
4437
+ severity: "warning",
4438
+ async run(ctx) {
4439
+ const dupFrontmatter = [];
4440
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4441
+ try {
4442
+ const text = await readPageCached(page, ctx.pageTextCache);
4443
+ const split = splitFrontmatter(text);
4444
+ if (split.ok && hasDuplicateFrontmatter(split.data.body)) dupFrontmatter.push(page.relPath);
4445
+ } catch {
4446
+ }
4447
+ });
4448
+ const buckets = {};
4449
+ if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
4450
+ return { buckets };
3731
4451
  }
3732
- const scan = scanResult.data;
3733
- const pageTextCache = /* @__PURE__ */ new Map();
3734
- if (!input.fix) {
3735
- await mapWithConcurrency(scan.allMarkdown, vaultIoConcurrency(), async (page) => {
4452
+ };
4453
+ var missingOverviewRule = {
4454
+ id: "missing_overview",
4455
+ severity: "warning",
4456
+ async run(ctx) {
4457
+ const noOverview = [];
4458
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
3736
4459
  try {
3737
- await readPageCached(page, pageTextCache);
4460
+ const text = await readPageCached(page, ctx.pageTextCache);
4461
+ const split = splitFrontmatter(text);
4462
+ if (!split.ok) return;
4463
+ if (!/^## Overview/m.test(split.data.body)) noOverview.push(page.relPath);
3738
4464
  } catch {
3739
4465
  }
3740
4466
  });
4467
+ const buckets = {};
4468
+ if (noOverview.length > 0) buckets.missing_overview = noOverview;
4469
+ return { buckets };
4470
+ },
4471
+ async fix(ctx, currentBucketItems) {
4472
+ const noOverview = currentBucketItems;
4473
+ if (!noOverview || noOverview.length === 0) return currentBucketItems;
4474
+ const fixedHere = [];
4475
+ for (const relPath of noOverview) {
4476
+ try {
4477
+ const absPath = `${ctx.input.vault}/${relPath}`;
4478
+ const raw = await readFile13(absPath, "utf8");
4479
+ const split = splitFrontmatter(raw);
4480
+ if (!split.ok) {
4481
+ ctx.unresolved.push(relPath);
4482
+ continue;
4483
+ }
4484
+ const body = split.data.body;
4485
+ const rawFm = split.data.rawFrontmatter;
4486
+ const fm = extractFrontmatter(raw);
4487
+ const title = fm.ok && typeof fm.data.title === "string" ? fm.data.title : "";
4488
+ const overviewSection = `## Overview
4489
+
4490
+ ${title}`;
4491
+ const trimmedBody = body.replace(/^\n+/, "");
4492
+ const newContent = `---
4493
+ ${rawFm}
4494
+ ---
4495
+
4496
+ ${overviewSection}
4497
+
4498
+ ${trimmedBody}`;
4499
+ const w = await safeWritePage(absPath, newContent);
4500
+ if (!w.ok) {
4501
+ ctx.unresolved.push(relPath);
4502
+ continue;
4503
+ }
4504
+ fixedHere.push(relPath);
4505
+ } catch {
4506
+ ctx.unresolved.push(relPath);
4507
+ }
4508
+ }
4509
+ ctx.fixed.push(...fixedHere);
4510
+ if (fixedHere.length > 0) {
4511
+ const fixedSet = new Set(fixedHere);
4512
+ const remaining = noOverview.filter((p) => !fixedSet.has(p));
4513
+ return remaining.length > 0 ? remaining : void 0;
4514
+ }
4515
+ return currentBucketItems;
3741
4516
  }
3742
- const links = await runLinks({ vault: lintVault, scan, pageTextCache });
3743
- if (links.result.ok && links.result.data.broken.length > 0) buckets.broken_wikilinks = links.result.data.broken;
3744
- if (!links.result.ok && links.result.error === "INVALID_FRONTMATTER") {
3745
- buckets.invalid_frontmatter = [links.result.detail ?? {}];
3746
- }
3747
- const tags = await runTagAudit({ vault: lintVault, scan, pageTextCache });
3748
- if (tags.result.ok && tags.result.data.violations.length > 0) buckets.tag_not_in_taxonomy = tags.result.data.violations;
3749
- if (!tags.result.ok && tags.result.error === "INVALID_FRONTMATTER") {
3750
- buckets.invalid_frontmatter = [...buckets.invalid_frontmatter ?? [], tags.result.detail ?? {}];
3751
- }
3752
- const idx = await runIndexCheck({ vault: lintVault, scan });
3753
- if (idx.result.ok && (idx.result.data.missing_from_index.length > 0 || idx.result.data.ghost_entries.length > 0)) {
3754
- buckets.index_incomplete = [{
3755
- missing_from_index: idx.result.data.missing_from_index,
3756
- ghost_entries: idx.result.data.ghost_entries
3757
- }];
3758
- }
3759
- const linkFmt = await runIndexLinkFormat({ vault: lintVault });
3760
- if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
3761
- buckets.index_link_format = linkFmt.result.data.markdown_links;
3762
- }
3763
- const staleResult = await runStale({ vault: lintVault, days: input.days, scan, pageTextCache });
3764
- if (staleResult.result.ok) {
3765
- const st = staleResult.result.data;
3766
- const staleList = [...st.stale_transcripts.map((t) => t.path), ...(st.unclaimed_transcripts ?? []).map((t) => t.path), ...st.incomplete_work_items.map((w) => w.path), ...(st.done_work_items ?? []).map((w) => w.path)];
3767
- if (staleList.length > 0) buckets.stale_page = staleList;
3768
- }
3769
- const pagesize = await runPagesize({ vault: lintVault, lines: input.lines, scan, pageTextCache });
3770
- if (pagesize.result.ok && pagesize.result.data.oversized.length > 0) buckets.page_too_large = pagesize.result.data.oversized;
3771
- const rotate = await runLogRotate({ vault: lintVault, threshold: input.logThreshold, apply: false });
3772
- if (rotate.result.ok && rotate.exitCode === ExitCode.LOG_ROTATE_NEEDED) {
3773
- buckets.log_rotate_needed = [{ entries: rotate.result.data.entries, threshold: rotate.result.data.threshold }];
3774
- }
3775
- const orphans = await runOrphans({ vault: lintVault, scan, pageTextCache });
3776
- if (orphans.result.ok) {
3777
- if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
3778
- if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
3779
- }
3780
- const sparse = await runSparseCommunity({ vault: lintVault, scan, pageTextCache });
3781
- if (sparse.result.ok && sparse.result.data.communities.length > 0) {
3782
- buckets.sparse_community = sparse.result.data.communities;
3783
- }
3784
- const topicMap = await runTopicMapCheck({ vault: lintVault, scan });
3785
- if (topicMap.result.ok && topicMap.result.data.recommended) {
3786
- buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
3787
- }
3788
- const dedup = await runDedup({ vault: lintVault, scan, pageTextCache });
3789
- if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
3790
- const bodyDedup = await runRawBodyDedup(lintVault, scan, pageTextCache);
3791
- if (bodyDedup.result.ok && bodyDedup.result.data.duplicates.length > 0) {
3792
- buckets.raw_body_duplicate = bodyDedup.result.data.duplicates.map((d) => ({
3793
- body_hash: d.bodyHash.slice(0, 12),
3794
- files: d.files.map((f) => `${f.relPath} (sha256: ${f.sha256 ?? "none"})`)
3795
- }));
3796
- }
3797
- const compoundRefs = await validateCompoundReferences(lintVault, scan, pageTextCache);
3798
- if (compoundRefs.ok && compoundRefs.data.length > 0) buckets.compound_refs = compoundRefs.data;
3799
- const pathCheck = await runPathTooLong({ vault: lintVault, scan });
3800
- if (pathCheck.result.ok && pathCheck.result.data.violations.length > 0) buckets.path_too_long = pathCheck.result.data.violations;
3801
- const wikilinkResolver = buildWikilinkResolver(scan.allMarkdown);
3802
- {
3803
- const allPageResults = await mapWithConcurrency(scan.allMarkdown, vaultIoConcurrency(), async (page) => {
3804
- const sensitiveFlags2 = [];
3805
- const conflictMarkers2 = [];
3806
- let fmYamlInvalid2 = null;
4517
+ };
4518
+ var frontmatterWikilinkRule = {
4519
+ id: "frontmatter_wikilink",
4520
+ severity: "info",
4521
+ async run(ctx) {
4522
+ const fmWikilinkFlags = [];
4523
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
3807
4524
  try {
3808
- const text = await readPageCached(page, pageTextCache);
3809
- sensitiveFlags2.push(...scanSensitiveContent(text, { file: page.relPath }));
3810
- conflictMarkers2.push(...scanConflictMarkerBlocks(page.relPath, text));
3811
- const fm = extractFrontmatter(text);
3812
- if (!fm.ok && fm.error === "INVALID_FRONTMATTER") {
3813
- const detail = fm.detail;
3814
- const message = detail?.message ?? "invalid YAML";
3815
- fmYamlInvalid2 = { path: page.relPath, message };
4525
+ const text = await readPageCached(page, ctx.pageTextCache);
4526
+ const split = splitFrontmatter(text);
4527
+ if (!split.ok) return;
4528
+ const rawFm = split.data.rawFrontmatter;
4529
+ const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
4530
+ for (const link of fmLinks) {
4531
+ const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
4532
+ if (!ctx.wikilinkResolver.resolve(target).path) {
4533
+ fmWikilinkFlags.push(`${page.relPath}: [[${target}]] does not resolve`);
4534
+ }
3816
4535
  }
3817
4536
  } catch {
3818
4537
  }
3819
- return { sensitiveFlags: sensitiveFlags2, conflictMarkers: conflictMarkers2, fmYamlInvalid: fmYamlInvalid2 };
3820
4538
  });
3821
- const sensitiveFlags = allPageResults.flatMap((result) => result.sensitiveFlags);
3822
- if (sensitiveFlags.length > 0) buckets.sensitive_content = sensitiveFlags;
3823
- const conflictMarkers = allPageResults.flatMap((result) => result.conflictMarkers);
3824
- if (conflictMarkers.length > 0) buckets.conflict_markers = conflictMarkers;
3825
- const fmYamlInvalid = allPageResults.map((result) => result.fmYamlInvalid).filter((item) => item !== null);
3826
- if (fmYamlInvalid.length > 0) buckets.frontmatter_yaml_invalid = fmYamlInvalid;
3827
- const subDirDupes = [];
3828
- const flatStems = /* @__PURE__ */ new Map();
3829
- const deepFiles = [];
3830
- for (const raw of scan.raw) {
3831
- const parts = raw.relPath.split("/");
3832
- if (parts.length === 3) {
3833
- const stem = parts[2].replace(/\.md$/, "");
3834
- flatStems.set(`${parts[1]}/${stem}`, raw.relPath);
3835
- } else if (parts.length > 3) {
3836
- const stem = parts[parts.length - 1].replace(/\.md$/, "");
3837
- deepFiles.push({ relPath: raw.relPath, stem, parentType: parts[1] });
4539
+ const buckets = {};
4540
+ if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
4541
+ return { buckets };
4542
+ }
4543
+ };
4544
+ var wikilinkCitationRule = {
4545
+ id: "wikilink_citation",
4546
+ severity: "info",
4547
+ async run(ctx) {
4548
+ const wikilinkCitationFlags = [];
4549
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4550
+ try {
4551
+ const text = await readPageCached(page, ctx.pageTextCache);
4552
+ const split = splitFrontmatter(text);
4553
+ if (split.ok && hasWikilinkCitations(split.data.body)) wikilinkCitationFlags.push(page.relPath);
4554
+ } catch {
3838
4555
  }
3839
- }
3840
- for (const df of deepFiles) {
3841
- const flatPath = flatStems.get(`${df.parentType}/${df.stem}`);
3842
- if (flatPath) {
3843
- subDirDupes.push(`${df.relPath} -> duplicate of ${flatPath}`);
4556
+ });
4557
+ const buckets = {};
4558
+ if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
4559
+ return { buckets };
4560
+ },
4561
+ async fix(ctx, currentBucketItems) {
4562
+ const wikilinkCitationFlags = currentBucketItems;
4563
+ if (!wikilinkCitationFlags || wikilinkCitationFlags.length === 0) return currentBucketItems;
4564
+ const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
4565
+ const FENCE_RE = /```[\s\S]*?```/g;
4566
+ const wikilinkFixed = [];
4567
+ for (const relPath of wikilinkCitationFlags) {
4568
+ try {
4569
+ const absPath = `${ctx.input.vault}/${relPath}`;
4570
+ const raw = await readFile13(absPath, "utf8");
4571
+ const split = splitFrontmatter(raw);
4572
+ if (!split.ok) {
4573
+ ctx.unresolved.push(relPath);
4574
+ continue;
4575
+ }
4576
+ const body = split.data.body;
4577
+ const rawFm = split.data.rawFrontmatter;
4578
+ const stripped = body.replace(FENCE_RE, "");
4579
+ const wikilinkMatches = [...stripped.matchAll(WIKILINK_RE)];
4580
+ if (wikilinkMatches.length === 0) {
4581
+ ctx.unresolved.push(relPath);
4582
+ continue;
4583
+ }
4584
+ const wikilinkPaths = [...new Set(wikilinkMatches.map((m) => m[1]))];
4585
+ const bodyLines = body.split("\n");
4586
+ let inSrc = false;
4587
+ const newBodyLines = [];
4588
+ for (const line of bodyLines) {
4589
+ if (/^## Sources\b/.test(line.trim())) {
4590
+ inSrc = true;
4591
+ newBodyLines.push(line);
4592
+ continue;
4593
+ }
4594
+ if (inSrc) {
4595
+ newBodyLines.push(line);
4596
+ continue;
4597
+ }
4598
+ let cleaned = line.replace(/\[\[raw\/[^\]|]+(?:\|[^\]]*)?\]\]/g, "");
4599
+ cleaned = cleaned.replace(/\s+\./g, ".").replace(/\s{2,}/g, " ").replace(/\s+$/, "");
4600
+ if (cleaned.length > 0 || line.trim().length === 0) {
4601
+ newBodyLines.push(cleaned);
4602
+ }
4603
+ }
4604
+ let newBody = newBodyLines.join("\n");
4605
+ const citationMarkers = wikilinkPaths.map((p) => `^[raw/${p}]`);
4606
+ const sourceEntries = extractSourceEntries(rawFm);
4607
+ const fmMarkers = [];
4608
+ for (const entry of sourceEntries) {
4609
+ let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
4610
+ rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
4611
+ if (rawPath.startsWith("raw/")) {
4612
+ fmMarkers.push(`^[${rawPath}]`);
4613
+ }
4614
+ }
4615
+ const allMarkers = [.../* @__PURE__ */ new Set([...citationMarkers, ...fmMarkers])];
4616
+ const hasSourcesSection = /^## Sources\b/m.test(newBody);
4617
+ if (hasSourcesSection) {
4618
+ const existingSources = new Set(
4619
+ newBody.split("\n").filter((l) => /^- \^\[raw\//.test(l.trim())).map((l) => l.trim().replace(/^- /, ""))
4620
+ );
4621
+ const newMarkers = allMarkers.filter((m) => !existingSources.has(m));
4622
+ const sourceLines = newMarkers.map((m) => `- ${m}`);
4623
+ if (sourceLines.length > 0) {
4624
+ newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
4625
+ }
4626
+ } else {
4627
+ const sourceLines = allMarkers.map((m) => `- ${m}`);
4628
+ newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
4629
+ }
4630
+ const newContent = `---
4631
+ ${rawFm}
4632
+ ---
4633
+ ${newBody}`;
4634
+ const w = await safeWritePage(absPath, newContent);
4635
+ if (!w.ok) {
4636
+ ctx.unresolved.push(relPath);
4637
+ continue;
4638
+ }
4639
+ wikilinkFixed.push(relPath);
4640
+ } catch {
4641
+ ctx.unresolved.push(relPath);
3844
4642
  }
3845
4643
  }
3846
- if (subDirDupes.length > 0) {
3847
- buckets.raw_subdirectory_duplicate = subDirDupes;
4644
+ ctx.fixed.push(...wikilinkFixed);
4645
+ if (wikilinkFixed.length > 0) {
4646
+ const fixedSet = new Set(wikilinkFixed);
4647
+ const remaining = wikilinkCitationFlags.filter((p) => !fixedSet.has(p));
4648
+ return remaining.length > 0 ? remaining : void 0;
3848
4649
  }
3849
- const fileSourceUrlFindings = await collectFileSourceUrlFindings(scan, pageTextCache, { includeRawIdentityConflicts: true });
3850
- let fileSourceUrlFlags = fileSourceUrlFindings.fileSourceUrlFlags;
3851
- const fileSourceUrlFrontmatterFlags = fileSourceUrlFindings.fileSourceUrlFrontmatterFlags;
3852
- const rawIdentityConflicts = fileSourceUrlFindings.rawIdentityConflicts;
3853
- if (fileSourceUrlFlags.size > 0) buckets.file_source_url = [...fileSourceUrlFlags];
3854
- if (rawIdentityConflicts.length > 0) buckets.raw_source_identity_conflict = rawIdentityConflicts;
3855
- const legacyPages = [];
3856
- const orphanedPages = [];
3857
- const structFlags = [];
3858
- const dupFrontmatter = [];
3859
- const noOverview = [];
3860
- const fmWikilinkFlags = [];
3861
- const wikilinkCitationFlags = [];
4650
+ return currentBucketItems;
4651
+ }
4652
+ };
4653
+ var brokenSourcesRule = {
4654
+ id: "broken_sources",
4655
+ severity: "error",
4656
+ async run(ctx) {
3862
4657
  const brokenSourceFlags = /* @__PURE__ */ new Set();
3863
- const missingTldrFlags = [];
3864
- const missingDiagramFlags = [];
3865
- const typedPageResults = await mapWithConcurrency(scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
3866
- const result = {
3867
- legacyPages: [],
3868
- orphanedPages: [],
3869
- structFlags: [],
3870
- dupFrontmatter: [],
3871
- noOverview: [],
3872
- fmWikilinkFlags: [],
3873
- wikilinkCitationFlags: [],
3874
- brokenSourceFlags: [],
3875
- missingTldrFlags: [],
3876
- missingDiagramFlags: []
3877
- };
4658
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
3878
4659
  try {
3879
- const text = await readPageCached(page, pageTextCache);
4660
+ const text = await readPageCached(page, ctx.pageTextCache);
3880
4661
  const split = splitFrontmatter(text);
3881
- if (!split.ok) return result;
4662
+ if (!split.ok) return;
3882
4663
  const body = split.data.body;
3883
4664
  const rawFm = split.data.rawFrontmatter;
3884
- if (hasDuplicateFrontmatter(body)) result.dupFrontmatter.push(page.relPath);
3885
- if (isLegacyCitationStyle(body)) result.legacyPages.push(page.relPath);
3886
- if (hasOrphanedCitations(body)) result.orphanedPages.push(page.relPath);
3887
- if (hasWikilinkCitations(body)) result.wikilinkCitationFlags.push(page.relPath);
3888
4665
  const sourcesEntries = extractSourceEntries(rawFm);
3889
4666
  for (const entry of sourcesEntries) {
3890
4667
  const rawPath = normalizeRawSourceTarget(entry);
3891
4668
  if (!rawPath) continue;
3892
- if (!rawSourceTargetExistsSync(lintVault, rawPath)) {
3893
- result.brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
4669
+ if (!rawSourceTargetExistsSync(ctx.vault, rawPath)) {
4670
+ brokenSourceFlags.add(`${page.relPath}: ${rawPath}`);
3894
4671
  }
3895
4672
  }
3896
4673
  for (const marker of extractCitationMarkers(body)) {
3897
- if (!rawSourceTargetExistsSync(lintVault, marker.target)) {
3898
- result.brokenSourceFlags.push(`${page.relPath}: ${marker.target}`);
4674
+ if (!rawSourceTargetExistsSync(ctx.vault, marker.target)) {
4675
+ brokenSourceFlags.add(`${page.relPath}: ${marker.target}`);
3899
4676
  }
3900
4677
  }
3901
- const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
3902
- for (const link of fmLinks) {
3903
- const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
3904
- if (!wikilinkResolver.resolve(target).path) {
3905
- result.fmWikilinkFlags.push(`${page.relPath}: [[${target}]] does not resolve`);
4678
+ } catch {
4679
+ }
4680
+ });
4681
+ const buckets = {};
4682
+ if (brokenSourceFlags.size > 0) buckets.broken_sources = [...brokenSourceFlags];
4683
+ return { buckets };
4684
+ }
4685
+ };
4686
+ var missingTldrRule = {
4687
+ id: "missing_tldr",
4688
+ severity: "info",
4689
+ async run(ctx) {
4690
+ const missingTldrFlags = [];
4691
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4692
+ try {
4693
+ const text = await readPageCached(page, ctx.pageTextCache);
4694
+ const split = splitFrontmatter(text);
4695
+ if (!split.ok) return;
4696
+ const body = split.data.body;
4697
+ const bodyFirst15 = body.split("\n").slice(0, 15).join("\n");
4698
+ if (!/^>\s*\*\*TL;DR:?\*\*/m.test(bodyFirst15) && !/^##\s+TL;\s*DR/m.test(bodyFirst15)) {
4699
+ missingTldrFlags.push(page.relPath);
4700
+ }
4701
+ } catch {
4702
+ }
4703
+ });
4704
+ const buckets = {};
4705
+ if (missingTldrFlags.length > 0) buckets.missing_tldr = missingTldrFlags;
4706
+ return { buckets };
4707
+ },
4708
+ async fix(ctx, currentBucketItems) {
4709
+ const missingTldrFlags = currentBucketItems;
4710
+ if (!missingTldrFlags || missingTldrFlags.length === 0) return currentBucketItems;
4711
+ const fixedHere = [];
4712
+ for (const relPath of missingTldrFlags) {
4713
+ try {
4714
+ const absPath = `${ctx.input.vault}/${relPath}`;
4715
+ const raw = await readFile13(absPath, "utf8");
4716
+ const split = splitFrontmatter(raw);
4717
+ if (!split.ok) {
4718
+ ctx.unresolved.push(relPath);
4719
+ continue;
4720
+ }
4721
+ const body = split.data.body;
4722
+ const rawFm = split.data.rawFrontmatter;
4723
+ const lines = body.split("\n");
4724
+ let insertIndex = 0;
4725
+ for (let i = 0; i < lines.length; i++) {
4726
+ if (/^# /.test(lines[i])) {
4727
+ insertIndex = i + 1;
4728
+ while (insertIndex < lines.length && lines[insertIndex].trim() === "") {
4729
+ insertIndex++;
4730
+ }
4731
+ break;
3906
4732
  }
3907
4733
  }
3908
- const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
3909
- const hasOverview = /^## Overview/m.test(body);
3910
- if (!hasOverview) result.noOverview.push(page.relPath);
3911
- const bodyFirst15 = body.split("\n").slice(0, 15).join("\n");
3912
- if (!/^>\s*\*\*TL;DR:?\*\*/m.test(bodyFirst15) && !/^##\s+TL;\s*DR/m.test(bodyFirst15)) result.missingTldrFlags.push(page.relPath);
4734
+ if (insertIndex === 0) {
4735
+ lines.splice(0, 0, "", "> **TL;DR:** ");
4736
+ } else {
4737
+ lines.splice(insertIndex, 0, "> **TL;DR:** ");
4738
+ }
4739
+ const trimmedFm = rawFm.endsWith("\n") ? rawFm : rawFm + "\n";
4740
+ const newContent = `---
4741
+ ${trimmedFm}---
4742
+ ${lines.join("\n")}`;
4743
+ const w = await safeWritePage(absPath, newContent);
4744
+ if (!w.ok) {
4745
+ ctx.unresolved.push(relPath);
4746
+ continue;
4747
+ }
4748
+ fixedHere.push(relPath);
4749
+ } catch {
4750
+ ctx.unresolved.push(relPath);
4751
+ }
4752
+ }
4753
+ ctx.fixed.push(...fixedHere);
4754
+ if (fixedHere.length > 0) {
4755
+ const fixedSet = new Set(fixedHere);
4756
+ const remaining = missingTldrFlags.filter((p) => !fixedSet.has(p));
4757
+ return remaining.length > 0 ? remaining : void 0;
4758
+ }
4759
+ return currentBucketItems;
4760
+ }
4761
+ };
4762
+ var missingDiagramRule = {
4763
+ id: "missing_diagram",
4764
+ severity: "warning",
4765
+ async run(ctx) {
4766
+ const missingDiagramFlags = [];
4767
+ await mapWithConcurrency(ctx.scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4768
+ try {
4769
+ const text = await readPageCached(page, ctx.pageTextCache);
3913
4770
  const fmData = extractFrontmatter(text);
3914
4771
  const pageTags = fmData.ok && Array.isArray(fmData.data.tags) ? fmData.data.tags : [];
3915
- if (pageTags.includes("architecture") && !body.includes("```mermaid")) {
3916
- result.missingDiagramFlags.push(page.relPath);
3917
- }
3918
- if (bodyLines < STRUCT_MIN_BODY_LINES) {
3919
- const hasRelated = /^## (Related|Relationships)/m.test(body);
3920
- const sectionCount = (body.match(/^## /gm) ?? []).length;
3921
- if (!hasRelated || sectionCount < STRUCT_MIN_SECTIONS) {
3922
- const reasons = [];
3923
- if (!hasRelated) reasons.push("no Related or Relationships");
3924
- if (sectionCount < STRUCT_MIN_SECTIONS) reasons.push(`only ${sectionCount} sections`);
3925
- result.structFlags.push(`${page.relPath}: ${bodyLines} lines, ${reasons.join(", ")}`);
3926
- }
4772
+ if (pageTags.includes("architecture") && !text.includes("```mermaid")) {
4773
+ missingDiagramFlags.push(page.relPath);
3927
4774
  }
3928
4775
  } catch {
3929
4776
  }
3930
- return result;
3931
4777
  });
3932
- for (const result of typedPageResults) {
3933
- legacyPages.push(...result.legacyPages);
3934
- orphanedPages.push(...result.orphanedPages);
3935
- structFlags.push(...result.structFlags);
3936
- dupFrontmatter.push(...result.dupFrontmatter);
3937
- noOverview.push(...result.noOverview);
3938
- fmWikilinkFlags.push(...result.fmWikilinkFlags);
3939
- wikilinkCitationFlags.push(...result.wikilinkCitationFlags);
3940
- for (const flag of result.brokenSourceFlags) brokenSourceFlags.add(flag);
3941
- missingTldrFlags.push(...result.missingTldrFlags);
3942
- missingDiagramFlags.push(...result.missingDiagramFlags);
3943
- }
3944
- if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
3945
- if (orphanedPages.length > 0) buckets.orphaned_citations = orphanedPages;
3946
- if (structFlags.length > 0) buckets.page_structure = structFlags;
3947
- if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
3948
- if (noOverview.length > 0) buckets.missing_overview = noOverview;
3949
- if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
3950
- if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
3951
- if (brokenSourceFlags.size > 0) buckets.broken_sources = [...brokenSourceFlags];
3952
- if (missingTldrFlags.length > 0) buckets.missing_tldr = missingTldrFlags;
4778
+ const buckets = {};
3953
4779
  if (missingDiagramFlags.length > 0) buckets.missing_diagram = missingDiagramFlags;
4780
+ return { buckets };
4781
+ }
4782
+ };
4783
+ var workItemHealthRule = {
4784
+ id: "work_item_health",
4785
+ severity: "warning",
4786
+ async run(ctx) {
3954
4787
  const workItemHealth = [];
3955
4788
  const workItemDirs = /* @__PURE__ */ new Map();
3956
- for (const page of scan.workItems) {
4789
+ for (const page of ctx.scan.workItems) {
3957
4790
  const dir = page.relPath.replace(/\/(spec|plan|log)\.md$/, "");
3958
4791
  const pages = workItemDirs.get(dir) ?? [];
3959
4792
  pages.push(page);
3960
4793
  workItemDirs.set(dir, pages);
3961
4794
  }
3962
- const workItemHealthResults = await mapWithConcurrency([...workItemDirs.entries()], vaultIoConcurrency(), async ([dir, pages]) => {
3963
- const flags = [];
3964
- const specPage = pages.find((p) => p.relPath.endsWith("/spec.md"));
3965
- const hasPlan = pages.some((p) => p.relPath.endsWith("/plan.md"));
3966
- let specStatus;
3967
- let specStarted;
3968
- if (specPage) {
3969
- const text = await readPageCached(specPage, pageTextCache);
3970
- const fm = extractFrontmatter(text);
3971
- if (fm.ok) {
3972
- specStatus = typeof fm.data.status === "string" ? fm.data.status : void 0;
3973
- specStarted = fm.data.started;
4795
+ const workItemHealthResults = await mapWithConcurrency(
4796
+ [...workItemDirs.entries()],
4797
+ vaultIoConcurrency(),
4798
+ async ([dir, pages]) => {
4799
+ const flags = [];
4800
+ const specPage = pages.find((p) => p.relPath.endsWith("/spec.md"));
4801
+ const hasPlan = pages.some((p) => p.relPath.endsWith("/plan.md"));
4802
+ let specStatus;
4803
+ let specStarted;
4804
+ if (specPage) {
4805
+ const text = await readPageCached(specPage, ctx.pageTextCache);
4806
+ const fm = extractFrontmatter(text);
4807
+ if (fm.ok) {
4808
+ specStatus = typeof fm.data.status === "string" ? fm.data.status : void 0;
4809
+ specStarted = fm.data.started;
4810
+ }
3974
4811
  }
3975
- }
3976
- const isClosed = specStatus === "completed" || specStatus === "abandoned";
3977
- if (specPage && !hasPlan && !isClosed) {
3978
- const lastSegment = dir.split("/").pop();
3979
- const dateMatch = lastSegment.match(/^(\d{4}-\d{2}-\d{2})/);
3980
- if (dateMatch) {
3981
- const dirDate = Date.parse(dateMatch[1]);
3982
- if (!isNaN(dirDate) && Date.now() - dirDate > 24 * 60 * 60 * 1e3) {
3983
- flags.push(`${dir}/spec.md: has spec but no plan after 24h`);
4812
+ const isClosed = specStatus === "completed" || specStatus === "abandoned";
4813
+ if (specPage && !hasPlan && !isClosed) {
4814
+ const lastSegment = dir.split("/").pop();
4815
+ const dateMatch = lastSegment.match(/^(\d{4}-\d{2}-\d{2})/);
4816
+ if (dateMatch) {
4817
+ const dirDate = Date.parse(dateMatch[1]);
4818
+ if (!isNaN(dirDate) && Date.now() - dirDate > 24 * 60 * 60 * 1e3) {
4819
+ flags.push(`${dir}/spec.md: has spec but no plan after 24h`);
4820
+ }
3984
4821
  }
3985
4822
  }
4823
+ if (specPage && specStatus === "in-progress" && !specStarted) {
4824
+ flags.push(`${specPage.relPath}: in-progress without started date`);
4825
+ }
4826
+ return flags;
3986
4827
  }
3987
- if (specPage && specStatus === "in-progress" && !specStarted) {
3988
- flags.push(`${specPage.relPath}: in-progress without started date`);
3989
- }
3990
- return flags;
3991
- });
4828
+ );
3992
4829
  workItemHealth.push(...workItemHealthResults.flat());
4830
+ const buckets = {};
3993
4831
  if (workItemHealth.length > 0) buckets.work_item_health = workItemHealth;
4832
+ return { buckets };
4833
+ }
4834
+ };
4835
+ var orphanedProjectPagesRule = {
4836
+ id: "orphaned_project_pages",
4837
+ severity: "warning",
4838
+ async run(ctx) {
3994
4839
  const orphanedProjectPages = [];
3995
4840
  const knowledgeContentCache = /* @__PURE__ */ new Map();
3996
4841
  const readKnowledgeContent = (slug) => {
3997
4842
  const existing = knowledgeContentCache.get(slug);
3998
4843
  if (existing) return existing;
3999
- const knowledgePath = join12(lintVault, "projects", slug, "knowledge.md");
4844
+ const knowledgePath = join13(ctx.vault, "projects", slug, "knowledge.md");
4000
4845
  const pending = existsSync4(knowledgePath) ? readFile13(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
4001
4846
  knowledgeContentCache.set(slug, pending);
4002
4847
  return pending;
4003
4848
  };
4004
- const orphanedProjectPageResults = await mapWithConcurrency(scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4005
- const flags = [];
4006
- try {
4007
- const text = await readPageCached(page, pageTextCache);
4008
- const fm = extractFrontmatter(text);
4009
- if (!fm.ok) return flags;
4010
- const pp = fm.data.provenance_projects;
4011
- if (!Array.isArray(pp)) return flags;
4012
- for (const entry of pp) {
4013
- const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
4014
- if (!slugMatch) continue;
4015
- const slug = slugMatch[1];
4016
- const knowledgeContent = await readKnowledgeContent(slug);
4017
- if (knowledgeContent === null) continue;
4018
- const pageRef = page.relPath.replace(/\.md$/, "");
4019
- if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
4020
- flags.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
4021
- }
4022
- }
4023
- } catch {
4024
- }
4025
- return flags;
4026
- });
4027
- orphanedProjectPages.push(...orphanedProjectPageResults.flat());
4028
- if (orphanedProjectPages.length > 0) buckets.orphaned_project_pages = orphanedProjectPages;
4029
- const cliRefFlags = [];
4030
- const cliSurface = buildCliSurface();
4031
- const allScanPages = [...scan.typedKnowledge];
4032
- const cliRefResults = await mapWithConcurrency(allScanPages, vaultIoConcurrency(), async (page) => {
4033
- const flags = [];
4034
- try {
4035
- const text = await readPageCached(page, pageTextCache);
4036
- const violations = validateCliRefs(text, page.relPath, cliSurface);
4037
- for (const v of violations) {
4038
- flags.push(`${v.page}: ${v.ref} (${v.reason})`);
4849
+ const orphanedProjectPageResults = await mapWithConcurrency(
4850
+ ctx.scan.typedKnowledge,
4851
+ vaultIoConcurrency(),
4852
+ async (page) => {
4853
+ const flags = [];
4854
+ try {
4855
+ const text = await readPageCached(page, ctx.pageTextCache);
4856
+ const fm = extractFrontmatter(text);
4857
+ if (!fm.ok) return flags;
4858
+ const pp = fm.data.provenance_projects;
4859
+ if (!Array.isArray(pp)) return flags;
4860
+ for (const entry of pp) {
4861
+ const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
4862
+ if (!slugMatch) continue;
4863
+ const slug = slugMatch[1];
4864
+ const knowledgeContent = await readKnowledgeContent(slug);
4865
+ if (knowledgeContent === null) continue;
4866
+ const pageRef = page.relPath.replace(/\.md$/, "");
4867
+ if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
4868
+ flags.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
4869
+ }
4870
+ }
4871
+ } catch {
4039
4872
  }
4040
- } catch {
4873
+ return flags;
4041
4874
  }
4042
- return flags;
4043
- });
4044
- cliRefFlags.push(...cliRefResults.flat());
4045
- if (cliRefFlags.length > 0) buckets.cli_refs = cliRefFlags;
4875
+ );
4876
+ orphanedProjectPages.push(...orphanedProjectPageResults.flat());
4877
+ const buckets = {};
4878
+ if (orphanedProjectPages.length > 0) buckets.orphaned_project_pages = orphanedProjectPages;
4879
+ return { buckets };
4880
+ }
4881
+ };
4882
+ var staleSectionsRule = {
4883
+ id: "stale_sections",
4884
+ severity: "info",
4885
+ async run(ctx) {
4046
4886
  const staleSectionFlags = [];
4047
4887
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4048
4888
  const approachingThreshold = 7;
4049
- const staleSectionResults = await mapWithConcurrency(scan.typedKnowledge, vaultIoConcurrency(), async (page) => {
4050
- const flags = [];
4051
- try {
4052
- const text = await readPageCached(page, pageTextCache);
4053
- const annotations = parseExpiryAnnotations(text, page.relPath);
4054
- for (const ann of annotations) {
4055
- if (ann.expires < today) {
4056
- flags.push(`${page.relPath}: section "${ann.heading}" expired on ${ann.expires}`);
4057
- } else {
4058
- const daysUntilExpiry = Math.floor((Date.parse(ann.expires) - Date.now()) / 864e5);
4059
- if (daysUntilExpiry <= approachingThreshold) {
4060
- flags.push(`${page.relPath}: section "${ann.heading}" expires in ${daysUntilExpiry} day(s) (${ann.expires})`);
4889
+ const staleSectionResults = await mapWithConcurrency(
4890
+ ctx.scan.typedKnowledge,
4891
+ vaultIoConcurrency(),
4892
+ async (page) => {
4893
+ const flags = [];
4894
+ try {
4895
+ const text = await readPageCached(page, ctx.pageTextCache);
4896
+ const annotations = parseExpiryAnnotations(text, page.relPath);
4897
+ for (const ann of annotations) {
4898
+ if (ann.expires < today) {
4899
+ flags.push(`${page.relPath}: section "${ann.heading}" expired on ${ann.expires}`);
4900
+ } else {
4901
+ const daysUntilExpiry = Math.floor((Date.parse(ann.expires) - Date.now()) / 864e5);
4902
+ if (daysUntilExpiry <= approachingThreshold) {
4903
+ flags.push(
4904
+ `${page.relPath}: section "${ann.heading}" expires in ${daysUntilExpiry} day(s) (${ann.expires})`
4905
+ );
4906
+ }
4061
4907
  }
4062
4908
  }
4909
+ } catch {
4063
4910
  }
4064
- } catch {
4911
+ return flags;
4065
4912
  }
4066
- return flags;
4067
- });
4913
+ );
4068
4914
  staleSectionFlags.push(...staleSectionResults.flat());
4915
+ const buckets = {};
4069
4916
  if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
4070
- if (shouldFix("sensitive_content") && buckets.sensitive_content) {
4071
- const sensitiveFixed = [];
4072
- for (const page of scan.allMarkdown) {
4073
- try {
4074
- const raw = await readPage(page);
4075
- const redacted = redactSensitiveContent(raw, { file: page.relPath });
4076
- if (!redacted.changed) continue;
4077
- if (page.relPath.startsWith("raw/")) {
4078
- unresolved.push(page.relPath);
4079
- continue;
4080
- }
4081
- const w = await safeWritePage(page.absPath, redacted.text, { minBodyRatio: null });
4082
- if (!w.ok) {
4083
- unresolved.push(page.relPath);
4084
- continue;
4917
+ return { buckets };
4918
+ }
4919
+ };
4920
+ var cycleTrapsRule = {
4921
+ id: "cycle_traps",
4922
+ severity: "warning",
4923
+ async run(ctx) {
4924
+ const pages = ctx.scan.typedKnowledge;
4925
+ const pagePaths = new Set(pages.map((p) => p.relPath));
4926
+ const adj = /* @__PURE__ */ new Map();
4927
+ for (const p of pages) {
4928
+ adj.set(p.relPath, []);
4929
+ }
4930
+ const perPageTargets = await mapWithConcurrency(pages, vaultIoConcurrency(), async (p) => {
4931
+ try {
4932
+ const text = await readPageCached(p, ctx.pageTextCache);
4933
+ const split = splitFrontmatter(text);
4934
+ const body = split.ok ? split.data.body : text;
4935
+ const targets = [];
4936
+ for (const slug of extractBodyWikilinks(body)) {
4937
+ const resolution = ctx.wikilinkResolver.resolve(slug);
4938
+ if (resolution.path && pagePaths.has(resolution.path)) {
4939
+ targets.push(resolution.path);
4085
4940
  }
4086
- sensitiveFixed.push(page.relPath);
4087
- } catch {
4088
- unresolved.push(page.relPath);
4089
4941
  }
4942
+ return { relPath: p.relPath, targets };
4943
+ } catch {
4944
+ return { relPath: p.relPath, targets: [] };
4090
4945
  }
4091
- fixed.push(...sensitiveFixed);
4092
- const remainingSensitiveFlags = [];
4093
- for (const page of scan.allMarkdown) {
4094
- try {
4095
- const text = await readPage(page);
4096
- remainingSensitiveFlags.push(...scanSensitiveContent(text, { file: page.relPath }));
4097
- } catch {
4946
+ });
4947
+ for (const item of perPageTargets) {
4948
+ adj.set(item.relPath, item.targets);
4949
+ }
4950
+ let index = 0;
4951
+ const indices = /* @__PURE__ */ new Map();
4952
+ const lowlink = /* @__PURE__ */ new Map();
4953
+ const onStack = /* @__PURE__ */ new Set();
4954
+ const stack = [];
4955
+ const cyclePages = /* @__PURE__ */ new Set();
4956
+ function strongConnect(v) {
4957
+ indices.set(v, index);
4958
+ lowlink.set(v, index);
4959
+ index++;
4960
+ stack.push(v);
4961
+ onStack.add(v);
4962
+ const neighbors = adj.get(v) ?? [];
4963
+ for (const w of neighbors) {
4964
+ if (!indices.has(w)) {
4965
+ strongConnect(w);
4966
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
4967
+ } else if (onStack.has(w)) {
4968
+ lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
4098
4969
  }
4099
4970
  }
4100
- if (remainingSensitiveFlags.length > 0) buckets.sensitive_content = remainingSensitiveFlags;
4101
- else delete buckets.sensitive_content;
4102
- }
4103
- if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
4104
- const FENCE_RE = /```[\s\S]*?```/g;
4105
- const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
4106
- for (const relPath of legacyPages) {
4107
- try {
4108
- const absPath = `${input.vault}/${relPath}`;
4109
- const raw = await readFile13(absPath, "utf8");
4110
- const split = splitFrontmatter(raw);
4111
- if (!split.ok) {
4112
- unresolved.push(relPath);
4113
- continue;
4114
- }
4115
- const body = split.data.body;
4116
- const rawFm = split.data.rawFrontmatter;
4117
- const stripped = body.replace(FENCE_RE, "");
4118
- const lines = stripped.split("\n");
4119
- const inlineMarkers = [];
4120
- let inSources = false;
4121
- for (const line of lines) {
4122
- if (/^## Sources\b/.test(line.trim())) {
4123
- inSources = true;
4124
- continue;
4125
- }
4126
- if (inSources) continue;
4127
- for (const m of line.matchAll(INLINE_MARKER)) {
4128
- inlineMarkers.push(m[0]);
4129
- }
4130
- }
4131
- if (inlineMarkers.length === 0) {
4132
- unresolved.push(relPath);
4133
- continue;
4134
- }
4135
- const bodyLines = body.split("\n");
4136
- let inSrc = false;
4137
- const newBodyLines = [];
4138
- for (const line of bodyLines) {
4139
- if (/^## Sources\b/.test(line.trim())) {
4140
- inSrc = true;
4141
- newBodyLines.push(line);
4142
- continue;
4143
- }
4144
- if (inSrc) {
4145
- newBodyLines.push(line);
4146
- continue;
4147
- }
4148
- INLINE_MARKER.lastIndex = 0;
4149
- const lineWithoutMarkers = line.replace(INLINE_MARKER, "").trim();
4150
- INLINE_MARKER.lastIndex = 0;
4151
- if (lineWithoutMarkers.length === 0 && INLINE_MARKER.test(line)) {
4152
- continue;
4153
- }
4154
- let cleaned = line;
4155
- for (const marker of inlineMarkers) {
4156
- const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4157
- const trailingRe = new RegExp(`([.!?]\\s*)${escapedMarker}`);
4158
- if (trailingRe.test(cleaned)) {
4159
- cleaned = cleaned.replace(trailingRe, "$1");
4160
- }
4161
- const midRe = new RegExp(`${escapedMarker}\\s*`);
4162
- if (midRe.test(cleaned)) {
4163
- cleaned = cleaned.replace(midRe, "");
4164
- }
4165
- }
4166
- newBodyLines.push(cleaned);
4167
- }
4168
- let newBody = newBodyLines.join("\n");
4169
- const dedupedMarkers = [...new Set(inlineMarkers)];
4170
- if (inSrc) {
4171
- const existingSources = new Set(
4172
- body.split("\n").filter((l) => /^- \^\[raw\//.test(l.trim())).map((l) => l.trim().replace(/^- /, ""))
4173
- );
4174
- const newMarkers = dedupedMarkers.filter((m) => !existingSources.has(m));
4175
- const sourceLines = newMarkers.map((m) => `- ${m}`);
4176
- if (sourceLines.length > 0) {
4177
- newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
4178
- }
4179
- } else {
4180
- const sourceLines = dedupedMarkers.map((m) => `- ${m}`);
4181
- newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
4971
+ if (lowlink.get(v) === indices.get(v)) {
4972
+ const scc = [];
4973
+ let node;
4974
+ do {
4975
+ node = stack.pop();
4976
+ onStack.delete(node);
4977
+ scc.push(node);
4978
+ } while (node !== v);
4979
+ if (scc.length > 1) {
4980
+ for (const member of scc) {
4981
+ cyclePages.add(member);
4182
4982
  }
4183
- const newContent = `---
4184
- ${rawFm}
4185
- ---
4186
- ${newBody}`;
4187
- const w = await safeWritePage(absPath, newContent);
4188
- if (!w.ok) {
4189
- unresolved.push(relPath);
4190
- continue;
4983
+ } else if (scc.length === 1) {
4984
+ const single = scc[0];
4985
+ if ((adj.get(single) ?? []).includes(single)) {
4986
+ cyclePages.add(single);
4191
4987
  }
4192
- fixed.push(relPath);
4193
- } catch {
4194
- unresolved.push(relPath);
4195
4988
  }
4196
4989
  }
4197
- if (fixed.length > 0) {
4198
- const fixedSet = new Set(fixed);
4199
- const remaining = legacyPages.filter((p) => !fixedSet.has(p));
4200
- if (remaining.length > 0) buckets.legacy_citation_style = remaining;
4201
- else delete buckets.legacy_citation_style;
4990
+ }
4991
+ for (const p of pages) {
4992
+ if (!indices.has(p.relPath)) {
4993
+ strongConnect(p.relPath);
4202
4994
  }
4203
4995
  }
4204
- if (shouldFix("missing_overview") && noOverview.length > 0) {
4205
- for (const relPath of noOverview) {
4206
- try {
4207
- const absPath = `${input.vault}/${relPath}`;
4208
- const raw = await readFile13(absPath, "utf8");
4209
- const split = splitFrontmatter(raw);
4210
- if (!split.ok) {
4211
- unresolved.push(relPath);
4212
- continue;
4213
- }
4214
- const body = split.data.body;
4215
- const rawFm = split.data.rawFrontmatter;
4216
- const fm = extractFrontmatter(raw);
4217
- const title = fm.ok && typeof fm.data.title === "string" ? fm.data.title : "";
4218
- const overviewSection = `## Overview
4219
-
4220
- ${title}`;
4221
- const trimmedBody = body.replace(/^\n+/, "");
4222
- const newContent = `---
4223
- ${rawFm}
4224
- ---
4225
-
4226
- ${overviewSection}
4996
+ const buckets = {};
4997
+ if (cyclePages.size > 0) {
4998
+ buckets.cycle_traps = [...cyclePages].sort();
4999
+ }
5000
+ return { buckets };
5001
+ }
5002
+ };
5003
+ var LINT_RULES = [
5004
+ brokenWikilinksRule,
5005
+ tagNotInTaxonomyRule,
5006
+ indexIncompleteRule,
5007
+ indexLinkFormatRule,
5008
+ stalePageRule,
5009
+ pageTooLargeRule,
5010
+ logRotateNeededRule,
5011
+ orphansRule,
5012
+ sparseCommunityRule,
5013
+ topicMapRecommendedRule,
5014
+ rawDedupRule,
5015
+ rawBodyDuplicateRule,
5016
+ compoundRefsRule,
5017
+ pathTooLongRule,
5018
+ sensitiveContentRule,
5019
+ conflictMarkersRule,
5020
+ frontmatterYamlInvalidRule,
5021
+ rawSubdirectoryDuplicateRule,
5022
+ fileSourceUrlRule,
5023
+ legacyCitationStyleRule,
5024
+ orphanedCitationsRule,
5025
+ pageStructureRule,
5026
+ duplicateFrontmatterRule,
5027
+ missingOverviewRule,
5028
+ frontmatterWikilinkRule,
5029
+ wikilinkCitationRule,
5030
+ brokenSourcesRule,
5031
+ missingTldrRule,
5032
+ missingDiagramRule,
5033
+ workItemHealthRule,
5034
+ orphanedProjectPagesRule,
5035
+ cliRefsRule,
5036
+ staleSectionsRule,
5037
+ cycleTrapsRule
5038
+ ];
4227
5039
 
4228
- ${trimmedBody}`;
4229
- const w = await safeWritePage(absPath, newContent);
4230
- if (!w.ok) {
4231
- unresolved.push(relPath);
4232
- continue;
4233
- }
4234
- fixed.push(relPath);
4235
- } catch {
4236
- unresolved.push(relPath);
5040
+ // src/lint/runner.ts
5041
+ var LintRunner = class {
5042
+ rules;
5043
+ constructor(rules = LINT_RULES) {
5044
+ this.rules = rules;
5045
+ }
5046
+ getRegisteredRules() {
5047
+ return this.rules;
5048
+ }
5049
+ findRuleForBucket(bucketName) {
5050
+ return this.rules.find((r) => r.id === bucketName || r.producedBuckets?.includes(bucketName));
5051
+ }
5052
+ async run(input) {
5053
+ if (input.only && !KNOWN_BUCKETS.includes(input.only)) {
5054
+ return {
5055
+ exitCode: ExitCode.USAGE,
5056
+ result: {
5057
+ ok: false,
5058
+ error: "UNKNOWN_BUCKET",
5059
+ detail: `Unknown bucket "${input.only}". Valid: ${KNOWN_BUCKETS.join(", ")}`
4237
5060
  }
5061
+ };
5062
+ }
5063
+ if (input.only) {
5064
+ const rule = this.findRuleForBucket(input.only);
5065
+ if (rule && rule.id === input.only && rule.runFastPath) {
5066
+ return rule.runFastPath(input);
4238
5067
  }
4239
- const fixedBeforeOverview = fixed.length;
4240
- const fixedSet = new Set(fixed);
4241
- const remaining = noOverview.filter((p) => !fixedSet.has(p));
4242
- if (remaining.length > 0) buckets.missing_overview = remaining;
4243
- else delete buckets.missing_overview;
4244
5068
  }
4245
- if (shouldFix("missing_tldr") && missingTldrFlags.length > 0) {
4246
- for (const relPath of missingTldrFlags) {
5069
+ const shouldFix = (bucket) => !!input.fix && (!input.only || input.only === bucket);
5070
+ const readVault = lintReadVault(input);
5071
+ const lintVault = readVault.readPath;
5072
+ const buckets = {};
5073
+ const fixed = [];
5074
+ const unresolved = [];
5075
+ const scanResult = await scanVault(lintVault);
5076
+ if (!scanResult.ok) {
5077
+ return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
5078
+ }
5079
+ const scan = scanResult.data;
5080
+ const pageTextCache = /* @__PURE__ */ new Map();
5081
+ if (!input.fix) {
5082
+ await mapWithConcurrency(scan.allMarkdown, vaultIoConcurrency(), async (page) => {
4247
5083
  try {
4248
- const absPath = `${input.vault}/${relPath}`;
4249
- const raw = await readFile13(absPath, "utf8");
4250
- const split = splitFrontmatter(raw);
4251
- if (!split.ok) {
4252
- unresolved.push(relPath);
4253
- continue;
4254
- }
4255
- const body = split.data.body;
4256
- const rawFm = split.data.rawFrontmatter;
4257
- const lines = body.split("\n");
4258
- let insertIndex = 0;
4259
- for (let i = 0; i < lines.length; i++) {
4260
- if (/^# /.test(lines[i])) {
4261
- insertIndex = i + 1;
4262
- while (insertIndex < lines.length && lines[insertIndex].trim() === "") {
4263
- insertIndex++;
4264
- }
4265
- break;
4266
- }
4267
- }
4268
- if (insertIndex === 0) {
4269
- lines.splice(0, 0, "", "> **TL;DR:** ");
4270
- } else {
4271
- lines.splice(insertIndex, 0, "> **TL;DR:** ");
4272
- }
4273
- const trimmedFm = rawFm.endsWith("\n") ? rawFm : rawFm + "\n";
4274
- const newContent = `---
4275
- ${trimmedFm}---
4276
- ${lines.join("\n")}`;
4277
- const w = await safeWritePage(absPath, newContent);
4278
- if (!w.ok) {
4279
- unresolved.push(relPath);
4280
- continue;
4281
- }
4282
- fixed.push(relPath);
5084
+ await readPageCached(page, pageTextCache);
4283
5085
  } catch {
4284
- unresolved.push(relPath);
4285
5086
  }
4286
- }
4287
- const fixedSet = new Set(fixed);
4288
- const remaining = missingTldrFlags.filter((p) => !fixedSet.has(p));
4289
- if (remaining.length > 0) buckets.missing_tldr = remaining;
4290
- else delete buckets.missing_tldr;
4291
- }
4292
- if (shouldFix("wikilink_citation") && wikilinkCitationFlags.length > 0) {
4293
- const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
4294
- const FENCE_RE = /```[\s\S]*?```/g;
4295
- const wikilinkFixed = [];
4296
- for (const relPath of wikilinkCitationFlags) {
4297
- try {
4298
- const absPath = `${input.vault}/${relPath}`;
4299
- const raw = await readFile13(absPath, "utf8");
4300
- const split = splitFrontmatter(raw);
4301
- if (!split.ok) {
4302
- unresolved.push(relPath);
4303
- continue;
4304
- }
4305
- const body = split.data.body;
4306
- const rawFm = split.data.rawFrontmatter;
4307
- const stripped = body.replace(FENCE_RE, "");
4308
- const wikilinkMatches = [...stripped.matchAll(WIKILINK_RE)];
4309
- if (wikilinkMatches.length === 0) {
4310
- unresolved.push(relPath);
4311
- continue;
4312
- }
4313
- const wikilinkPaths = [...new Set(wikilinkMatches.map((m) => m[1]))];
4314
- const bodyLines = body.split("\n");
4315
- let inSrc = false;
4316
- const newBodyLines = [];
4317
- for (const line of bodyLines) {
4318
- if (/^## Sources\b/.test(line.trim())) {
4319
- inSrc = true;
4320
- newBodyLines.push(line);
4321
- continue;
4322
- }
4323
- if (inSrc) {
4324
- newBodyLines.push(line);
4325
- continue;
4326
- }
4327
- let cleaned = line.replace(/\[\[raw\/[^\]|]+(?:\|[^\]]*)?\]\]/g, "");
4328
- cleaned = cleaned.replace(/\s+\./g, ".").replace(/\s{2,}/g, " ").replace(/\s+$/, "");
4329
- if (cleaned.length > 0 || line.trim().length === 0) {
4330
- newBodyLines.push(cleaned);
4331
- }
4332
- }
4333
- let newBody = newBodyLines.join("\n");
4334
- const citationMarkers = wikilinkPaths.map((p) => `^[raw/${p}]`);
4335
- const sourceEntries = extractSourceEntries(rawFm);
4336
- const fmMarkers = [];
4337
- for (const entry of sourceEntries) {
4338
- let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
4339
- rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
4340
- if (rawPath.startsWith("raw/")) {
4341
- fmMarkers.push(`^[${rawPath}]`);
4342
- }
4343
- }
4344
- const allMarkers = [.../* @__PURE__ */ new Set([...citationMarkers, ...fmMarkers])];
4345
- const hasSourcesSection = /^## Sources\b/m.test(newBody);
4346
- if (hasSourcesSection) {
4347
- const existingSources = new Set(
4348
- newBody.split("\n").filter((l) => /^- \^\[raw\//.test(l.trim())).map((l) => l.trim().replace(/^- /, ""))
4349
- );
4350
- const newMarkers = allMarkers.filter((m) => !existingSources.has(m));
4351
- const sourceLines = newMarkers.map((m) => `- ${m}`);
4352
- if (sourceLines.length > 0) {
4353
- newBody = newBody.trimEnd() + "\n" + sourceLines.join("\n") + "\n";
4354
- }
5087
+ });
5088
+ }
5089
+ const wikilinkResolver = buildWikilinkResolver(scan.allMarkdown);
5090
+ const cliSurface = buildCliSurface();
5091
+ const ctx = {
5092
+ vault: lintVault,
5093
+ scan,
5094
+ pageTextCache,
5095
+ days: input.days,
5096
+ lines: input.lines,
5097
+ logThreshold: input.logThreshold,
5098
+ wikilinkResolver,
5099
+ cliSurface
5100
+ };
5101
+ for (const rule of this.rules) {
5102
+ const ruleRes = await rule.run(ctx);
5103
+ for (const [kind, items] of Object.entries(ruleRes.buckets)) {
5104
+ if (items && items.length > 0) {
5105
+ if (buckets[kind]) {
5106
+ buckets[kind] = [...buckets[kind], ...items];
4355
5107
  } else {
4356
- const sourceLines = allMarkers.map((m) => `- ${m}`);
4357
- newBody = newBody.trimEnd() + "\n\n## Sources\n\n" + sourceLines.join("\n") + "\n";
4358
- }
4359
- const newContent = `---
4360
- ${rawFm}
4361
- ---
4362
- ${newBody}`;
4363
- const w = await safeWritePage(absPath, newContent);
4364
- if (!w.ok) {
4365
- unresolved.push(relPath);
4366
- continue;
5108
+ buckets[kind] = items;
4367
5109
  }
4368
- wikilinkFixed.push(relPath);
4369
- } catch {
4370
- unresolved.push(relPath);
4371
- }
4372
- }
4373
- fixed.push(...wikilinkFixed);
4374
- if (wikilinkFixed.length > 0) {
4375
- const fixedSet = new Set(wikilinkFixed);
4376
- const remaining = wikilinkCitationFlags.filter((p) => !fixedSet.has(p));
4377
- if (remaining.length > 0) buckets.wikilink_citation = remaining;
4378
- else delete buckets.wikilink_citation;
4379
- }
4380
- }
4381
- if (shouldFix("file_source_url") && fileSourceUrlFrontmatterFlags.size > 0) {
4382
- fileSourceUrlFlags = await applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSourceUrlFrontmatterFlags, fixed, unresolved);
4383
- if (fileSourceUrlFlags.size > 0) buckets.file_source_url = [...fileSourceUrlFlags];
4384
- else delete buckets.file_source_url;
4385
- }
4386
- const pathViolations = buckets.path_too_long;
4387
- if (shouldFix("path_too_long") && pathViolations && pathViolations.length > 0) {
4388
- const pathFix = await fixPathTooLong({ vault: input.vault });
4389
- const pathFixed = pathFix.result.ok ? pathFix.result.data.fixed.map((f) => f.from) : [];
4390
- if (pathFix.result.ok) unresolved.push(...pathFix.result.data.unresolved);
4391
- else unresolved.push(...pathViolations.map((v) => v.relPath));
4392
- fixed.push(...pathFixed);
4393
- if (pathFixed.length > 0) {
4394
- const fixedSet = new Set(pathFixed);
4395
- const remaining = pathViolations.filter((v) => !fixedSet.has(v.relPath));
4396
- if (remaining.length > 0) buckets.path_too_long = remaining;
4397
- else delete buckets.path_too_long;
4398
- }
4399
- if (buckets.path_too_long) {
4400
- const rawRemaining = buckets.path_too_long.filter((v) => v.relPath.startsWith("raw/"));
4401
- if (rawRemaining.length > 0) {
4402
- unresolved.push(...rawRemaining.map((v) => v.relPath));
4403
- const nonRaw = buckets.path_too_long.filter((v) => !v.relPath.startsWith("raw/"));
4404
- if (nonRaw.length > 0) buckets.path_too_long = nonRaw;
4405
- else delete buckets.path_too_long;
4406
5110
  }
4407
5111
  }
4408
5112
  }
4409
- if (shouldFix("frontmatter_yaml_invalid") && buckets.frontmatter_yaml_invalid) {
4410
- const invalidItems = buckets.frontmatter_yaml_invalid;
4411
- const remaining = [];
4412
- for (const item of invalidItems) {
4413
- if (item.path.startsWith("raw/")) {
4414
- unresolved.push(item.path);
4415
- remaining.push(item);
4416
- continue;
4417
- }
4418
- const page = scan.allMarkdown.find((p) => p.relPath === item.path);
4419
- if (!page) {
4420
- unresolved.push(item.path);
4421
- remaining.push(item);
4422
- continue;
4423
- }
4424
- try {
4425
- const text = await readPage(page);
4426
- const split = splitFrontmatter(text);
4427
- if (!split.ok) {
4428
- unresolved.push(item.path);
4429
- remaining.push(item);
4430
- continue;
4431
- }
4432
- const newFm = fixFrontmatter(split.data.rawFrontmatter);
4433
- const newText = `---
4434
- ${newFm}
4435
- ---
4436
- ${split.data.body}`;
4437
- const recheck = extractFrontmatter(newText);
4438
- if (!recheck.ok) {
4439
- unresolved.push(item.path);
4440
- remaining.push(item);
4441
- continue;
4442
- }
4443
- const w = await safeWritePage(page.absPath, newText, { minBodyRatio: null });
4444
- if (!w.ok) {
4445
- unresolved.push(item.path);
4446
- remaining.push(item);
4447
- continue;
5113
+ if (input.fix) {
5114
+ const fixCtx = {
5115
+ vault: lintVault,
5116
+ scan,
5117
+ pageTextCache,
5118
+ input,
5119
+ fixed,
5120
+ unresolved
5121
+ };
5122
+ for (const rule of this.rules) {
5123
+ const produced = rule.producedBuckets ?? [rule.id];
5124
+ for (const bucketName of produced) {
5125
+ if (shouldFix(bucketName) && buckets[bucketName] && rule.fix) {
5126
+ const remaining = await rule.fix(fixCtx, buckets[bucketName]);
5127
+ if (remaining && remaining.length > 0) {
5128
+ buckets[bucketName] = remaining;
5129
+ } else {
5130
+ delete buckets[bucketName];
5131
+ }
4448
5132
  }
4449
- fixed.push(item.path);
4450
- } catch {
4451
- unresolved.push(item.path);
4452
- remaining.push(item);
4453
5133
  }
4454
5134
  }
4455
- if (remaining.length > 0) buckets.frontmatter_yaml_invalid = remaining;
4456
- else delete buckets.frontmatter_yaml_invalid;
4457
- }
4458
- }
4459
- const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
4460
- const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
4461
- const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
4462
- if (input.only) {
4463
- const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
4464
- return outputForOnlyBucket(input, match, fixed, unresolved, readVault);
4465
- }
4466
- const summary = {
4467
- errors: errorOut.reduce((n, b) => n + b.items.length, 0),
4468
- warnings: warningOut.reduce((n, b) => n + b.items.length, 0),
4469
- info: infoOut.reduce((n, b) => n + b.items.length, 0)
4470
- };
4471
- let exitCode = ExitCode.OK;
4472
- if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
4473
- else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
4474
- const vault = lintVaultOutput(input, readVault);
4475
- const hintLines = [];
4476
- hintLines.push(...readMirrorHintLines(vault));
4477
- if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
4478
- if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
4479
- if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
4480
- const allBuckets = [...errorOut, ...warningOut, ...infoOut];
4481
- for (const b of allBuckets) {
4482
- hintLines.push(` ${b.kind}: ${b.items.length}`);
4483
- }
4484
- if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
4485
- if (input.fix) appendLintFixLastOp(input.vault, fixed);
4486
- const output = {
4487
- vault,
4488
- summary,
4489
- by_severity: { error: errorOut, warning: warningOut, info: infoOut },
4490
- fixed,
4491
- unresolved,
4492
- humanHint: hintLines.join("\n")
4493
- };
4494
- return {
4495
- exitCode,
4496
- result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
4497
- };
4498
- }
4499
- function lintIssueFingerprint(bucket, item) {
4500
- const page = extractIssuePage(item);
4501
- const detail = normalizeIssueDetail(item);
4502
- return `${bucket}\0${page}\0${detail}`;
4503
- }
4504
- function extractIssuePage(item) {
4505
- if (typeof item === "string") {
4506
- const m = item.match(/^([^:]+?)(?::\s|$)/);
4507
- return (m?.[1] ?? item).trim();
4508
- }
4509
- if (item && typeof item === "object") {
4510
- const obj = item;
4511
- for (const key of ["path", "file", "page", "relPath"]) {
4512
- if (typeof obj[key] === "string") return obj[key];
4513
- }
4514
- }
4515
- return "";
4516
- }
4517
- function normalizeIssueDetail(item) {
4518
- if (typeof item === "string") {
4519
- return item.replace(/\s+/g, " ").trim();
4520
- }
4521
- try {
4522
- return JSON.stringify(item, Object.keys(item).sort());
4523
- } catch {
4524
- return String(item);
4525
- }
4526
- }
4527
- function collectLintErrorFingerprints(output) {
4528
- const fps = /* @__PURE__ */ new Set();
4529
- for (const bucket of output.by_severity.error) {
4530
- for (const item of bucket.items) {
4531
- fps.add(lintIssueFingerprint(bucket.kind, item));
4532
5135
  }
5136
+ const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
5137
+ const warningOut = WARNING_ORDER.flatMap(
5138
+ (k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []
5139
+ );
5140
+ const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
5141
+ if (input.only) {
5142
+ const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
5143
+ const out = outputForOnlyBucket(input, match, fixed, unresolved, readVault);
5144
+ if (input.fix) appendLintFixLastOp(input.vault, fixed);
5145
+ return out;
5146
+ }
5147
+ const summary = {
5148
+ errors: errorOut.reduce((n, b) => n + b.items.length, 0),
5149
+ warnings: warningOut.reduce((n, b) => n + b.items.length, 0),
5150
+ info: infoOut.reduce((n, b) => n + b.items.length, 0)
5151
+ };
5152
+ let exitCode = ExitCode.OK;
5153
+ if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
5154
+ else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
5155
+ const vault = lintVaultOutput(input, readVault);
5156
+ const hintLines = [];
5157
+ hintLines.push(...readMirrorHintLines(vault));
5158
+ if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
5159
+ if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
5160
+ if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
5161
+ const allBuckets = [...errorOut, ...warningOut, ...infoOut];
5162
+ for (const b of allBuckets) {
5163
+ hintLines.push(` ${b.kind}: ${b.items.length}`);
5164
+ }
5165
+ if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
5166
+ if (input.fix) appendLintFixLastOp(input.vault, fixed);
5167
+ const output = {
5168
+ vault,
5169
+ summary,
5170
+ by_severity: { error: errorOut, warning: warningOut, info: infoOut },
5171
+ fixed,
5172
+ unresolved,
5173
+ humanHint: hintLines.join("\n")
5174
+ };
5175
+ return {
5176
+ exitCode,
5177
+ result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
5178
+ };
4533
5179
  }
4534
- return fps;
5180
+ };
5181
+ var defaultLintRunner = new LintRunner();
5182
+ function runLint(input) {
5183
+ return defaultLintRunner.run(input);
4535
5184
  }
4536
5185
  async function runSyncLintDelta(input) {
4537
5186
  const { mkdtempSync, rmSync, existsSync: fsExists } = await import("fs");
@@ -4665,7 +5314,7 @@ function gitStrict(cwd, args) {
4665
5314
 
4666
5315
  // src/utils/sync-lock.ts
4667
5316
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
4668
- import { join as join13 } from "path";
5317
+ import { join as join14 } from "path";
4669
5318
  import { createHash as createHash5, randomBytes } from "crypto";
4670
5319
  function getEnvSessionId() {
4671
5320
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
@@ -4688,7 +5337,7 @@ function getCliSessionId(cwd) {
4688
5337
  return `cli-${getCwdHash(cwd)}`;
4689
5338
  }
4690
5339
  function lockPath(vault) {
4691
- return join13(vault, ".skillwiki", "sync.lock");
5340
+ return join14(vault, ".skillwiki", "sync.lock");
4692
5341
  }
4693
5342
  function readLock(vault) {
4694
5343
  const path = lockPath(vault);
@@ -4707,7 +5356,7 @@ function isStale(lock, now) {
4707
5356
  }
4708
5357
  function acquireLock(vault, opts = {}) {
4709
5358
  const path = lockPath(vault);
4710
- const dir = join13(vault, ".skillwiki");
5359
+ const dir = join14(vault, ".skillwiki");
4711
5360
  if (!existsSync5(dir)) {
4712
5361
  mkdirSync3(dir, { recursive: true });
4713
5362
  }
@@ -4792,7 +5441,7 @@ function acquireOwnedSyncLock(vault, opts) {
4792
5441
  };
4793
5442
  const path = lockPath(vault);
4794
5443
  try {
4795
- mkdirSync3(join13(vault, ".skillwiki"), { recursive: true });
5444
+ mkdirSync3(join14(vault, ".skillwiki"), { recursive: true });
4796
5445
  } catch (error) {
4797
5446
  return err("WRITE_FAILED", { path, message: String(error) });
4798
5447
  }
@@ -4844,7 +5493,7 @@ function stageVaultContentChanges(vault) {
4844
5493
 
4845
5494
  // src/utils/remote-health.ts
4846
5495
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
4847
- import { join as join14 } from "path";
5496
+ import { join as join15 } from "path";
4848
5497
  import { execFileSync as execFileSync2 } from "child_process";
4849
5498
  var REMOTE_PROBE_TIMEOUT_MS = 3e3;
4850
5499
  var defaultExec = (file, args, cwd) => execFileSync2(file, args, {
@@ -4855,7 +5504,7 @@ var defaultExec = (file, args, cwd) => execFileSync2(file, args, {
4855
5504
  }).trim();
4856
5505
  function readWikiS3RemoteConfigured(home) {
4857
5506
  try {
4858
- const content = readFileSync4(join14(home, ".skillwiki", ".env"), "utf8");
5507
+ const content = readFileSync4(join15(home, ".skillwiki", ".env"), "utf8");
4859
5508
  for (const line of content.split(/\r?\n/)) {
4860
5509
  const trimmed = line.trim();
4861
5510
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -4880,7 +5529,7 @@ function resolveWikiS3Remote(input) {
4880
5529
  return readWikiS3RemoteConfigured(input.home);
4881
5530
  }
4882
5531
  function probeGithubReachability(vaultPath, exec = defaultExec) {
4883
- if (!existsSync6(join14(vaultPath, ".git"))) return "unknown";
5532
+ if (!existsSync6(join15(vaultPath, ".git"))) return "unknown";
4884
5533
  try {
4885
5534
  exec("git", ["remote", "get-url", "origin"], vaultPath);
4886
5535
  } catch {
@@ -4948,7 +5597,7 @@ function probeRemoteHealth(input) {
4948
5597
  import { spawnSync } from "child_process";
4949
5598
  import { existsSync as existsSync7 } from "fs";
4950
5599
  import { homedir, platform } from "os";
4951
- import { dirname as dirname6, join as join15 } from "path";
5600
+ import { dirname as dirname6, join as join16 } from "path";
4952
5601
  import { fileURLToPath } from "url";
4953
5602
  var HELPER_NAME = "wiki-pull-with-auto-resolve.sh";
4954
5603
  function candidateHelperPaths(input = { vault: "" }) {
@@ -4965,10 +5614,10 @@ function candidateHelperPaths(input = { vault: "" }) {
4965
5614
  }
4966
5615
  }
4967
5616
  if (here) {
4968
- paths.push(join15(here, "vault-sync", "scripts", HELPER_NAME));
4969
- paths.push(join15(here, "..", "vault-sync", "scripts", HELPER_NAME));
4970
- paths.push(join15(here, "..", "..", "vault-sync", "scripts", HELPER_NAME));
4971
- paths.push(join15(here, "..", "..", "..", "vault-sync", "scripts", HELPER_NAME));
5617
+ paths.push(join16(here, "vault-sync", "scripts", HELPER_NAME));
5618
+ paths.push(join16(here, "..", "vault-sync", "scripts", HELPER_NAME));
5619
+ paths.push(join16(here, "..", "..", "vault-sync", "scripts", HELPER_NAME));
5620
+ paths.push(join16(here, "..", "..", "..", "vault-sync", "scripts", HELPER_NAME));
4972
5621
  }
4973
5622
  const home = input.home ?? env.HOME ?? env.USERPROFILE ?? (() => {
4974
5623
  try {
@@ -4981,11 +5630,11 @@ function candidateHelperPaths(input = { vault: "" }) {
4981
5630
  const xdg = env.XDG_DATA_HOME;
4982
5631
  const isDarwin = platform() === "darwin";
4983
5632
  if (isDarwin) {
4984
- paths.push(join15(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
5633
+ paths.push(join16(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
4985
5634
  }
4986
- paths.push(join15(xdg || join15(home, ".local", "share"), "vault-sync", "bin", HELPER_NAME));
5635
+ paths.push(join16(xdg || join16(home, ".local", "share"), "vault-sync", "bin", HELPER_NAME));
4987
5636
  if (!isDarwin) {
4988
- paths.push(join15(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
5637
+ paths.push(join16(home, "Library", "Application Support", "vault-sync", "bin", HELPER_NAME));
4989
5638
  }
4990
5639
  }
4991
5640
  return paths;
@@ -5079,7 +5728,7 @@ function refHasPath(vault, ref, path) {
5079
5728
  function runSyncStatus(input) {
5080
5729
  const vault = input.vault;
5081
5730
  const includeStashes = input.includeStashes ?? false;
5082
- if (!existsSync8(join16(vault, ".git"))) {
5731
+ if (!existsSync8(join17(vault, ".git"))) {
5083
5732
  return {
5084
5733
  exitCode: ExitCode.VAULT_PATH_INVALID,
5085
5734
  result: ok({
@@ -5186,7 +5835,7 @@ function runSyncStatus(input) {
5186
5835
  }
5187
5836
  async function runSyncPush(input) {
5188
5837
  const vault = input.vault;
5189
- if (!existsSync8(join16(vault, ".git"))) {
5838
+ if (!existsSync8(join17(vault, ".git"))) {
5190
5839
  return {
5191
5840
  exitCode: ExitCode.VAULT_PATH_INVALID,
5192
5841
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5346,7 +5995,7 @@ function enableGitLongPathsOnWindows(vault) {
5346
5995
  }
5347
5996
  async function runSyncPull(input) {
5348
5997
  const vault = input.vault;
5349
- if (!existsSync8(join16(vault, ".git"))) {
5998
+ if (!existsSync8(join17(vault, ".git"))) {
5350
5999
  return {
5351
6000
  exitCode: ExitCode.VAULT_PATH_INVALID,
5352
6001
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5643,11 +6292,11 @@ import {
5643
6292
  writeFileSync as writeFileSync4
5644
6293
  } from "fs";
5645
6294
  import { hostname } from "os";
5646
- import { dirname as dirname7, join as join17, resolve as resolve4 } from "path";
6295
+ import { dirname as dirname7, join as join18, resolve as resolve4 } from "path";
5647
6296
  function managedWriteLockPath(vault) {
5648
6297
  const gitPath = git(vault, ["rev-parse", "--git-path", "vault-sync/managed-write.lock"]);
5649
- if (gitPath) return gitPath.startsWith("/") ? gitPath : join17(vault, gitPath);
5650
- return join17(vault, ".skillwiki", "managed-write.lock");
6298
+ if (gitPath) return gitPath.startsWith("/") ? gitPath : join18(vault, gitPath);
6299
+ return join18(vault, ".skillwiki", "managed-write.lock");
5651
6300
  }
5652
6301
  function readLockRecord(path) {
5653
6302
  try {
@@ -5670,12 +6319,12 @@ function hasUnsafeGitState(vault) {
5670
6319
  if (!isGitBackedVault(vault)) return false;
5671
6320
  const gitDirRaw = git(vault, ["rev-parse", "--git-dir"]);
5672
6321
  if (!gitDirRaw) return true;
5673
- const gitDir = gitDirRaw.startsWith("/") ? gitDirRaw : join17(vault, gitDirRaw);
6322
+ const gitDir = gitDirRaw.startsWith("/") ? gitDirRaw : join18(vault, gitDirRaw);
5674
6323
  for (const rel of ["rebase-merge", "rebase-apply"]) {
5675
- if (existsSync9(join17(gitDir, rel))) return true;
6324
+ if (existsSync9(join18(gitDir, rel))) return true;
5676
6325
  }
5677
6326
  for (const rel of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
5678
- if (existsSync9(join17(gitDir, rel))) return true;
6327
+ if (existsSync9(join18(gitDir, rel))) return true;
5679
6328
  }
5680
6329
  const unmerged = git(vault, ["ls-files", "-u"]);
5681
6330
  return Boolean(unmerged && unmerged.trim().length > 0);
@@ -5713,10 +6362,10 @@ function reclaimDeadManagedWriteLockOwner(vault, options = {}) {
5713
6362
  });
5714
6363
  }
5715
6364
  try {
5716
- const recoveryDir = join17(dirname7(path), "recovery");
6365
+ const recoveryDir = join18(dirname7(path), "recovery");
5717
6366
  mkdirSync4(recoveryDir, { recursive: true });
5718
6367
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
5719
- const recoveryPath = join17(recoveryDir, `stale-managed-write-lock-${stamp}-${process.pid}.json`);
6368
+ const recoveryPath = join18(recoveryDir, `stale-managed-write-lock-${stamp}-${process.pid}.json`);
5720
6369
  const meta = {
5721
6370
  recovered_at: (/* @__PURE__ */ new Date()).toISOString(),
5722
6371
  recovery_reason: "owner_pid_dead",
@@ -5795,11 +6444,11 @@ import {
5795
6444
  renameSync as renameSync2,
5796
6445
  writeFileSync as writeFileSync5
5797
6446
  } from "fs";
5798
- import { join as join18 } from "path";
6447
+ import { join as join19 } from "path";
5799
6448
  function journalDir(vault) {
5800
6449
  const gitPath = git(vault, ["rev-parse", "--git-path", "vault-sync/operations"]);
5801
6450
  if (!gitPath) return null;
5802
- return gitPath.startsWith("/") ? gitPath : join18(vault, gitPath);
6451
+ return gitPath.startsWith("/") ? gitPath : join19(vault, gitPath);
5803
6452
  }
5804
6453
  function parseJournalEnv(text) {
5805
6454
  return Object.fromEntries(
@@ -5823,7 +6472,7 @@ function serializeJournalEnv(fields, preferredOrder = []) {
5823
6472
  function readJournal(vault, opId) {
5824
6473
  const dir = journalDir(vault);
5825
6474
  if (!dir) return null;
5826
- const path = join18(dir, `${opId}.env`);
6475
+ const path = join19(dir, `${opId}.env`);
5827
6476
  if (!existsSync10(path)) return null;
5828
6477
  try {
5829
6478
  return parseJournalEnv(readFileSync6(path, "utf8"));
@@ -5836,7 +6485,7 @@ function writeJournal(vault, opId, fields) {
5836
6485
  if (!dir) return false;
5837
6486
  try {
5838
6487
  mkdirSync5(dir, { recursive: true });
5839
- const path = join18(dir, `${opId}.env`);
6488
+ const path = join19(dir, `${opId}.env`);
5840
6489
  const tmp = `${path}.tmp.${process.pid}`;
5841
6490
  const order = [
5842
6491
  "operation_id",
@@ -5902,9 +6551,9 @@ function hasActiveGitSequencer(vault) {
5902
6551
  const gitDir = git(vault, ["rev-parse", "--absolute-git-dir"]);
5903
6552
  if (!gitDir) return false;
5904
6553
  for (const m of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
5905
- if (existsSync10(join18(gitDir, m))) return true;
6554
+ if (existsSync10(join19(gitDir, m))) return true;
5906
6555
  }
5907
- if (existsSync10(join18(gitDir, "rebase-merge")) || existsSync10(join18(gitDir, "rebase-apply"))) {
6556
+ if (existsSync10(join19(gitDir, "rebase-merge")) || existsSync10(join19(gitDir, "rebase-apply"))) {
5908
6557
  return true;
5909
6558
  }
5910
6559
  return false;
@@ -5975,7 +6624,7 @@ function supersedeStaleReviewRequiredJournals(vault, opts = {}) {
5975
6624
 
5976
6625
  // src/utils/snapshot-worktree.ts
5977
6626
  import { readFileSync as readFileSync7 } from "fs";
5978
- import { join as join19, resolve as resolve5 } from "path";
6627
+ import { join as join20, resolve as resolve5 } from "path";
5979
6628
  function readSkillWikiConfig(path) {
5980
6629
  try {
5981
6630
  return parseDotenvText(readFileSync7(path, "utf8"));
@@ -6002,7 +6651,7 @@ function readSnapshotProfile(path) {
6002
6651
  }
6003
6652
  function resolveConfiguredSnapshotWorktree(home) {
6004
6653
  if (!home) return void 0;
6005
- const config = readSkillWikiConfig(join19(home, ".skillwiki", ".env"));
6654
+ const config = readSkillWikiConfig(join20(home, ".skillwiki", ".env"));
6006
6655
  const explicit = config["vault_sync.snapshot_worktree"];
6007
6656
  if (explicit) return resolve5(explicit);
6008
6657
  const snapshotProfile = config["vault_sync.snapshot_profile"];
@@ -6154,7 +6803,7 @@ function preflightBlocker(vault) {
6154
6803
  return null;
6155
6804
  }
6156
6805
  function hasFleetManifest(vault) {
6157
- return existsSync11(join20(vault, FLEET_REL_PATH));
6806
+ return existsSync11(join21(vault, FLEET_REL_PATH));
6158
6807
  }
6159
6808
  function isGitVault(vault) {
6160
6809
  return Boolean(git(vault, ["rev-parse", "--absolute-git-dir"]));
@@ -6421,35 +7070,21 @@ export {
6421
7070
  getCliSessionId,
6422
7071
  acquireOwnedSyncLock,
6423
7072
  releaseOwnedSyncLock,
7073
+ extractBodyWikilinks,
7074
+ buildWikilinkResolver,
6424
7075
  buildWikilinkAdjacency,
6425
7076
  toUndirectedWeighted,
6426
7077
  louvain,
6427
7078
  communityCohesion,
6428
- CONFIG_KEYS,
6429
- isValidWikiProfileKey,
6430
- profileKey,
6431
- parseDotenvText,
6432
- parseDotenvFile,
6433
- writeDotenv,
6434
- resolveInitTimePath,
6435
- resolveRuntimePath,
6436
- runOrphans,
6437
- runAudit,
7079
+ extractIssuePage,
7080
+ runLinks,
6438
7081
  extractTaxonomy,
6439
7082
  taxonomyCommentForPage,
6440
7083
  reconcileTaxonomyDocument,
6441
7084
  mergeTaxonomyConflict,
6442
- runLinks,
6443
7085
  runTagAudit,
6444
7086
  runIndexCheck,
6445
- FLEET_REL_PATH,
6446
- runFleetValidate,
6447
- runFleetContext,
6448
- loadFleetManifestAndHost,
6449
- snapshotterAliasForLocalHost,
6450
- satelliteGateFromFleetLoad,
6451
- loadFleetManifest,
6452
- resolveFleetHostId,
7087
+ runIndexLinkFormat,
6453
7088
  planRawStructuralMove,
6454
7089
  applyRawStructuralMove,
6455
7090
  REDACTED_MALFORMED_REFERENCE,
@@ -6460,8 +7095,16 @@ export {
6460
7095
  runStale,
6461
7096
  runPagesize,
6462
7097
  runLogRotate,
7098
+ CONFIG_KEYS,
7099
+ isValidWikiProfileKey,
7100
+ profileKey,
7101
+ parseDotenvText,
7102
+ parseDotenvFile,
7103
+ writeDotenv,
7104
+ resolveInitTimePath,
7105
+ resolveRuntimePath,
7106
+ runOrphans,
6463
7107
  runTopicMapCheck,
6464
- runIndexLinkFormat,
6465
7108
  normalizeRemoteRoot,
6466
7109
  buildRemoteObjectPath,
6467
7110
  isValidRemoteDeleteCap,
@@ -6470,10 +7113,20 @@ export {
6470
7113
  rewriteRawSourceReferences,
6471
7114
  snapshotMaintainedPageState,
6472
7115
  runDedup,
7116
+ runAudit,
6473
7117
  runFrontmatterFix,
6474
7118
  assessSourceIdentity,
7119
+ defaultLintRunner,
6475
7120
  runLint,
6476
7121
  runSyncLintDelta,
7122
+ FLEET_REL_PATH,
7123
+ runFleetValidate,
7124
+ runFleetContext,
7125
+ loadFleetManifestAndHost,
7126
+ snapshotterAliasForLocalHost,
7127
+ satelliteGateFromFleetLoad,
7128
+ loadFleetManifest,
7129
+ resolveFleetHostId,
6477
7130
  git,
6478
7131
  gitStrict,
6479
7132
  VAULT_COMMIT_PATHSPEC,