skillwiki 0.2.0-beta.6 → 0.2.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +421 -118
- package/package.json +5 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/package.json +9 -2
- package/skills/proj-distill/SKILL.md +26 -4
- package/skills/proj-work/SKILL.md +19 -0
- package/skills/using-skillwiki/SKILL.md +9 -1
- package/skills/wiki-adapter-prd/SKILL.md +87 -0
- package/skills/wiki-archive/SKILL.md +42 -0
- package/skills/wiki-ingest/SKILL.md +12 -1
- package/skills/wiki-reingest/SKILL.md +54 -0
- package/templates/SCHEMA.md +16 -0
package/dist/cli.js
CHANGED
|
@@ -63,7 +63,10 @@ var ExitCode = {
|
|
|
63
63
|
INVALID_CONFIG_KEY: 26,
|
|
64
64
|
CONFIG_WRITE_FAILED: 27,
|
|
65
65
|
DOCTOR_HAS_WARNINGS: 28,
|
|
66
|
-
DOCTOR_HAS_ERRORS: 29
|
|
66
|
+
DOCTOR_HAS_ERRORS: 29,
|
|
67
|
+
ARCHIVE_TARGET_NOT_FOUND: 30,
|
|
68
|
+
ARCHIVE_ALREADY_ARCHIVED: 31,
|
|
69
|
+
DRIFT_DETECTED: 32
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
// ../shared/src/json-output.ts
|
|
@@ -241,7 +244,7 @@ async function runHash(input) {
|
|
|
241
244
|
const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
|
|
242
245
|
return {
|
|
243
246
|
exitCode: ExitCode.OK,
|
|
244
|
-
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
|
|
247
|
+
result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
|
|
245
248
|
};
|
|
246
249
|
}
|
|
247
250
|
|
|
@@ -271,7 +274,7 @@ function runFetchGuardSync(input) {
|
|
|
271
274
|
result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
|
|
272
275
|
};
|
|
273
276
|
}
|
|
274
|
-
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
|
|
277
|
+
return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
|
|
275
278
|
}
|
|
276
279
|
function sanitizeUrl(u) {
|
|
277
280
|
const clone = new URL(u.toString());
|
|
@@ -311,17 +314,18 @@ async function runValidate(input) {
|
|
|
311
314
|
}
|
|
312
315
|
const det = detectSchema(fm.data);
|
|
313
316
|
if (!det.schema) {
|
|
314
|
-
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
|
|
317
|
+
return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
|
|
315
318
|
}
|
|
316
319
|
const parsed = SCHEMAS[det.schema].safeParse(fm.data);
|
|
317
320
|
if (!parsed.success) {
|
|
318
321
|
const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
|
|
319
322
|
return {
|
|
320
323
|
exitCode: ExitCode.INVALID_FRONTMATTER,
|
|
321
|
-
result: ok({ schema: det.schema, valid: false, errors })
|
|
324
|
+
result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
|
|
325
|
+
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
|
|
322
326
|
};
|
|
323
327
|
}
|
|
324
|
-
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
|
|
328
|
+
return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
|
|
325
329
|
}
|
|
326
330
|
|
|
327
331
|
// src/commands/graph.ts
|
|
@@ -407,7 +411,8 @@ async function runGraphBuild(input) {
|
|
|
407
411
|
}
|
|
408
412
|
return {
|
|
409
413
|
exitCode: ExitCode.OK,
|
|
410
|
-
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count }
|
|
414
|
+
result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
|
|
415
|
+
written: ${input.out}` })
|
|
411
416
|
};
|
|
412
417
|
}
|
|
413
418
|
function computeAdamicAdar(adj) {
|
|
@@ -482,7 +487,8 @@ async function runOverlap(input) {
|
|
|
482
487
|
}
|
|
483
488
|
return { id, members, score };
|
|
484
489
|
});
|
|
485
|
-
|
|
490
|
+
const humanHint = clusters.length === 0 ? "no overlap clusters found" : clusters.map((c) => `cluster (${c.members.length} pages, score ${c.score}): ${c.members.join(", ")}`).join("\n");
|
|
491
|
+
return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
|
|
486
492
|
}
|
|
487
493
|
|
|
488
494
|
// src/utils/wiki-path.ts
|
|
@@ -668,7 +674,11 @@ async function runOrphans(input) {
|
|
|
668
674
|
}
|
|
669
675
|
}
|
|
670
676
|
}
|
|
671
|
-
|
|
677
|
+
const hintLines = [];
|
|
678
|
+
if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
|
|
679
|
+
if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
|
|
680
|
+
if (hintLines.length === 0) hintLines.push("no orphans or bridges");
|
|
681
|
+
return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
|
|
672
682
|
}
|
|
673
683
|
function simulateRemoval(adj, removed) {
|
|
674
684
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -731,13 +741,20 @@ async function runAudit(input) {
|
|
|
731
741
|
const referenced = new Set(resolved.map((m) => m.target));
|
|
732
742
|
const unused_sources = sources.filter((s) => !referenced.has(s));
|
|
733
743
|
const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
|
|
744
|
+
const broken = resolved.filter((m) => !m.resolved);
|
|
745
|
+
const hintLines = [];
|
|
746
|
+
hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
|
|
747
|
+
if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
|
|
748
|
+
if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
|
|
749
|
+
if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
|
|
750
|
+
const humanHint = hintLines.join("\n");
|
|
734
751
|
if (resolved.some((m) => !m.resolved)) {
|
|
735
|
-
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
752
|
+
return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
736
753
|
}
|
|
737
754
|
if (unused_sources.length > 0 || missing_from_sources.length > 0) {
|
|
738
|
-
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
755
|
+
return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
739
756
|
}
|
|
740
|
-
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
|
|
757
|
+
return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
|
|
741
758
|
}
|
|
742
759
|
async function findVaultRoot(start) {
|
|
743
760
|
let cur = start;
|
|
@@ -817,7 +834,12 @@ async function runInstall(input) {
|
|
|
817
834
|
}
|
|
818
835
|
const manifest_path = join4(input.target, "wiki-manifest.json");
|
|
819
836
|
if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
|
|
820
|
-
|
|
837
|
+
const hintLines = [
|
|
838
|
+
`installed: ${installed.length}`,
|
|
839
|
+
input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
|
|
840
|
+
`manifest: ${manifest_path}`
|
|
841
|
+
];
|
|
842
|
+
return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
|
|
821
843
|
}
|
|
822
844
|
|
|
823
845
|
// src/commands/path.ts
|
|
@@ -829,7 +851,7 @@ async function runPath(input) {
|
|
|
829
851
|
home: input.home,
|
|
830
852
|
explain: input.explain
|
|
831
853
|
});
|
|
832
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
|
|
854
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
|
|
833
855
|
}
|
|
834
856
|
const r = await resolveRuntimePath({
|
|
835
857
|
flag: input.flag,
|
|
@@ -838,7 +860,7 @@ async function runPath(input) {
|
|
|
838
860
|
explain: input.explain
|
|
839
861
|
});
|
|
840
862
|
if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
|
|
841
|
-
return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {} }) };
|
|
863
|
+
return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {}, humanHint: `${r.data.path} (via ${r.data.source})` }) };
|
|
842
864
|
}
|
|
843
865
|
|
|
844
866
|
// src/utils/lang.ts
|
|
@@ -892,14 +914,42 @@ async function runLang(input) {
|
|
|
892
914
|
value: resolved.value,
|
|
893
915
|
source: resolved.source,
|
|
894
916
|
canonical: resolved.canonical,
|
|
895
|
-
...chain ? { chain } : {}
|
|
917
|
+
...chain ? { chain } : {},
|
|
918
|
+
humanHint: `${resolved.value} (via ${resolved.source})`
|
|
896
919
|
})
|
|
897
920
|
};
|
|
898
921
|
}
|
|
899
922
|
|
|
900
923
|
// src/commands/init.ts
|
|
901
|
-
import { mkdir as mkdir4, readFile as readFile6,
|
|
902
|
-
import { join as join7
|
|
924
|
+
import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
|
|
925
|
+
import { join as join7 } from "path";
|
|
926
|
+
|
|
927
|
+
// src/parsers/taxonomy.ts
|
|
928
|
+
import yaml2 from "js-yaml";
|
|
929
|
+
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
930
|
+
function extractTaxonomy(schemaText) {
|
|
931
|
+
const m = schemaText.match(FENCE_RE);
|
|
932
|
+
if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
|
|
933
|
+
let parsed;
|
|
934
|
+
try {
|
|
935
|
+
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
936
|
+
} catch (e) {
|
|
937
|
+
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
938
|
+
}
|
|
939
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
940
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
941
|
+
}
|
|
942
|
+
const tax = parsed.taxonomy;
|
|
943
|
+
if (!Array.isArray(tax)) {
|
|
944
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
945
|
+
}
|
|
946
|
+
if (!tax.every((x) => typeof x === "string")) {
|
|
947
|
+
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
948
|
+
}
|
|
949
|
+
return ok(tax);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/commands/init.ts
|
|
903
953
|
var DEFAULT_TAXONOMY = [
|
|
904
954
|
"research",
|
|
905
955
|
"comparison",
|
|
@@ -924,25 +974,59 @@ var VAULT_DIRS = [
|
|
|
924
974
|
"meta",
|
|
925
975
|
"projects"
|
|
926
976
|
];
|
|
977
|
+
function extractDomainFromSchema(text) {
|
|
978
|
+
const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
|
|
979
|
+
if (!m) return "";
|
|
980
|
+
const d = m[1].trim();
|
|
981
|
+
return d.startsWith("##") ? "" : d;
|
|
982
|
+
}
|
|
983
|
+
async function discoverTagsFromPages(target, knownSlugs) {
|
|
984
|
+
const knownSet = new Set(knownSlugs);
|
|
985
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
986
|
+
for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
|
|
987
|
+
let entries;
|
|
988
|
+
try {
|
|
989
|
+
entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
|
|
990
|
+
} catch {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
for (const file of entries) {
|
|
994
|
+
try {
|
|
995
|
+
const text = await readFile6(join7(target, dir, file), "utf8");
|
|
996
|
+
const fm = extractFrontmatter(text);
|
|
997
|
+
if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
|
|
998
|
+
for (const t of fm.data.tags) {
|
|
999
|
+
if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return [...discovered].sort();
|
|
1006
|
+
}
|
|
927
1007
|
async function runInit(input) {
|
|
928
1008
|
const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
|
|
929
1009
|
const target = pathRes.path;
|
|
930
1010
|
const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
|
|
931
1011
|
const canonicalLang = langRes.canonical;
|
|
932
|
-
let
|
|
1012
|
+
let oldSchemaText;
|
|
933
1013
|
try {
|
|
934
|
-
await
|
|
935
|
-
hasSchema = true;
|
|
1014
|
+
oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
|
|
936
1015
|
} catch {
|
|
937
1016
|
}
|
|
938
|
-
if (
|
|
1017
|
+
if (oldSchemaText && !input.force) {
|
|
939
1018
|
return {
|
|
940
1019
|
exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
|
|
941
1020
|
result: err("INIT_TARGET_NOT_EMPTY", { target })
|
|
942
1021
|
};
|
|
943
1022
|
}
|
|
944
1023
|
const envPath = join7(input.home, ".skillwiki", ".env");
|
|
945
|
-
|
|
1024
|
+
let existingEnvRaw = "";
|
|
1025
|
+
try {
|
|
1026
|
+
existingEnvRaw = await readFile6(envPath, "utf8");
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
const existingEnv = parseDotenvText(existingEnvRaw);
|
|
946
1030
|
const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
|
|
947
1031
|
if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
|
|
948
1032
|
return {
|
|
@@ -967,64 +1051,116 @@ async function runInit(input) {
|
|
|
967
1051
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
968
1052
|
}
|
|
969
1053
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
970
|
-
|
|
971
|
-
|
|
1054
|
+
let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
|
|
1055
|
+
let domain = input.domain;
|
|
1056
|
+
let oldTaxonomy = [];
|
|
1057
|
+
if (oldSchemaText) {
|
|
1058
|
+
if (!domain) {
|
|
1059
|
+
const oldDomain = extractDomainFromSchema(oldSchemaText);
|
|
1060
|
+
if (oldDomain) domain = oldDomain;
|
|
1061
|
+
}
|
|
1062
|
+
const oldTax = extractTaxonomy(oldSchemaText);
|
|
1063
|
+
if (oldTax.ok) oldTaxonomy = oldTax.data;
|
|
1064
|
+
}
|
|
1065
|
+
const taxonomySet = new Set(taxonomy);
|
|
1066
|
+
for (const t of oldTaxonomy) {
|
|
1067
|
+
if (!taxonomySet.has(t)) {
|
|
1068
|
+
taxonomy.push(t);
|
|
1069
|
+
taxonomySet.add(t);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const discovered = await discoverTagsFromPages(target, taxonomy);
|
|
1073
|
+
const discovered_tags = discovered.length;
|
|
1074
|
+
const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
|
|
972
1075
|
try {
|
|
973
1076
|
const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
|
|
974
|
-
const schema = schemaTpl.replace("{{DOMAIN}}",
|
|
1077
|
+
const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
|
|
975
1078
|
await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
|
|
976
1079
|
created.push("SCHEMA.md");
|
|
977
1080
|
} catch (e) {
|
|
978
1081
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
|
|
979
1082
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1083
|
+
const preserved = [];
|
|
1084
|
+
async function writeOrPreserve(fileName, render) {
|
|
1085
|
+
try {
|
|
1086
|
+
const existing = await readFile6(join7(target, fileName), "utf8");
|
|
1087
|
+
if (existing.split("\n").length > 10) {
|
|
1088
|
+
preserved.push(fileName);
|
|
1089
|
+
return void 0;
|
|
1090
|
+
}
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
await writeFile4(join7(target, fileName), await render(), "utf8");
|
|
1095
|
+
created.push(fileName);
|
|
1096
|
+
return void 0;
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
|
|
1099
|
+
}
|
|
995
1100
|
}
|
|
996
|
-
|
|
997
|
-
await
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
return {
|
|
1101
|
+
const err1 = await writeOrPreserve("index.md", async () => {
|
|
1102
|
+
const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
|
|
1103
|
+
return tpl.replace("{{INIT_DATE}}", today);
|
|
1104
|
+
});
|
|
1105
|
+
if (err1) return err1;
|
|
1106
|
+
const err2 = await writeOrPreserve("log.md", async () => {
|
|
1107
|
+
const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
|
|
1108
|
+
return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
|
|
1109
|
+
});
|
|
1110
|
+
if (err2) return err2;
|
|
1111
|
+
const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
|
|
1112
|
+
const skipEnv = !!input.noEnv || isTempPath;
|
|
1113
|
+
let envWritten = "";
|
|
1114
|
+
if (!skipEnv) {
|
|
1115
|
+
try {
|
|
1116
|
+
await writeDotenv(envPath, { WIKI_PATH: target, WIKI_LANG: canonicalLang }, existingEnvRaw);
|
|
1117
|
+
envWritten = envPath;
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
|
|
1120
|
+
}
|
|
1004
1121
|
}
|
|
1005
1122
|
const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
|
|
1123
|
+
const humanHint = [
|
|
1124
|
+
`vault: ${target}`,
|
|
1125
|
+
`domain: ${domain}`,
|
|
1126
|
+
`lang: ${canonicalLang}`,
|
|
1127
|
+
`created: ${created.length}, preserved: ${preserved.length}`,
|
|
1128
|
+
`discovered tags: ${discovered_tags}`,
|
|
1129
|
+
skipEnv ? "env: skipped" : `env: ${envWritten}`
|
|
1130
|
+
].join("\n");
|
|
1006
1131
|
return {
|
|
1007
1132
|
exitCode: ExitCode.OK,
|
|
1008
1133
|
result: ok({
|
|
1009
1134
|
vault: target,
|
|
1010
|
-
domain
|
|
1135
|
+
domain,
|
|
1011
1136
|
taxonomy,
|
|
1012
1137
|
lang: canonicalLang,
|
|
1013
1138
|
created,
|
|
1014
|
-
|
|
1015
|
-
|
|
1139
|
+
preserved,
|
|
1140
|
+
env_written: envWritten,
|
|
1141
|
+
env_skipped: skipEnv,
|
|
1142
|
+
imported_from_hermes: importedFromHermes,
|
|
1143
|
+
discovered_tags,
|
|
1144
|
+
humanHint
|
|
1016
1145
|
})
|
|
1017
1146
|
};
|
|
1018
1147
|
}
|
|
1019
1148
|
|
|
1149
|
+
// src/utils/slug.ts
|
|
1150
|
+
function buildSlugMap(pages) {
|
|
1151
|
+
const map = /* @__PURE__ */ new Map();
|
|
1152
|
+
for (const p of pages) {
|
|
1153
|
+
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
1154
|
+
map.set(slug.toLowerCase(), slug);
|
|
1155
|
+
}
|
|
1156
|
+
return map;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1020
1159
|
// src/commands/links.ts
|
|
1021
1160
|
async function runLinks(input) {
|
|
1022
1161
|
const scan = await scanVault(input.vault);
|
|
1023
1162
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1024
|
-
const slugs =
|
|
1025
|
-
for (const p of scan.data.typedKnowledge) {
|
|
1026
|
-
slugs.add(p.relPath.replace(/\.md$/, "").split("/").pop());
|
|
1027
|
-
}
|
|
1163
|
+
const slugs = buildSlugMap(scan.data.typedKnowledge);
|
|
1028
1164
|
const broken = [];
|
|
1029
1165
|
for (const p of scan.data.typedKnowledge) {
|
|
1030
1166
|
const text = await readPage(p);
|
|
@@ -1033,48 +1169,22 @@ async function runLinks(input) {
|
|
|
1033
1169
|
const lines = body.split("\n");
|
|
1034
1170
|
for (const slug of extractBodyWikilinks(body)) {
|
|
1035
1171
|
const tail = slug.split("/").pop();
|
|
1036
|
-
if (!slugs.has(tail)) {
|
|
1172
|
+
if (!slugs.has(tail.toLowerCase())) {
|
|
1037
1173
|
const line = lines.findIndex((l) => l.includes(`[[${slug}`));
|
|
1038
1174
|
broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
|
|
1039
1175
|
}
|
|
1040
1176
|
}
|
|
1041
1177
|
}
|
|
1042
1178
|
if (broken.length > 0) {
|
|
1043
|
-
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken
|
|
1179
|
+
return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
|
|
1180
|
+
${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
|
|
1044
1181
|
}
|
|
1045
|
-
return { exitCode: ExitCode.OK, result: ok({ broken }) };
|
|
1182
|
+
return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
|
|
1046
1183
|
}
|
|
1047
1184
|
|
|
1048
1185
|
// src/commands/tag-audit.ts
|
|
1049
1186
|
import { readFile as readFile7 } from "fs/promises";
|
|
1050
1187
|
import { join as join8 } from "path";
|
|
1051
|
-
|
|
1052
|
-
// src/parsers/taxonomy.ts
|
|
1053
|
-
import yaml2 from "js-yaml";
|
|
1054
|
-
var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
|
|
1055
|
-
function extractTaxonomy(schemaText) {
|
|
1056
|
-
const m = schemaText.match(FENCE_RE);
|
|
1057
|
-
if (!m) return ok([]);
|
|
1058
|
-
let parsed;
|
|
1059
|
-
try {
|
|
1060
|
-
parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
|
|
1061
|
-
} catch (e) {
|
|
1062
|
-
return err("INVALID_FRONTMATTER", { message: e.message });
|
|
1063
|
-
}
|
|
1064
|
-
if (parsed === null || typeof parsed !== "object") {
|
|
1065
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
|
|
1066
|
-
}
|
|
1067
|
-
const tax = parsed.taxonomy;
|
|
1068
|
-
if (!Array.isArray(tax)) {
|
|
1069
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
|
|
1070
|
-
}
|
|
1071
|
-
if (!tax.every((x) => typeof x === "string")) {
|
|
1072
|
-
return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
|
|
1073
|
-
}
|
|
1074
|
-
return ok(tax);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// src/commands/tag-audit.ts
|
|
1078
1188
|
async function runTagAudit(input) {
|
|
1079
1189
|
const scan = await scanVault(input.vault);
|
|
1080
1190
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -1096,9 +1206,9 @@ async function runTagAudit(input) {
|
|
|
1096
1206
|
}
|
|
1097
1207
|
}
|
|
1098
1208
|
if (violations.length > 0) {
|
|
1099
|
-
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
|
|
1209
|
+
return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data, humanHint: violations.map((v) => `${v.page}: "${v.tag}" not in taxonomy`).join("\n") }) };
|
|
1100
1210
|
}
|
|
1101
|
-
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
|
|
1211
|
+
return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
|
|
1102
1212
|
}
|
|
1103
1213
|
|
|
1104
1214
|
// src/commands/index-check.ts
|
|
@@ -1112,7 +1222,11 @@ async function runIndexCheck(input) {
|
|
|
1112
1222
|
indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
|
|
1113
1223
|
} catch {
|
|
1114
1224
|
}
|
|
1115
|
-
const
|
|
1225
|
+
const indexSlugsLower = /* @__PURE__ */ new Map();
|
|
1226
|
+
for (const s of extractBodyWikilinks(indexText)) {
|
|
1227
|
+
const tail = s.split("/").pop();
|
|
1228
|
+
indexSlugsLower.set(tail.toLowerCase(), tail);
|
|
1229
|
+
}
|
|
1116
1230
|
const fileSlugs = /* @__PURE__ */ new Map();
|
|
1117
1231
|
for (const p of scan.data.typedKnowledge) {
|
|
1118
1232
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
@@ -1120,16 +1234,21 @@ async function runIndexCheck(input) {
|
|
|
1120
1234
|
}
|
|
1121
1235
|
const missing_from_index = [];
|
|
1122
1236
|
for (const [slug, relPath] of fileSlugs.entries()) {
|
|
1123
|
-
if (!
|
|
1237
|
+
if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
|
|
1124
1238
|
}
|
|
1239
|
+
const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
|
|
1125
1240
|
const ghost_entries = [];
|
|
1126
|
-
for (const
|
|
1127
|
-
if (!
|
|
1241
|
+
for (const [lower, orig] of indexSlugsLower) {
|
|
1242
|
+
if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
|
|
1128
1243
|
}
|
|
1244
|
+
const hintLines = [];
|
|
1245
|
+
if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
|
|
1246
|
+
if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
|
|
1247
|
+
if (hintLines.length === 0) hintLines.push("index OK");
|
|
1129
1248
|
if (missing_from_index.length > 0 || ghost_entries.length > 0) {
|
|
1130
|
-
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
|
|
1249
|
+
return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1131
1250
|
}
|
|
1132
|
-
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
|
|
1251
|
+
return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
|
|
1133
1252
|
}
|
|
1134
1253
|
|
|
1135
1254
|
// src/commands/stale.ts
|
|
@@ -1169,8 +1288,8 @@ async function runStale(input) {
|
|
|
1169
1288
|
stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
|
|
1170
1289
|
}
|
|
1171
1290
|
}
|
|
1172
|
-
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
|
|
1173
|
-
return { exitCode: ExitCode.OK, result: ok({ stale }) };
|
|
1291
|
+
if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale, humanHint: stale.map((s) => `${s.page} (${s.gap_days}d stale)`).join("\n") }) };
|
|
1292
|
+
return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
|
|
1174
1293
|
}
|
|
1175
1294
|
|
|
1176
1295
|
// src/commands/pagesize.ts
|
|
@@ -1185,17 +1304,17 @@ async function runPagesize(input) {
|
|
|
1185
1304
|
const count = body.split("\n").length;
|
|
1186
1305
|
if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
|
|
1187
1306
|
}
|
|
1188
|
-
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
|
|
1189
|
-
return { exitCode: ExitCode.OK, result: ok({ oversized }) };
|
|
1307
|
+
if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
|
|
1308
|
+
return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
|
|
1190
1309
|
}
|
|
1191
1310
|
|
|
1192
1311
|
// src/commands/log-rotate.ts
|
|
1193
|
-
import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as
|
|
1312
|
+
import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
|
|
1194
1313
|
import { join as join11 } from "path";
|
|
1195
1314
|
var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
|
|
1196
1315
|
async function runLogRotate(input) {
|
|
1197
1316
|
try {
|
|
1198
|
-
await
|
|
1317
|
+
await stat5(join11(input.vault, "SCHEMA.md"));
|
|
1199
1318
|
} catch {
|
|
1200
1319
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
1201
1320
|
}
|
|
@@ -1209,12 +1328,12 @@ async function runLogRotate(input) {
|
|
|
1209
1328
|
const matches = [...logText.matchAll(ENTRY_RE)];
|
|
1210
1329
|
const entries = matches.length;
|
|
1211
1330
|
if (entries < input.threshold) {
|
|
1212
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
|
|
1331
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
|
|
1213
1332
|
}
|
|
1214
1333
|
if (!input.apply) {
|
|
1215
1334
|
return {
|
|
1216
1335
|
exitCode: ExitCode.LOG_ROTATE_NEEDED,
|
|
1217
|
-
result: ok({ entries, threshold: input.threshold, rotated: false })
|
|
1336
|
+
result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
|
|
1218
1337
|
};
|
|
1219
1338
|
}
|
|
1220
1339
|
const newestYear = matches[matches.length - 1][1];
|
|
@@ -1235,13 +1354,51 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
|
|
|
1235
1354
|
} catch (e) {
|
|
1236
1355
|
return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
|
|
1237
1356
|
}
|
|
1238
|
-
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
|
|
1357
|
+
return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/commands/topic-map-check.ts
|
|
1361
|
+
var DEFAULT_THRESHOLD = 200;
|
|
1362
|
+
async function runTopicMapCheck(input) {
|
|
1363
|
+
const threshold = input.threshold ?? DEFAULT_THRESHOLD;
|
|
1364
|
+
const scan = await scanVault(input.vault);
|
|
1365
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1366
|
+
const page_count = scan.data.typedKnowledge.length;
|
|
1367
|
+
const recommended = page_count >= threshold;
|
|
1368
|
+
return {
|
|
1369
|
+
exitCode: ExitCode.OK,
|
|
1370
|
+
result: ok({
|
|
1371
|
+
recommended,
|
|
1372
|
+
page_count,
|
|
1373
|
+
threshold,
|
|
1374
|
+
humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
|
|
1375
|
+
})
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/commands/index-link-format.ts
|
|
1380
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1381
|
+
import { join as join12 } from "path";
|
|
1382
|
+
var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
|
|
1383
|
+
async function runIndexLinkFormat(input) {
|
|
1384
|
+
let text = "";
|
|
1385
|
+
try {
|
|
1386
|
+
text = await readFile11(join12(input.vault, "index.md"), "utf8");
|
|
1387
|
+
} catch {
|
|
1388
|
+
}
|
|
1389
|
+
const markdown_links = [];
|
|
1390
|
+
for (const [i, line] of text.split("\n").entries()) {
|
|
1391
|
+
if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
|
|
1392
|
+
}
|
|
1393
|
+
const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
|
|
1394
|
+
${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
|
|
1395
|
+
return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
|
|
1239
1396
|
}
|
|
1240
1397
|
|
|
1241
1398
|
// src/commands/lint.ts
|
|
1242
1399
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_drift", "tag_not_in_taxonomy"];
|
|
1243
|
-
var WARNING_ORDER = ["index_incomplete", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
|
|
1244
|
-
var INFO_ORDER = ["bridges", "low_confidence_single_source"];
|
|
1400
|
+
var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
|
|
1401
|
+
var INFO_ORDER = ["bridges", "low_confidence_single_source", "topic_map_recommended"];
|
|
1245
1402
|
async function runLint(input) {
|
|
1246
1403
|
const buckets = {};
|
|
1247
1404
|
const links = await runLinks({ vault: input.vault });
|
|
@@ -1261,6 +1418,10 @@ async function runLint(input) {
|
|
|
1261
1418
|
ghost_entries: idx.result.data.ghost_entries
|
|
1262
1419
|
}];
|
|
1263
1420
|
}
|
|
1421
|
+
const linkFmt = await runIndexLinkFormat({ vault: input.vault });
|
|
1422
|
+
if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
|
|
1423
|
+
buckets.index_link_format = linkFmt.result.data.markdown_links;
|
|
1424
|
+
}
|
|
1264
1425
|
const stale = await runStale({ vault: input.vault, days: input.days });
|
|
1265
1426
|
if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
|
|
1266
1427
|
const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
|
|
@@ -1274,6 +1435,10 @@ async function runLint(input) {
|
|
|
1274
1435
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
1275
1436
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
1276
1437
|
}
|
|
1438
|
+
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
1439
|
+
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
1440
|
+
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
1441
|
+
}
|
|
1277
1442
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1278
1443
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
1279
1444
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -1285,25 +1450,35 @@ async function runLint(input) {
|
|
|
1285
1450
|
let exitCode = ExitCode.OK;
|
|
1286
1451
|
if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
|
|
1287
1452
|
else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
|
|
1453
|
+
const hintLines = [];
|
|
1454
|
+
if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
|
|
1455
|
+
if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
|
|
1456
|
+
if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
|
|
1457
|
+
const allBuckets = [...errorOut, ...warningOut, ...infoOut];
|
|
1458
|
+
for (const b of allBuckets) {
|
|
1459
|
+
hintLines.push(` ${b.kind}: ${b.items.length}`);
|
|
1460
|
+
}
|
|
1461
|
+
if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
|
|
1288
1462
|
return {
|
|
1289
1463
|
exitCode,
|
|
1290
1464
|
result: ok({
|
|
1291
1465
|
vault: { path: input.vault, source: input.source ?? "resolved" },
|
|
1292
1466
|
summary,
|
|
1293
|
-
by_severity: { error: errorOut, warning: warningOut, info: infoOut }
|
|
1467
|
+
by_severity: { error: errorOut, warning: warningOut, info: infoOut },
|
|
1468
|
+
humanHint: hintLines.join("\n")
|
|
1294
1469
|
})
|
|
1295
1470
|
};
|
|
1296
1471
|
}
|
|
1297
1472
|
|
|
1298
1473
|
// src/commands/config.ts
|
|
1299
|
-
import { readFile as
|
|
1474
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1300
1475
|
import { existsSync } from "fs";
|
|
1301
|
-
import { join as
|
|
1476
|
+
import { join as join13 } from "path";
|
|
1302
1477
|
function validateKey(key) {
|
|
1303
1478
|
return CONFIG_KEYS.includes(key);
|
|
1304
1479
|
}
|
|
1305
1480
|
function configPath(home) {
|
|
1306
|
-
return
|
|
1481
|
+
return join13(home, ".skillwiki", ".env");
|
|
1307
1482
|
}
|
|
1308
1483
|
async function runConfigGet(input) {
|
|
1309
1484
|
if (!validateKey(input.key)) {
|
|
@@ -1321,7 +1496,7 @@ async function runConfigSet(input) {
|
|
|
1321
1496
|
try {
|
|
1322
1497
|
let originalContent;
|
|
1323
1498
|
try {
|
|
1324
|
-
originalContent = await
|
|
1499
|
+
originalContent = await readFile12(filePath, "utf8");
|
|
1325
1500
|
} catch {
|
|
1326
1501
|
}
|
|
1327
1502
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -1344,7 +1519,7 @@ async function runConfigPath(input) {
|
|
|
1344
1519
|
|
|
1345
1520
|
// src/commands/doctor.ts
|
|
1346
1521
|
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
1347
|
-
import { join as
|
|
1522
|
+
import { join as join14 } from "path";
|
|
1348
1523
|
import { execSync } from "child_process";
|
|
1349
1524
|
function check(status, id, label, detail) {
|
|
1350
1525
|
return { id, label, status, detail };
|
|
@@ -1400,9 +1575,9 @@ function checkVaultStructure(resolvedPath) {
|
|
|
1400
1575
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
1401
1576
|
}
|
|
1402
1577
|
const missing = [];
|
|
1403
|
-
if (!existsSync2(
|
|
1578
|
+
if (!existsSync2(join14(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
1404
1579
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
1405
|
-
if (!existsSync2(
|
|
1580
|
+
if (!existsSync2(join14(resolvedPath, dir))) missing.push(dir + "/");
|
|
1406
1581
|
}
|
|
1407
1582
|
if (missing.length === 0) {
|
|
1408
1583
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -1410,7 +1585,7 @@ function checkVaultStructure(resolvedPath) {
|
|
|
1410
1585
|
return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
|
|
1411
1586
|
}
|
|
1412
1587
|
function checkSkillsInstalled(home) {
|
|
1413
|
-
const skillsDir =
|
|
1588
|
+
const skillsDir = join14(home, ".claude", "skills");
|
|
1414
1589
|
if (!existsSync2(skillsDir)) {
|
|
1415
1590
|
return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
|
|
1416
1591
|
}
|
|
@@ -1430,9 +1605,9 @@ function findSkillMd(dir) {
|
|
|
1430
1605
|
}
|
|
1431
1606
|
for (const entry of entries) {
|
|
1432
1607
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1433
|
-
results.push(
|
|
1608
|
+
results.push(join14(dir, entry.name));
|
|
1434
1609
|
} else if (entry.isDirectory()) {
|
|
1435
|
-
results.push(...findSkillMd(
|
|
1610
|
+
results.push(...findSkillMd(join14(dir, entry.name)));
|
|
1436
1611
|
}
|
|
1437
1612
|
}
|
|
1438
1613
|
return results;
|
|
@@ -1470,6 +1645,123 @@ async function runDoctor(input) {
|
|
|
1470
1645
|
return { exitCode, result: ok({ checks, summary, humanHint }) };
|
|
1471
1646
|
}
|
|
1472
1647
|
|
|
1648
|
+
// src/commands/archive.ts
|
|
1649
|
+
import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
1650
|
+
import { join as join15, dirname as dirname6 } from "path";
|
|
1651
|
+
async function runArchive(input) {
|
|
1652
|
+
const scan = await scanVault(input.vault);
|
|
1653
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1654
|
+
let relPath;
|
|
1655
|
+
if (input.page.includes("/")) {
|
|
1656
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
|
|
1657
|
+
} else {
|
|
1658
|
+
relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
|
|
1659
|
+
}
|
|
1660
|
+
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
1661
|
+
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
1662
|
+
const archivePath = join15("_archive", relPath);
|
|
1663
|
+
await mkdir5(dirname6(join15(input.vault, archivePath)), { recursive: true });
|
|
1664
|
+
let indexUpdated = false;
|
|
1665
|
+
const indexPath = join15(input.vault, "index.md");
|
|
1666
|
+
try {
|
|
1667
|
+
const idx = await readFile13(indexPath, "utf8");
|
|
1668
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
1669
|
+
const originalLines = idx.split("\n");
|
|
1670
|
+
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
1671
|
+
if (filtered.length !== originalLines.length) {
|
|
1672
|
+
await writeFile6(indexPath, filtered.join("\n"), "utf8");
|
|
1673
|
+
indexUpdated = true;
|
|
1674
|
+
}
|
|
1675
|
+
} catch (e) {
|
|
1676
|
+
if (e?.code !== "ENOENT") throw e;
|
|
1677
|
+
}
|
|
1678
|
+
await rename3(join15(input.vault, relPath), join15(input.vault, archivePath));
|
|
1679
|
+
return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/commands/drift.ts
|
|
1683
|
+
import { createHash as createHash2 } from "crypto";
|
|
1684
|
+
|
|
1685
|
+
// src/utils/fetch.ts
|
|
1686
|
+
async function controlledFetch(url, opts) {
|
|
1687
|
+
let current = url;
|
|
1688
|
+
for (let hop = 0; hop <= opts.maxRedirects; hop++) {
|
|
1689
|
+
const guard = runFetchGuardSync({ url: current });
|
|
1690
|
+
if (!guard.result.ok) return guard.result;
|
|
1691
|
+
const ctrl = new AbortController();
|
|
1692
|
+
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
|
|
1693
|
+
let res;
|
|
1694
|
+
try {
|
|
1695
|
+
res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
|
|
1696
|
+
} catch (e) {
|
|
1697
|
+
clearTimeout(timer);
|
|
1698
|
+
if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
|
|
1699
|
+
return err("FETCH_FAILED", { message: String(e) });
|
|
1700
|
+
}
|
|
1701
|
+
clearTimeout(timer);
|
|
1702
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1703
|
+
const loc = res.headers.get("location");
|
|
1704
|
+
if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
|
|
1705
|
+
current = new URL(loc, current).toString();
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const declared = Number(res.headers.get("content-length") ?? "0");
|
|
1709
|
+
if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
|
|
1710
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
1711
|
+
if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
|
|
1712
|
+
return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
|
|
1713
|
+
}
|
|
1714
|
+
return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/commands/drift.ts
|
|
1718
|
+
var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
|
|
1719
|
+
async function runDrift(input) {
|
|
1720
|
+
const doFetch = input.fetchFn ?? controlledFetch;
|
|
1721
|
+
const scan = await scanVault(input.vault);
|
|
1722
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
1723
|
+
const results = [];
|
|
1724
|
+
for (const raw of scan.data.raw) {
|
|
1725
|
+
const fm = extractFrontmatter(await readPage(raw));
|
|
1726
|
+
if (!fm.ok) continue;
|
|
1727
|
+
const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
|
|
1728
|
+
const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
|
|
1729
|
+
if (!sourceUrl || !storedHash) continue;
|
|
1730
|
+
const resp = await doFetch(sourceUrl, FETCH_OPTS);
|
|
1731
|
+
if (!resp.ok) {
|
|
1732
|
+
results.push({
|
|
1733
|
+
raw_path: raw.relPath,
|
|
1734
|
+
source_url: sourceUrl,
|
|
1735
|
+
stored_sha256: storedHash,
|
|
1736
|
+
current_sha256: null,
|
|
1737
|
+
status: "fetch_failed",
|
|
1738
|
+
fetch_error: resp.error
|
|
1739
|
+
});
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
|
|
1743
|
+
const drifted2 = currentHash !== storedHash;
|
|
1744
|
+
results.push({
|
|
1745
|
+
raw_path: raw.relPath,
|
|
1746
|
+
source_url: sourceUrl,
|
|
1747
|
+
stored_sha256: storedHash,
|
|
1748
|
+
current_sha256: currentHash,
|
|
1749
|
+
status: drifted2 ? "drifted" : "unchanged"
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
const drifted = results.filter((r) => r.status === "drifted");
|
|
1753
|
+
const fetchFailed = results.filter((r) => r.status === "fetch_failed");
|
|
1754
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
1755
|
+
const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
|
|
1756
|
+
const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
|
|
1757
|
+
if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
|
|
1758
|
+
if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
|
|
1759
|
+
return {
|
|
1760
|
+
exitCode,
|
|
1761
|
+
result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1473
1765
|
// src/cli.ts
|
|
1474
1766
|
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
1475
1767
|
var program = new Command();
|
|
@@ -1514,7 +1806,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
|
|
|
1514
1806
|
explain: !!opts.explain
|
|
1515
1807
|
}));
|
|
1516
1808
|
});
|
|
1517
|
-
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).action(async (opts) => {
|
|
1809
|
+
program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").action(async (opts) => {
|
|
1518
1810
|
const templates = new URL("../templates/", import.meta.url).pathname;
|
|
1519
1811
|
const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
|
|
1520
1812
|
emit(await runInit({
|
|
@@ -1525,7 +1817,8 @@ program.command("init").option("--target <dir>", "explicit target directory").re
|
|
|
1525
1817
|
domain: opts.domain,
|
|
1526
1818
|
taxonomy,
|
|
1527
1819
|
lang: opts.lang,
|
|
1528
|
-
force: !!opts.force
|
|
1820
|
+
force: !!opts.force,
|
|
1821
|
+
noEnv: opts.env === false
|
|
1529
1822
|
}));
|
|
1530
1823
|
});
|
|
1531
1824
|
async function resolveVaultArg(arg) {
|
|
@@ -1589,6 +1882,16 @@ program.command("doctor").description("diagnose skillwiki setup issues").action(
|
|
|
1589
1882
|
envValue: process.env.WIKI_PATH,
|
|
1590
1883
|
argv: process.argv
|
|
1591
1884
|
})));
|
|
1885
|
+
program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
|
|
1886
|
+
const v = await resolveVaultArg(vault);
|
|
1887
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1888
|
+
else emit(await runArchive({ vault: v.vault, page }));
|
|
1889
|
+
});
|
|
1890
|
+
program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
|
|
1891
|
+
const v = await resolveVaultArg(vault);
|
|
1892
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
1893
|
+
else emit(await runDrift({ vault: v.vault }));
|
|
1894
|
+
});
|
|
1592
1895
|
program.parseAsync(process.argv).catch((e) => {
|
|
1593
1896
|
process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
|
|
1594
1897
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skillwiki": "dist/cli.js"
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"typescript": "^5.7.0",
|
|
31
31
|
"vitest": "^2.1.0"
|
|
32
32
|
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/karlorz/llm-wiki.git"
|
|
36
|
+
},
|
|
33
37
|
"engines": {
|
|
34
38
|
"node": ">=20"
|
|
35
39
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.8",
|
|
4
4
|
"skills": "./",
|
|
5
5
|
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 11 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI (8 subcommands, JSON-by-default).",
|
|
6
6
|
"author": {
|
package/skills/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillwiki/skills",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.8",
|
|
4
4
|
"private": true,
|
|
5
|
-
"files": [
|
|
5
|
+
"files": [
|
|
6
|
+
"wiki-*",
|
|
7
|
+
"proj-*",
|
|
8
|
+
"using-skillwiki",
|
|
9
|
+
".claude-plugin",
|
|
10
|
+
"hooks",
|
|
11
|
+
"README.md"
|
|
12
|
+
]
|
|
6
13
|
}
|
|
@@ -12,10 +12,32 @@ description: 2-step distillation (E4) — analyze project compound entry, then g
|
|
|
12
12
|
Standard four + project context.
|
|
13
13
|
|
|
14
14
|
## Steps (E4 — 2-step pattern)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
### Source selection
|
|
17
|
+
|
|
18
|
+
Check `projects/{slug}/compound/` first. If empty, fall back to retro
|
|
19
|
+
entries in vault `log.md` (lines matching `## [YYYY-MM-DD] retro`).
|
|
20
|
+
|
|
21
|
+
When reading retros as source material:
|
|
22
|
+
- Collect all retros for the project, focusing on entries with
|
|
23
|
+
`Generalize?: yes`.
|
|
24
|
+
- Group by recurring theme (≥2 occurrences across cycles).
|
|
25
|
+
- Each group becomes a candidate concept outline.
|
|
26
|
+
|
|
27
|
+
1. **Step 1 — Analyze.** Read the source compound entry + linked work
|
|
28
|
+
items (or retro groups from log.md). Output a candidate concept
|
|
29
|
+
outline. STOP if no clear universal pattern is found — surface the
|
|
30
|
+
reasoning instead of forcing a page.
|
|
31
|
+
2. **Step 2 — Generate.** Compose the vault concept page with
|
|
32
|
+
`provenance: project` and
|
|
33
|
+
`provenance_projects: ["[[slug]]"]`. Validate with
|
|
34
|
+
`npx skillwiki validate`.
|
|
35
|
+
3. **Backlink.** Set `promoted_to: "[[concept-slug]]"` on the source
|
|
36
|
+
compound entry. For retro-sourced distillation, skip backlink (log.md
|
|
37
|
+
entries are append-only) and instead add `sources:` citing the vault
|
|
38
|
+
log with date range.
|
|
39
|
+
4. **Apply writes in order.** Vault concept page → backlink update →
|
|
40
|
+
project `log.md` → vault `index.md` → vault `log.md`.
|
|
19
41
|
|
|
20
42
|
## Stop conditions
|
|
21
43
|
- No clear universal pattern.
|
|
@@ -20,6 +20,25 @@ Standard four + project context (project README, last ~5 work logs).
|
|
|
20
20
|
5. Manage status transitions: `planned` → `in-progress` → `completed` (set `completed:` date) or `abandoned`.
|
|
21
21
|
6. Append vault `log.md` entry on creation and on each status transition.
|
|
22
22
|
|
|
23
|
+
## Redirect Output
|
|
24
|
+
|
|
25
|
+
After step 3 (output path override), emit redirect paths for the active PRD skill:
|
|
26
|
+
|
|
27
|
+
> Work item created: projects/{slug}/work/YYYY-MM-DD-{work-slug}/
|
|
28
|
+
>
|
|
29
|
+
> Redirect paths for PRD skills:
|
|
30
|
+
> spec → <vault-root>/projects/{slug}/work/YYYY-MM-DD-{work-slug}/spec.md
|
|
31
|
+
> plan → <vault-root>/projects/{slug}/work/YYYY-MM-DD-{work-slug}/plan.md
|
|
32
|
+
>
|
|
33
|
+
> Pass these paths to your PRD skill (superpowers:brainstorming, superpowers:writing-plans,
|
|
34
|
+
> CodeStable, or any other). Files land in the vault natively — no separate ingest needed.
|
|
35
|
+
|
|
36
|
+
Rules:
|
|
37
|
+
- Emit redirect paths as the first output after folder creation, before any PRD skill runs.
|
|
38
|
+
- Resolve `<vault-root>` via `skillwiki path` (never hardcode).
|
|
39
|
+
- proj-work does NOT invoke any PRD skill — it provides paths only.
|
|
40
|
+
- If the PRD skill cannot accept custom save paths, fall back to manual `wiki-ingest`.
|
|
41
|
+
|
|
23
42
|
## Stop conditions
|
|
24
43
|
- `validate` non-zero.
|
|
25
44
|
- Conflicting work folder name.
|
|
@@ -20,6 +20,9 @@ Invoke a skillwiki skill when the user:
|
|
|
20
20
|
- Wants a health check or lint on their vault
|
|
21
21
|
- Mentions crystallizing a session into a note
|
|
22
22
|
- Talks about project workspaces, ADRs, or distillation
|
|
23
|
+
- Wants to archive or clean up old vault pages
|
|
24
|
+
- Needs to detect source drift or re-ingest updated content
|
|
25
|
+
- Has a spec/plan in a non-skillwiki format (CodeStable, RFC, AIDE)
|
|
23
26
|
- Asks about their skillwiki configuration or setup health
|
|
24
27
|
|
|
25
28
|
## Skill Map
|
|
@@ -32,6 +35,9 @@ Invoke a skillwiki skill when the user:
|
|
|
32
35
|
| `wiki-lint` | Vault health check (stale pages, oversized pages, log rotation) |
|
|
33
36
|
| `wiki-crystallize` | Distill the current working session into a typed-knowledge page |
|
|
34
37
|
| `wiki-audit` | Verify raw provenance references and source frontmatter integrity |
|
|
38
|
+
| `wiki-archive` | Archive a typed-knowledge page — move to `_archive/`, remove from index |
|
|
39
|
+
| `wiki-reingest` | Detect drift in raw sources (sha256 comparison) and re-ingest updated content |
|
|
40
|
+
| `wiki-adapter-prd` | Map foreign PRD formats (CodeStable, RFC, AIDE, Hermes) into vault pages |
|
|
35
41
|
| `proj-init` | Bootstrap a project workspace (README, requirements, architecture) |
|
|
36
42
|
| `proj-work` | Open or run a work item under a project's work/ directory |
|
|
37
43
|
| `proj-distill` | Distill project compound entries into vault concept pages |
|
|
@@ -41,7 +47,7 @@ Invoke a skillwiki skill when the user:
|
|
|
41
47
|
|
|
42
48
|
All skills are backed by the `skillwiki` CLI — a deterministic tool with no LLM calls. It handles path resolution, config management, validation, and linting. Skills invoke it via Bash for the mechanical parts and use Claude for the creative parts.
|
|
43
49
|
|
|
44
|
-
Key CLI subcommands: `init`, `lint`, `config`, `doctor`, `path`, `lang`, `install`, `graph build`.
|
|
50
|
+
Key CLI subcommands: `init`, `lint`, `config`, `doctor`, `path`, `lang`, `install`, `graph build`, `archive`, `drift`.
|
|
45
51
|
|
|
46
52
|
Run `skillwiki doctor` to diagnose setup issues. Run `skillwiki config list` to see current configuration.
|
|
47
53
|
|
|
@@ -55,3 +61,5 @@ Run `skillwiki doctor` to diagnose setup issues. Run `skillwiki config list` to
|
|
|
55
61
|
6. **Audit** (`wiki-audit`) — verify source integrity
|
|
56
62
|
|
|
57
63
|
For longer-running project work, use `proj-init` → `proj-work` → `proj-distill` / `proj-decide`.
|
|
64
|
+
|
|
65
|
+
Maintenance: **Archive** (`wiki-archive`) superseded pages, **Drift** (`wiki-reingest`) to detect stale sources, **Adapter** (`wiki-adapter-prd`) for foreign PRD format ingestion.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wiki-adapter-prd
|
|
3
|
+
description: Map foreign PRD formats (CodeStable, RFCs, structured markdown) into skillwiki raw + typed-knowledge pages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wiki-adapter-prd
|
|
7
|
+
|
|
8
|
+
## When This Skill Activates
|
|
9
|
+
|
|
10
|
+
- User provides a document or URL in a non-skillwiki PRD format and wants it captured in the vault.
|
|
11
|
+
- User mentions CodeStable, RFC, AIDE, or another structured design document format.
|
|
12
|
+
- A foreign spec/plan needs to be normalized into the vault's raw + concept structure.
|
|
13
|
+
|
|
14
|
+
## Output language
|
|
15
|
+
|
|
16
|
+
Run `skillwiki lang` at the start. Generate page prose in the resolved language. Frontmatter keys, file names, and structural markers stay English.
|
|
17
|
+
|
|
18
|
+
## Pre-orientation reads
|
|
19
|
+
|
|
20
|
+
Standard four reads (SCHEMA, index, log, project context if applicable).
|
|
21
|
+
|
|
22
|
+
## Recognized PRD Formats
|
|
23
|
+
|
|
24
|
+
| Format | Structural cues |
|
|
25
|
+
|--------|----------------|
|
|
26
|
+
| CodeStable | `REQ-NNN` requirement IDs, `## Requirements` / `## Architecture` headers |
|
|
27
|
+
| RFC | `## Motivation` / `## Proposal` / `## Drawbacks` headers |
|
|
28
|
+
| AIDE directives | Specific YAML frontmatter keys (`aide-*`) |
|
|
29
|
+
| Hermes spec | `N1`–`N18` normative requirement markers |
|
|
30
|
+
| Generic structured | Clear `##` section hierarchy with requirements, decisions, or designs |
|
|
31
|
+
|
|
32
|
+
If the format is unrecognized, treat as generic structured markdown and map by section hierarchy.
|
|
33
|
+
|
|
34
|
+
## Mapping Strategy
|
|
35
|
+
|
|
36
|
+
### Raw capture (verbatim)
|
|
37
|
+
|
|
38
|
+
- Write the full source document to `raw/articles/<slug>.md` with RawSourceSchema frontmatter (`sha256`, `source_url`, `ingested`, `ingested_by: "wiki-ingest"`).
|
|
39
|
+
- If the source is a URL, run `skillwiki fetch-guard <url>` first.
|
|
40
|
+
- Run `skillwiki hash <raw-file>` to compute sha256.
|
|
41
|
+
|
|
42
|
+
### Knowledge extraction
|
|
43
|
+
|
|
44
|
+
Map source sections to typed-knowledge pages:
|
|
45
|
+
|
|
46
|
+
| Source section | Target type | Notes |
|
|
47
|
+
|----------------|-------------|-------|
|
|
48
|
+
| Requirements list | `concepts/` or `entities/` | Each major requirement becomes its own page or a section in a compound page |
|
|
49
|
+
| Architecture decisions | `concepts/` | Map to concept pages with `tags: [architecture]` |
|
|
50
|
+
| Motivation / context | `entities/` | Capture as entity pages describing the system or component |
|
|
51
|
+
| Trade-offs / comparisons | `comparisons/` | Create comparison pages when the source weighs alternatives |
|
|
52
|
+
| Action items / next steps | Skip | Not knowledge — track in project work items instead |
|
|
53
|
+
|
|
54
|
+
### Cross-reference handling
|
|
55
|
+
|
|
56
|
+
- Requirement IDs (`REQ-NNN`, `N1`–`N18`) → preserve as frontmatter tags or inline references.
|
|
57
|
+
- Internal links within the source → convert to `[[wikilinks]]` where corresponding pages exist.
|
|
58
|
+
- External URLs → keep as-is in body text.
|
|
59
|
+
|
|
60
|
+
## Steps
|
|
61
|
+
|
|
62
|
+
0. Resolve vault and language: `skillwiki path` and `skillwiki lang`.
|
|
63
|
+
1. Classify the input format using the structural cues above.
|
|
64
|
+
2. If URL source: run `skillwiki fetch-guard <url>`, then fetch.
|
|
65
|
+
3. Write raw capture: frontmatter + full body → `raw/articles/<slug>.md`.
|
|
66
|
+
4. Run `skillwiki hash <raw-file>`, embed sha256.
|
|
67
|
+
5. Generate typed-knowledge pages following the mapping strategy.
|
|
68
|
+
6. For each page: run `skillwiki validate <page>`. If any fails, STOP.
|
|
69
|
+
7. Write pages, then update `index.md` and `log.md`.
|
|
70
|
+
|
|
71
|
+
## Provenance defaults
|
|
72
|
+
|
|
73
|
+
- `provenance: research` (external PRD sources).
|
|
74
|
+
- `sources: ["^[raw/articles/<slug>.md]"]` on every generated page.
|
|
75
|
+
|
|
76
|
+
## Stop conditions
|
|
77
|
+
|
|
78
|
+
- `fetch-guard` non-zero.
|
|
79
|
+
- `validate` non-zero on any page.
|
|
80
|
+
- sha256 already exists for the same source (skip — already ingested).
|
|
81
|
+
|
|
82
|
+
## Forbidden
|
|
83
|
+
|
|
84
|
+
- Skipping `fetch-guard` for URL sources.
|
|
85
|
+
- Writing index/log before all pages validate.
|
|
86
|
+
- Modifying existing raw files (N9).
|
|
87
|
+
- Auto-generating pages for action items, timelines, or process steps — those are project management, not knowledge.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wiki-archive
|
|
3
|
+
description: Archive a superseded typed-knowledge page. Moves page to _archive/, removes from index.md, logs the action.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wiki-archive
|
|
7
|
+
|
|
8
|
+
## When This Skill Activates
|
|
9
|
+
|
|
10
|
+
- User wants to retire, supersede, or remove a typed-knowledge page from active use.
|
|
11
|
+
- A page has been replaced by a newer version and should be kept for reference but excluded from lint and queries.
|
|
12
|
+
|
|
13
|
+
## Output language
|
|
14
|
+
|
|
15
|
+
Run `skillwiki lang` at the start. Generate log entries in the resolved language.
|
|
16
|
+
|
|
17
|
+
## Pre-orientation reads
|
|
18
|
+
|
|
19
|
+
Standard four reads (SCHEMA, index, log, project context if applicable).
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
0. Resolve vault: `skillwiki path` and `skillwiki lang`.
|
|
24
|
+
1. Identify the target page. Confirm with the user which page to archive (show full relPath).
|
|
25
|
+
2. Run `skillwiki archive <page> [vault]`. Read the JSON output.
|
|
26
|
+
3. Verify with `skillwiki index-check [vault]` — confirm no ghost entries remain.
|
|
27
|
+
4. Append a `log.md` entry: `## [{date}] archive | {relPath} → _archive/{subdir}/`.
|
|
28
|
+
|
|
29
|
+
## Reversibility
|
|
30
|
+
|
|
31
|
+
Archiving is reversible: move the file back from `_archive/` to its original directory and re-add the wikilink entry to `index.md`. No data is deleted.
|
|
32
|
+
|
|
33
|
+
## Stop conditions
|
|
34
|
+
|
|
35
|
+
- `skillwiki archive` returns non-zero exit code (page not found, already archived, invalid vault).
|
|
36
|
+
- User declines to proceed.
|
|
37
|
+
|
|
38
|
+
## Forbidden
|
|
39
|
+
|
|
40
|
+
- Archiving `raw/` files (N9 — raw is immutable).
|
|
41
|
+
- Archiving without user confirmation.
|
|
42
|
+
- Deleting files (archive moves, never deletes).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wiki-ingest
|
|
3
|
-
description: Convert URLs, files, or pasted text into typed-knowledge pages with raw provenance.
|
|
3
|
+
description: Convert URLs, files, or pasted text into typed-knowledge pages with raw provenance. Supports single and batch mode.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# wiki-ingest
|
|
@@ -45,3 +45,14 @@ Run `skillwiki lang` at the start. Generate page-body prose, narrative sections,
|
|
|
45
45
|
- Skipping `fetch-guard`.
|
|
46
46
|
- Updating `index.md` or `log.md` before all pages validate.
|
|
47
47
|
- Modifying any existing file in `raw/`.
|
|
48
|
+
|
|
49
|
+
## Batch Mode
|
|
50
|
+
|
|
51
|
+
When the user provides multiple sources (a directory of files, a list of URLs, or a multi-document input):
|
|
52
|
+
|
|
53
|
+
1. **Loop per source.** Execute steps 1–5 for each source individually (guard → fetch → hash → generate → validate).
|
|
54
|
+
2. **Accumulate, don't write yet.** Collect all raw files and pages in memory. Do not write `index.md` or `log.md` until every source has validated.
|
|
55
|
+
3. **Fail fast.** If any page fails validation, STOP. Report all failures. Do not write index/log for any source.
|
|
56
|
+
4. **Deduplication.** Before writing each raw file, check `sha256` against existing vault raw sources. Skip sources whose content is already present.
|
|
57
|
+
5. **Single index/log update.** After all sources validate, write all raw files and pages, then update `index.md` and `log.md` once.
|
|
58
|
+
6. **Progress.** After each source completes validation, report progress (e.g., "Validated 3/10 sources").
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wiki-reingest
|
|
3
|
+
description: Detect and act on source drift. Runs skillwiki drift, reviews changes, archives old raw + ingests new content.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wiki-reingest
|
|
7
|
+
|
|
8
|
+
## When This Skill Activates
|
|
9
|
+
|
|
10
|
+
- User wants to check if any vault sources have changed since ingestion.
|
|
11
|
+
- Periodic drift check during lint or maintenance cycles.
|
|
12
|
+
- User explicitly asks to re-ingest a specific source.
|
|
13
|
+
|
|
14
|
+
## Output language
|
|
15
|
+
|
|
16
|
+
Run `skillwiki lang` at the start. Generate log entries in the resolved language.
|
|
17
|
+
|
|
18
|
+
## Pre-orientation reads
|
|
19
|
+
|
|
20
|
+
Standard four reads (SCHEMA, index, log, project context if applicable).
|
|
21
|
+
|
|
22
|
+
## Steps
|
|
23
|
+
|
|
24
|
+
0. Resolve vault: `skillwiki path` and `skillwiki lang`.
|
|
25
|
+
1. Run `skillwiki drift [vault]`. Read the JSON output.
|
|
26
|
+
2. Present findings grouped by status:
|
|
27
|
+
- **drifted:** Source content has changed. Show stored vs current sha256.
|
|
28
|
+
- **fetch_failed:** Could not re-fetch. Show error details.
|
|
29
|
+
- **unchanged:** No action needed.
|
|
30
|
+
3. For each drifted source, ask the user: archive old + ingest new, or skip?
|
|
31
|
+
4. If the user approves re-ingest for a source:
|
|
32
|
+
a. Run `skillwiki archive <raw-path>` to archive the old raw file.
|
|
33
|
+
b. Follow the `wiki-ingest` skill to ingest the updated content as a new raw file.
|
|
34
|
+
c. Update any concept/entity pages that cite the old source to reference the new one.
|
|
35
|
+
5. Append a `log.md` entry summarizing: scanned, drifted, re-ingested, skipped.
|
|
36
|
+
|
|
37
|
+
## N9 Compliance
|
|
38
|
+
|
|
39
|
+
Raw files are immutable (N9). Re-ingest never modifies an existing raw file. Instead:
|
|
40
|
+
- Archive the old raw file (moves to `_archive/raw/`).
|
|
41
|
+
- Create a new raw file with updated content and new sha256.
|
|
42
|
+
- This preserves full provenance history.
|
|
43
|
+
|
|
44
|
+
## Stop conditions
|
|
45
|
+
|
|
46
|
+
- `skillwiki drift` returns non-zero exit code other than DRIFT_DETECTED.
|
|
47
|
+
- User declines all re-ingest actions.
|
|
48
|
+
- No raw sources have `source_url` (nothing to check).
|
|
49
|
+
|
|
50
|
+
## Forbidden
|
|
51
|
+
|
|
52
|
+
- Modifying files in `raw/` directly (N9).
|
|
53
|
+
- Re-ingesting without user approval for each drifted source.
|
|
54
|
+
- Skipping the drift check and assuming sources have changed.
|
package/templates/SCHEMA.md
CHANGED
|
@@ -51,3 +51,19 @@ Rule: every tag on every page MUST appear in this taxonomy. Add new tags here fi
|
|
|
51
51
|
- Wikilinks in YAML: quoted, `"[[name]]"`. Body wikilinks: unquoted `[[name]]`.
|
|
52
52
|
- Citations in body: `^[raw/...]` markers; every entry in `sources:` MUST appear in body.
|
|
53
53
|
- sha256 in `raw/` frontmatter is computed by `skillwiki hash` over body bytes after closing `---`.
|
|
54
|
+
|
|
55
|
+
## Obsidian Integration
|
|
56
|
+
|
|
57
|
+
- **Attachment folder:** `raw/assets/` — binary assets (images, diagrams) live here.
|
|
58
|
+
Set Obsidian's "Attachment folder path" to `raw/assets` for automatic filing.
|
|
59
|
+
- **Dataview queries** (read-only; do not replace index.md):
|
|
60
|
+
|
|
61
|
+
```dataview
|
|
62
|
+
LIST WHERE type = "concept" AND contains(tags, "architecture")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```dataview
|
|
66
|
+
TABLE updated, length(sources) AS sources
|
|
67
|
+
WHERE file.folder = "concepts"
|
|
68
|
+
SORT updated DESC
|
|
69
|
+
```
|