mdkg 0.3.6 → 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 (48) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/CLI_COMMAND_MATRIX.md +56 -7
  3. package/README.md +33 -3
  4. package/dist/cli.js +132 -11
  5. package/dist/command-contract.json +431 -16
  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 +148 -0
  12. package/dist/commands/handoff.js +295 -0
  13. package/dist/commands/mcp.js +9 -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 +24 -3
  33. package/dist/init/README.md +17 -2
  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/package.json +6 -2
@@ -3,6 +3,7 @@ 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.CHECKPOINT_KINDS = void 0;
6
7
  exports.createCheckpoint = createCheckpoint;
7
8
  exports.runCheckpointNewCommand = runCheckpointNewCommand;
8
9
  const fs_1 = __importDefault(require("fs"));
@@ -17,6 +18,20 @@ const atomic_1 = require("../util/atomic");
17
18
  const lock_1 = require("../util/lock");
18
19
  const sqlite_index_1 = require("../graph/sqlite_index");
19
20
  const event_support_1 = require("./event_support");
21
+ exports.CHECKPOINT_KINDS = [
22
+ "implementation",
23
+ "test-proof",
24
+ "goal-closeout",
25
+ "audit",
26
+ "handoff",
27
+ ];
28
+ function normalizeCheckpointKind(value) {
29
+ const normalized = (value ?? "implementation").toLowerCase();
30
+ if (exports.CHECKPOINT_KINDS.includes(normalized)) {
31
+ return normalized;
32
+ }
33
+ throw new errors_1.UsageError(`--kind must be one of ${exports.CHECKPOINT_KINDS.join(", ")}`);
34
+ }
20
35
  function parseCsvList(raw) {
21
36
  if (!raw) {
22
37
  return [];
@@ -83,6 +98,125 @@ function normalizeWorkspace(value) {
83
98
  }
84
99
  return normalized;
85
100
  }
101
+ function kindSpecificSection(kind) {
102
+ switch (kind) {
103
+ case "implementation":
104
+ return [
105
+ "# Implementation Details",
106
+ "",
107
+ "- Code or graph surfaces changed:",
108
+ "- Architecture or data-shape notes:",
109
+ "- Compatibility notes:",
110
+ ];
111
+ case "test-proof":
112
+ return [
113
+ "# Test Proof",
114
+ "",
115
+ "- Test target:",
116
+ "- Fixtures or temp repos:",
117
+ "- Coverage gaps:",
118
+ ];
119
+ case "goal-closeout":
120
+ return [
121
+ "# Goal Closeout",
122
+ "",
123
+ "- Goal condition result:",
124
+ "- Scoped nodes closed:",
125
+ "- Remaining deferred work:",
126
+ ];
127
+ case "audit":
128
+ return [
129
+ "# Audit Findings",
130
+ "",
131
+ "- Reviewed surfaces:",
132
+ "- Findings:",
133
+ "- Residual risk:",
134
+ ];
135
+ case "handoff":
136
+ return [
137
+ "# Handoff Summary",
138
+ "",
139
+ "- Recipient/context:",
140
+ "- Starting node or command:",
141
+ "- Explicit boundaries:",
142
+ ];
143
+ }
144
+ }
145
+ function checkpointBody(kind) {
146
+ return [
147
+ "# Summary",
148
+ "",
149
+ "What was completed in this phase? What is now true?",
150
+ "",
151
+ "# Scope Covered",
152
+ "",
153
+ "Keep `scope` frontmatter updated when possible.",
154
+ "",
155
+ "## Changed Surfaces",
156
+ "",
157
+ "- files, commands, nodes, docs, or runtime surfaces changed",
158
+ "",
159
+ "## Boundaries",
160
+ "",
161
+ "- in scope:",
162
+ "- out of scope:",
163
+ "- raw secrets, raw prompts, raw payloads, and bulky execution traces excluded:",
164
+ "",
165
+ "# Decisions Captured",
166
+ "",
167
+ "Link the most important decision records.",
168
+ "",
169
+ "# Implementation Summary",
170
+ "",
171
+ "What changed? What patterns or architecture emerged?",
172
+ "",
173
+ ...kindSpecificSection(kind),
174
+ "",
175
+ "# Verification / Testing",
176
+ "",
177
+ "## Command Evidence",
178
+ "",
179
+ "- command:",
180
+ "- result:",
181
+ "",
182
+ "## Pass / Fail Status",
183
+ "",
184
+ "- status:",
185
+ "",
186
+ "## Known Warnings",
187
+ "",
188
+ "- warning:",
189
+ "",
190
+ "# Known Issues / Follow-ups",
191
+ "",
192
+ "- issue 1",
193
+ "- issue 2",
194
+ "",
195
+ "## Follow-up Refs",
196
+ "",
197
+ "- task/test/goal refs:",
198
+ "",
199
+ "# Links / Artifacts",
200
+ "",
201
+ "- packs",
202
+ "- PRs/commits",
203
+ "- docs",
204
+ "- dashboards",
205
+ "",
206
+ "# Raw Content Safety",
207
+ "",
208
+ "- Summarize evidence and use refs, hashes, and artifact links instead of raw secrets, raw prompts, raw payloads, or bulky execution traces.",
209
+ "",
210
+ ].join("\n");
211
+ }
212
+ function replaceRenderedBody(content, body) {
213
+ const marker = "\n---\n";
214
+ const start = content.indexOf(marker);
215
+ if (!content.startsWith("---\n") || start === -1) {
216
+ return content;
217
+ }
218
+ return `${content.slice(0, start + marker.length)}${body}`;
219
+ }
86
220
  function createCheckpointLocked(options) {
87
221
  const title = options.title.trim();
88
222
  if (!title) {
@@ -130,12 +264,14 @@ function createCheckpointLocked(options) {
130
264
  }
131
265
  }
132
266
  const scope = parseCsvList(options.scope).map((value) => normalizeId(value, "--scope"));
267
+ const kind = normalizeCheckpointKind(options.kind);
133
268
  const now = options.now ?? new Date();
134
269
  const today = (0, date_1.formatDate)(now);
135
270
  const template = (0, loader_1.loadTemplate)(options.root, config, "checkpoint", options.template);
136
271
  const content = (0, loader_1.renderTemplate)(template, {
137
272
  id,
138
273
  title,
274
+ checkpoint_kind: kind,
139
275
  status,
140
276
  priority,
141
277
  created: today,
@@ -143,8 +279,9 @@ function createCheckpointLocked(options) {
143
279
  relates,
144
280
  scope,
145
281
  });
282
+ const rendered = replaceRenderedBody(content, checkpointBody(kind));
146
283
  try {
147
- (0, atomic_1.writeFileExclusive)(filePath, content);
284
+ (0, atomic_1.writeFileExclusive)(filePath, rendered);
148
285
  }
149
286
  catch (err) {
150
287
  const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
@@ -168,6 +305,7 @@ function createCheckpointLocked(options) {
168
305
  id,
169
306
  qid: `${ws}:${id}`,
170
307
  path: path_1.default.relative(options.root, filePath),
308
+ kind,
171
309
  };
172
310
  }
173
311
  function createCheckpoint(options) {
@@ -9,6 +9,7 @@ exports.runDbMigrateCommand = runDbMigrateCommand;
9
9
  exports.runDbVerifyCommand = runDbVerifyCommand;
10
10
  exports.runDbStatsCommand = runDbStatsCommand;
11
11
  exports.runDbQueueCreateCommand = runDbQueueCreateCommand;
12
+ exports.runDbQueueContractCommand = runDbQueueContractCommand;
12
13
  exports.runDbQueuePauseCommand = runDbQueuePauseCommand;
13
14
  exports.runDbQueueResumeCommand = runDbQueueResumeCommand;
14
15
  exports.runDbQueueEnqueueCommand = runDbQueueEnqueueCommand;
@@ -36,6 +37,7 @@ const project_db_1 = require("../core/project_db");
36
37
  const project_db_migrations_1 = require("../core/project_db_migrations");
37
38
  const project_db_snapshot_1 = require("../core/project_db_snapshot");
38
39
  const project_db_queue_1 = require("../core/project_db_queue");
40
+ const project_db_queue_contract_1 = require("../core/project_db_queue_contract");
39
41
  const version_1 = require("../core/version");
40
42
  const index_1 = require("./index");
41
43
  const capabilities_indexer_1 = require("../graph/capabilities_indexer");
@@ -539,6 +541,12 @@ function runDbQueueCreateCommand(options) {
539
541
  reason: options.reason,
540
542
  }), "db-queue-create");
541
543
  }
544
+ function runDbQueueContractCommand(options) {
545
+ writeQueueJsonOrText("db-queue-contract", {
546
+ mdkg_version: (0, version_1.readPackageVersion)(),
547
+ contract: (0, project_db_queue_contract_1.projectDbQueueAdapterContract)(),
548
+ }, options.json);
549
+ }
542
550
  function runDbQueuePauseCommand(options) {
543
551
  runQueueMutation(options, (databasePath) => ({ queue: (0, project_db_queue_1.pauseProjectQueue)(databasePath, { queue_name: requireQueueName(options), reason: options.reason }) }), "db-queue-pause");
544
552
  }
@@ -19,6 +19,7 @@ const id_1 = require("../util/id");
19
19
  const refs_1 = require("../util/refs");
20
20
  const atomic_1 = require("../util/atomic");
21
21
  const lock_1 = require("../util/lock");
22
+ const validate_1 = require("./validate");
22
23
  const DEC_ID_RE = /^dec-[0-9]+$/;
23
24
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
24
25
  const ID_LIST_KEYS = new Set(["refs", "scope"]);
@@ -46,6 +47,27 @@ function isValidId(value) {
46
47
  function isCoreListFile(filePath) {
47
48
  return path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core";
48
49
  }
50
+ function normalizeHeading(value) {
51
+ return value.trim().toLowerCase();
52
+ }
53
+ function existingHeadings(body) {
54
+ const headings = new Set();
55
+ for (const line of body.split(/\r?\n/)) {
56
+ const match = /^#+\s+(.*)$/.exec(line);
57
+ if (match) {
58
+ headings.add(normalizeHeading(match[1] ?? ""));
59
+ }
60
+ }
61
+ return headings;
62
+ }
63
+ function appendMissingHeadings(body, headings) {
64
+ if (headings.length === 0) {
65
+ return body;
66
+ }
67
+ const trimmed = body.replace(/\s+$/g, "");
68
+ const prefix = trimmed.length > 0 ? `${trimmed}\n\n` : "";
69
+ return `${prefix}${headings.map((heading) => `# ${heading}\n`).join("\n")}`;
70
+ }
49
71
  function normalizeScalar(value) {
50
72
  return value.trim();
51
73
  }
@@ -250,7 +272,101 @@ function normalizeFrontmatter(frontmatter, schema, type, workStatusEnum, priorit
250
272
  }
251
273
  return { normalized, errors };
252
274
  }
275
+ function runHeadingFormatCommandLocked(options) {
276
+ if (options.dryRun && options.apply) {
277
+ throw new errors_1.ValidationError("format --headings cannot use --dry-run and --apply together");
278
+ }
279
+ const config = (0, config_1.loadConfig)(options.root);
280
+ const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, config);
281
+ const errors = [];
282
+ const changes = [];
283
+ for (const files of Object.values(filesByAlias)) {
284
+ for (const filePath of files) {
285
+ if (isCoreListFile(filePath)) {
286
+ continue;
287
+ }
288
+ let content = "";
289
+ try {
290
+ content = fs_1.default.readFileSync(filePath, "utf8");
291
+ }
292
+ catch (err) {
293
+ const message = err instanceof Error ? err.message : "unknown error";
294
+ errors.push(`${filePath}: failed to read file: ${message}`);
295
+ continue;
296
+ }
297
+ let parsed;
298
+ try {
299
+ parsed = (0, frontmatter_1.parseFrontmatter)(content, filePath);
300
+ }
301
+ catch (err) {
302
+ const message = err instanceof Error ? err.message : "unknown error";
303
+ errors.push(message);
304
+ continue;
305
+ }
306
+ const typeValue = parsed.frontmatter.type;
307
+ if (typeof typeValue !== "string") {
308
+ errors.push(`${filePath}: type is required`);
309
+ continue;
310
+ }
311
+ const type = typeValue.toLowerCase();
312
+ const recommended = validate_1.RECOMMENDED_HEADINGS[type];
313
+ if (!recommended) {
314
+ continue;
315
+ }
316
+ if (!node_1.ALLOWED_TYPES.has(type)) {
317
+ errors.push(`${filePath}: type must be one of ${Array.from(node_1.ALLOWED_TYPES).join(", ")}`);
318
+ continue;
319
+ }
320
+ const present = existingHeadings(parsed.body);
321
+ const addedHeadings = recommended.filter((heading) => !present.has(normalizeHeading(heading)));
322
+ if (addedHeadings.length === 0) {
323
+ continue;
324
+ }
325
+ const frontmatterEnd = content.indexOf("---", 3);
326
+ const frontmatterBlock = frontmatterEnd >= 0 ? content.slice(0, frontmatterEnd + 3) : ["---", ...(0, frontmatter_1.formatFrontmatter)(parsed.frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER), "---"].join("\n");
327
+ const nextBody = appendMissingHeadings(parsed.body, addedHeadings);
328
+ const nextContent = `${frontmatterBlock}\n${nextBody.endsWith("\n") ? nextBody : `${nextBody}\n`}`;
329
+ changes.push({
330
+ filePath,
331
+ path: path_1.default.relative(options.root, filePath).split(path_1.default.sep).join("/"),
332
+ id: typeof parsed.frontmatter.id === "string" ? parsed.frontmatter.id : undefined,
333
+ type,
334
+ added_headings: addedHeadings,
335
+ content: nextContent,
336
+ });
337
+ }
338
+ }
339
+ if (errors.length > 0) {
340
+ for (const error of errors) {
341
+ console.error(error);
342
+ }
343
+ throw new errors_1.ValidationError(`format --headings failed with ${errors.length} error(s)`);
344
+ }
345
+ const apply = options.apply === true;
346
+ if (apply) {
347
+ for (const change of changes) {
348
+ (0, atomic_1.atomicWriteFile)(change.filePath, change.content);
349
+ }
350
+ }
351
+ const receipt = {
352
+ action: "format.headings",
353
+ ok: true,
354
+ dry_run: !apply,
355
+ applied: apply,
356
+ changed_count: changes.length,
357
+ changes: changes.map(({ filePath: _filePath, content: _content, ...change }) => change),
358
+ };
359
+ if (options.json) {
360
+ console.log(JSON.stringify(receipt, null, 2));
361
+ return;
362
+ }
363
+ console.log(`${apply ? "format headings updated" : "format headings dry-run"} ${changes.length} file(s)`);
364
+ }
253
365
  function runFormatCommandLocked(options) {
366
+ if (options.headings) {
367
+ runHeadingFormatCommandLocked(options);
368
+ return;
369
+ }
254
370
  const config = (0, config_1.loadConfig)(options.root);
255
371
  const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(options.root, config, node_1.ALLOWED_TYPES);
256
372
  const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, config);
@@ -227,6 +227,7 @@ function goalReceipt(root, loaded) {
227
227
  goal_condition: String(fm.goal_condition ?? ""),
228
228
  scope_refs: toStringList(fm.scope_refs),
229
229
  active_node: optionalString(fm.active_node),
230
+ last_active_node: optionalString(fm.last_active_node),
230
231
  required_skills: toStringList(fm.required_skills),
231
232
  required_checks: toStringList(fm.required_checks),
232
233
  max_iterations: optionalString(fm.max_iterations),
@@ -276,6 +277,11 @@ function isConcreteCandidate(node, statusRanks) {
276
277
  }
277
278
  return node.status !== "done";
278
279
  }
280
+ function isAchievedGoal(node, frontmatter) {
281
+ const status = frontmatter ? String(frontmatter.status ?? "") : node.status;
282
+ const goalState = frontmatter ? String(frontmatter.goal_state ?? "") : String(node.attributes.goal_state ?? "");
283
+ return status === "done" || goalState === "achieved";
284
+ }
279
285
  function resolveCandidate(index, idOrQid, ws) {
280
286
  const resolved = (0, qid_1.resolveQid)(index, idOrQid, ws);
281
287
  if (resolved.status !== "ok") {
@@ -283,6 +289,29 @@ function resolveCandidate(index, idOrQid, ws) {
283
289
  }
284
290
  return index.nodes[resolved.qid];
285
291
  }
292
+ function readOnlySubgraphBlockerWarnings(index, qids) {
293
+ const warnings = [];
294
+ const seen = new Set();
295
+ for (const qid of [...qids].sort()) {
296
+ const node = index.nodes[qid];
297
+ if (!node || node.source?.imported || node.status === "done") {
298
+ continue;
299
+ }
300
+ for (const blockerQid of node.edges.blocked_by) {
301
+ const blocker = index.nodes[blockerQid];
302
+ if (!blocker?.source?.imported) {
303
+ continue;
304
+ }
305
+ const subgraph = blocker.source.subgraph_alias;
306
+ const warning = `${node.qid} is blocked by read-only subgraph node ${blocker.qid}; update the source workspace for subgraph ${subgraph} or refresh the subgraph bundle before claiming local work`;
307
+ if (!seen.has(warning)) {
308
+ seen.add(warning);
309
+ warnings.push(warning);
310
+ }
311
+ }
312
+ }
313
+ return warnings;
314
+ }
286
315
  function runGoalShowCommand(options) {
287
316
  const loaded = loadGoal(options.root, options.id, options.ws);
288
317
  const receipt = { action: "showed", goal: goalReceipt(options.root, loaded) };
@@ -296,6 +325,9 @@ function runGoalShowCommand(options) {
296
325
  if (loaded.frontmatter.active_node) {
297
326
  console.log(`active_node: ${loaded.frontmatter.active_node}`);
298
327
  }
328
+ if (loaded.frontmatter.last_active_node) {
329
+ console.log(`last_active_node: ${loaded.frontmatter.last_active_node}`);
330
+ }
299
331
  const checks = toStringList(loaded.frontmatter.required_checks);
300
332
  console.log(`required_checks: ${checks.length === 0 ? "none" : checks.join(", ")}`);
301
333
  }
@@ -351,6 +383,23 @@ function runGoalNextCommand(options) {
351
383
  console.error("no actionable local work found for goal");
352
384
  return;
353
385
  }
386
+ if (isAchievedGoal(loaded.node, loaded.frontmatter)) {
387
+ if (options.json) {
388
+ console.log(JSON.stringify({
389
+ action: "selected",
390
+ goal: goalReceipt(options.root, loaded),
391
+ goal_source: loaded.resolutionSource,
392
+ node: null,
393
+ warnings: loaded.warnings,
394
+ }, null, 2));
395
+ return;
396
+ }
397
+ for (const warning of loaded.warnings) {
398
+ console.error(`warning: ${warning}`);
399
+ }
400
+ console.error("no actionable local work found for achieved goal");
401
+ return;
402
+ }
354
403
  const statusPreference = loaded.config.work.next.status_preference.map((status) => status.toLowerCase());
355
404
  const statusRanks = new Set(statusPreference);
356
405
  const warnings = [...loaded.warnings];
@@ -362,6 +411,7 @@ function runGoalNextCommand(options) {
362
411
  for (const invalid of scope.invalidRefs) {
363
412
  warnings.push(`scope contains non-actionable or unsupported node: ${invalid}`);
364
413
  }
414
+ warnings.push(...readOnlySubgraphBlockerWarnings(loaded.index, scope.actionableQids));
365
415
  if (activeNode) {
366
416
  const node = resolveCandidate(loaded.index, activeNode, loaded.node.ws);
367
417
  if (node && scope.actionableQids.has(node.qid) && isConcreteCandidate(node, statusRanks)) {
@@ -549,6 +599,7 @@ function runGoalCurrentCommand(options) {
549
599
  goal_condition: String(node.attributes.goal_condition ?? ""),
550
600
  scope_refs: toStringList(node.attributes.scope_refs),
551
601
  active_node: optionalString(node.attributes.active_node),
602
+ last_active_node: optionalString(node.attributes.last_active_node),
552
603
  required_skills: toStringList(node.attributes.required_skills),
553
604
  required_checks: toStringList(node.attributes.required_checks),
554
605
  max_iterations: optionalString(node.attributes.max_iterations),
@@ -643,6 +694,15 @@ function runGoalStateMutationLocked(action, options) {
643
694
  throw new errors_1.UsageError(`cannot ${action} archived goal ${loaded.node.qid}`);
644
695
  }
645
696
  const now = options.now ?? new Date();
697
+ if (action === "done" || action === "archive") {
698
+ const activeNode = optionalString(loaded.frontmatter.active_node);
699
+ if (activeNode) {
700
+ if (!loaded.frontmatter.last_active_node) {
701
+ loaded.frontmatter.last_active_node = activeNode;
702
+ }
703
+ delete loaded.frontmatter.active_node;
704
+ }
705
+ }
646
706
  loaded.frontmatter.goal_state = GOAL_STATE_BY_ACTION[action];
647
707
  loaded.frontmatter.status = ensureStatusAllowed(loaded.config, STATUS_BY_ACTION[action]);
648
708
  writeGoalFile(loaded, now);
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runGraphCloneCommand = runGraphCloneCommand;
7
7
  exports.runGraphForkCommand = runGraphForkCommand;
8
8
  exports.runGraphImportTemplateCommand = runGraphImportTemplateCommand;
9
+ exports.runGraphRefsCommand = runGraphRefsCommand;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const bundle_1 = require("./bundle");
@@ -14,6 +15,7 @@ const validate_1 = require("./validate");
14
15
  const config_1 = require("../core/config");
15
16
  const workspace_path_1 = require("../core/workspace_path");
16
17
  const indexer_1 = require("../graph/indexer");
18
+ const index_cache_1 = require("../graph/index_cache");
17
19
  const frontmatter_1 = require("../graph/frontmatter");
18
20
  const errors_1 = require("../util/errors");
19
21
  const qid_1 = require("../util/qid");
@@ -21,9 +23,116 @@ const atomic_1 = require("../util/atomic");
21
23
  const zip_1 = require("../util/zip");
22
24
  const lock_1 = require("../util/lock");
23
25
  const date_1 = require("../util/date");
26
+ const refs_1 = require("../util/refs");
24
27
  function writeJson(value) {
25
28
  console.log(JSON.stringify(value, null, 2));
26
29
  }
30
+ function toStringList(value) {
31
+ if (!Array.isArray(value)) {
32
+ return [];
33
+ }
34
+ return value.filter((item) => typeof item === "string");
35
+ }
36
+ function summarizeNode(node) {
37
+ return {
38
+ qid: node.qid,
39
+ id: node.id,
40
+ workspace: node.ws,
41
+ type: node.type,
42
+ title: node.title,
43
+ status: node.status,
44
+ path: node.path,
45
+ read_only: Boolean(node.source?.read_only ?? node.source?.imported),
46
+ source: node.source,
47
+ };
48
+ }
49
+ function summarizeRef(index, value, ws) {
50
+ if ((0, refs_1.isUriRef)(value)) {
51
+ return {
52
+ ref: value,
53
+ kind: "uri",
54
+ exists: true,
55
+ };
56
+ }
57
+ const resolved = (0, qid_1.resolveQid)(index, value, ws);
58
+ if (resolved.status !== "ok") {
59
+ return {
60
+ ref: value,
61
+ kind: "missing",
62
+ exists: false,
63
+ };
64
+ }
65
+ const node = index.nodes[resolved.qid];
66
+ if (!node) {
67
+ return {
68
+ ref: value,
69
+ kind: "missing",
70
+ exists: false,
71
+ qid: resolved.qid,
72
+ };
73
+ }
74
+ return {
75
+ ref: value,
76
+ kind: "node",
77
+ exists: true,
78
+ qid: node.qid,
79
+ node: summarizeNode(node),
80
+ };
81
+ }
82
+ function summarizeRefs(index, values, ws) {
83
+ return [...new Set(values)].sort().map((value) => summarizeRef(index, value, ws));
84
+ }
85
+ function compactOutgoing(node) {
86
+ return {
87
+ scope_refs: toStringList(node.attributes.scope_refs),
88
+ epic: node.edges.epic ? [node.edges.epic] : [],
89
+ parent: node.edges.parent ? [node.edges.parent] : [],
90
+ prev: node.edges.prev ? [node.edges.prev] : [],
91
+ next: node.edges.next ? [node.edges.next] : [],
92
+ relates: node.edges.relates,
93
+ blocked_by: node.edges.blocked_by,
94
+ blocks: node.edges.blocks,
95
+ context_refs: node.edges.context_refs ?? [],
96
+ evidence_refs: node.edges.evidence_refs ?? [],
97
+ };
98
+ }
99
+ function incomingScopeRefs(index, targetQid) {
100
+ const sources = [];
101
+ for (const node of Object.values(index.nodes)) {
102
+ for (const ref of toStringList(node.attributes.scope_refs)) {
103
+ const resolved = (0, qid_1.resolveQid)(index, ref, node.ws);
104
+ if (resolved.status === "ok" && resolved.qid === targetQid) {
105
+ sources.push(node.qid);
106
+ }
107
+ }
108
+ }
109
+ return sources.sort();
110
+ }
111
+ function buildGraphRefsReceipt(index, node, warnings) {
112
+ const outgoingRaw = compactOutgoing(node);
113
+ const incomingRaw = {
114
+ scope_refs: incomingScopeRefs(index, node.qid),
115
+ epic: index.reverse_edges.epic?.[node.qid] ?? [],
116
+ parent: index.reverse_edges.parent?.[node.qid] ?? [],
117
+ prev: index.reverse_edges.prev?.[node.qid] ?? [],
118
+ next: index.reverse_edges.next?.[node.qid] ?? [],
119
+ relates: index.reverse_edges.relates?.[node.qid] ?? [],
120
+ blocked_by: index.reverse_edges.blocked_by?.[node.qid] ?? [],
121
+ blocks: index.reverse_edges.blocks?.[node.qid] ?? [],
122
+ context_refs: index.reverse_edges.context_refs?.[node.qid] ?? [],
123
+ evidence_refs: index.reverse_edges.evidence_refs?.[node.qid] ?? [],
124
+ };
125
+ const outgoing = Object.fromEntries(Object.entries(outgoingRaw).map(([key, values]) => [key, summarizeRefs(index, values, node.ws)]));
126
+ const incoming = Object.fromEntries(Object.entries(incomingRaw).map(([key, values]) => [key, summarizeRefs(index, values, node.ws)]));
127
+ return {
128
+ action: "graph.refs",
129
+ ok: true,
130
+ target: summarizeNode(node),
131
+ outgoing,
132
+ incoming,
133
+ warnings,
134
+ };
135
+ }
27
136
  function toPosixPath(value) {
28
137
  return value.split(path_1.default.sep).join("/");
29
138
  }
@@ -702,3 +811,42 @@ function runGraphImportTemplateCommand(options) {
702
811
  console.log(`selected_goal: ${receipt.selected_goal.qid}${receipt.selected_goal.planned ? " (planned)" : ""}`);
703
812
  }
704
813
  }
814
+ function runGraphRefsCommand(options) {
815
+ const config = (0, config_1.loadConfig)(options.root);
816
+ const { index, warnings } = (0, index_cache_1.loadIndex)({ root: options.root, config });
817
+ const resolved = (0, qid_1.resolveQid)(index, options.id, options.ws);
818
+ if (resolved.status !== "ok") {
819
+ throw new errors_1.NotFoundError((0, qid_1.formatResolveError)("node", options.id, resolved, options.ws));
820
+ }
821
+ const node = index.nodes[resolved.qid];
822
+ if (!node) {
823
+ throw new errors_1.NotFoundError(`node not found: ${options.id}`);
824
+ }
825
+ const receipt = buildGraphRefsReceipt(index, node, warnings);
826
+ if (options.json) {
827
+ writeJson(receipt);
828
+ return;
829
+ }
830
+ console.log(`graph refs: ${node.qid}`);
831
+ if (node.source?.imported) {
832
+ console.log(`source: subgraph:${node.source.subgraph_alias} read-only`);
833
+ }
834
+ for (const [direction, lanes] of Object.entries({ outgoing: receipt.outgoing, incoming: receipt.incoming })) {
835
+ console.log(`${direction}:`);
836
+ for (const [lane, refs] of Object.entries(lanes)) {
837
+ if (refs.length === 0) {
838
+ continue;
839
+ }
840
+ console.log(` ${lane}:`);
841
+ for (const ref of refs) {
842
+ const target = ref.node
843
+ ? `${ref.node.qid}${ref.node.read_only ? " (read-only)" : ""}`
844
+ : ref.ref;
845
+ console.log(` - ${target}`);
846
+ }
847
+ }
848
+ }
849
+ for (const warning of warnings) {
850
+ console.error(`warning: ${warning}`);
851
+ }
852
+ }