@bartolli/kmd 0.3.0 → 0.4.0
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/kmd.mjs +292 -105
- package/dist/kmd.mjs.map +4 -4
- package/package.json +1 -1
package/dist/kmd.mjs
CHANGED
|
@@ -82,6 +82,9 @@ import { readFile } from "node:fs/promises";
|
|
|
82
82
|
import { join } from "node:path";
|
|
83
83
|
import { parse } from "yaml";
|
|
84
84
|
import { z } from "zod";
|
|
85
|
+
function kindName(entry) {
|
|
86
|
+
return typeof entry === "string" ? entry : entry.name;
|
|
87
|
+
}
|
|
85
88
|
async function loadVaultConfig(vaultRoot2) {
|
|
86
89
|
const path = join(vaultRoot2, "vault.yaml");
|
|
87
90
|
let raw;
|
|
@@ -98,18 +101,26 @@ ${issues}`);
|
|
|
98
101
|
}
|
|
99
102
|
return parsed.data;
|
|
100
103
|
}
|
|
101
|
-
var ScopeSchema, VaultConfigSchema;
|
|
104
|
+
var ScopeSchema, KindEntrySchema, VaultConfigSchema, BUILT_IN_KINDS;
|
|
102
105
|
var init_config = __esm({
|
|
103
106
|
"../cli/src/config.ts"() {
|
|
104
107
|
"use strict";
|
|
105
108
|
ScopeSchema = z.object({
|
|
106
109
|
repo: z.string().optional(),
|
|
107
|
-
methodology: z.
|
|
110
|
+
methodology: z.string().optional(),
|
|
108
111
|
status: z.string()
|
|
109
112
|
});
|
|
113
|
+
KindEntrySchema = z.union([
|
|
114
|
+
z.string(),
|
|
115
|
+
z.object({
|
|
116
|
+
name: z.string(),
|
|
117
|
+
signal: z.string(),
|
|
118
|
+
where: z.string()
|
|
119
|
+
})
|
|
120
|
+
]);
|
|
110
121
|
VaultConfigSchema = z.object({
|
|
111
122
|
scopes: z.record(z.string(), ScopeSchema),
|
|
112
|
-
kinds: z.array(
|
|
123
|
+
kinds: z.array(KindEntrySchema),
|
|
113
124
|
statuses: z.array(z.string()),
|
|
114
125
|
methodologies: z.array(z.string()),
|
|
115
126
|
tags: z.object({
|
|
@@ -117,8 +128,34 @@ var init_config = __esm({
|
|
|
117
128
|
aliases: z.record(z.string(), z.string())
|
|
118
129
|
}),
|
|
119
130
|
authoring_rules: z.string().optional(),
|
|
120
|
-
|
|
131
|
+
authoring_rules_extra: z.string().optional(),
|
|
132
|
+
sync_protocol: z.string().optional(),
|
|
133
|
+
sync_protocol_extra: z.string().optional()
|
|
134
|
+
}).superRefine((config, ctx) => {
|
|
135
|
+
for (const [name, scope] of Object.entries(config.scopes)) {
|
|
136
|
+
if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
|
|
137
|
+
ctx.addIssue({
|
|
138
|
+
code: "custom",
|
|
139
|
+
path: ["scopes", name, "methodology"],
|
|
140
|
+
message: `"${scope.methodology}" is not in the methodologies list`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
121
144
|
});
|
|
145
|
+
BUILT_IN_KINDS = /* @__PURE__ */ new Set([
|
|
146
|
+
"project",
|
|
147
|
+
"spec",
|
|
148
|
+
"adr",
|
|
149
|
+
"plan",
|
|
150
|
+
"story",
|
|
151
|
+
"ops",
|
|
152
|
+
"topic",
|
|
153
|
+
"article",
|
|
154
|
+
"src",
|
|
155
|
+
"note",
|
|
156
|
+
"artifact",
|
|
157
|
+
"prompt"
|
|
158
|
+
]);
|
|
122
159
|
}
|
|
123
160
|
});
|
|
124
161
|
|
|
@@ -380,9 +417,13 @@ var init_sync = __esm({
|
|
|
380
417
|
});
|
|
381
418
|
|
|
382
419
|
// ../cli/src/validate.ts
|
|
383
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
420
|
+
import { readFile as readFile3, stat } from "node:fs/promises";
|
|
421
|
+
import { join as join3 } from "node:path";
|
|
422
|
+
function hasIndexableTitle(data) {
|
|
423
|
+
return typeof data.title === "string" && data.title.trim() !== "";
|
|
424
|
+
}
|
|
384
425
|
function isIndexed(relPath, data) {
|
|
385
|
-
if (
|
|
426
|
+
if (!hasIndexableTitle(data)) return false;
|
|
386
427
|
if (typeof data.kind === "string" && data.kind !== "") return true;
|
|
387
428
|
return relPath.startsWith("notes/");
|
|
388
429
|
}
|
|
@@ -404,6 +445,45 @@ function checkRequiredFields(relPath, data) {
|
|
|
404
445
|
});
|
|
405
446
|
}
|
|
406
447
|
}
|
|
448
|
+
if (Object.hasOwn(data, "title") && !hasIndexableTitle(data)) {
|
|
449
|
+
findings.push({
|
|
450
|
+
path: relPath,
|
|
451
|
+
rule: "required-fields",
|
|
452
|
+
severity: "error",
|
|
453
|
+
message: `"title" must be a non-empty string for kind "${kind}" \u2014 sync skips the page otherwise`
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return findings;
|
|
457
|
+
}
|
|
458
|
+
function customKindNames(cfg) {
|
|
459
|
+
const names = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const entry of cfg.kinds) {
|
|
461
|
+
if (typeof entry !== "string" && !BUILT_IN_KINDS.has(entry.name)) names.add(entry.name);
|
|
462
|
+
}
|
|
463
|
+
return names;
|
|
464
|
+
}
|
|
465
|
+
function checkCustomKindFloor(relPath, data, cfg) {
|
|
466
|
+
const kind = data.kind;
|
|
467
|
+
if (typeof kind !== "string" || !customKindNames(cfg).has(kind)) return [];
|
|
468
|
+
const findings = [];
|
|
469
|
+
for (const field of UNIVERSAL_FLOOR) {
|
|
470
|
+
if (!Object.hasOwn(data, field)) {
|
|
471
|
+
findings.push({
|
|
472
|
+
path: relPath,
|
|
473
|
+
rule: "custom-kind-floor",
|
|
474
|
+
severity: "warning",
|
|
475
|
+
message: `custom kind "${kind}": missing "${field}" \u2014 the universal floor (title, summary, updated) keeps the page retrievable`
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (Object.hasOwn(data, "title") && !hasIndexableTitle(data)) {
|
|
480
|
+
findings.push({
|
|
481
|
+
path: relPath,
|
|
482
|
+
rule: "custom-kind-floor",
|
|
483
|
+
severity: "warning",
|
|
484
|
+
message: `custom kind "${kind}": "title" is empty \u2014 sync skips title-less pages; this page will not be indexed`
|
|
485
|
+
});
|
|
486
|
+
}
|
|
407
487
|
return findings;
|
|
408
488
|
}
|
|
409
489
|
function checkTagsRequired(relPath, data) {
|
|
@@ -435,7 +515,7 @@ function checkFolderSlug(relPath, data) {
|
|
|
435
515
|
function checkVocabulary(relPath, data, cfg) {
|
|
436
516
|
const findings = [];
|
|
437
517
|
const kind = data.kind;
|
|
438
|
-
if (typeof kind === "string" && !cfg.kinds.
|
|
518
|
+
if (typeof kind === "string" && !cfg.kinds.some((k) => kindName(k) === kind)) {
|
|
439
519
|
findings.push({
|
|
440
520
|
path: relPath,
|
|
441
521
|
rule: "kind-vocabulary",
|
|
@@ -574,7 +654,6 @@ function checkSupersededLink(relPath, data) {
|
|
|
574
654
|
function checkIndexedPage(relPath, data, body, cfg, refIndex) {
|
|
575
655
|
if (!isIndexed(relPath, data)) return [];
|
|
576
656
|
return [
|
|
577
|
-
...checkRequiredFields(relPath, data),
|
|
578
657
|
...checkTagsRequired(relPath, data),
|
|
579
658
|
...checkFolderSlug(relPath, data),
|
|
580
659
|
...checkVocabulary(relPath, data, cfg),
|
|
@@ -601,7 +680,11 @@ function validatePage(relPath, raw, cfg, refIndex) {
|
|
|
601
680
|
if (isPrimer(relPath)) {
|
|
602
681
|
return checkBodyLinks(relPath, parsed.content, refIndex);
|
|
603
682
|
}
|
|
604
|
-
return
|
|
683
|
+
return [
|
|
684
|
+
...checkRequiredFields(relPath, parsed.data),
|
|
685
|
+
...checkCustomKindFloor(relPath, parsed.data, cfg),
|
|
686
|
+
...checkIndexedPage(relPath, parsed.data, parsed.content, cfg, refIndex)
|
|
687
|
+
];
|
|
605
688
|
}
|
|
606
689
|
function hasErrors(findings) {
|
|
607
690
|
return findings.some((f) => f.severity === "error");
|
|
@@ -704,9 +787,22 @@ async function validateVault(root) {
|
|
|
704
787
|
}
|
|
705
788
|
findings.push(...validateSupersession(pages));
|
|
706
789
|
findings.push(...validateAmbiguousLinks(linkPages, basenameToPaths));
|
|
790
|
+
for (const name of customKindNames(cfg)) {
|
|
791
|
+
const file = `templates/${name}.md`;
|
|
792
|
+
try {
|
|
793
|
+
await stat(join3(root, file));
|
|
794
|
+
} catch {
|
|
795
|
+
findings.push({
|
|
796
|
+
path: file,
|
|
797
|
+
rule: "custom-kind-template",
|
|
798
|
+
severity: "warning",
|
|
799
|
+
message: `custom kind "${name}" declared in vault.yaml has no template file`
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
707
803
|
return findings;
|
|
708
804
|
}
|
|
709
|
-
var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS;
|
|
805
|
+
var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS, UNIVERSAL_FLOOR;
|
|
710
806
|
var init_validate = __esm({
|
|
711
807
|
"../cli/src/validate.ts"() {
|
|
712
808
|
"use strict";
|
|
@@ -757,6 +853,7 @@ var init_validate = __esm({
|
|
|
757
853
|
topic: /^research\/[^/]+\/index\.md$/,
|
|
758
854
|
src: /^research\/[^/]+\/src-[^/]+\.md$/
|
|
759
855
|
};
|
|
856
|
+
UNIVERSAL_FLOOR = ["title", "summary", "updated"];
|
|
760
857
|
}
|
|
761
858
|
});
|
|
762
859
|
|
|
@@ -865,10 +962,10 @@ var init_config2 = __esm({
|
|
|
865
962
|
// ../mcp/src/db.ts
|
|
866
963
|
import { mkdirSync as mkdirSync2 } from "node:fs";
|
|
867
964
|
import { homedir as homedir2 } from "node:os";
|
|
868
|
-
import { join as
|
|
965
|
+
import { join as join4 } from "node:path";
|
|
869
966
|
function createDatabase() {
|
|
870
|
-
const dbDir =
|
|
871
|
-
const dbPath =
|
|
967
|
+
const dbDir = join4(homedir2(), ".kmd", "db");
|
|
968
|
+
const dbPath = join4(dbDir, "index.db");
|
|
872
969
|
mkdirSync2(dbDir, { recursive: true });
|
|
873
970
|
return openDatabase(dbPath);
|
|
874
971
|
}
|
|
@@ -882,7 +979,7 @@ var init_db = __esm({
|
|
|
882
979
|
// ../mcp/src/lib/diag.ts
|
|
883
980
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
884
981
|
import { homedir as homedir3 } from "node:os";
|
|
885
|
-
import { join as
|
|
982
|
+
import { join as join5 } from "node:path";
|
|
886
983
|
function diag(msg, data) {
|
|
887
984
|
try {
|
|
888
985
|
const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
|
|
@@ -896,8 +993,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
|
|
|
896
993
|
var init_diag = __esm({
|
|
897
994
|
"../mcp/src/lib/diag.ts"() {
|
|
898
995
|
"use strict";
|
|
899
|
-
DIAG_DIR =
|
|
900
|
-
DIAG_LOG_PATH =
|
|
996
|
+
DIAG_DIR = join5(homedir3(), ".local", "state", "wiki-mcp");
|
|
997
|
+
DIAG_LOG_PATH = join5(DIAG_DIR, "server.log");
|
|
901
998
|
try {
|
|
902
999
|
mkdirSync3(DIAG_DIR, { recursive: true });
|
|
903
1000
|
} catch {
|
|
@@ -924,23 +1021,111 @@ var init_logger = __esm({
|
|
|
924
1021
|
}
|
|
925
1022
|
});
|
|
926
1023
|
|
|
1024
|
+
// ../mcp/src/vault-config.ts
|
|
1025
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1026
|
+
import { join as join6 } from "node:path";
|
|
1027
|
+
import { parse as parse2 } from "yaml";
|
|
1028
|
+
import { z as z4 } from "zod";
|
|
1029
|
+
function kindName2(entry) {
|
|
1030
|
+
return typeof entry === "string" ? entry : entry.name;
|
|
1031
|
+
}
|
|
1032
|
+
async function loadVaultConfig2(vaultRoot2) {
|
|
1033
|
+
const path = join6(vaultRoot2, "vault.yaml");
|
|
1034
|
+
let raw;
|
|
1035
|
+
try {
|
|
1036
|
+
raw = await readFile4(path, "utf8");
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
throw new Error(`vault.yaml not found at ${path}`, { cause: err });
|
|
1039
|
+
}
|
|
1040
|
+
const parsed = VaultConfigSchema2.safeParse(parse2(raw));
|
|
1041
|
+
if (!parsed.success) {
|
|
1042
|
+
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
1043
|
+
throw new Error(`Invalid vault.yaml at ${path}:
|
|
1044
|
+
${issues}`);
|
|
1045
|
+
}
|
|
1046
|
+
return parsed.data;
|
|
1047
|
+
}
|
|
1048
|
+
var ScopeSchema2, KindEntrySchema2, VaultConfigSchema2, BUILT_IN_KINDS2;
|
|
1049
|
+
var init_vault_config = __esm({
|
|
1050
|
+
"../mcp/src/vault-config.ts"() {
|
|
1051
|
+
"use strict";
|
|
1052
|
+
ScopeSchema2 = z4.object({
|
|
1053
|
+
repo: z4.string().optional(),
|
|
1054
|
+
methodology: z4.string().optional(),
|
|
1055
|
+
status: z4.string()
|
|
1056
|
+
});
|
|
1057
|
+
KindEntrySchema2 = z4.union([
|
|
1058
|
+
z4.string(),
|
|
1059
|
+
z4.object({
|
|
1060
|
+
name: z4.string(),
|
|
1061
|
+
signal: z4.string(),
|
|
1062
|
+
where: z4.string()
|
|
1063
|
+
})
|
|
1064
|
+
]);
|
|
1065
|
+
VaultConfigSchema2 = z4.object({
|
|
1066
|
+
scopes: z4.record(z4.string(), ScopeSchema2),
|
|
1067
|
+
kinds: z4.array(KindEntrySchema2),
|
|
1068
|
+
statuses: z4.array(z4.string()),
|
|
1069
|
+
methodologies: z4.array(z4.string()),
|
|
1070
|
+
tags: z4.object({
|
|
1071
|
+
canonical: z4.array(z4.string()),
|
|
1072
|
+
aliases: z4.record(z4.string(), z4.string())
|
|
1073
|
+
}),
|
|
1074
|
+
authoring_rules: z4.string().optional(),
|
|
1075
|
+
authoring_rules_extra: z4.string().optional(),
|
|
1076
|
+
sync_protocol: z4.string().optional(),
|
|
1077
|
+
sync_protocol_extra: z4.string().optional()
|
|
1078
|
+
}).superRefine((config, ctx) => {
|
|
1079
|
+
for (const [name, scope] of Object.entries(config.scopes)) {
|
|
1080
|
+
if (scope.methodology !== void 0 && !config.methodologies.includes(scope.methodology)) {
|
|
1081
|
+
ctx.addIssue({
|
|
1082
|
+
code: "custom",
|
|
1083
|
+
path: ["scopes", name, "methodology"],
|
|
1084
|
+
message: `"${scope.methodology}" is not in the methodologies list`
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
BUILT_IN_KINDS2 = /* @__PURE__ */ new Set([
|
|
1090
|
+
"project",
|
|
1091
|
+
"spec",
|
|
1092
|
+
"adr",
|
|
1093
|
+
"plan",
|
|
1094
|
+
"story",
|
|
1095
|
+
"ops",
|
|
1096
|
+
"topic",
|
|
1097
|
+
"article",
|
|
1098
|
+
"src",
|
|
1099
|
+
"note",
|
|
1100
|
+
"artifact",
|
|
1101
|
+
"prompt"
|
|
1102
|
+
]);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
927
1106
|
// ../mcp/src/resources/authoring.ts
|
|
928
1107
|
function buildAuthoringRules(config) {
|
|
929
|
-
|
|
1108
|
+
const parts = [(config.authoring_rules ?? DEFAULT_AUTHORING_RULES).trim()];
|
|
1109
|
+
if (config.authoring_rules_extra) parts.push(config.authoring_rules_extra.trim());
|
|
1110
|
+
return ["## Authoring rules", "", parts.join("\n\n")].join("\n");
|
|
930
1111
|
}
|
|
931
1112
|
function buildSyncProtocol(config) {
|
|
932
|
-
|
|
1113
|
+
const parts = [(config.sync_protocol ?? DEFAULT_SYNC_PROTOCOL).trim()];
|
|
1114
|
+
if (config.sync_protocol_extra) parts.push(config.sync_protocol_extra.trim());
|
|
1115
|
+
return ["## Resync protocol", "", parts.join("\n\n")].join("\n");
|
|
933
1116
|
}
|
|
934
1117
|
function buildKindSelector(kinds) {
|
|
935
1118
|
const lines = ["## Kind selector", "", "| Signal | Kind | Where |", "|---|---|---|"];
|
|
936
|
-
for (const
|
|
937
|
-
const
|
|
1119
|
+
for (const entry of kinds) {
|
|
1120
|
+
const name = kindName2(entry);
|
|
1121
|
+
const pedagogy = typeof entry === "string" ? KIND_PEDAGOGY.get(entry) : entry;
|
|
938
1122
|
const signal = pedagogy?.signal ?? "\u2014";
|
|
939
1123
|
const where = pedagogy?.where ?? "\u2014";
|
|
940
|
-
lines.push(`| ${signal} | **${
|
|
1124
|
+
lines.push(`| ${signal} | **${name}** | ${where} |`);
|
|
941
1125
|
}
|
|
942
|
-
const
|
|
943
|
-
const
|
|
1126
|
+
const names = kinds.map(kindName2);
|
|
1127
|
+
const hasNote = names.includes("note");
|
|
1128
|
+
const hasAdrAndSpec = names.includes("adr") && names.includes("spec");
|
|
944
1129
|
if (hasNote || hasAdrAndSpec) {
|
|
945
1130
|
const hints = [];
|
|
946
1131
|
if (hasNote) hints.push("If none fits \u2192 note.");
|
|
@@ -953,12 +1138,16 @@ function buildKindSelector(kinds) {
|
|
|
953
1138
|
}
|
|
954
1139
|
return lines.join("\n");
|
|
955
1140
|
}
|
|
1141
|
+
function buildStatusLine(statuses) {
|
|
1142
|
+
const isCanonical = statuses.length === CANONICAL_STATUS_FLOW.length && statuses.every((s, i) => s === CANONICAL_STATUS_FLOW[i]);
|
|
1143
|
+
return isCanonical ? `**Statuses:** ${statuses.join(" \u2192 ")} (one-directional; superseded requires superseded_by link)` : `**Statuses:** ${statuses.join(", ")}`;
|
|
1144
|
+
}
|
|
956
1145
|
function buildVocabulary(config) {
|
|
957
1146
|
const lines = [
|
|
958
1147
|
"## Controlled vocabulary",
|
|
959
1148
|
"",
|
|
960
|
-
`**Kinds:** ${config.kinds.join(", ")}`,
|
|
961
|
-
|
|
1149
|
+
`**Kinds:** ${config.kinds.map(kindName2).join(", ")}`,
|
|
1150
|
+
buildStatusLine(config.statuses),
|
|
962
1151
|
`**Methodologies:** ${config.methodologies.join(", ")}`,
|
|
963
1152
|
`**Canonical tags:** ${config.tags.canonical.join(", ")}`
|
|
964
1153
|
];
|
|
@@ -1004,10 +1193,11 @@ function registerAuthoringResource(mcp, _vaultRoot, vaultConfig) {
|
|
|
1004
1193
|
}
|
|
1005
1194
|
);
|
|
1006
1195
|
}
|
|
1007
|
-
var KIND_PEDAGOGY, DEFAULT_AUTHORING_RULES, DEFAULT_SYNC_PROTOCOL;
|
|
1196
|
+
var KIND_PEDAGOGY, DEFAULT_AUTHORING_RULES, DEFAULT_SYNC_PROTOCOL, CANONICAL_STATUS_FLOW;
|
|
1008
1197
|
var init_authoring = __esm({
|
|
1009
1198
|
"../mcp/src/resources/authoring.ts"() {
|
|
1010
1199
|
"use strict";
|
|
1200
|
+
init_vault_config();
|
|
1011
1201
|
KIND_PEDAGOGY = /* @__PURE__ */ new Map([
|
|
1012
1202
|
[
|
|
1013
1203
|
"project",
|
|
@@ -1089,33 +1279,73 @@ var init_authoring = __esm({
|
|
|
1089
1279
|
]
|
|
1090
1280
|
]);
|
|
1091
1281
|
DEFAULT_AUTHORING_RULES = [
|
|
1282
|
+
"**Where things go**",
|
|
1283
|
+
"",
|
|
1092
1284
|
"- **Use the matching template** via `wiki://template/{domain}/{kind}` (MCP) or from `templates/` (filesystem). Don't hand-roll frontmatter.",
|
|
1093
|
-
'- **Quote prose-bearing frontmatter scalars.** `summary: "..."` \u2014 unquoted `Word: phrase` patterns break the YAML parser.',
|
|
1094
|
-
"- **On any edit, update the frontmatter `updated` field.**",
|
|
1095
1285
|
"- **Folder name = slug prefix in `projects/`.** `spec/spec-x.md`, `adr/adr-y.md`, `plan/plan-z.md`, `ops/ops-w.md`. Stories use `story-` prefix under `plan/{plan-name}/`.",
|
|
1096
|
-
"- **Research is flat.** Articles are `{subject}.md`; sources are `src-{slug}.md`. Avoid generic slugs (`architecture.md`, `notes.md`).",
|
|
1286
|
+
"- **Research is flat.** Articles are `{subject}.md`; sources are `src-{slug}.md`. The topic folder names a *frame*, the article slug names a *subject*. Avoid generic slugs (`architecture.md`, `notes.md`) and slugs that collide with project scopes.",
|
|
1287
|
+
"- **Extend before you split.** Prefer sharpening the existing spec, ADR, or article over creating a near-duplicate page. A new page needs a new subject, not a new session.",
|
|
1288
|
+
"",
|
|
1289
|
+
"**Frontmatter**",
|
|
1290
|
+
"",
|
|
1291
|
+
'- **`summary` is the retrieval contract.** One sentence stating the page\'s decision or claim, not its topic. Search and `prime` rank by it \u2014 "Chose X over Y because Z" surfaces; "About the sync pipeline" sinks.',
|
|
1292
|
+
'- **Quote prose-bearing scalars.** `summary: "..."` \u2014 unquoted `Word: phrase` patterns break the YAML parser.',
|
|
1293
|
+
"- **On any edit, update `updated`.** Never change `created` \u2014 it is write-once.",
|
|
1097
1294
|
"- **Notes have no `kind` field** \u2014 implied by location. Sync sets `kind: note`.",
|
|
1098
1295
|
"- **Reuse existing tags** (visible in `prime` response `top_tags`). No synonyms.",
|
|
1296
|
+
"",
|
|
1297
|
+
"**Content**",
|
|
1298
|
+
"",
|
|
1299
|
+
"- **ADR and ops pages are predicate-only.** No definitional preambles for established vocabulary, no narrative, no marketing. The audience is the project team \u2014 assume fluency.",
|
|
1300
|
+
"- **Spec / ADR edits land inline with the change that surfaces them.** Don't queue corrections in plans \u2014 the spec reflects current code at every commit.",
|
|
1301
|
+
"",
|
|
1302
|
+
"**Linking**",
|
|
1303
|
+
"",
|
|
1304
|
+
"- **Cross-reference with `[[wikilinks]]`, and link every mention of another vault page** \u2014 backlinks are the navigation graph for humans and agents alike. Don't link pages that don't exist yet: dangling links fail validation.",
|
|
1099
1305
|
"- **ADR supersession is bidirectional**: `superseded_by` on the old ADR + `supersedes` on the new one.",
|
|
1100
|
-
"- **Sources convention**: external paths/URLs go inline in body text. Vault-internal `raw/` paths go in frontmatter `sources:` array. Don't mix the two surfaces."
|
|
1101
|
-
"- **Spec / ADR edits land inline with the slice that surfaces them.** Don't queue corrections in plans. The spec must reflect current code at every commit."
|
|
1306
|
+
"- **Sources convention**: external paths/URLs go inline in body text. Vault-internal `raw/` paths go in frontmatter `sources:` array. Don't mix the two surfaces."
|
|
1102
1307
|
].join("\n");
|
|
1103
|
-
DEFAULT_SYNC_PROTOCOL =
|
|
1308
|
+
DEFAULT_SYNC_PROTOCOL = [
|
|
1309
|
+
"Edit the smallest set of files that reflects the change. A milestone tick is plan-only; don't cascade to index.md unless phase or status changed. Controlled-vocabulary edits (`vault.yaml`) need explicit user approval.",
|
|
1310
|
+
"After editing wiki pages, run `kmd validate` and fix findings before `kmd sync` \u2014 it checks frontmatter shape, vocabulary membership, and link integrity."
|
|
1311
|
+
].join("\n");
|
|
1312
|
+
CANONICAL_STATUS_FLOW = ["draft", "active", "superseded", "archived"];
|
|
1104
1313
|
}
|
|
1105
1314
|
});
|
|
1106
1315
|
|
|
1107
1316
|
// ../mcp/src/resources/templates.ts
|
|
1108
|
-
import { readFile as
|
|
1109
|
-
import { join as
|
|
1110
|
-
function
|
|
1111
|
-
const
|
|
1112
|
-
for (const
|
|
1317
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
1318
|
+
import { join as join7 } from "node:path";
|
|
1319
|
+
function customTemplates(config) {
|
|
1320
|
+
const specs = [];
|
|
1321
|
+
for (const entry of config.kinds) {
|
|
1322
|
+
if (typeof entry === "string" || BUILT_IN_KINDS2.has(entry.name)) continue;
|
|
1323
|
+
specs.push({
|
|
1324
|
+
uri: `wiki://template/${entry.name}`,
|
|
1325
|
+
name: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
|
|
1326
|
+
file: `${entry.name}.md`,
|
|
1327
|
+
description: entry.signal
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
return specs;
|
|
1331
|
+
}
|
|
1332
|
+
function registerTemplateResources(mcp, vaultRoot2, vaultConfig) {
|
|
1333
|
+
const dir = join7(vaultRoot2, "templates");
|
|
1334
|
+
const templates = [...TEMPLATES, ...customTemplates(vaultConfig)];
|
|
1335
|
+
for (const tmpl of templates) {
|
|
1113
1336
|
mcp.registerResource(
|
|
1114
1337
|
tmpl.name,
|
|
1115
1338
|
tmpl.uri,
|
|
1116
1339
|
{ description: tmpl.description, mimeType: "text/markdown" },
|
|
1117
1340
|
async (uri) => {
|
|
1118
|
-
|
|
1341
|
+
let text;
|
|
1342
|
+
try {
|
|
1343
|
+
text = await readFile5(join7(dir, tmpl.file), "utf8");
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
throw new Error(`template file missing: templates/${tmpl.file} (${tmpl.uri})`, {
|
|
1346
|
+
cause: err
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1119
1349
|
return {
|
|
1120
1350
|
contents: [
|
|
1121
1351
|
{
|
|
@@ -1129,7 +1359,7 @@ function registerTemplateResources(mcp, vaultRoot2) {
|
|
|
1129
1359
|
);
|
|
1130
1360
|
}
|
|
1131
1361
|
const indexLines = ["# Wiki Templates", ""];
|
|
1132
|
-
for (const tmpl of
|
|
1362
|
+
for (const tmpl of templates) {
|
|
1133
1363
|
indexLines.push(`- **${tmpl.name}** \u2014 \`${tmpl.uri}\` `);
|
|
1134
1364
|
indexLines.push(` ${tmpl.description}`);
|
|
1135
1365
|
}
|
|
@@ -1150,6 +1380,7 @@ var TEMPLATES;
|
|
|
1150
1380
|
var init_templates = __esm({
|
|
1151
1381
|
"../mcp/src/resources/templates.ts"() {
|
|
1152
1382
|
"use strict";
|
|
1383
|
+
init_vault_config();
|
|
1153
1384
|
TEMPLATES = [
|
|
1154
1385
|
{
|
|
1155
1386
|
uri: "wiki://template/project/index",
|
|
@@ -1289,15 +1520,15 @@ var init_toolResponse = __esm({
|
|
|
1289
1520
|
});
|
|
1290
1521
|
|
|
1291
1522
|
// ../mcp/src/tools/prime.ts
|
|
1292
|
-
import { readFile as
|
|
1293
|
-
import { basename as basename2, join as
|
|
1294
|
-
import { z as
|
|
1523
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1524
|
+
import { basename as basename2, join as join8 } from "node:path";
|
|
1525
|
+
import { z as z5 } from "zod";
|
|
1295
1526
|
function pathSlug(p) {
|
|
1296
1527
|
return basename2(p).replace(/\.md$/, "");
|
|
1297
1528
|
}
|
|
1298
1529
|
async function readIndexFm(vaultRoot2, scope) {
|
|
1299
1530
|
try {
|
|
1300
|
-
const raw = await
|
|
1531
|
+
const raw = await readFile6(join8(vaultRoot2, "projects", scope, "index.md"), "utf8");
|
|
1301
1532
|
return parseFrontmatter2(raw).data;
|
|
1302
1533
|
} catch {
|
|
1303
1534
|
return {};
|
|
@@ -1305,7 +1536,7 @@ async function readIndexFm(vaultRoot2, scope) {
|
|
|
1305
1536
|
}
|
|
1306
1537
|
async function readPrimer(vaultRoot2, scope) {
|
|
1307
1538
|
try {
|
|
1308
|
-
const raw = await
|
|
1539
|
+
const raw = await readFile6(join8(vaultRoot2, "projects", scope, "primer.md"), "utf8");
|
|
1309
1540
|
return parseFrontmatter2(raw).content.trim().replace(/^#\s+[^\n]+\n+/, "");
|
|
1310
1541
|
} catch {
|
|
1311
1542
|
return "";
|
|
@@ -1432,7 +1663,7 @@ function renderMarkdown(d, config, task) {
|
|
|
1432
1663
|
lines.push(countEntries.map(([k, n]) => `${k}: ${n}`).join(" | "));
|
|
1433
1664
|
}
|
|
1434
1665
|
lines.push("", "## Vocabulary");
|
|
1435
|
-
lines.push(`kinds: ${config.kinds.join(", ")}`);
|
|
1666
|
+
lines.push(`kinds: ${config.kinds.map(kindName2).join(", ")}`);
|
|
1436
1667
|
lines.push(`statuses: ${config.statuses.join(", ")}`);
|
|
1437
1668
|
lines.push(`tags: ${config.tags.canonical.join(", ")}`);
|
|
1438
1669
|
if (d.top_tags.length > 0) {
|
|
@@ -1466,7 +1697,7 @@ function renderMarkdown(d, config, task) {
|
|
|
1466
1697
|
lines.push(
|
|
1467
1698
|
"",
|
|
1468
1699
|
"---",
|
|
1469
|
-
"Authoring wiki content? Read `wiki://authoring` for kind selector, rules, and templates."
|
|
1700
|
+
"Authoring wiki content? Read `wiki://authoring` for kind selector, rules, and templates. After edits: `kmd validate`, then `kmd sync`."
|
|
1470
1701
|
);
|
|
1471
1702
|
return lines.join("\n");
|
|
1472
1703
|
}
|
|
@@ -1495,15 +1726,16 @@ var init_prime = __esm({
|
|
|
1495
1726
|
init_frontmatter2();
|
|
1496
1727
|
init_fts();
|
|
1497
1728
|
init_toolResponse();
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1729
|
+
init_vault_config();
|
|
1730
|
+
PrimeInputSchema = z5.object({
|
|
1731
|
+
scope: z5.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
|
|
1732
|
+
task: z5.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
|
|
1501
1733
|
});
|
|
1502
1734
|
}
|
|
1503
1735
|
});
|
|
1504
1736
|
|
|
1505
1737
|
// ../mcp/src/tools/search.ts
|
|
1506
|
-
import { z as
|
|
1738
|
+
import { z as z6 } from "zod";
|
|
1507
1739
|
function search(deps, input) {
|
|
1508
1740
|
const ftsQuery = sanitizeFtsQuery(input.query);
|
|
1509
1741
|
if (!ftsQuery) return { results: [] };
|
|
@@ -1550,15 +1782,15 @@ var init_search = __esm({
|
|
|
1550
1782
|
"use strict";
|
|
1551
1783
|
init_fts();
|
|
1552
1784
|
init_toolResponse();
|
|
1553
|
-
SearchInputSchema =
|
|
1554
|
-
query:
|
|
1785
|
+
SearchInputSchema = z6.object({
|
|
1786
|
+
query: z6.string().min(1).describe(
|
|
1555
1787
|
"Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
|
|
1556
1788
|
),
|
|
1557
|
-
scope:
|
|
1558
|
-
kind:
|
|
1789
|
+
scope: z6.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
|
|
1790
|
+
kind: z6.string().optional().describe(
|
|
1559
1791
|
"Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
|
|
1560
1792
|
),
|
|
1561
|
-
limit:
|
|
1793
|
+
limit: z6.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
|
|
1562
1794
|
});
|
|
1563
1795
|
}
|
|
1564
1796
|
});
|
|
@@ -1586,7 +1818,7 @@ function buildServer(args) {
|
|
|
1586
1818
|
return handleSearch({ db }, input);
|
|
1587
1819
|
}
|
|
1588
1820
|
);
|
|
1589
|
-
registerTemplateResources(mcp, vaultRoot2);
|
|
1821
|
+
registerTemplateResources(mcp, vaultRoot2, vaultConfig);
|
|
1590
1822
|
registerAuthoringResource(mcp, vaultRoot2, vaultConfig);
|
|
1591
1823
|
return mcp;
|
|
1592
1824
|
}
|
|
@@ -1600,51 +1832,6 @@ var init_server = __esm({
|
|
|
1600
1832
|
}
|
|
1601
1833
|
});
|
|
1602
1834
|
|
|
1603
|
-
// ../mcp/src/vault-config.ts
|
|
1604
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
1605
|
-
import { join as join7 } from "node:path";
|
|
1606
|
-
import { parse as parse2 } from "yaml";
|
|
1607
|
-
import { z as z6 } from "zod";
|
|
1608
|
-
async function loadVaultConfig2(vaultRoot2) {
|
|
1609
|
-
const path = join7(vaultRoot2, "vault.yaml");
|
|
1610
|
-
let raw;
|
|
1611
|
-
try {
|
|
1612
|
-
raw = await readFile6(path, "utf8");
|
|
1613
|
-
} catch (err) {
|
|
1614
|
-
throw new Error(`vault.yaml not found at ${path}`, { cause: err });
|
|
1615
|
-
}
|
|
1616
|
-
const parsed = VaultConfigSchema2.safeParse(parse2(raw));
|
|
1617
|
-
if (!parsed.success) {
|
|
1618
|
-
const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
1619
|
-
throw new Error(`Invalid vault.yaml at ${path}:
|
|
1620
|
-
${issues}`);
|
|
1621
|
-
}
|
|
1622
|
-
return parsed.data;
|
|
1623
|
-
}
|
|
1624
|
-
var ScopeSchema2, VaultConfigSchema2;
|
|
1625
|
-
var init_vault_config = __esm({
|
|
1626
|
-
"../mcp/src/vault-config.ts"() {
|
|
1627
|
-
"use strict";
|
|
1628
|
-
ScopeSchema2 = z6.object({
|
|
1629
|
-
repo: z6.string().optional(),
|
|
1630
|
-
methodology: z6.enum(["sdd", "tdd", "hybrid"]).optional(),
|
|
1631
|
-
status: z6.string()
|
|
1632
|
-
});
|
|
1633
|
-
VaultConfigSchema2 = z6.object({
|
|
1634
|
-
scopes: z6.record(z6.string(), ScopeSchema2),
|
|
1635
|
-
kinds: z6.array(z6.string()),
|
|
1636
|
-
statuses: z6.array(z6.string()),
|
|
1637
|
-
methodologies: z6.array(z6.string()),
|
|
1638
|
-
tags: z6.object({
|
|
1639
|
-
canonical: z6.array(z6.string()),
|
|
1640
|
-
aliases: z6.record(z6.string(), z6.string())
|
|
1641
|
-
}),
|
|
1642
|
-
authoring_rules: z6.string().optional(),
|
|
1643
|
-
sync_protocol: z6.string().optional()
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
});
|
|
1647
|
-
|
|
1648
1835
|
// ../mcp/src/start.ts
|
|
1649
1836
|
var start_exports = {};
|
|
1650
1837
|
__export(start_exports, {
|
|
@@ -1759,9 +1946,9 @@ async function run() {
|
|
|
1759
1946
|
const sub = positionals[1];
|
|
1760
1947
|
if (sub === "reset") {
|
|
1761
1948
|
const { homedir: homedir4 } = await import("node:os");
|
|
1762
|
-
const { join:
|
|
1949
|
+
const { join: join9 } = await import("node:path");
|
|
1763
1950
|
const { unlinkSync } = await import("node:fs");
|
|
1764
|
-
const dbPath =
|
|
1951
|
+
const dbPath = join9(homedir4(), ".kmd", "db", "index.db");
|
|
1765
1952
|
let deleted = false;
|
|
1766
1953
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
1767
1954
|
try {
|
|
@@ -1781,10 +1968,10 @@ async function run() {
|
|
|
1781
1968
|
case "--version":
|
|
1782
1969
|
case "-v": {
|
|
1783
1970
|
const { readFileSync } = await import("node:fs");
|
|
1784
|
-
const { join:
|
|
1971
|
+
const { join: join9, dirname } = await import("node:path");
|
|
1785
1972
|
const { fileURLToPath } = await import("node:url");
|
|
1786
1973
|
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
1787
|
-
const pkg = JSON.parse(readFileSync(
|
|
1974
|
+
const pkg = JSON.parse(readFileSync(join9(pkgDir, "package.json"), "utf8"));
|
|
1788
1975
|
console.log(pkg.version);
|
|
1789
1976
|
break;
|
|
1790
1977
|
}
|