mdkg 0.0.7 → 0.0.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/README.md CHANGED
@@ -94,6 +94,8 @@ mdkg task update task-1 --add-artifacts tests://unit.txt --add-tags release
94
94
  mdkg task done task-1 --checkpoint "release readiness milestone"
95
95
  ```
96
96
 
97
+ `mdkg task ...` remains limited to `task`, `bug`, and `test`, but `skills: [...]` is valid metadata on all work items (`epic`, `feat`, `task`, `bug`, `checkpoint`, `test`). mdkg-generated work items should validate and round-trip through the task mutators without manual frontmatter repair.
98
+
97
99
  Ensure and append baseline event memory:
98
100
 
99
101
  ```bash
@@ -184,6 +186,7 @@ Current source behavior:
184
186
  - `mdkg skill search "<query>" --xml|--toon|--md`
185
187
  - `mdkg skill show <slug> --xml|--toon|--md`
186
188
  - work items may reference `skills: [slug,...]`
189
+ - `skills` is valid on all work items (`epic`, `feat`, `task`, `bug`, `checkpoint`, `test`)
187
190
  - packs may include skills with `--skills` and `--skills-depth`
188
191
  - mdkg indexes and discovers skills but does not execute skill scripts
189
192
  - `SKILL.md` is canonical
@@ -219,6 +222,7 @@ Current direction:
219
222
  - use `init --agent` for deeper AI-agent bootstrap
220
223
  - keep `pack <id>` at the center of the human/agent loop
221
224
  - use `mdkg task ...` for structured state changes and markdown edits for narrative/body content
225
+ - keep validation strict and fix schema drift instead of papering over mdkg-generated inconsistencies
222
226
  - make event logging guided instead of purely manual
223
227
  - dogfood real skills inside the repo
224
228
  - make skill authoring first-class through `mdkg skill`
@@ -10,6 +10,7 @@ const config_1 = require("../core/config");
10
10
  const index_cache_1 = require("../graph/index_cache");
11
11
  const node_1 = require("../graph/node");
12
12
  const indexer_1 = require("../graph/indexer");
13
+ const template_schema_1 = require("../graph/template_schema");
13
14
  const loader_1 = require("../templates/loader");
14
15
  const date_1 = require("../util/date");
15
16
  const errors_1 = require("../util/errors");
@@ -172,7 +173,7 @@ function runNewCommand(options) {
172
173
  throw new errors_1.UsageError(`--status must be one of ${Array.from(allowed).join(", ")}`);
173
174
  }
174
175
  }
175
- else if (type === "dec") {
176
+ else if (node_1.DEC_TYPES.has(type)) {
176
177
  status = statusInput ?? "proposed";
177
178
  if (!DEC_STATUS.has(status)) {
178
179
  throw new errors_1.UsageError(`--status must be one of ${Array.from(DEC_STATUS).join(", ")}`);
@@ -196,12 +197,20 @@ function runNewCommand(options) {
196
197
  else if (options.priority !== undefined) {
197
198
  throw new errors_1.UsageError("--priority is only valid for work items");
198
199
  }
199
- if (!node_1.WORK_TYPES.has(type)) {
200
- if (options.epic || options.parent || options.prev || options.next || options.blockedBy || options.blocks) {
201
- throw new errors_1.UsageError("epic/parent/prev/next/blocked-by/blocks are only valid for work items");
200
+ const graphFields = [
201
+ ["epic", options.epic],
202
+ ["parent", options.parent],
203
+ ["prev", options.prev],
204
+ ["next", options.next],
205
+ ["blocked_by", options.blockedBy],
206
+ ["blocks", options.blocks],
207
+ ];
208
+ for (const [key, raw] of graphFields) {
209
+ if (raw && !(0, template_schema_1.schemaAllowsKey)(type, key)) {
210
+ throw new errors_1.UsageError(`--${key.replace("_", "-")} is not valid for ${type} nodes`);
202
211
  }
203
212
  }
204
- if (options.cases && type !== "test") {
213
+ if (options.cases && !(0, template_schema_1.schemaAllowsKey)(type, "cases")) {
205
214
  throw new errors_1.UsageError("--cases is only valid for test nodes");
206
215
  }
207
216
  const epic = options.epic ? normalizeIdRef(options.epic, "--epic") : undefined;
@@ -219,7 +228,7 @@ function runNewCommand(options) {
219
228
  const skills = normalizeSkillList(options.skills);
220
229
  const links = normalizeList(options.links);
221
230
  const artifacts = normalizeList(options.artifacts);
222
- if (skills.length > 0 && !node_1.WORK_TYPES.has(type)) {
231
+ if (skills.length > 0 && !(0, template_schema_1.schemaAllowsKey)(type, "skills")) {
223
232
  throw new errors_1.UsageError("--skills is only valid for work items");
224
233
  }
225
234
  if (type === "dec" && options.supersedes) {
@@ -258,6 +267,7 @@ function runNewCommand(options) {
258
267
  }
259
268
  const today = (0, date_1.formatDate)(options.now ?? new Date());
260
269
  const template = (0, loader_1.loadTemplate)(options.root, config, type, options.template);
270
+ const schema = (0, template_schema_1.getNodeSchema)(type);
261
271
  const content = (0, loader_1.renderTemplate)(template, {
262
272
  id,
263
273
  type,
@@ -282,7 +292,7 @@ function runNewCommand(options) {
282
292
  supersedes: options.supersedes ? options.supersedes.toLowerCase() : undefined,
283
293
  created: today,
284
294
  updated: today,
285
- });
295
+ }, schema?.allowedKeys);
286
296
  fs_1.default.mkdirSync(targetDir, { recursive: true });
287
297
  fs_1.default.writeFileSync(filePath, content, "utf8");
288
298
  if (config.index.auto_reindex && !noReindex) {
@@ -9,10 +9,12 @@ exports.runTaskDoneCommand = runTaskDoneCommand;
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const config_1 = require("../core/config");
12
+ const node_1 = require("../graph/node");
12
13
  const indexer_1 = require("../graph/indexer");
13
14
  const index_cache_1 = require("../graph/index_cache");
14
15
  const frontmatter_1 = require("../graph/frontmatter");
15
16
  const skills_indexer_1 = require("../graph/skills_indexer");
17
+ const template_schema_1 = require("../graph/template_schema");
16
18
  const date_1 = require("../util/date");
17
19
  const errors_1 = require("../util/errors");
18
20
  const qid_1 = require("../util/qid");
@@ -112,7 +114,12 @@ function loadMutableTaskNode(root, idOrQid, wsHint) {
112
114
  }
113
115
  const filePath = path_1.default.resolve(root, node.path);
114
116
  const content = fs_1.default.readFileSync(filePath, "utf8");
115
- const parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
117
+ const parsed = (0, node_1.parseNode)(content, filePath, {
118
+ workStatusEnum: config.work.status_enum,
119
+ priorityMin: config.work.priority_min,
120
+ priorityMax: config.work.priority_max,
121
+ templateSchemas: (0, template_schema_1.loadTemplateSchemas)(root, config, node_1.ALLOWED_TYPES),
122
+ });
116
123
  return {
117
124
  config,
118
125
  index,
@@ -4,24 +4,13 @@ exports.ALLOWED_TYPES = exports.DEC_TYPES = exports.WORK_TYPES = void 0;
4
4
  exports.parseNode = parseNode;
5
5
  const frontmatter_1 = require("./frontmatter");
6
6
  const edges_1 = require("./edges");
7
+ const template_schema_1 = require("./template_schema");
8
+ Object.defineProperty(exports, "ALLOWED_TYPES", { enumerable: true, get: function () { return template_schema_1.ALLOWED_TYPES; } });
9
+ Object.defineProperty(exports, "DEC_TYPES", { enumerable: true, get: function () { return template_schema_1.DEC_TYPES; } });
10
+ Object.defineProperty(exports, "WORK_TYPES", { enumerable: true, get: function () { return template_schema_1.WORK_TYPES; } });
7
11
  const id_1 = require("../util/id");
8
12
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
9
13
  const DEC_ID_RE = /^dec-[0-9]+$/;
10
- exports.WORK_TYPES = new Set(["epic", "feat", "task", "bug", "checkpoint", "test"]);
11
- exports.DEC_TYPES = new Set(["dec"]);
12
- exports.ALLOWED_TYPES = new Set([
13
- "rule",
14
- "prd",
15
- "edd",
16
- "dec",
17
- "prop",
18
- "epic",
19
- "feat",
20
- "task",
21
- "bug",
22
- "checkpoint",
23
- "test",
24
- ]);
25
14
  const DEC_STATUS = new Set(["proposed", "accepted", "rejected", "superseded"]);
26
15
  const SKILL_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
27
16
  function formatError(filePath, message) {
@@ -153,8 +142,8 @@ function validateTemplateKeys(frontmatter, schema, filePath) {
153
142
  function parseNode(content, filePath, options) {
154
143
  const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
155
144
  const type = requireLowercase(expectString(frontmatter, "type", filePath), "type", filePath);
156
- if (!exports.ALLOWED_TYPES.has(type)) {
157
- throw formatError(filePath, `type must be one of ${Array.from(exports.ALLOWED_TYPES).join(", ")}`);
145
+ if (!template_schema_1.ALLOWED_TYPES.has(type)) {
146
+ throw formatError(filePath, `type must be one of ${Array.from(template_schema_1.ALLOWED_TYPES).join(", ")}`);
158
147
  }
159
148
  const schema = requireTemplateSchema(type, options.templateSchemas, filePath);
160
149
  validateTemplateKeys(frontmatter, schema, filePath);
@@ -165,7 +154,7 @@ function parseNode(content, filePath, options) {
165
154
  const statusValue = optionalString(frontmatter, "status", filePath);
166
155
  let status = undefined;
167
156
  const workStatus = new Set(options.workStatusEnum.map((value) => value.toLowerCase()));
168
- if (exports.WORK_TYPES.has(type)) {
157
+ if (template_schema_1.WORK_TYPES.has(type)) {
169
158
  if (!statusValue) {
170
159
  throw formatError(filePath, "status is required for work items");
171
160
  }
@@ -175,7 +164,7 @@ function parseNode(content, filePath, options) {
175
164
  }
176
165
  status = normalized;
177
166
  }
178
- else if (exports.DEC_TYPES.has(type)) {
167
+ else if (template_schema_1.DEC_TYPES.has(type)) {
179
168
  if (!statusValue) {
180
169
  throw formatError(filePath, "status is required for decision records");
181
170
  }
@@ -191,7 +180,7 @@ function parseNode(content, filePath, options) {
191
180
  const priorityValue = optionalString(frontmatter, "priority", filePath);
192
181
  let priority = undefined;
193
182
  if (priorityValue !== undefined) {
194
- if (!exports.WORK_TYPES.has(type)) {
183
+ if (!template_schema_1.WORK_TYPES.has(type)) {
195
184
  throw formatError(filePath, "priority is only allowed for work items");
196
185
  }
197
186
  priority = parsePriority(priorityValue, filePath, options.priorityMin, options.priorityMax);
@@ -204,13 +193,13 @@ function parseNode(content, filePath, options) {
204
193
  const aliases = requireLowercaseList(optionalList(frontmatter, "aliases", filePath), "aliases", filePath);
205
194
  const skillsRaw = optionalList(frontmatter, "skills", filePath);
206
195
  const skills = normalizeSkillList(skillsRaw, filePath);
207
- if (skills.length > 0 && !exports.WORK_TYPES.has(type)) {
196
+ if (skills.length > 0 && !template_schema_1.WORK_TYPES.has(type)) {
208
197
  throw formatError(filePath, "skills is only allowed for work items");
209
198
  }
210
199
  normalizeIdList(optionalList(frontmatter, "scope", filePath), "scope", filePath);
211
200
  const supersedesValue = optionalString(frontmatter, "supersedes", filePath);
212
201
  if (supersedesValue !== undefined) {
213
- if (!exports.DEC_TYPES.has(type)) {
202
+ if (!template_schema_1.DEC_TYPES.has(type)) {
214
203
  throw formatError(filePath, "supersedes is only allowed for decision records");
215
204
  }
216
205
  const normalized = requireLowercase(supersedesValue, "supersedes", filePath);
@@ -3,10 +3,77 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ALLOWED_TYPES = exports.DEC_TYPES = exports.WORK_TYPES = void 0;
7
+ exports.buildNodeSchemas = buildNodeSchemas;
8
+ exports.getNodeSchema = getNodeSchema;
9
+ exports.schemaAllowsKey = schemaAllowsKey;
6
10
  exports.loadTemplateSchemas = loadTemplateSchemas;
7
11
  const fs_1 = __importDefault(require("fs"));
8
12
  const path_1 = __importDefault(require("path"));
9
13
  const frontmatter_1 = require("./frontmatter");
14
+ exports.WORK_TYPES = new Set(["epic", "feat", "task", "bug", "checkpoint", "test"]);
15
+ exports.DEC_TYPES = new Set(["dec"]);
16
+ exports.ALLOWED_TYPES = new Set([
17
+ "rule",
18
+ "prd",
19
+ "edd",
20
+ "dec",
21
+ "prop",
22
+ "epic",
23
+ "feat",
24
+ "task",
25
+ "bug",
26
+ "checkpoint",
27
+ "test",
28
+ ]);
29
+ const COMMON_SCALAR_KEYS = ["id", "type", "title", "created", "updated"];
30
+ const COMMON_LIST_KEYS = ["tags", "owners", "links", "artifacts", "relates", "refs", "aliases"];
31
+ const NODE_SCHEMA_DEFS = {
32
+ rule: {
33
+ scalars: [...COMMON_SCALAR_KEYS],
34
+ lists: [...COMMON_LIST_KEYS],
35
+ },
36
+ prd: {
37
+ scalars: [...COMMON_SCALAR_KEYS],
38
+ lists: [...COMMON_LIST_KEYS],
39
+ },
40
+ edd: {
41
+ scalars: [...COMMON_SCALAR_KEYS],
42
+ lists: [...COMMON_LIST_KEYS],
43
+ },
44
+ prop: {
45
+ scalars: [...COMMON_SCALAR_KEYS],
46
+ lists: [...COMMON_LIST_KEYS],
47
+ },
48
+ dec: {
49
+ scalars: [...COMMON_SCALAR_KEYS, "status", "supersedes"],
50
+ lists: [...COMMON_LIST_KEYS],
51
+ },
52
+ epic: {
53
+ scalars: [...COMMON_SCALAR_KEYS, "status", "priority"],
54
+ lists: [...COMMON_LIST_KEYS, "blocked_by", "blocks", "skills"],
55
+ },
56
+ feat: {
57
+ scalars: [...COMMON_SCALAR_KEYS, "status", "priority", "epic", "parent", "prev", "next"],
58
+ lists: [...COMMON_LIST_KEYS, "blocked_by", "blocks", "skills"],
59
+ },
60
+ task: {
61
+ scalars: [...COMMON_SCALAR_KEYS, "status", "priority", "epic", "parent", "prev", "next"],
62
+ lists: [...COMMON_LIST_KEYS, "blocked_by", "blocks", "skills"],
63
+ },
64
+ bug: {
65
+ scalars: [...COMMON_SCALAR_KEYS, "status", "priority", "epic", "parent", "prev", "next"],
66
+ lists: [...COMMON_LIST_KEYS, "blocked_by", "blocks", "skills"],
67
+ },
68
+ checkpoint: {
69
+ scalars: [...COMMON_SCALAR_KEYS, "status", "priority", "epic", "parent", "prev", "next"],
70
+ lists: [...COMMON_LIST_KEYS, "blocked_by", "blocks", "skills", "scope"],
71
+ },
72
+ test: {
73
+ scalars: [...COMMON_SCALAR_KEYS, "status", "priority", "epic", "parent", "prev", "next"],
74
+ lists: [...COMMON_LIST_KEYS, "blocked_by", "blocks", "skills", "cases"],
75
+ },
76
+ };
10
77
  function listMarkdownFiles(dir) {
11
78
  if (!fs_1.default.existsSync(dir)) {
12
79
  return [];
@@ -44,13 +111,65 @@ function addKeyToSchema(schema, key, kind, filePath) {
44
111
  schema.listKeys.add(key);
45
112
  }
46
113
  }
114
+ function buildSchema(type, definition) {
115
+ const schema = {
116
+ type,
117
+ allowedKeys: new Set(),
118
+ keyKinds: {},
119
+ listKeys: new Set(),
120
+ };
121
+ for (const key of definition.scalars) {
122
+ addKeyToSchema(schema, key, "scalar", type);
123
+ }
124
+ for (const key of definition.lists ?? []) {
125
+ addKeyToSchema(schema, key, "list", type);
126
+ }
127
+ for (const key of definition.booleans ?? []) {
128
+ addKeyToSchema(schema, key, "boolean", type);
129
+ }
130
+ return schema;
131
+ }
132
+ function buildNodeSchemas(requiredTypes) {
133
+ const schemas = {};
134
+ for (const [type, definition] of Object.entries(NODE_SCHEMA_DEFS)) {
135
+ schemas[type] = buildSchema(type, definition);
136
+ }
137
+ if (requiredTypes) {
138
+ const required = Array.from(requiredTypes, (value) => value.toLowerCase());
139
+ const missing = required.filter((value) => !schemas[value]);
140
+ if (missing.length > 0) {
141
+ throw new Error(`template schema missing for type(s): ${missing.join(", ")}`);
142
+ }
143
+ }
144
+ return schemas;
145
+ }
146
+ function getNodeSchema(type) {
147
+ return buildNodeSchemas()[type.toLowerCase()];
148
+ }
149
+ function schemaAllowsKey(type, key) {
150
+ const schema = getNodeSchema(type);
151
+ return Boolean(schema?.allowedKeys.has(key));
152
+ }
153
+ function validateTemplateAgainstSchema(frontmatter, schema, filePath) {
154
+ for (const [key, value] of Object.entries(frontmatter)) {
155
+ const expected = schema.keyKinds[key];
156
+ if (!expected) {
157
+ throw new Error(`template key not allowed for ${schema.type}: ${key} (${filePath})`);
158
+ }
159
+ const kind = getValueKind(value);
160
+ if (expected !== kind) {
161
+ throw new Error(`template schema mismatch for ${schema.type}.${key}: expected ${expected} but found ${kind} (${filePath})`);
162
+ }
163
+ }
164
+ }
47
165
  function loadTemplateSchemas(root, config, requiredTypes) {
166
+ const schemas = buildNodeSchemas(requiredTypes);
48
167
  const templateRoot = path_1.default.resolve(root, config.templates.root_path, config.templates.default_set);
49
168
  const files = listMarkdownFiles(templateRoot);
50
169
  if (files.length === 0) {
51
170
  throw new Error(`no templates found at ${templateRoot}`);
52
171
  }
53
- const schemas = {};
172
+ const discoveredTypes = new Set();
54
173
  for (const filePath of files) {
55
174
  const content = fs_1.default.readFileSync(filePath, "utf8");
56
175
  const { frontmatter } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
@@ -62,22 +181,16 @@ function loadTemplateSchemas(root, config, requiredTypes) {
62
181
  if (normalizedType !== typeValue) {
63
182
  throw new Error(`template type must be lowercase in ${filePath}`);
64
183
  }
65
- const schema = schemas[normalizedType] ??
66
- {
67
- type: normalizedType,
68
- allowedKeys: new Set(),
69
- keyKinds: {},
70
- listKeys: new Set(),
71
- };
72
- schemas[normalizedType] = schema;
73
- for (const [key, value] of Object.entries(frontmatter)) {
74
- const kind = getValueKind(value);
75
- addKeyToSchema(schema, key, kind, filePath);
184
+ const schema = schemas[normalizedType];
185
+ if (!schema) {
186
+ throw new Error(`template schema missing for type ${normalizedType}`);
76
187
  }
188
+ validateTemplateAgainstSchema(frontmatter, schema, filePath);
189
+ discoveredTypes.add(normalizedType);
77
190
  }
78
191
  if (requiredTypes) {
79
192
  const required = Array.from(requiredTypes, (value) => value.toLowerCase());
80
- const missing = required.filter((value) => !schemas[value]);
193
+ const missing = required.filter((value) => !discoveredTypes.has(value));
81
194
  if (missing.length > 0) {
82
195
  throw new Error(`template schema missing for type(s): ${missing.join(", ")}`);
83
196
  }
@@ -41,6 +41,8 @@ Conventions:
41
41
  - mdkg does not execute skill scripts; runtimes decide when and whether to do that.
42
42
  - Prefer packs over ad-hoc file lists.
43
43
  - Prefer task/event commands for structured work-state changes and use markdown edits for narrative/body updates.
44
+ - `skills` is valid metadata on all work items (`epic`, `feat`, `task`, `bug`, `checkpoint`, `test`), but `mdkg task ...` still mutates only `task`, `bug`, and `test`.
45
+ - Treat mdkg-generated work items as schema-consistent by default: `mdkg new`, `mdkg validate`, and `mdkg task ...` should not require manual frontmatter repair.
44
46
  - Use `mdkg task done <id> --checkpoint "<title>"` for milestone compression, not every routine task completion.
45
47
  - Prefer checkpoints for feat/epic closeout summaries; parent status edits remain manual.
46
48
  - If `events.jsonl` is missing, `mdkg task start` and `mdkg task done` will remind you how to recreate it.
@@ -95,7 +95,9 @@ Work items (`epic/feat/task/bug/checkpoint/test`):
95
95
  - `status` (enum)
96
96
  - optional `priority` (0..9)
97
97
  - optional `skills: [slug, ...]` (kebab-case skill slugs)
98
- - optional graph edges: `epic`, `parent`, `relates`, `blocked_by`, `blocks`, `prev`, `next`
98
+ - optional graph fields are type-specific by the shared schema/templates
99
+ - `relates`, `blocked_by`, and `blocks` are common work-item list fields
100
+ - `epic`, `parent`, `prev`, and `next` are only valid on the work-item types whose templates/schema include them
99
101
 
100
102
  Decision records (`dec-*`):
101
103
  - `status` (enum: `proposed`, `accepted`, `rejected`, `superseded`)
@@ -48,8 +48,8 @@ function renderTokenValue(value) {
48
48
  }
49
49
  return value;
50
50
  }
51
- function renderTemplate(template, context) {
52
- const allowedKeys = new Set(Object.keys(template.frontmatter));
51
+ function renderTemplate(template, context, allowedKeysInput) {
52
+ const allowedKeys = new Set(allowedKeysInput ?? Object.keys(template.frontmatter));
53
53
  const rendered = {};
54
54
  for (const [key, value] of Object.entries(template.frontmatter)) {
55
55
  if (isTokenPlaceholder(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdkg",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Markdown Knowledge Graph",
5
5
  "license": "MIT",
6
6
  "bin": {