@bartolli/kmd 0.2.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 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,25 +101,61 @@ ${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.enum(["sdd", "tdd", "hybrid"]).optional(),
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(z.string()),
123
+ kinds: z.array(KindEntrySchema),
113
124
  statuses: z.array(z.string()),
114
125
  methodologies: z.array(z.string()),
115
126
  tags: z.object({
116
127
  canonical: z.array(z.string()),
117
128
  aliases: z.record(z.string(), z.string())
118
- })
129
+ }),
130
+ authoring_rules: z.string().optional(),
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
+ }
119
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
+ ]);
120
159
  }
121
160
  });
122
161
 
@@ -378,9 +417,13 @@ var init_sync = __esm({
378
417
  });
379
418
 
380
419
  // ../cli/src/validate.ts
381
- 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
+ }
382
425
  function isIndexed(relPath, data) {
383
- if (typeof data.title !== "string" || data.title.trim() === "") return false;
426
+ if (!hasIndexableTitle(data)) return false;
384
427
  if (typeof data.kind === "string" && data.kind !== "") return true;
385
428
  return relPath.startsWith("notes/");
386
429
  }
@@ -402,6 +445,45 @@ function checkRequiredFields(relPath, data) {
402
445
  });
403
446
  }
404
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
+ }
405
487
  return findings;
406
488
  }
407
489
  function checkTagsRequired(relPath, data) {
@@ -433,7 +515,7 @@ function checkFolderSlug(relPath, data) {
433
515
  function checkVocabulary(relPath, data, cfg) {
434
516
  const findings = [];
435
517
  const kind = data.kind;
436
- if (typeof kind === "string" && !cfg.kinds.includes(kind)) {
518
+ if (typeof kind === "string" && !cfg.kinds.some((k) => kindName(k) === kind)) {
437
519
  findings.push({
438
520
  path: relPath,
439
521
  rule: "kind-vocabulary",
@@ -572,7 +654,6 @@ function checkSupersededLink(relPath, data) {
572
654
  function checkIndexedPage(relPath, data, body, cfg, refIndex) {
573
655
  if (!isIndexed(relPath, data)) return [];
574
656
  return [
575
- ...checkRequiredFields(relPath, data),
576
657
  ...checkTagsRequired(relPath, data),
577
658
  ...checkFolderSlug(relPath, data),
578
659
  ...checkVocabulary(relPath, data, cfg),
@@ -599,7 +680,11 @@ function validatePage(relPath, raw, cfg, refIndex) {
599
680
  if (isPrimer(relPath)) {
600
681
  return checkBodyLinks(relPath, parsed.content, refIndex);
601
682
  }
602
- return checkIndexedPage(relPath, parsed.data, parsed.content, cfg, refIndex);
683
+ return [
684
+ ...checkRequiredFields(relPath, parsed.data),
685
+ ...checkCustomKindFloor(relPath, parsed.data, cfg),
686
+ ...checkIndexedPage(relPath, parsed.data, parsed.content, cfg, refIndex)
687
+ ];
603
688
  }
604
689
  function hasErrors(findings) {
605
690
  return findings.some((f) => f.severity === "error");
@@ -702,9 +787,22 @@ async function validateVault(root) {
702
787
  }
703
788
  findings.push(...validateSupersession(pages));
704
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
+ }
705
803
  return findings;
706
804
  }
707
- var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS;
805
+ var REQUIRED_FIELDS, TAG_OPTIONAL_KINDS, FOLDER_PATTERNS, UNIVERSAL_FLOOR;
708
806
  var init_validate = __esm({
709
807
  "../cli/src/validate.ts"() {
710
808
  "use strict";
@@ -755,6 +853,7 @@ var init_validate = __esm({
755
853
  topic: /^research\/[^/]+\/index\.md$/,
756
854
  src: /^research\/[^/]+\/src-[^/]+\.md$/
757
855
  };
856
+ UNIVERSAL_FLOOR = ["title", "summary", "updated"];
758
857
  }
759
858
  });
760
859
 
@@ -863,10 +962,10 @@ var init_config2 = __esm({
863
962
  // ../mcp/src/db.ts
864
963
  import { mkdirSync as mkdirSync2 } from "node:fs";
865
964
  import { homedir as homedir2 } from "node:os";
866
- import { join as join3 } from "node:path";
965
+ import { join as join4 } from "node:path";
867
966
  function createDatabase() {
868
- const dbDir = join3(homedir2(), ".kmd", "db");
869
- const dbPath = join3(dbDir, "index.db");
967
+ const dbDir = join4(homedir2(), ".kmd", "db");
968
+ const dbPath = join4(dbDir, "index.db");
870
969
  mkdirSync2(dbDir, { recursive: true });
871
970
  return openDatabase(dbPath);
872
971
  }
@@ -880,7 +979,7 @@ var init_db = __esm({
880
979
  // ../mcp/src/lib/diag.ts
881
980
  import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
882
981
  import { homedir as homedir3 } from "node:os";
883
- import { join as join4 } from "node:path";
982
+ import { join as join5 } from "node:path";
884
983
  function diag(msg, data) {
885
984
  try {
886
985
  const line = data ? `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${msg} ${JSON.stringify(data)}
@@ -894,8 +993,8 @@ var DIAG_DIR, DIAG_LOG_PATH;
894
993
  var init_diag = __esm({
895
994
  "../mcp/src/lib/diag.ts"() {
896
995
  "use strict";
897
- DIAG_DIR = join4(homedir3(), ".local", "state", "wiki-mcp");
898
- DIAG_LOG_PATH = join4(DIAG_DIR, "server.log");
996
+ DIAG_DIR = join5(homedir3(), ".local", "state", "wiki-mcp");
997
+ DIAG_LOG_PATH = join5(DIAG_DIR, "server.log");
899
998
  try {
900
999
  mkdirSync3(DIAG_DIR, { recursive: true });
901
1000
  } catch {
@@ -922,31 +1021,111 @@ var init_logger = __esm({
922
1021
  }
923
1022
  });
924
1023
 
925
- // ../mcp/src/resources/authoring.ts
1024
+ // ../mcp/src/vault-config.ts
926
1025
  import { readFile as readFile4 } from "node:fs/promises";
927
- import { join as join5 } from "node:path";
928
- async function readAuthoringRules(vaultRoot2) {
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;
929
1035
  try {
930
- const raw = await readFile4(join5(vaultRoot2, "CLAUDE.md"), "utf8");
931
- const start = raw.indexOf("## Authoring rules");
932
- if (start === -1) return "";
933
- const after = raw.indexOf("\n## ", start + 1);
934
- const section = after === -1 ? raw.slice(start) : raw.slice(start, after);
935
- return section.trim();
936
- } catch {
937
- return "";
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}`);
938
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
+
1106
+ // ../mcp/src/resources/authoring.ts
1107
+ function buildAuthoringRules(config) {
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");
1111
+ }
1112
+ function buildSyncProtocol(config) {
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");
939
1116
  }
940
1117
  function buildKindSelector(kinds) {
941
1118
  const lines = ["## Kind selector", "", "| Signal | Kind | Where |", "|---|---|---|"];
942
- for (const kind of kinds) {
943
- const pedagogy = KIND_PEDAGOGY.get(kind);
1119
+ for (const entry of kinds) {
1120
+ const name = kindName2(entry);
1121
+ const pedagogy = typeof entry === "string" ? KIND_PEDAGOGY.get(entry) : entry;
944
1122
  const signal = pedagogy?.signal ?? "\u2014";
945
1123
  const where = pedagogy?.where ?? "\u2014";
946
- lines.push(`| ${signal} | **${kind}** | ${where} |`);
1124
+ lines.push(`| ${signal} | **${name}** | ${where} |`);
947
1125
  }
948
- const hasNote = kinds.includes("note");
949
- const hasAdrAndSpec = kinds.includes("adr") && kinds.includes("spec");
1126
+ const names = kinds.map(kindName2);
1127
+ const hasNote = names.includes("note");
1128
+ const hasAdrAndSpec = names.includes("adr") && names.includes("spec");
950
1129
  if (hasNote || hasAdrAndSpec) {
951
1130
  const hints = [];
952
1131
  if (hasNote) hints.push("If none fits \u2192 note.");
@@ -959,12 +1138,16 @@ function buildKindSelector(kinds) {
959
1138
  }
960
1139
  return lines.join("\n");
961
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
+ }
962
1145
  function buildVocabulary(config) {
963
1146
  const lines = [
964
1147
  "## Controlled vocabulary",
965
1148
  "",
966
- `**Kinds:** ${config.kinds.join(", ")}`,
967
- `**Statuses:** ${config.statuses.join(" \u2192 ")} (one-directional; superseded requires superseded_by link)`,
1149
+ `**Kinds:** ${config.kinds.map(kindName2).join(", ")}`,
1150
+ buildStatusLine(config.statuses),
968
1151
  `**Methodologies:** ${config.methodologies.join(", ")}`,
969
1152
  `**Canonical tags:** ${config.tags.canonical.join(", ")}`
970
1153
  ];
@@ -974,16 +1157,15 @@ function buildVocabulary(config) {
974
1157
  }
975
1158
  return lines.join("\n");
976
1159
  }
977
- function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
1160
+ function registerAuthoringResource(mcp, _vaultRoot, vaultConfig) {
978
1161
  mcp.registerResource(
979
1162
  "Authoring guide",
980
1163
  "wiki://authoring",
981
1164
  {
982
- description: "Wiki authoring pedagogy: kind selector, controlled vocabulary, authoring rules, and template URIs. Read before creating or editing wiki pages.",
1165
+ description: "Wiki authoring pedagogy: kind selector, controlled vocabulary, authoring rules, resync protocol, and template URIs. Read before creating or editing wiki pages.",
983
1166
  mimeType: "text/markdown"
984
1167
  },
985
1168
  async (uri) => {
986
- const rules = await readAuthoringRules(vaultRoot2);
987
1169
  const sections = [
988
1170
  "# Wiki authoring guide",
989
1171
  "",
@@ -993,11 +1175,12 @@ function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
993
1175
  "",
994
1176
  "## Templates",
995
1177
  "",
996
- "Full index with URIs and descriptions: `wiki://templates`"
1178
+ "Full index with URIs and descriptions: `wiki://templates`",
1179
+ "",
1180
+ buildAuthoringRules(vaultConfig),
1181
+ "",
1182
+ buildSyncProtocol(vaultConfig)
997
1183
  ];
998
- if (rules) {
999
- sections.push("", rules);
1000
- }
1001
1184
  return {
1002
1185
  contents: [
1003
1186
  {
@@ -1010,10 +1193,11 @@ function registerAuthoringResource(mcp, vaultRoot2, vaultConfig) {
1010
1193
  }
1011
1194
  );
1012
1195
  }
1013
- var KIND_PEDAGOGY;
1196
+ var KIND_PEDAGOGY, DEFAULT_AUTHORING_RULES, DEFAULT_SYNC_PROTOCOL, CANONICAL_STATUS_FLOW;
1014
1197
  var init_authoring = __esm({
1015
1198
  "../mcp/src/resources/authoring.ts"() {
1016
1199
  "use strict";
1200
+ init_vault_config();
1017
1201
  KIND_PEDAGOGY = /* @__PURE__ */ new Map([
1018
1202
  [
1019
1203
  "project",
@@ -1094,21 +1278,74 @@ var init_authoring = __esm({
1094
1278
  }
1095
1279
  ]
1096
1280
  ]);
1281
+ DEFAULT_AUTHORING_RULES = [
1282
+ "**Where things go**",
1283
+ "",
1284
+ "- **Use the matching template** via `wiki://template/{domain}/{kind}` (MCP) or from `templates/` (filesystem). Don't hand-roll frontmatter.",
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}/`.",
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.",
1294
+ "- **Notes have no `kind` field** \u2014 implied by location. Sync sets `kind: note`.",
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.",
1305
+ "- **ADR supersession is bidirectional**: `superseded_by` on the old ADR + `supersedes` on the new one.",
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."
1307
+ ].join("\n");
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"];
1097
1313
  }
1098
1314
  });
1099
1315
 
1100
1316
  // ../mcp/src/resources/templates.ts
1101
1317
  import { readFile as readFile5 } from "node:fs/promises";
1102
- import { join as join6 } from "node:path";
1103
- function registerTemplateResources(mcp, vaultRoot2) {
1104
- const dir = join6(vaultRoot2, "templates");
1105
- for (const tmpl of TEMPLATES) {
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) {
1106
1336
  mcp.registerResource(
1107
1337
  tmpl.name,
1108
1338
  tmpl.uri,
1109
1339
  { description: tmpl.description, mimeType: "text/markdown" },
1110
1340
  async (uri) => {
1111
- const text = await readFile5(join6(dir, tmpl.file), "utf8");
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
+ }
1112
1349
  return {
1113
1350
  contents: [
1114
1351
  {
@@ -1122,7 +1359,7 @@ function registerTemplateResources(mcp, vaultRoot2) {
1122
1359
  );
1123
1360
  }
1124
1361
  const indexLines = ["# Wiki Templates", ""];
1125
- for (const tmpl of TEMPLATES) {
1362
+ for (const tmpl of templates) {
1126
1363
  indexLines.push(`- **${tmpl.name}** \u2014 \`${tmpl.uri}\` `);
1127
1364
  indexLines.push(` ${tmpl.description}`);
1128
1365
  }
@@ -1143,6 +1380,7 @@ var TEMPLATES;
1143
1380
  var init_templates = __esm({
1144
1381
  "../mcp/src/resources/templates.ts"() {
1145
1382
  "use strict";
1383
+ init_vault_config();
1146
1384
  TEMPLATES = [
1147
1385
  {
1148
1386
  uri: "wiki://template/project/index",
@@ -1283,14 +1521,14 @@ var init_toolResponse = __esm({
1283
1521
 
1284
1522
  // ../mcp/src/tools/prime.ts
1285
1523
  import { readFile as readFile6 } from "node:fs/promises";
1286
- import { basename as basename2, join as join7 } from "node:path";
1287
- import { z as z4 } from "zod";
1524
+ import { basename as basename2, join as join8 } from "node:path";
1525
+ import { z as z5 } from "zod";
1288
1526
  function pathSlug(p) {
1289
1527
  return basename2(p).replace(/\.md$/, "");
1290
1528
  }
1291
1529
  async function readIndexFm(vaultRoot2, scope) {
1292
1530
  try {
1293
- const raw = await readFile6(join7(vaultRoot2, "projects", scope, "index.md"), "utf8");
1531
+ const raw = await readFile6(join8(vaultRoot2, "projects", scope, "index.md"), "utf8");
1294
1532
  return parseFrontmatter2(raw).data;
1295
1533
  } catch {
1296
1534
  return {};
@@ -1298,7 +1536,7 @@ async function readIndexFm(vaultRoot2, scope) {
1298
1536
  }
1299
1537
  async function readPrimer(vaultRoot2, scope) {
1300
1538
  try {
1301
- const raw = await readFile6(join7(vaultRoot2, "projects", scope, "primer.md"), "utf8");
1539
+ const raw = await readFile6(join8(vaultRoot2, "projects", scope, "primer.md"), "utf8");
1302
1540
  return parseFrontmatter2(raw).content.trim().replace(/^#\s+[^\n]+\n+/, "");
1303
1541
  } catch {
1304
1542
  return "";
@@ -1425,7 +1663,7 @@ function renderMarkdown(d, config, task) {
1425
1663
  lines.push(countEntries.map(([k, n]) => `${k}: ${n}`).join(" | "));
1426
1664
  }
1427
1665
  lines.push("", "## Vocabulary");
1428
- lines.push(`kinds: ${config.kinds.join(", ")}`);
1666
+ lines.push(`kinds: ${config.kinds.map(kindName2).join(", ")}`);
1429
1667
  lines.push(`statuses: ${config.statuses.join(", ")}`);
1430
1668
  lines.push(`tags: ${config.tags.canonical.join(", ")}`);
1431
1669
  if (d.top_tags.length > 0) {
@@ -1459,7 +1697,7 @@ function renderMarkdown(d, config, task) {
1459
1697
  lines.push(
1460
1698
  "",
1461
1699
  "---",
1462
- "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`."
1463
1701
  );
1464
1702
  return lines.join("\n");
1465
1703
  }
@@ -1488,15 +1726,16 @@ var init_prime = __esm({
1488
1726
  init_frontmatter2();
1489
1727
  init_fts();
1490
1728
  init_toolResponse();
1491
- PrimeInputSchema = z4.object({
1492
- scope: z4.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1493
- task: z4.string().optional().describe("Optional task description; surfaces top-3 tsvector-ranked relevant pages.")
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.")
1494
1733
  });
1495
1734
  }
1496
1735
  });
1497
1736
 
1498
1737
  // ../mcp/src/tools/search.ts
1499
- import { z as z5 } from "zod";
1738
+ import { z as z6 } from "zod";
1500
1739
  function search(deps, input) {
1501
1740
  const ftsQuery = sanitizeFtsQuery(input.query);
1502
1741
  if (!ftsQuery) return { results: [] };
@@ -1543,15 +1782,15 @@ var init_search = __esm({
1543
1782
  "use strict";
1544
1783
  init_fts();
1545
1784
  init_toolResponse();
1546
- SearchInputSchema = z5.object({
1547
- query: z5.string().min(1).describe(
1785
+ SearchInputSchema = z6.object({
1786
+ query: z6.string().min(1).describe(
1548
1787
  "Natural-language search query. Matched against title, summary, and body via SQLite FTS5."
1549
1788
  ),
1550
- scope: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1551
- kind: z5.string().optional().describe(
1789
+ scope: z6.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1790
+ kind: z6.string().optional().describe(
1552
1791
  "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1553
1792
  ),
1554
- limit: z5.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1793
+ limit: z6.number().int().min(1).max(50).default(5).describe("Maximum number of ranked results to return. Default 5.")
1555
1794
  });
1556
1795
  }
1557
1796
  });
@@ -1579,7 +1818,7 @@ function buildServer(args) {
1579
1818
  return handleSearch({ db }, input);
1580
1819
  }
1581
1820
  );
1582
- registerTemplateResources(mcp, vaultRoot2);
1821
+ registerTemplateResources(mcp, vaultRoot2, vaultConfig);
1583
1822
  registerAuthoringResource(mcp, vaultRoot2, vaultConfig);
1584
1823
  return mcp;
1585
1824
  }
@@ -1593,49 +1832,6 @@ var init_server = __esm({
1593
1832
  }
1594
1833
  });
1595
1834
 
1596
- // ../mcp/src/vault-config.ts
1597
- import { readFile as readFile7 } from "node:fs/promises";
1598
- import { join as join8 } from "node:path";
1599
- import { parse as parse2 } from "yaml";
1600
- import { z as z6 } from "zod";
1601
- async function loadVaultConfig2(vaultRoot2) {
1602
- const path = join8(vaultRoot2, "vault.yaml");
1603
- let raw;
1604
- try {
1605
- raw = await readFile7(path, "utf8");
1606
- } catch (err) {
1607
- throw new Error(`vault.yaml not found at ${path}`, { cause: err });
1608
- }
1609
- const parsed = VaultConfigSchema2.safeParse(parse2(raw));
1610
- if (!parsed.success) {
1611
- const issues = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
1612
- throw new Error(`Invalid vault.yaml at ${path}:
1613
- ${issues}`);
1614
- }
1615
- return parsed.data;
1616
- }
1617
- var ScopeSchema2, VaultConfigSchema2;
1618
- var init_vault_config = __esm({
1619
- "../mcp/src/vault-config.ts"() {
1620
- "use strict";
1621
- ScopeSchema2 = z6.object({
1622
- repo: z6.string().optional(),
1623
- methodology: z6.enum(["sdd", "tdd", "hybrid"]).optional(),
1624
- status: z6.string()
1625
- });
1626
- VaultConfigSchema2 = z6.object({
1627
- scopes: z6.record(z6.string(), ScopeSchema2),
1628
- kinds: z6.array(z6.string()),
1629
- statuses: z6.array(z6.string()),
1630
- methodologies: z6.array(z6.string()),
1631
- tags: z6.object({
1632
- canonical: z6.array(z6.string()),
1633
- aliases: z6.record(z6.string(), z6.string())
1634
- })
1635
- });
1636
- }
1637
- });
1638
-
1639
1835
  // ../mcp/src/start.ts
1640
1836
  var start_exports = {};
1641
1837
  __export(start_exports, {