mdkg 0.3.5 → 0.3.7

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +97 -2
  2. package/CLI_COMMAND_MATRIX.md +90 -10
  3. package/README.md +54 -7
  4. package/dist/cli.js +222 -13
  5. package/dist/command-contract.json +587 -19
  6. package/dist/commands/bundle.js +2 -0
  7. package/dist/commands/checkpoint.js +139 -1
  8. package/dist/commands/db.js +8 -0
  9. package/dist/commands/format.js +116 -0
  10. package/dist/commands/goal.js +60 -0
  11. package/dist/commands/graph.js +280 -10
  12. package/dist/commands/handoff.js +295 -0
  13. package/dist/commands/mcp.js +656 -0
  14. package/dist/commands/pack.js +3 -1
  15. package/dist/commands/query_output.js +2 -0
  16. package/dist/commands/show.js +8 -0
  17. package/dist/commands/status.js +1 -0
  18. package/dist/commands/task.js +2 -0
  19. package/dist/commands/upgrade.js +61 -0
  20. package/dist/commands/validate.js +162 -3
  21. package/dist/commands/work.js +164 -0
  22. package/dist/core/project_db_queue_contract.js +101 -0
  23. package/dist/graph/edges.js +15 -0
  24. package/dist/graph/frontmatter.js +4 -1
  25. package/dist/graph/indexer.js +8 -0
  26. package/dist/graph/node.js +12 -1
  27. package/dist/graph/sqlite_index.js +2 -0
  28. package/dist/graph/subgraphs.js +2 -0
  29. package/dist/graph/validate_graph.js +5 -0
  30. package/dist/graph/visibility.js +6 -0
  31. package/dist/init/AGENT_START.md +4 -1
  32. package/dist/init/CLI_COMMAND_MATRIX.md +36 -4
  33. package/dist/init/README.md +26 -3
  34. package/dist/init/init-manifest.json +12 -12
  35. package/dist/init/templates/default/bug.md +2 -0
  36. package/dist/init/templates/default/chk.md +3 -0
  37. package/dist/init/templates/default/epic.md +2 -0
  38. package/dist/init/templates/default/feat.md +2 -0
  39. package/dist/init/templates/default/goal.md +3 -0
  40. package/dist/init/templates/default/spike.md +2 -0
  41. package/dist/init/templates/default/task.md +2 -0
  42. package/dist/init/templates/default/test.md +2 -0
  43. package/dist/pack/export_json.js +20 -8
  44. package/dist/pack/export_md.js +15 -4
  45. package/dist/pack/export_xml.js +9 -4
  46. package/dist/pack/metrics.js +12 -4
  47. package/dist/pack/pack.js +9 -1
  48. package/dist/util/argparse.js +1 -0
  49. package/package.json +7 -2
@@ -229,6 +229,7 @@ function collectStatus(root) {
229
229
  selected_exists: selected.state === null ? null : !selectedMissing,
230
230
  selected_achieved: selected.state === null ? null : selectedAchieved,
231
231
  active_node: selectedNode?.attributes.active_node ?? null,
232
+ last_active_node: selectedNode?.attributes.last_active_node ?? null,
232
233
  goal_state: selectedNode?.attributes.goal_state ?? null,
233
234
  status: selectedNode?.status ?? null,
234
235
  },
@@ -297,6 +297,7 @@ function runTaskDoneCommandLocked(options) {
297
297
  ws: loaded.ws,
298
298
  relates: loaded.id,
299
299
  scope: loaded.id,
300
+ kind: options.checkpointKind,
300
301
  runId: options.runId,
301
302
  note: `checkpoint created from mdkg task done for ${loaded.id}`,
302
303
  now,
@@ -309,6 +310,7 @@ function runTaskDoneCommandLocked(options) {
309
310
  ws: loaded.ws,
310
311
  relates: loaded.id,
311
312
  scope: loaded.id,
313
+ kind: options.checkpointKind,
312
314
  runId: options.runId,
313
315
  note: `checkpoint created from mdkg task done for ${loaded.id}`,
314
316
  now,
@@ -13,6 +13,8 @@ const paths_1 = require("../core/paths");
13
13
  const version_1 = require("../core/version");
14
14
  const project_db_1 = require("../core/project_db");
15
15
  const errors_1 = require("../util/errors");
16
+ const frontmatter_1 = require("../graph/frontmatter");
17
+ const workspace_files_1 = require("../graph/workspace_files");
16
18
  const init_manifest_1 = require("./init_manifest");
17
19
  const skill_support_1 = require("./skill_support");
18
20
  const skill_mirror_1 = require("./skill_mirror");
@@ -273,6 +275,64 @@ function migrateConfigIfNeeded(root, dryRun, summary, changes) {
273
275
  writeFile(cfgPath, `${JSON.stringify(nextConfig.config, null, 2)}\n`);
274
276
  }
275
277
  }
278
+ function migrateClosedGoalActiveNodes(root, dryRun, summary, changes) {
279
+ const config = (0, config_1.validateConfigSchema)((0, migrate_1.migrateConfig)(JSON.parse(fs_1.default.readFileSync((0, paths_1.configPath)(root), "utf8"))).config);
280
+ const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(root, config);
281
+ let planned = 0;
282
+ for (const files of Object.values(filesByAlias)) {
283
+ for (const filePath of files) {
284
+ const content = fs_1.default.readFileSync(filePath, "utf8");
285
+ let parsed;
286
+ try {
287
+ parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
288
+ }
289
+ catch {
290
+ continue;
291
+ }
292
+ const frontmatter = { ...parsed.frontmatter };
293
+ if (frontmatter.type !== "goal") {
294
+ continue;
295
+ }
296
+ const status = typeof frontmatter.status === "string" ? frontmatter.status : "";
297
+ const goalState = typeof frontmatter.goal_state === "string" ? frontmatter.goal_state : "";
298
+ const activeNode = typeof frontmatter.active_node === "string" ? frontmatter.active_node : undefined;
299
+ if (!activeNode || (status !== "done" && goalState !== "achieved")) {
300
+ continue;
301
+ }
302
+ const relativePath = path_1.default.relative(root, filePath).split(path_1.default.sep).join("/");
303
+ const lastActiveNode = typeof frontmatter.last_active_node === "string" ? frontmatter.last_active_node : undefined;
304
+ if (lastActiveNode && lastActiveNode !== activeNode) {
305
+ planned += 1;
306
+ record(summary, changes, {
307
+ path: relativePath,
308
+ category: "goal_lifecycle",
309
+ action: "conflict",
310
+ reason: `closed goal has active_node ${activeNode} but different last_active_node ${lastActiveNode}; local content preserved`,
311
+ });
312
+ continue;
313
+ }
314
+ planned += 1;
315
+ record(summary, changes, {
316
+ path: relativePath,
317
+ category: "goal_lifecycle",
318
+ action: "migrate",
319
+ reason: "move closed goal active_node to last_active_node",
320
+ });
321
+ if (!dryRun) {
322
+ if (!lastActiveNode) {
323
+ frontmatter.last_active_node = activeNode;
324
+ }
325
+ delete frontmatter.active_node;
326
+ const frontmatterBlock = ["---", ...(0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER), "---"].join("\n");
327
+ const body = parsed.body.length > 0 ? parsed.body : "";
328
+ writeFile(filePath, body.length > 0 ? `${frontmatterBlock}\n${body}` : frontmatterBlock);
329
+ }
330
+ }
331
+ }
332
+ if (planned === 0) {
333
+ summary.unchanged += 1;
334
+ }
335
+ }
276
336
  function isIgnoredBySimpleGitignore(root, relativePath) {
277
337
  const ignorePath = path_1.default.join(root, ".gitignore");
278
338
  if (!fs_1.default.existsSync(ignorePath)) {
@@ -440,6 +500,7 @@ function runUpgradeCommand(options) {
440
500
  const agentWorkspace = isAgentWorkspace(root);
441
501
  const managedCurrentFiles = [];
442
502
  migrateConfigIfNeeded(root, dryRun, summary, changes);
503
+ migrateClosedGoalActiveNodes(root, dryRun, summary, changes);
443
504
  for (const file of currentManifest.files) {
444
505
  if (!shouldIncludeFile(file, agentWorkspace)) {
445
506
  continue;
@@ -3,13 +3,16 @@ 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.RECOMMENDED_HEADINGS = void 0;
6
7
  exports.collectValidateReceipt = collectValidateReceipt;
7
8
  exports.runValidateCommand = runValidateCommand;
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
11
+ const child_process_1 = require("child_process");
10
12
  const config_1 = require("../core/config");
11
13
  const template_schema_1 = require("../graph/template_schema");
12
14
  const node_1 = require("../graph/node");
15
+ const agent_file_types_1 = require("../graph/agent_file_types");
13
16
  const skills_indexer_1 = require("../graph/skills_indexer");
14
17
  const workspace_files_1 = require("../graph/workspace_files");
15
18
  const validate_graph_1 = require("../graph/validate_graph");
@@ -18,7 +21,7 @@ const visibility_1 = require("../graph/visibility");
18
21
  const sqlite_index_1 = require("../graph/sqlite_index");
19
22
  const errors_1 = require("../util/errors");
20
23
  const skill_mirror_1 = require("./skill_mirror");
21
- const RECOMMENDED_HEADINGS = {
24
+ exports.RECOMMENDED_HEADINGS = {
22
25
  task: [
23
26
  "Overview",
24
27
  "Acceptance Criteria",
@@ -103,6 +106,143 @@ function extractHeadings(body) {
103
106
  }
104
107
  return headings;
105
108
  }
109
+ const RAW_CONTENT_MARKERS = [
110
+ { id: "raw_prompt", pattern: /\bRAW_PROMPT_MARKER\b/i, description: "raw prompt marker" },
111
+ { id: "raw_payload", pattern: /\bRAW_PAYLOAD_MARKER\b/i, description: "raw payload marker" },
112
+ { id: "raw_secret", pattern: /\bRAW_SECRET_MARKER\b|BEGIN [A-Z ]*PRIVATE KEY|secret\s*=/i, description: "raw secret marker" },
113
+ ];
114
+ function shouldCheckRawContentWarnings(node) {
115
+ if ((0, agent_file_types_1.isAgentFileType)(node.type)) {
116
+ return true;
117
+ }
118
+ return node.type === "checkpoint" && typeof node.frontmatter.checkpoint_kind === "string";
119
+ }
120
+ function collectRawContentWarnings(qid, node) {
121
+ if (!shouldCheckRawContentWarnings(node)) {
122
+ return [];
123
+ }
124
+ const warnings = [];
125
+ for (const marker of RAW_CONTENT_MARKERS) {
126
+ if (marker.pattern.test(node.body)) {
127
+ warnings.push(`${qid}: raw-content.${marker.id} warning: ${marker.description} detected; use refs, hashes, summaries, or artifact links instead of raw secrets/prompts/payloads`);
128
+ }
129
+ }
130
+ return warnings;
131
+ }
132
+ function collectChangedPaths(root) {
133
+ const result = (0, child_process_1.spawnSync)("git", ["-C", root, "status", "--porcelain", "--", ".mdkg"], {
134
+ encoding: "utf8",
135
+ });
136
+ if (result.status !== 0) {
137
+ return new Set();
138
+ }
139
+ const changed = new Set();
140
+ for (const line of result.stdout.split(/\r?\n/)) {
141
+ if (!line.trim()) {
142
+ continue;
143
+ }
144
+ const rawPath = line.slice(3).trim();
145
+ const filePath = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() ?? rawPath : rawPath;
146
+ changed.add(filePath.replace(/\\/g, "/"));
147
+ }
148
+ return changed;
149
+ }
150
+ function qidFromWarning(message) {
151
+ const match = /^([a-z0-9_-]+:[^\s:]+):/.exec(message);
152
+ return match?.[1];
153
+ }
154
+ function warningPath(message, nodes) {
155
+ const qid = qidFromWarning(message);
156
+ if (qid && nodes[qid]) {
157
+ return nodes[qid].path;
158
+ }
159
+ for (const node of Object.values(nodes)) {
160
+ if (message.includes(node.path)) {
161
+ return node.path;
162
+ }
163
+ }
164
+ const match = /([.]mdkg\/[^\s:]+\.md|[.]mdkg\/[^\s:]+\/SKILLS?\.md)/.exec(message);
165
+ return match?.[1];
166
+ }
167
+ function warningDiagnostic(message, nodes) {
168
+ const qid = qidFromWarning(message);
169
+ const pathValue = warningPath(message, nodes);
170
+ const rawMatch = /raw-content\.([a-z_]+)/.exec(message);
171
+ if (rawMatch) {
172
+ return {
173
+ id: `raw-content.${rawMatch[1]}`,
174
+ category: "raw-content",
175
+ severity: "warning",
176
+ message,
177
+ qid,
178
+ path: pathValue,
179
+ ref: qid,
180
+ remediation: "Replace raw secrets, prompts, tokens, or payloads with refs, hashes, redacted summaries, or archive/artifact links.",
181
+ };
182
+ }
183
+ if (message.includes("missing recommended heading")) {
184
+ return {
185
+ id: "heading.missing",
186
+ category: "headings",
187
+ severity: "warning",
188
+ message,
189
+ qid,
190
+ path: pathValue,
191
+ ref: qid,
192
+ remediation: "Run mdkg format --headings --dry-run to review missing heading additions, then --apply if acceptable.",
193
+ };
194
+ }
195
+ if (message.includes("bundled template schema fallback")) {
196
+ return {
197
+ id: "template_schema.fallback",
198
+ category: "templates",
199
+ severity: "warning",
200
+ message,
201
+ path: pathValue,
202
+ remediation: "Run mdkg upgrade --apply to vendor missing built-in template schemas when the managed asset update is safe.",
203
+ };
204
+ }
205
+ if (message.includes("sqlite") || message.includes("index")) {
206
+ return {
207
+ id: "cache.index",
208
+ category: "cache",
209
+ severity: "warning",
210
+ message,
211
+ path: pathValue,
212
+ remediation: "Run mdkg index or mdkg db index rebuild when generated cache state should be refreshed.",
213
+ };
214
+ }
215
+ if (message.includes("skill") || message.includes("mirror")) {
216
+ return {
217
+ id: "skill_mirror.warning",
218
+ category: "skills",
219
+ severity: "warning",
220
+ message,
221
+ path: pathValue,
222
+ remediation: "Run mdkg skill sync after reviewing managed skill mirror drift.",
223
+ };
224
+ }
225
+ if (message.includes("subgraph")) {
226
+ return {
227
+ id: "subgraph.warning",
228
+ category: "subgraph",
229
+ severity: "warning",
230
+ message,
231
+ path: pathValue,
232
+ remediation: "Run mdkg subgraph verify or refresh the source bundle after reviewing child graph freshness.",
233
+ };
234
+ }
235
+ return {
236
+ id: "warning.generic",
237
+ category: "general",
238
+ severity: "warning",
239
+ message,
240
+ qid,
241
+ path: pathValue,
242
+ ref: qid,
243
+ remediation: "Review the warning and apply the focused mdkg command suggested by the message when appropriate.",
244
+ };
245
+ }
106
246
  function isCoreListFile(filePath) {
107
247
  return path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core";
108
248
  }
@@ -121,6 +261,8 @@ function normalizeEdges(edges, ws) {
121
261
  relates: edges.relates.map((value) => normalizeEdgeTarget(value, ws)),
122
262
  blocked_by: edges.blocked_by.map((value) => normalizeEdgeTarget(value, ws)),
123
263
  blocks: edges.blocks.map((value) => normalizeEdgeTarget(value, ws)),
264
+ context_refs: (edges.context_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
265
+ evidence_refs: (edges.evidence_refs ?? []).map((value) => normalizeEdgeTarget(value, ws)),
124
266
  };
125
267
  }
126
268
  function buildIndexNode(root, ws, filePath, node) {
@@ -178,6 +320,12 @@ function buildReverseEdges(nodes) {
178
320
  for (const target of node.edges.blocks) {
179
321
  addReverseEdge(reverse, "blocks", target, qid);
180
322
  }
323
+ for (const target of node.edges.context_refs ?? []) {
324
+ addReverseEdge(reverse, "context_refs", target, qid);
325
+ }
326
+ for (const target of node.edges.evidence_refs ?? []) {
327
+ addReverseEdge(reverse, "evidence_refs", target, qid);
328
+ }
181
329
  }
182
330
  for (const targets of Object.values(reverse)) {
183
331
  for (const sources of Object.values(targets)) {
@@ -292,7 +440,7 @@ function collectValidateReceipt(options) {
292
440
  idsByWorkspace[alias].set(node.id, filePath);
293
441
  const qid = `${alias}:${node.id}`;
294
442
  nodes[qid] = buildIndexNode(options.root, alias, filePath, node);
295
- const recommended = RECOMMENDED_HEADINGS[node.type];
443
+ const recommended = exports.RECOMMENDED_HEADINGS[node.type];
296
444
  if (recommended) {
297
445
  const headings = extractHeadings(node.body);
298
446
  for (const heading of recommended) {
@@ -301,6 +449,7 @@ function collectValidateReceipt(options) {
301
449
  }
302
450
  }
303
451
  }
452
+ warnings.push(...collectRawContentWarnings(qid, node));
304
453
  }
305
454
  catch (err) {
306
455
  const message = err instanceof Error ? err.message : "unknown error";
@@ -372,7 +521,13 @@ function collectValidateReceipt(options) {
372
521
  }
373
522
  warnings.push(...(0, skill_mirror_1.auditSkillMirrors)(options.root, config));
374
523
  validateEventsJsonl(options.root, config, errors);
375
- const uniqueWarnings = Array.from(new Set(warnings));
524
+ const allUniqueWarnings = Array.from(new Set(warnings));
525
+ const allWarningDiagnostics = allUniqueWarnings.map((warning) => warningDiagnostic(warning, nodes));
526
+ const changedPaths = options.changedOnly ? collectChangedPaths(options.root) : new Set();
527
+ const filteredWarningDiagnostics = options.changedOnly
528
+ ? allWarningDiagnostics.filter((warning) => warning.path !== undefined && changedPaths.has(warning.path))
529
+ : allWarningDiagnostics;
530
+ const uniqueWarnings = filteredWarningDiagnostics.map((warning) => warning.message);
376
531
  const uniqueErrors = Array.from(new Set(errors));
377
532
  const reportLines = [
378
533
  ...uniqueWarnings.map((warning) => `warning: ${warning}`),
@@ -390,8 +545,12 @@ function collectValidateReceipt(options) {
390
545
  warning_count: uniqueWarnings.length,
391
546
  error_count: uniqueErrors.length,
392
547
  warnings: uniqueWarnings,
548
+ warning_diagnostics: filteredWarningDiagnostics,
393
549
  errors: uniqueErrors,
394
550
  ...(outPath ? { report_path: outPath } : {}),
551
+ ...(options.changedOnly
552
+ ? { warning_filter: { mode: "changed-only", changed_paths: Array.from(changedPaths).sort() } }
553
+ : {}),
395
554
  };
396
555
  return receipt;
397
556
  }
@@ -11,6 +11,7 @@ exports.runWorkTriggerCommand = runWorkTriggerCommand;
11
11
  exports.runWorkReceiptNewCommand = runWorkReceiptNewCommand;
12
12
  exports.runWorkReceiptUpdateCommand = runWorkReceiptUpdateCommand;
13
13
  exports.runWorkReceiptVerifyCommand = runWorkReceiptVerifyCommand;
14
+ exports.runWorkValidateCommand = runWorkValidateCommand;
14
15
  exports.runWorkArtifactAddCommand = runWorkArtifactAddCommand;
15
16
  const fs_1 = __importDefault(require("fs"));
16
17
  const path_1 = __importDefault(require("path"));
@@ -34,11 +35,15 @@ const archive_1 = require("./archive");
34
35
  const project_db_1 = require("../core/project_db");
35
36
  const project_db_migrations_1 = require("../core/project_db_migrations");
36
37
  const project_db_queue_1 = require("../core/project_db_queue");
38
+ const validate_1 = require("./validate");
39
+ const query_output_1 = require("./query_output");
40
+ const workspace_files_1 = require("../graph/workspace_files");
37
41
  const PRICING_MODELS = new Set(["free", "included", "quoted", "fixed", "metered", "subscription"]);
38
42
  const ORDER_STATUSES = new Set(["submitted", "accepted", "running", "completed", "cancelled", "failed"]);
39
43
  const RECEIPT_STATUSES = new Set(["recorded", "verified", "rejected", "superseded"]);
40
44
  const OUTCOMES = new Set(["success", "partial", "failure"]);
41
45
  const REDACTION_POLICIES = new Set(["refs_and_hashes_only", "redacted_summary", "external_private"]);
46
+ const WORKFLOW_VALIDATE_TYPES = new Set(agent_file_types_1.AGENT_FILE_TYPES);
42
47
  function parseCsvList(raw) {
43
48
  if (!raw) {
44
49
  return [];
@@ -210,6 +215,162 @@ function resolveReadableWorkNode(index, idOrQid, ws, type, label) {
210
215
  }
211
216
  return node;
212
217
  }
218
+ function normalizeWorkflowValidateType(value) {
219
+ if (value === undefined) {
220
+ return undefined;
221
+ }
222
+ const normalized = value.toLowerCase();
223
+ if (!WORKFLOW_VALIDATE_TYPES.has(normalized)) {
224
+ throw new errors_1.UsageError(`--type must be one of ${agent_file_types_1.AGENT_FILE_TYPES.join(", ")}`);
225
+ }
226
+ return normalized;
227
+ }
228
+ function isWorkflowNode(node) {
229
+ return WORKFLOW_VALIDATE_TYPES.has(node.type);
230
+ }
231
+ function resolveWorkflowTarget(index, idOrQid, ws) {
232
+ const resolved = (0, qid_1.resolveQid)(index, idOrQid, ws);
233
+ if (resolved.status !== "ok") {
234
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("workflow record", idOrQid, resolved, ws));
235
+ }
236
+ const node = index.nodes[resolved.qid];
237
+ if (!node || !isWorkflowNode(node)) {
238
+ throw new errors_1.NotFoundError(`workflow record not found: ${idOrQid}`);
239
+ }
240
+ return node;
241
+ }
242
+ function workflowDiagnosticCode(message) {
243
+ const rawMatch = /raw-content\.([a-z_]+)/.exec(message);
244
+ if (rawMatch) {
245
+ return `raw-content.${rawMatch[1]}`;
246
+ }
247
+ if (message.includes("missing recommended heading")) {
248
+ return "heading.missing";
249
+ }
250
+ if (message.includes("references missing") || message.includes("references missing node")) {
251
+ return "reference.missing";
252
+ }
253
+ if (message.includes("must be named")) {
254
+ return "schema.basename";
255
+ }
256
+ if (message.includes("must be") || message.includes("is required")) {
257
+ return "schema.invalid";
258
+ }
259
+ if (message.includes("visibility:")) {
260
+ return "visibility.policy";
261
+ }
262
+ return "validation.message";
263
+ }
264
+ function workflowDiagnosticQid(message, nodes) {
265
+ for (const node of nodes) {
266
+ if (message.includes(node.qid) || message.includes(node.path)) {
267
+ return node.qid;
268
+ }
269
+ }
270
+ return undefined;
271
+ }
272
+ function workflowCandidatePaths(options) {
273
+ const values = new Set();
274
+ if (options.target) {
275
+ values.add(options.target.path);
276
+ values.add(path_1.default.resolve(options.root, options.target.path));
277
+ return Array.from(values);
278
+ }
279
+ const basenames = options.type
280
+ ? new Set([agent_file_types_1.AGENT_FILE_BASENAMES[options.type]])
281
+ : new Set(Object.values(agent_file_types_1.AGENT_FILE_BASENAMES));
282
+ const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, options.config);
283
+ for (const [alias, files] of Object.entries(filesByAlias)) {
284
+ if (options.ws !== "root" && alias !== options.ws) {
285
+ continue;
286
+ }
287
+ for (const filePath of files) {
288
+ if (!basenames.has(path_1.default.basename(filePath))) {
289
+ continue;
290
+ }
291
+ values.add(filePath);
292
+ values.add(path_1.default.relative(options.root, filePath).split(path_1.default.sep).join("/"));
293
+ }
294
+ }
295
+ return Array.from(values);
296
+ }
297
+ function filterWorkflowMessages(messages, nodes, candidatePaths) {
298
+ if (nodes.length === 0 && candidatePaths.length === 0) {
299
+ return [];
300
+ }
301
+ return messages.filter((message) => nodes.some((node) => message.includes(node.qid) || message.includes(node.path)) ||
302
+ candidatePaths.some((filePath) => message.includes(filePath)));
303
+ }
304
+ function buildWorkValidateReceipt(options) {
305
+ const config = (0, config_1.loadConfig)(options.root);
306
+ const ws = normalizeWorkspace(options.ws);
307
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config, tolerant: true });
308
+ const type = normalizeWorkflowValidateType(options.type);
309
+ let targets;
310
+ let target;
311
+ if (options.id) {
312
+ target = resolveWorkflowTarget(index, options.id, ws);
313
+ if (type && target.type !== type) {
314
+ throw new errors_1.UsageError(`workflow record ${target.qid} is ${target.type}, not ${type}`);
315
+ }
316
+ targets = [target];
317
+ }
318
+ else {
319
+ targets = Object.values(index.nodes)
320
+ .filter((node) => isWorkflowNode(node) && (!type || node.type === type))
321
+ .sort((a, b) => a.qid.localeCompare(b.qid));
322
+ }
323
+ const candidatePaths = workflowCandidatePaths({ root: options.root, config, ws, type, target });
324
+ const validation = (0, validate_1.collectValidateReceipt)({ root: options.root });
325
+ const warnings = filterWorkflowMessages(validation.warnings, targets, candidatePaths);
326
+ const errors = filterWorkflowMessages(validation.errors, targets, candidatePaths);
327
+ const diagnostics = [
328
+ ...warnings.map((message) => ({
329
+ severity: "warning",
330
+ code: workflowDiagnosticCode(message),
331
+ message,
332
+ qid: workflowDiagnosticQid(message, targets),
333
+ })),
334
+ ...errors.map((message) => ({
335
+ severity: "error",
336
+ code: workflowDiagnosticCode(message),
337
+ message,
338
+ qid: workflowDiagnosticQid(message, targets),
339
+ })),
340
+ ];
341
+ return {
342
+ action: "work.validate",
343
+ ok: errors.length === 0,
344
+ type: type ?? "all",
345
+ ...(target ? { target: (0, query_output_1.toNodeSummaryJson)(target) } : {}),
346
+ checked_count: target ? 1 : candidatePaths.filter((value) => !path_1.default.isAbsolute(value)).length,
347
+ nodes: targets.map((node) => (0, query_output_1.toNodeSummaryJson)(node)),
348
+ warning_count: warnings.length,
349
+ error_count: errors.length,
350
+ warnings,
351
+ errors,
352
+ diagnostics,
353
+ };
354
+ }
355
+ function printWorkValidateReceipt(receipt, json) {
356
+ if (json) {
357
+ (0, query_output_1.writeJson)(receipt);
358
+ if (!receipt.ok) {
359
+ throw new errors_1.ValidationError(`workflow validation failed with ${receipt.error_count} error(s)`);
360
+ }
361
+ return;
362
+ }
363
+ for (const warning of receipt.warnings) {
364
+ console.error(`warning: ${warning}`);
365
+ }
366
+ if (!receipt.ok) {
367
+ for (const error of receipt.errors) {
368
+ console.error(error);
369
+ }
370
+ throw new errors_1.ValidationError(`workflow validation failed with ${receipt.error_count} error(s)`);
371
+ }
372
+ console.log(`workflow validation ok: ${receipt.checked_count} file(s)`);
373
+ }
213
374
  function resolveTriggerWorkNode(index, ws, refRaw) {
214
375
  const ref = normalizePortableIdRef(refRaw, "<work-or-capability-ref>");
215
376
  const resolved = (0, qid_1.resolveQid)(index, ref, ws);
@@ -943,6 +1104,9 @@ function runWorkReceiptUpdateCommand(options) {
943
1104
  function runWorkReceiptVerifyCommand(options) {
944
1105
  return runWorkReceiptVerifyCommandLocked(options);
945
1106
  }
1107
+ function runWorkValidateCommand(options) {
1108
+ return printWorkValidateReceipt(buildWorkValidateReceipt(options), options.json);
1109
+ }
946
1110
  function runWorkArtifactAddCommand(options) {
947
1111
  return withWorkLock(options.root, () => runWorkArtifactAddCommandLocked(options));
948
1112
  }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.projectDbQueueAdapterContract = projectDbQueueAdapterContract;
4
+ function projectDbQueueAdapterContract() {
5
+ return {
6
+ schema_version: 1,
7
+ contract_id: "mdkg.project_db.queue.adapter.v1",
8
+ stability: "public",
9
+ boundary: {
10
+ role: "durable local delivery state for mdkg project DB integrations",
11
+ canonical_history: "queue rows are not canonical event history or durable runtime transcripts",
12
+ raw_payload_policy: "store compact refs and redacted payloads; do not store raw secrets, prompts, provider payloads, or bulky runtime artifacts",
13
+ internal_surfaces: ["event", "receipt", "reducer", "writer_lease", "materializer"],
14
+ },
15
+ runtime: {
16
+ database: "node:sqlite",
17
+ transactions: "short BEGIN IMMEDIATE transactions for writes and claims",
18
+ external_dependencies: [],
19
+ },
20
+ commands: [
21
+ "mdkg db queue create <queue>",
22
+ "mdkg db queue pause <queue>",
23
+ "mdkg db queue resume <queue>",
24
+ "mdkg db queue enqueue <queue> <message-id>",
25
+ "mdkg db queue claim <queue>",
26
+ "mdkg db queue ack <queue> <message-id>",
27
+ "mdkg db queue fail <queue> <message-id>",
28
+ "mdkg db queue dead-letter <queue> <message-id>",
29
+ "mdkg db queue release-expired [queue]",
30
+ "mdkg db queue stats [queue]",
31
+ "mdkg db queue list <queue>",
32
+ "mdkg db queue show <queue> <message-id>",
33
+ "mdkg db queue contract",
34
+ ],
35
+ queue_control: {
36
+ table: "project_queue",
37
+ states: ["active", "paused"],
38
+ paused_behavior: {
39
+ rejects: ["enqueue", "claim"],
40
+ allows: ["ack", "fail", "dead-letter", "release-expired", "stats", "list", "show"],
41
+ },
42
+ },
43
+ message: {
44
+ table: "project_queue_message",
45
+ states: ["ready", "leased", "acked", "dead_letter"],
46
+ fields: [
47
+ "queue_name",
48
+ "message_id",
49
+ "dedupe_key",
50
+ "payload_json",
51
+ "payload_hash",
52
+ "status",
53
+ "available_at_ms",
54
+ "attempt_count",
55
+ "max_attempts",
56
+ "lease_owner",
57
+ "lease_deadline_ms",
58
+ "created_at_ms",
59
+ "updated_at_ms",
60
+ "last_error",
61
+ ],
62
+ terminal_states: ["acked", "dead_letter"],
63
+ },
64
+ payload_hash: {
65
+ algorithm: "sha256",
66
+ encoding: "sha256:<64 lowercase hex chars>",
67
+ canonicalization: "payload JSON is serialized deterministically with object keys sorted before hashing and storage",
68
+ },
69
+ dedupe: {
70
+ key: "queue_name + dedupe_key",
71
+ scope: "only non-null dedupe keys participate in the partial unique index",
72
+ duplicate_behavior: "enqueue with an existing dedupe key returns the existing message without replacing payload_json or payload_hash",
73
+ },
74
+ claim: {
75
+ selection: "oldest ready or expired leased message ordered by available_at_ms, created_at_ms, then message_id",
76
+ lease: "claim sets status=leased, lease_owner, and lease_deadline_ms; ack/fail/dead-letter must use the same lease owner",
77
+ transactional: true,
78
+ },
79
+ settlement: {
80
+ ack: "leased message becomes acked and clears lease owner/deadline",
81
+ fail: "leased message increments attempt_count; if attempts remain it becomes ready at now + retry_after_ms, otherwise it becomes dead_letter",
82
+ dead_letter: "leased message becomes dead_letter immediately and records last_error",
83
+ release_expired: "expired leased messages become ready and clear lease owner/deadline without changing attempt_count",
84
+ },
85
+ stats: {
86
+ counters: ["total", "ready", "leased", "acked", "dead_letter", "ready_available", "leased_expired"],
87
+ snapshot_summary: ["total", "ready", "leased", "acked", "dead_letter", "paused_ready", "active_ready"],
88
+ },
89
+ snapshot_policy: {
90
+ drain: "default seal policy; requires no ready or leased delivery work",
91
+ paused: "allows ready messages only when their queues are paused; leased messages are never allowed",
92
+ },
93
+ adapter_guidance: [
94
+ "create queues explicitly before enqueueing integration work",
95
+ "use dedupe keys for idempotent delivery",
96
+ "treat message payloads as refs and redacted envelopes, not canonical runtime state",
97
+ "settle or pause queues before committing sealed project DB state",
98
+ "use stats/list/show for operator review and avoid direct SQL coupling",
99
+ ],
100
+ };
101
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractEdges = extractEdges;
4
4
  const id_1 = require("../util/id");
5
+ const refs_1 = require("../util/refs");
5
6
  function formatError(filePath, key, message) {
6
7
  return new Error(`${filePath}: ${key} ${message}`);
7
8
  }
@@ -18,6 +19,12 @@ function normalizeIdRef(value, filePath, key, options) {
18
19
  }
19
20
  return normalized;
20
21
  }
22
+ function normalizeSemanticRef(value, filePath, key) {
23
+ if (!(0, refs_1.validatePortableOrUriRef)(value)) {
24
+ throw formatError(filePath, key, `invalid semantic reference: ${value}`);
25
+ }
26
+ return value.includes("://") ? value : value.toLowerCase();
27
+ }
21
28
  function readString(fm, key) {
22
29
  const value = fm[key];
23
30
  if (value === undefined) {
@@ -46,10 +53,18 @@ function extractEdges(frontmatter, filePath, options = {}) {
46
53
  const relates = readStringList(frontmatter, "relates") ?? [];
47
54
  const blocked_by = readStringList(frontmatter, "blocked_by") ?? [];
48
55
  const blocks = readStringList(frontmatter, "blocks") ?? [];
56
+ const context_refs = options.includeSemanticRefs
57
+ ? readStringList(frontmatter, "context_refs") ?? []
58
+ : [];
59
+ const evidence_refs = options.includeSemanticRefs
60
+ ? readStringList(frontmatter, "evidence_refs") ?? []
61
+ : [];
49
62
  const edges = {
50
63
  relates: relates.map((value) => normalizeIdRef(value, filePath, "relates", options)),
51
64
  blocked_by: blocked_by.map((value) => normalizeIdRef(value, filePath, "blocked_by", options)),
52
65
  blocks: blocks.map((value) => normalizeIdRef(value, filePath, "blocks", options)),
66
+ context_refs: context_refs.map((value) => normalizeSemanticRef(value, filePath, "context_refs")),
67
+ evidence_refs: evidence_refs.map((value) => normalizeSemanticRef(value, filePath, "evidence_refs")),
53
68
  };
54
69
  if (epic) {
55
70
  edges.epic = normalizeIdRef(epic, filePath, "epic", options);