mdkg 0.3.0 → 0.3.2

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.
@@ -17,6 +17,8 @@ const errors_1 = require("../util/errors");
17
17
  const date_1 = require("../util/date");
18
18
  const id_1 = require("../util/id");
19
19
  const refs_1 = require("../util/refs");
20
+ const atomic_1 = require("../util/atomic");
21
+ const lock_1 = require("../util/lock");
20
22
  const DEC_ID_RE = /^dec-[0-9]+$/;
21
23
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
22
24
  const ID_LIST_KEYS = new Set(["refs", "scope"]);
@@ -248,7 +250,7 @@ function normalizeFrontmatter(frontmatter, schema, type, workStatusEnum, priorit
248
250
  }
249
251
  return { normalized, errors };
250
252
  }
251
- function runFormatCommand(options) {
253
+ function runFormatCommandLocked(options) {
252
254
  const config = (0, config_1.loadConfig)(options.root);
253
255
  const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(options.root, config, node_1.ALLOWED_TYPES);
254
256
  const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, config);
@@ -324,7 +326,11 @@ function runFormatCommand(options) {
324
326
  throw new errors_1.ValidationError(`format failed with ${errors.length} error(s)`);
325
327
  }
326
328
  for (const update of updates) {
327
- fs_1.default.writeFileSync(update.filePath, update.content, "utf8");
329
+ (0, atomic_1.atomicWriteFile)(update.filePath, update.content);
328
330
  }
329
331
  console.log(`format updated ${updates.length} file(s)`);
330
332
  }
333
+ function runFormatCommand(options) {
334
+ const config = (0, config_1.loadConfig)(options.root);
335
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runFormatCommandLocked(options));
336
+ }
@@ -29,7 +29,7 @@ const qid_1 = require("../util/qid");
29
29
  const sort_1 = require("../util/sort");
30
30
  const event_support_1 = require("./event_support");
31
31
  const node_card_1 = require("./node_card");
32
- const CONCRETE_GOAL_NEXT_TYPES = new Set(["feat", "task", "bug", "test"]);
32
+ const CONCRETE_GOAL_NEXT_TYPES = new Set(["feat", "task", "bug", "test", "spike"]);
33
33
  const SELECTED_GOAL_STATE_PATH = path_1.default.join(".mdkg", "state", "selected-goal.json");
34
34
  const GOAL_STATE_BY_ACTION = {
35
35
  pause: "paused",
@@ -7,8 +7,8 @@ const errors_1 = require("../util/errors");
7
7
  const qid_1 = require("../util/qid");
8
8
  const sort_1 = require("../util/sort");
9
9
  const node_card_1 = require("./node_card");
10
- const NEXT_TYPES = new Set(["feat", "task", "bug", "test"]);
11
- const NO_MATCH_MESSAGE = 'no matching work items found; consider `mdkg new task "..."` or `mdkg new test "..."`';
10
+ const NEXT_TYPES = new Set(["feat", "task", "bug", "test", "spike"]);
11
+ const NO_MATCH_MESSAGE = 'no matching work items found; consider `mdkg new task "..."`, `mdkg new test "..."`, or `mdkg new spike "..."`';
12
12
  function normalizeWorkspace(value) {
13
13
  if (!value || value === "all") {
14
14
  return undefined;
@@ -20,6 +20,8 @@ const skill_support_1 = require("./skill_support");
20
20
  const query_output_1 = require("./query_output");
21
21
  const event_support_1 = require("./event_support");
22
22
  const skill_mirror_1 = require("./skill_mirror");
23
+ const atomic_1 = require("../util/atomic");
24
+ const lock_1 = require("../util/lock");
23
25
  function parseCsvList(raw) {
24
26
  if (!raw) {
25
27
  return [];
@@ -142,7 +144,7 @@ function matchesSkillQuery(skill, terms) {
142
144
  }
143
145
  return true;
144
146
  }
145
- function runSkillNewCommand(options) {
147
+ function runSkillNewCommandLocked(options) {
146
148
  const root = options.root;
147
149
  const config = (0, config_1.loadConfig)(root);
148
150
  const slug = normalizeSlug(options.slug);
@@ -181,7 +183,7 @@ function runSkillNewCommand(options) {
181
183
  authors,
182
184
  links,
183
185
  });
184
- fs_1.default.writeFileSync(canonicalPath, content, "utf8");
186
+ (0, atomic_1.atomicWriteFile)(canonicalPath, content);
185
187
  (0, skill_support_1.ensureSkillsRegistry)(root, config);
186
188
  (0, skill_support_1.refreshSkillsRegistry)(root, config);
187
189
  if ((0, skill_mirror_1.shouldMaintainSkillMirrors)(root)) {
@@ -220,6 +222,10 @@ function runSkillNewCommand(options) {
220
222
  }
221
223
  console.log(`skill created: ${receipt.qid} (${receipt.path})`);
222
224
  }
225
+ function runSkillNewCommand(options) {
226
+ const config = (0, config_1.loadConfig)(options.root);
227
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runSkillNewCommandLocked(options));
228
+ }
223
229
  function runSkillListCommand(options) {
224
230
  const config = (0, config_1.loadConfig)(options.root);
225
231
  const { index, rebuilt, stale } = (0, skills_index_cache_1.loadSkillsIndex)({
@@ -426,7 +432,7 @@ function runSkillValidateCommand(options) {
426
432
  }
427
433
  console.log(`skill validation ok: ${checkedCount} skill${checkedCount === 1 ? "" : "s"} checked`);
428
434
  }
429
- function runSkillSyncCommand(options) {
435
+ function runSkillSyncCommandLocked(options) {
430
436
  const config = (0, config_1.loadConfig)(options.root);
431
437
  const result = (0, skill_mirror_1.syncSkillMirrors)({
432
438
  root: options.root,
@@ -443,3 +449,7 @@ function runSkillSyncCommand(options) {
443
449
  }
444
450
  console.log(`skill mirror sync ok: ${result.synced} synced, ${result.pruned} pruned across ${result.targets} target${result.targets === 1 ? "" : "s"}`);
445
451
  }
452
+ function runSkillSyncCommand(options) {
453
+ const config = (0, config_1.loadConfig)(options.root);
454
+ return (0, lock_1.withMutationLock)(options.root, config.index.lock_timeout_ms, () => runSkillSyncCommandLocked(options));
455
+ }
@@ -14,6 +14,7 @@ exports.formatSkillCard = formatSkillCard;
14
14
  const fs_1 = __importDefault(require("fs"));
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const skills_indexer_1 = require("../graph/skills_indexer");
17
+ const atomic_1 = require("../util/atomic");
17
18
  exports.SKILL_REGISTRY_START = "<!-- mdkg:skill-registry:start -->";
18
19
  exports.SKILL_REGISTRY_END = "<!-- mdkg:skill-registry:end -->";
19
20
  function renderYamlList(values) {
@@ -105,8 +106,7 @@ function ensureSkillsRegistry(root, config) {
105
106
  const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
106
107
  const registryPath = path_1.default.join(skillsRoot, "registry.md");
107
108
  if (!fs_1.default.existsSync(registryPath)) {
108
- fs_1.default.mkdirSync(path_1.default.dirname(registryPath), { recursive: true });
109
- fs_1.default.writeFileSync(registryPath, registryTemplate(), "utf8");
109
+ (0, atomic_1.atomicWriteFile)(registryPath, registryTemplate());
110
110
  }
111
111
  return registryPath;
112
112
  }
@@ -115,7 +115,7 @@ function refreshSkillsRegistry(root, config) {
115
115
  const raw = fs_1.default.readFileSync(registryPath, "utf8");
116
116
  const index = (0, skills_indexer_1.buildSkillsIndex)(root, config);
117
117
  const updated = replaceManagedSection(raw, renderRegistryLines(index));
118
- fs_1.default.writeFileSync(registryPath, updated, "utf8");
118
+ (0, atomic_1.atomicWriteFile)(registryPath, updated);
119
119
  }
120
120
  function formatSkillCard(skill) {
121
121
  return [skill.qid, "skill", "-/-", skill.name, skill.path].join(" | ");
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectStatus = collectStatus;
7
+ exports.runStatusCommand = runStatusCommand;
8
+ const child_process_1 = require("child_process");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const config_1 = require("../core/config");
12
+ const project_db_migrations_1 = require("../core/project_db_migrations");
13
+ const version_1 = require("../core/version");
14
+ const capabilities_indexer_1 = require("../graph/capabilities_indexer");
15
+ const capabilities_index_cache_1 = require("../graph/capabilities_index_cache");
16
+ const index_cache_1 = require("../graph/index_cache");
17
+ const skills_indexer_1 = require("../graph/skills_indexer");
18
+ const skills_index_cache_1 = require("../graph/skills_index_cache");
19
+ const staleness_1 = require("../graph/staleness");
20
+ const subgraphs_1 = require("../graph/subgraphs");
21
+ const validate_graph_1 = require("../graph/validate_graph");
22
+ function rel(root, target) {
23
+ return path_1.default.relative(root, target).replace(/\\/g, "/") || ".";
24
+ }
25
+ function runGit(root, args) {
26
+ const result = (0, child_process_1.spawnSync)("git", args, { cwd: root, encoding: "utf8" });
27
+ if (result.status !== 0) {
28
+ return undefined;
29
+ }
30
+ return result.stdout.trim();
31
+ }
32
+ function gitStatus(root) {
33
+ const inside = runGit(root, ["rev-parse", "--is-inside-work-tree"]) === "true";
34
+ if (!inside) {
35
+ return {
36
+ inside: false,
37
+ branch: null,
38
+ dirty: false,
39
+ dirty_count: 0,
40
+ untracked_count: 0,
41
+ ahead: null,
42
+ behind: null,
43
+ };
44
+ }
45
+ const branch = runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
46
+ const porcelain = runGit(root, ["status", "--porcelain"]) ?? "";
47
+ const lines = porcelain.split(/\r?\n/).filter(Boolean);
48
+ const untracked = lines.filter((line) => line.startsWith("??")).length;
49
+ const aheadBehindRaw = runGit(root, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
50
+ const [aheadRaw, behindRaw] = aheadBehindRaw ? aheadBehindRaw.split(/\s+/) : [];
51
+ return {
52
+ inside: true,
53
+ branch,
54
+ dirty: lines.length > 0,
55
+ dirty_count: lines.length,
56
+ untracked_count: untracked,
57
+ ahead: aheadRaw === undefined ? null : Number.parseInt(aheadRaw, 10),
58
+ behind: behindRaw === undefined ? null : Number.parseInt(behindRaw, 10),
59
+ };
60
+ }
61
+ function cacheStatus(root, filePath, stale) {
62
+ let staleValue = null;
63
+ try {
64
+ staleValue = stale();
65
+ }
66
+ catch {
67
+ staleValue = null;
68
+ }
69
+ return {
70
+ path: rel(root, filePath),
71
+ exists: fs_1.default.existsSync(filePath),
72
+ stale: staleValue,
73
+ };
74
+ }
75
+ function readSelectedGoalState(root) {
76
+ const filePath = path_1.default.join(root, ".mdkg", "state", "selected-goal.json");
77
+ if (!fs_1.default.existsSync(filePath)) {
78
+ return { state: null };
79
+ }
80
+ try {
81
+ const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf8"));
82
+ if (typeof parsed.qid === "string" &&
83
+ typeof parsed.id === "string" &&
84
+ typeof parsed.ws === "string" &&
85
+ typeof parsed.selected_at === "string") {
86
+ return {
87
+ state: {
88
+ qid: parsed.qid.toLowerCase(),
89
+ id: parsed.id.toLowerCase(),
90
+ ws: parsed.ws.toLowerCase(),
91
+ selected_at: parsed.selected_at,
92
+ },
93
+ };
94
+ }
95
+ return { state: null, warning: "selected goal state is malformed" };
96
+ }
97
+ catch {
98
+ return { state: null, warning: "selected goal state is unreadable" };
99
+ }
100
+ }
101
+ function releaseStatus(root) {
102
+ const version = (0, version_1.readPackageVersion)();
103
+ const changelogPath = path_1.default.join(root, "CHANGELOG.md");
104
+ const changelogHasVersion = fs_1.default.existsSync(changelogPath)
105
+ ? fs_1.default.readFileSync(changelogPath, "utf8").includes(`## ${version}`) ||
106
+ fs_1.default.readFileSync(changelogPath, "utf8").includes(`## [${version}]`)
107
+ : false;
108
+ return {
109
+ package_version: version,
110
+ changelog_path: fs_1.default.existsSync(changelogPath) ? "CHANGELOG.md" : null,
111
+ changelog_has_version: changelogHasVersion,
112
+ };
113
+ }
114
+ function collectStatus(root) {
115
+ const warnings = [];
116
+ const errors = [];
117
+ const config = (0, config_1.loadConfig)(root);
118
+ let index;
119
+ let graphStale = false;
120
+ let graphWarnings = [];
121
+ let graphErrors = [];
122
+ try {
123
+ const loaded = (0, index_cache_1.loadIndex)({
124
+ root,
125
+ config,
126
+ useCache: true,
127
+ allowReindex: false,
128
+ includeImports: true,
129
+ });
130
+ index = loaded.index;
131
+ graphStale = loaded.stale;
132
+ graphWarnings = loaded.warnings;
133
+ graphErrors = (0, validate_graph_1.collectGraphErrors)(loaded.index);
134
+ }
135
+ catch (err) {
136
+ graphStale = true;
137
+ graphErrors = [err instanceof Error ? err.message : String(err)];
138
+ }
139
+ const selected = readSelectedGoalState(root);
140
+ if (selected.warning) {
141
+ warnings.push(selected.warning);
142
+ }
143
+ const selectedNode = selected.state && index ? index.nodes[selected.state.qid] : undefined;
144
+ const selectedAchieved = selectedNode?.status === "done" || String(selectedNode?.attributes.goal_state ?? "") === "achieved";
145
+ const selectedMissing = selected.state !== null && !selectedNode;
146
+ if (selectedMissing) {
147
+ warnings.push("selected goal is missing from the graph index");
148
+ }
149
+ if (selectedAchieved) {
150
+ warnings.push("selected goal is already achieved");
151
+ }
152
+ const git = gitStatus(root);
153
+ if (git.inside && git.dirty) {
154
+ warnings.push(`git worktree is dirty (${git.dirty_count} changed paths)`);
155
+ }
156
+ const generated = {
157
+ index: cacheStatus(root, path_1.default.resolve(root, config.index.global_index_path), () => (0, staleness_1.isIndexStale)(root, config)),
158
+ skills: cacheStatus(root, (0, skills_indexer_1.resolveSkillsIndexPath)(root), () => (0, skills_index_cache_1.isSkillsIndexStale)(root, config)),
159
+ capabilities: cacheStatus(root, (0, capabilities_indexer_1.resolveCapabilitiesIndexPath)(root, config), () => (0, capabilities_index_cache_1.isCapabilitiesIndexStale)(root, config)),
160
+ subgraphs: cacheStatus(root, (0, subgraphs_1.resolveSubgraphsIndexPath)(root), () => (0, subgraphs_1.isSubgraphsIndexStale)(root, config)),
161
+ };
162
+ let db = {
163
+ enabled: false,
164
+ ok: null,
165
+ database: null,
166
+ failure_count: 0,
167
+ warning_count: 0,
168
+ };
169
+ if (config.db.enabled) {
170
+ const verification = (0, project_db_migrations_1.verifyProjectDb)(root, config);
171
+ db = {
172
+ enabled: verification.enabled,
173
+ ok: verification.ok,
174
+ database: verification.database,
175
+ failure_count: verification.failure_count,
176
+ warning_count: verification.warning_count,
177
+ };
178
+ }
179
+ if (graphErrors.length > 0) {
180
+ errors.push(...graphErrors.map((error) => `graph: ${error}`));
181
+ }
182
+ if (db.enabled && db.ok === false) {
183
+ errors.push("db: project DB verification failed");
184
+ }
185
+ if (graphStale) {
186
+ warnings.push("graph index cache is stale");
187
+ }
188
+ warnings.push(...graphWarnings.map((warning) => `graph: ${warning}`));
189
+ const cacheEntries = [
190
+ ["index", generated.index, true],
191
+ ["skills", generated.skills, true],
192
+ ["capabilities", generated.capabilities, true],
193
+ ["subgraphs", generated.subgraphs, Object.keys(config.subgraphs).length > 0],
194
+ ];
195
+ for (const [name, cache, required] of cacheEntries) {
196
+ if (!required) {
197
+ continue;
198
+ }
199
+ if (!cache.exists) {
200
+ warnings.push(`${name} cache is missing`);
201
+ }
202
+ else if (cache.stale) {
203
+ warnings.push(`${name} cache is stale`);
204
+ }
205
+ }
206
+ const level = errors.length > 0 ? "fail" : warnings.length > 0 ? "warn" : "ok";
207
+ return {
208
+ action: "status",
209
+ ok: errors.length === 0,
210
+ level,
211
+ root,
212
+ mdkg: {
213
+ version: (0, version_1.readPackageVersion)(),
214
+ config_schema_version: config.schema_version,
215
+ index_backend: config.index.backend,
216
+ },
217
+ git,
218
+ release: releaseStatus(root),
219
+ graph: {
220
+ ok: graphErrors.length === 0,
221
+ node_count: index ? Object.keys(index.nodes).length : null,
222
+ workspace_count: index ? Object.keys(index.workspaces).length : null,
223
+ stale: graphStale,
224
+ warning_count: graphWarnings.length,
225
+ error_count: graphErrors.length,
226
+ },
227
+ goal: {
228
+ selected: selected.state,
229
+ selected_exists: selected.state === null ? null : !selectedMissing,
230
+ selected_achieved: selected.state === null ? null : selectedAchieved,
231
+ active_node: selectedNode?.attributes.active_node ?? null,
232
+ goal_state: selectedNode?.attributes.goal_state ?? null,
233
+ status: selectedNode?.status ?? null,
234
+ },
235
+ db,
236
+ generated,
237
+ summary: {
238
+ level,
239
+ warning_count: warnings.length,
240
+ error_count: errors.length,
241
+ warnings,
242
+ errors,
243
+ },
244
+ };
245
+ }
246
+ function runStatusCommand(options) {
247
+ const payload = collectStatus(options.root);
248
+ if (options.json) {
249
+ console.log(JSON.stringify(payload, null, 2));
250
+ return;
251
+ }
252
+ console.log(`status ${payload.level}`);
253
+ console.log(`root: ${payload.root}`);
254
+ console.log(`mdkg: ${payload.mdkg.version}`);
255
+ console.log(`git: ${payload.git.inside ? `${payload.git.branch ?? "detached"} dirty=${payload.git.dirty}` : "not a git repo"}`);
256
+ console.log(`graph: ${payload.graph.ok ? "ok" : "fail"} nodes=${payload.graph.node_count ?? "unknown"} stale=${payload.graph.stale}`);
257
+ console.log(`db: ${payload.db.enabled ? (payload.db.ok ? "ok" : "fail") : "disabled"}`);
258
+ if (payload.goal.selected) {
259
+ console.log(`goal: ${payload.goal.selected.qid} status=${payload.goal.status ?? "unknown"} state=${payload.goal.goal_state ?? "unknown"}`);
260
+ }
261
+ else {
262
+ console.log("goal: none selected");
263
+ }
264
+ for (const warning of payload.summary.warnings) {
265
+ console.log(`warn: ${warning}`);
266
+ }
267
+ for (const error of payload.summary.errors) {
268
+ console.log(`fail: ${error}`);
269
+ }
270
+ }