@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 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.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({
@@ -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
- sync_protocol: 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
+ }
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 (typeof data.title !== "string" || data.title.trim() === "") return false;
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.includes(kind)) {
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 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
+ ];
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 join3 } from "node:path";
965
+ import { join as join4 } from "node:path";
869
966
  function createDatabase() {
870
- const dbDir = join3(homedir2(), ".kmd", "db");
871
- const dbPath = join3(dbDir, "index.db");
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 join4 } from "node:path";
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 = join4(homedir3(), ".local", "state", "wiki-mcp");
900
- 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");
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
- return ["## Authoring rules", "", config.authoring_rules ?? DEFAULT_AUTHORING_RULES].join("\n");
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
- return ["## Resync protocol", "", config.sync_protocol ?? DEFAULT_SYNC_PROTOCOL].join("\n");
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 kind of kinds) {
937
- 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;
938
1122
  const signal = pedagogy?.signal ?? "\u2014";
939
1123
  const where = pedagogy?.where ?? "\u2014";
940
- lines.push(`| ${signal} | **${kind}** | ${where} |`);
1124
+ lines.push(`| ${signal} | **${name}** | ${where} |`);
941
1125
  }
942
- const hasNote = kinds.includes("note");
943
- 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");
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
- `**Statuses:** ${config.statuses.join(" \u2192 ")} (one-directional; superseded requires superseded_by link)`,
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 = "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 need explicit user approval.";
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 readFile4 } from "node:fs/promises";
1109
- import { join as join5 } from "node:path";
1110
- function registerTemplateResources(mcp, vaultRoot2) {
1111
- const dir = join5(vaultRoot2, "templates");
1112
- for (const tmpl of TEMPLATES) {
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
- const text = await readFile4(join5(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
+ }
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 TEMPLATES) {
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 readFile5 } from "node:fs/promises";
1293
- import { basename as basename2, join as join6 } from "node:path";
1294
- import { z as z4 } from "zod";
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 readFile5(join6(vaultRoot2, "projects", scope, "index.md"), "utf8");
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 readFile5(join6(vaultRoot2, "projects", scope, "primer.md"), "utf8");
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
- PrimeInputSchema = z4.object({
1499
- scope: z4.string().min(1).describe("Project scope to prime (matches projects/{scope}/ in the vault)."),
1500
- 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.")
1501
1733
  });
1502
1734
  }
1503
1735
  });
1504
1736
 
1505
1737
  // ../mcp/src/tools/search.ts
1506
- import { z as z5 } from "zod";
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 = z5.object({
1554
- query: z5.string().min(1).describe(
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: z5.string().optional().describe('Optional: restrict to a project scope (e.g. "ontology", "sotto").'),
1558
- 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(
1559
1791
  "Optional: restrict to a kind. Project kinds: spec, adr, plan, ops. Research kinds: article, src. Misc: note."
1560
1792
  ),
1561
- 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.")
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: join8 } = await import("node:path");
1949
+ const { join: join9 } = await import("node:path");
1763
1950
  const { unlinkSync } = await import("node:fs");
1764
- const dbPath = join8(homedir4(), ".kmd", "db", "index.db");
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: join8, dirname } = await import("node:path");
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(join8(pkgDir, "package.json"), "utf8"));
1974
+ const pkg = JSON.parse(readFileSync(join9(pkgDir, "package.json"), "utf8"));
1788
1975
  console.log(pkg.version);
1789
1976
  break;
1790
1977
  }