mdkg 0.0.8 → 0.0.9

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 +56 -0
  2. package/CONTRIBUTING.md +124 -0
  3. package/README.md +33 -14
  4. package/dist/cli.js +80 -32
  5. package/dist/commands/checkpoint.js +19 -2
  6. package/dist/commands/event.js +12 -0
  7. package/dist/commands/new.js +57 -21
  8. package/dist/commands/pack.js +14 -0
  9. package/dist/commands/query_output.js +2 -0
  10. package/dist/commands/search.js +8 -0
  11. package/dist/commands/show.js +7 -0
  12. package/dist/commands/skill.js +80 -12
  13. package/dist/commands/task.js +42 -12
  14. package/dist/commands/validate.js +31 -3
  15. package/dist/commands/workspace.js +105 -13
  16. package/dist/core/config.js +217 -22
  17. package/dist/core/migrate.js +39 -5
  18. package/dist/core/workspace_path.js +41 -0
  19. package/dist/graph/agent_file_types.js +392 -0
  20. package/dist/graph/edges.js +13 -10
  21. package/dist/graph/frontmatter.js +33 -0
  22. package/dist/graph/indexer.js +1 -0
  23. package/dist/graph/node.js +43 -16
  24. package/dist/graph/skills_indexer.js +14 -1
  25. package/dist/graph/template_schema.js +13 -126
  26. package/dist/graph/validate_graph.js +302 -2
  27. package/dist/init/AGENT_START.md +13 -2
  28. package/dist/init/CLI_COMMAND_MATRIX.md +43 -1
  29. package/dist/init/README.md +7 -0
  30. package/dist/init/core/rule-6-templates-and-schemas.md +1 -3
  31. package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +1 -1
  32. package/dist/init/templates/default/dispute.md +31 -0
  33. package/dist/init/templates/default/feedback.md +27 -0
  34. package/dist/init/templates/default/proposal.md +35 -0
  35. package/dist/init/templates/default/receipt.md +31 -0
  36. package/dist/init/templates/default/spec.md +43 -0
  37. package/dist/init/templates/default/work.md +44 -0
  38. package/dist/init/templates/default/work_order.md +32 -0
  39. package/dist/pack/export_json.js +3 -0
  40. package/dist/pack/export_md.js +9 -0
  41. package/dist/pack/export_xml.js +9 -0
  42. package/dist/pack/order.js +7 -0
  43. package/dist/pack/pack.js +1 -0
  44. package/dist/templates/loader.js +2 -2
  45. package/dist/util/argparse.js +1 -0
  46. package/dist/util/id.js +19 -0
  47. package/package.json +9 -2
  48. package/scripts/postinstall.js +89 -0
@@ -6,11 +6,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runWorkspaceListCommand = runWorkspaceListCommand;
7
7
  exports.runWorkspaceAddCommand = runWorkspaceAddCommand;
8
8
  exports.runWorkspaceRemoveCommand = runWorkspaceRemoveCommand;
9
+ exports.runWorkspaceEnableCommand = runWorkspaceEnableCommand;
10
+ exports.runWorkspaceDisableCommand = runWorkspaceDisableCommand;
9
11
  const fs_1 = __importDefault(require("fs"));
10
12
  const path_1 = __importDefault(require("path"));
11
13
  const config_1 = require("../core/config");
14
+ const migrate_1 = require("../core/migrate");
15
+ const workspace_path_1 = require("../core/workspace_path");
12
16
  const errors_1 = require("../util/errors");
13
17
  const ALIAS_RE = /^[a-z][a-z0-9_]*$/;
18
+ function workspaceReceipt(alias, workspace) {
19
+ return {
20
+ alias,
21
+ path: workspace.path,
22
+ enabled: workspace.enabled,
23
+ mdkg_dir: workspace.mdkg_dir,
24
+ };
25
+ }
26
+ function printWorkspaceMutationReceipt(action, workspace, json) {
27
+ if (json) {
28
+ console.log(JSON.stringify({ action, workspace }, null, 2));
29
+ return;
30
+ }
31
+ if (action === "added") {
32
+ console.log(`workspace added: ${workspace.alias} (${workspace.path})`);
33
+ return;
34
+ }
35
+ console.log(`workspace ${action}: ${workspace.alias}`);
36
+ }
14
37
  function readRawConfig(root) {
15
38
  const configPath = path_1.default.join(root, ".mdkg", "config.json");
16
39
  if (!fs_1.default.existsSync(configPath)) {
@@ -27,24 +50,51 @@ function readRawConfig(root) {
27
50
  if (typeof raw !== "object" || raw === null) {
28
51
  throw new errors_1.UsageError("config must be a JSON object");
29
52
  }
30
- return { path: configPath, raw: raw };
53
+ const migrated = (0, migrate_1.migrateConfig)(raw).config;
54
+ (0, config_1.validateConfigSchema)(migrated);
55
+ if (typeof migrated !== "object" || migrated === null || Array.isArray(migrated)) {
56
+ throw new errors_1.UsageError("config must be a JSON object");
57
+ }
58
+ return { path: configPath, raw: migrated };
31
59
  }
32
60
  function writeRawConfig(configPath, raw) {
33
61
  fs_1.default.writeFileSync(configPath, JSON.stringify(raw, null, 2), "utf8");
34
62
  }
35
63
  function normalizeAlias(alias) {
36
- const normalized = alias.toLowerCase();
37
- if (normalized === "all") {
64
+ if (alias.toLowerCase() === "all") {
38
65
  throw new errors_1.UsageError("workspace alias cannot be 'all'");
39
66
  }
40
- if (!ALIAS_RE.test(normalized)) {
67
+ if (alias !== alias.toLowerCase() || !ALIAS_RE.test(alias)) {
41
68
  throw new errors_1.UsageError("workspace alias must be lowercase and use [a-z0-9_]");
42
69
  }
43
- return normalized;
70
+ return alias;
71
+ }
72
+ function normalizeCommandWorkspacePath(value, label) {
73
+ try {
74
+ return (0, workspace_path_1.normalizeContainedWorkspacePath)(value, label);
75
+ }
76
+ catch (err) {
77
+ const message = err instanceof Error ? err.message : String(err);
78
+ throw new errors_1.UsageError(message);
79
+ }
44
80
  }
45
81
  function runWorkspaceListCommand(options) {
46
82
  const config = (0, config_1.loadConfig)(options.root);
47
83
  const aliases = Object.keys(config.workspaces).sort();
84
+ if (options.json) {
85
+ console.log(JSON.stringify({
86
+ workspaces: aliases.map((alias) => {
87
+ const ws = config.workspaces[alias];
88
+ return {
89
+ alias,
90
+ path: ws.path,
91
+ enabled: ws.enabled,
92
+ mdkg_dir: ws.mdkg_dir,
93
+ };
94
+ }),
95
+ }, null, 2));
96
+ return;
97
+ }
48
98
  if (aliases.length === 0) {
49
99
  console.log("no workspaces registered");
50
100
  return;
@@ -57,11 +107,11 @@ function runWorkspaceListCommand(options) {
57
107
  }
58
108
  function runWorkspaceAddCommand(options) {
59
109
  const alias = normalizeAlias(options.alias);
60
- const workspacePath = options.workspacePath.trim();
61
- if (!workspacePath) {
62
- throw new errors_1.UsageError("workspace path cannot be empty");
110
+ const workspacePath = normalizeCommandWorkspacePath(options.workspacePath, "workspace path");
111
+ const mdkgDir = normalizeCommandWorkspacePath(options.mdkgDir ?? ".mdkg", "workspace mdkg dir");
112
+ if ((0, workspace_path_1.isRootWorkspacePath)(workspacePath)) {
113
+ throw new errors_1.UsageError('workspace path must not be "." for non-root workspaces');
63
114
  }
64
- const mdkgDir = options.mdkgDir?.trim() || ".mdkg";
65
115
  const { path: configPath, raw } = readRawConfig(options.root);
66
116
  const workspacesRaw = raw.workspaces;
67
117
  if (typeof workspacesRaw !== "object" || workspacesRaw === null) {
@@ -71,14 +121,27 @@ function runWorkspaceAddCommand(options) {
71
121
  if (workspaces[alias]) {
72
122
  throw new errors_1.UsageError(`workspace already exists: ${alias}`);
73
123
  }
74
- workspaces[alias] = { path: workspacePath, enabled: true, mdkg_dir: mdkgDir };
124
+ const docRootKey = (0, workspace_path_1.workspaceDocumentRootKey)(workspacePath, mdkgDir);
125
+ for (const [existingAlias, entry] of Object.entries(workspaces)) {
126
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
127
+ continue;
128
+ }
129
+ const existing = entry;
130
+ if (typeof existing.path === "string" &&
131
+ typeof existing.mdkg_dir === "string" &&
132
+ (0, workspace_path_1.workspaceDocumentRootKey)(existing.path, existing.mdkg_dir) === docRootKey) {
133
+ throw new errors_1.UsageError(`workspace document root already registered by ${existingAlias}`);
134
+ }
135
+ }
136
+ const workspace = { path: workspacePath, enabled: true, mdkg_dir: mdkgDir };
137
+ workspaces[alias] = workspace;
75
138
  raw.workspaces = workspaces;
76
139
  writeRawConfig(configPath, raw);
77
140
  const wsRoot = path_1.default.resolve(options.root, workspacePath, mdkgDir);
78
141
  fs_1.default.mkdirSync(path_1.default.join(wsRoot, "core"), { recursive: true });
79
142
  fs_1.default.mkdirSync(path_1.default.join(wsRoot, "design"), { recursive: true });
80
143
  fs_1.default.mkdirSync(path_1.default.join(wsRoot, "work"), { recursive: true });
81
- console.log(`workspace added: ${alias} (${workspacePath})`);
144
+ printWorkspaceMutationReceipt("added", workspaceReceipt(alias, workspace), options.json);
82
145
  }
83
146
  function runWorkspaceRemoveCommand(options) {
84
147
  const alias = normalizeAlias(options.alias);
@@ -91,11 +154,40 @@ function runWorkspaceRemoveCommand(options) {
91
154
  throw new errors_1.UsageError("config.workspaces must be an object");
92
155
  }
93
156
  const workspaces = workspacesRaw;
94
- if (!workspaces[alias]) {
157
+ const workspace = workspaces[alias];
158
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
95
159
  throw new errors_1.NotFoundError(`workspace not found: ${alias}`);
96
160
  }
161
+ const removed = workspaceReceipt(alias, workspace);
97
162
  delete workspaces[alias];
98
163
  raw.workspaces = workspaces;
99
164
  writeRawConfig(configPath, raw);
100
- console.log(`workspace removed: ${alias}`);
165
+ printWorkspaceMutationReceipt("removed", removed, options.json);
166
+ }
167
+ function setWorkspaceEnabled(options, enabled) {
168
+ const alias = normalizeAlias(options.alias);
169
+ if (alias === "root" && !enabled) {
170
+ throw new errors_1.UsageError("cannot disable root workspace");
171
+ }
172
+ const { path: configPath, raw } = readRawConfig(options.root);
173
+ const workspacesRaw = raw.workspaces;
174
+ if (typeof workspacesRaw !== "object" || workspacesRaw === null) {
175
+ throw new errors_1.UsageError("config.workspaces must be an object");
176
+ }
177
+ const workspaces = workspacesRaw;
178
+ const workspace = workspaces[alias];
179
+ if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) {
180
+ throw new errors_1.NotFoundError(`workspace not found: ${alias}`);
181
+ }
182
+ const updated = { ...workspace, enabled };
183
+ workspaces[alias] = updated;
184
+ raw.workspaces = workspaces;
185
+ writeRawConfig(configPath, raw);
186
+ printWorkspaceMutationReceipt(enabled ? "enabled" : "disabled", workspaceReceipt(alias, updated), options.json);
187
+ }
188
+ function runWorkspaceEnableCommand(options) {
189
+ setWorkspaceEnabled(options, true);
190
+ }
191
+ function runWorkspaceDisableCommand(options) {
192
+ setWorkspaceEnabled(options, false);
101
193
  }
@@ -3,10 +3,23 @@ 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.validateConfigSchema = validateConfigSchema;
6
7
  exports.loadConfig = loadConfig;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const paths_1 = require("./paths");
9
10
  const migrate_1 = require("./migrate");
11
+ const workspace_path_1 = require("./workspace_path");
12
+ const WORKSPACE_ALIAS_RE = /^[a-z][a-z0-9_]*$/;
13
+ const PACK_EDGE_KEYS = new Set([
14
+ "parent",
15
+ "epic",
16
+ "relates",
17
+ "blocked_by",
18
+ "blocks",
19
+ "prev",
20
+ "next",
21
+ ]);
22
+ const NEXT_WORK_STRATEGIES = new Set(["chain_then_priority"]);
10
23
  function isObject(value) {
11
24
  return typeof value === "object" && value !== null && !Array.isArray(value);
12
25
  }
@@ -17,6 +30,17 @@ function requireString(value, path, errors) {
17
30
  }
18
31
  return value;
19
32
  }
33
+ function requireStringInSet(value, path, allowed, errors) {
34
+ const raw = requireString(value, path, errors);
35
+ if (raw === undefined) {
36
+ return undefined;
37
+ }
38
+ if (!allowed.has(raw)) {
39
+ errors.push(`${path} must be one of ${Array.from(allowed).join(", ")}`);
40
+ return undefined;
41
+ }
42
+ return raw;
43
+ }
20
44
  function requireBoolean(value, path, errors) {
21
45
  if (typeof value !== "boolean") {
22
46
  errors.push(`${path} must be a boolean`);
@@ -31,6 +55,39 @@ function requireNumber(value, path, errors) {
31
55
  }
32
56
  return value;
33
57
  }
58
+ function requireInteger(value, path, errors) {
59
+ const number = requireNumber(value, path, errors);
60
+ if (number === undefined) {
61
+ return undefined;
62
+ }
63
+ if (!Number.isInteger(number)) {
64
+ errors.push(`${path} must be an integer`);
65
+ return undefined;
66
+ }
67
+ return number;
68
+ }
69
+ function requireNonNegativeInteger(value, path, errors) {
70
+ const integer = requireInteger(value, path, errors);
71
+ if (integer === undefined) {
72
+ return undefined;
73
+ }
74
+ if (integer < 0) {
75
+ errors.push(`${path} must be a non-negative integer`);
76
+ return undefined;
77
+ }
78
+ return integer;
79
+ }
80
+ function requirePositiveInteger(value, path, errors) {
81
+ const integer = requireInteger(value, path, errors);
82
+ if (integer === undefined) {
83
+ return undefined;
84
+ }
85
+ if (integer <= 0) {
86
+ errors.push(`${path} must be a positive integer`);
87
+ return undefined;
88
+ }
89
+ return integer;
90
+ }
34
91
  function requireStringArray(value, path, errors) {
35
92
  if (!Array.isArray(value)) {
36
93
  errors.push(`${path} must be an array of strings`);
@@ -46,6 +103,53 @@ function requireStringArray(value, path, errors) {
46
103
  }
47
104
  return items;
48
105
  }
106
+ function requireLowercaseUniqueStringArray(value, path, errors, allowEmpty = false) {
107
+ const items = requireStringArray(value, path, errors);
108
+ if (items === undefined) {
109
+ return undefined;
110
+ }
111
+ if (items.length === 0) {
112
+ if (!allowEmpty) {
113
+ errors.push(`${path} must not be empty`);
114
+ }
115
+ return items;
116
+ }
117
+ const seen = new Set();
118
+ for (let i = 0; i < items.length; i += 1) {
119
+ const item = items[i];
120
+ if (item.trim().length === 0) {
121
+ errors.push(`${path}[${i}] must not be empty`);
122
+ continue;
123
+ }
124
+ if (item !== item.trim()) {
125
+ errors.push(`${path}[${i}] must not include surrounding whitespace`);
126
+ }
127
+ if (item !== item.toLowerCase()) {
128
+ errors.push(`${path}[${i}] must be lowercase`);
129
+ }
130
+ if (seen.has(item)) {
131
+ errors.push(`${path} must not contain duplicate value "${item}"`);
132
+ continue;
133
+ }
134
+ seen.add(item);
135
+ }
136
+ return items;
137
+ }
138
+ function requireKnownLowercaseUniqueStringArray(value, path, allowed, errors, allowEmpty = false) {
139
+ const items = requireLowercaseUniqueStringArray(value, path, errors, allowEmpty);
140
+ if (items === undefined) {
141
+ return undefined;
142
+ }
143
+ for (let i = 0; i < items.length; i += 1) {
144
+ if (items[i].trim().length === 0) {
145
+ continue;
146
+ }
147
+ if (!allowed.has(items[i])) {
148
+ errors.push(`${path}[${i}] must be one of ${Array.from(allowed).join(", ")}`);
149
+ }
150
+ }
151
+ return items;
152
+ }
49
153
  function requireObject(value, path, errors) {
50
154
  if (!isObject(value)) {
51
155
  errors.push(`${path} must be an object`);
@@ -53,6 +157,29 @@ function requireObject(value, path, errors) {
53
157
  }
54
158
  return value;
55
159
  }
160
+ function validateWorkspaceAlias(alias, errors) {
161
+ if (alias === "all") {
162
+ errors.push("workspaces.all alias is reserved");
163
+ return;
164
+ }
165
+ if (alias !== alias.toLowerCase() || !WORKSPACE_ALIAS_RE.test(alias)) {
166
+ errors.push(`workspaces.${alias} alias must be lowercase and use [a-z0-9_]`);
167
+ }
168
+ }
169
+ function requireContainedPath(value, path, errors) {
170
+ const raw = requireString(value, path, errors);
171
+ if (!raw) {
172
+ return undefined;
173
+ }
174
+ try {
175
+ return (0, workspace_path_1.normalizeContainedWorkspacePath)(raw, path);
176
+ }
177
+ catch (err) {
178
+ const message = err instanceof Error ? err.message : String(err);
179
+ errors.push(message);
180
+ return undefined;
181
+ }
182
+ }
56
183
  function validateConfigSchema(raw) {
57
184
  const errors = [];
58
185
  if (!isObject(raw)) {
@@ -70,50 +197,70 @@ function validateConfigSchema(raw) {
70
197
  ? {
71
198
  auto_reindex: requireBoolean(indexRaw.auto_reindex, "index.auto_reindex", errors),
72
199
  tolerant: requireBoolean(indexRaw.tolerant, "index.tolerant", errors),
73
- global_index_path: requireString(indexRaw.global_index_path, "index.global_index_path", errors),
200
+ global_index_path: requireContainedPath(indexRaw.global_index_path, "index.global_index_path", errors),
74
201
  }
75
202
  : undefined;
76
203
  const packLimitsRaw = packRaw ? requireObject(packRaw.limits, "pack.limits", errors) : undefined;
77
204
  const pack = packRaw
78
205
  ? {
79
- default_depth: requireNumber(packRaw.default_depth, "pack.default_depth", errors),
80
- default_edges: requireStringArray(packRaw.default_edges, "pack.default_edges", errors),
81
- verbose_core_list_path: requireString(packRaw.verbose_core_list_path, "pack.verbose_core_list_path", errors),
206
+ default_depth: requireNonNegativeInteger(packRaw.default_depth, "pack.default_depth", errors),
207
+ default_edges: requireKnownLowercaseUniqueStringArray(packRaw.default_edges, "pack.default_edges", PACK_EDGE_KEYS, errors, true),
208
+ verbose_core_list_path: requireContainedPath(packRaw.verbose_core_list_path, "pack.verbose_core_list_path", errors),
82
209
  limits: packLimitsRaw
83
210
  ? {
84
- max_nodes: requireNumber(packLimitsRaw.max_nodes, "pack.limits.max_nodes", errors),
85
- max_bytes: requireNumber(packLimitsRaw.max_bytes, "pack.limits.max_bytes", errors),
211
+ max_nodes: requirePositiveInteger(packLimitsRaw.max_nodes, "pack.limits.max_nodes", errors),
212
+ max_bytes: requirePositiveInteger(packLimitsRaw.max_bytes, "pack.limits.max_bytes", errors),
86
213
  }
87
214
  : undefined,
88
215
  }
89
216
  : undefined;
90
217
  const templates = templatesRaw
91
218
  ? {
92
- root_path: requireString(templatesRaw.root_path, "templates.root_path", errors),
219
+ root_path: requireContainedPath(templatesRaw.root_path, "templates.root_path", errors),
93
220
  default_set: requireString(templatesRaw.default_set, "templates.default_set", errors),
94
221
  workspace_overrides_enabled: requireBoolean(templatesRaw.workspace_overrides_enabled, "templates.workspace_overrides_enabled", errors),
95
222
  }
96
223
  : undefined;
97
224
  const workNextRaw = workRaw ? requireObject(workRaw.next, "work.next", errors) : undefined;
98
225
  const work = workRaw
99
- ? {
100
- status_enum: requireStringArray(workRaw.status_enum, "work.status_enum", errors),
101
- priority_min: requireNumber(workRaw.priority_min, "work.priority_min", errors),
102
- priority_max: requireNumber(workRaw.priority_max, "work.priority_max", errors),
103
- next: workNextRaw
104
- ? {
105
- strategy: requireString(workNextRaw.strategy, "work.next.strategy", errors),
106
- status_preference: requireStringArray(workNextRaw.status_preference, "work.next.status_preference", errors),
226
+ ? (() => {
227
+ const statusEnum = requireLowercaseUniqueStringArray(workRaw.status_enum, "work.status_enum", errors);
228
+ const priorityMin = requireInteger(workRaw.priority_min, "work.priority_min", errors);
229
+ const priorityMax = requireInteger(workRaw.priority_max, "work.priority_max", errors);
230
+ if (priorityMin !== undefined &&
231
+ priorityMax !== undefined &&
232
+ priorityMin > priorityMax) {
233
+ errors.push("work.priority_min must be less than or equal to work.priority_max");
234
+ }
235
+ const statusPreference = workNextRaw
236
+ ? requireLowercaseUniqueStringArray(workNextRaw.status_preference, "work.next.status_preference", errors)
237
+ : undefined;
238
+ if (statusEnum !== undefined && statusPreference !== undefined) {
239
+ const allowedStatuses = new Set(statusEnum);
240
+ for (let i = 0; i < statusPreference.length; i += 1) {
241
+ if (!allowedStatuses.has(statusPreference[i])) {
242
+ errors.push(`work.next.status_preference[${i}] must be listed in work.status_enum`);
243
+ }
107
244
  }
108
- : undefined,
109
- }
245
+ }
246
+ return {
247
+ status_enum: statusEnum,
248
+ priority_min: priorityMin,
249
+ priority_max: priorityMax,
250
+ next: workNextRaw
251
+ ? {
252
+ strategy: requireStringInSet(workNextRaw.strategy, "work.next.strategy", NEXT_WORK_STRATEGIES, errors),
253
+ status_preference: statusPreference,
254
+ }
255
+ : undefined,
256
+ };
257
+ })()
110
258
  : undefined;
111
259
  const workspaces = {};
260
+ const workspaceDocRootOwners = new Map();
112
261
  if (workspacesRaw) {
113
262
  for (const [alias, entry] of Object.entries(workspacesRaw)) {
114
- if (alias !== alias.toLowerCase()) {
115
- errors.push(`workspaces.${alias} alias must be lowercase`);
116
- }
263
+ validateWorkspaceAlias(alias, errors);
117
264
  const ws = requireObject(entry, `workspaces.${alias}`, errors);
118
265
  if (!ws) {
119
266
  continue;
@@ -122,14 +269,62 @@ function validateConfigSchema(raw) {
122
269
  const wsEnabled = requireBoolean(ws.enabled, `workspaces.${alias}.enabled`, errors);
123
270
  const wsMdkgDir = requireString(ws.mdkg_dir, `workspaces.${alias}.mdkg_dir`, errors);
124
271
  if (wsPath && wsEnabled !== undefined && wsMdkgDir) {
272
+ let normalizedPath;
273
+ let normalizedMdkgDir;
274
+ try {
275
+ normalizedPath = (0, workspace_path_1.normalizeContainedWorkspacePath)(wsPath, `workspaces.${alias}.path`);
276
+ }
277
+ catch (err) {
278
+ const message = err instanceof Error ? err.message : String(err);
279
+ errors.push(message);
280
+ }
281
+ try {
282
+ normalizedMdkgDir = (0, workspace_path_1.normalizeContainedWorkspacePath)(wsMdkgDir, `workspaces.${alias}.mdkg_dir`);
283
+ }
284
+ catch (err) {
285
+ const message = err instanceof Error ? err.message : String(err);
286
+ errors.push(message);
287
+ }
288
+ if (!normalizedPath || !normalizedMdkgDir) {
289
+ continue;
290
+ }
291
+ if (alias !== "root" && (0, workspace_path_1.isRootWorkspacePath)(normalizedPath)) {
292
+ errors.push(`workspaces.${alias}.path must not be "." for non-root workspaces`);
293
+ continue;
294
+ }
295
+ const docRootKey = (0, workspace_path_1.workspaceDocumentRootKey)(normalizedPath, normalizedMdkgDir);
296
+ const existingAlias = workspaceDocRootOwners.get(docRootKey);
297
+ if (existingAlias) {
298
+ errors.push(`workspaces.${alias} document root duplicates workspaces.${existingAlias}`);
299
+ continue;
300
+ }
301
+ workspaceDocRootOwners.set(docRootKey, alias);
125
302
  workspaces[alias] = {
126
- path: wsPath,
303
+ path: normalizedPath,
127
304
  enabled: wsEnabled,
128
- mdkg_dir: wsMdkgDir,
305
+ mdkg_dir: normalizedMdkgDir,
129
306
  };
130
307
  }
131
308
  }
132
309
  }
310
+ const rootWorkspace = workspaces.root;
311
+ if (root_required !== undefined && root_required !== true) {
312
+ errors.push("root_required must be true");
313
+ }
314
+ if (!rootWorkspace) {
315
+ errors.push("workspaces.root is required");
316
+ }
317
+ else {
318
+ if (rootWorkspace.path !== ".") {
319
+ errors.push('workspaces.root.path must be "."');
320
+ }
321
+ if (rootWorkspace.enabled !== true) {
322
+ errors.push("workspaces.root.enabled must be true");
323
+ }
324
+ if (rootWorkspace.mdkg_dir !== ".mdkg") {
325
+ errors.push('workspaces.root.mdkg_dir must be ".mdkg"');
326
+ }
327
+ }
133
328
  if (errors.length > 0) {
134
329
  throw new Error(`config validation failed:\n${errors.join("\n")}`);
135
330
  }
@@ -3,15 +3,49 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LATEST_SCHEMA_VERSION = void 0;
4
4
  exports.migrateConfig = migrateConfig;
5
5
  exports.LATEST_SCHEMA_VERSION = 1;
6
- const MIGRATIONS = {};
7
- function migrateConfig(raw) {
8
- if (typeof raw !== "object" || raw === null) {
9
- throw new Error("config must be a JSON object");
10
- }
6
+ const LEGACY_SCHEMA_VERSION = 0;
7
+ function isJsonObject(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function getSchemaVersion(raw) {
11
11
  const version = raw.schema_version;
12
+ if (version === undefined) {
13
+ return LEGACY_SCHEMA_VERSION;
14
+ }
12
15
  if (typeof version !== "number" || !Number.isInteger(version)) {
13
16
  throw new Error("config schema_version must be an integer");
14
17
  }
18
+ if (version < LEGACY_SCHEMA_VERSION) {
19
+ throw new Error("config schema_version must be non-negative");
20
+ }
21
+ return version;
22
+ }
23
+ function migrateLegacyConfig(input) {
24
+ if (!isJsonObject(input)) {
25
+ throw new Error("config must be a JSON object");
26
+ }
27
+ return {
28
+ ...input,
29
+ schema_version: 1,
30
+ workspaces: input.workspaces === undefined
31
+ ? {
32
+ root: {
33
+ path: ".",
34
+ enabled: true,
35
+ mdkg_dir: ".mdkg",
36
+ },
37
+ }
38
+ : input.workspaces,
39
+ };
40
+ }
41
+ const MIGRATIONS = {
42
+ [LEGACY_SCHEMA_VERSION]: migrateLegacyConfig,
43
+ };
44
+ function migrateConfig(raw) {
45
+ if (!isJsonObject(raw)) {
46
+ throw new Error("config must be a JSON object");
47
+ }
48
+ const version = getSchemaVersion(raw);
15
49
  if (version > exports.LATEST_SCHEMA_VERSION) {
16
50
  throw new Error(`config schema_version ${version} is newer than supported ${exports.LATEST_SCHEMA_VERSION}`);
17
51
  }
@@ -0,0 +1,41 @@
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.normalizeContainedWorkspacePath = normalizeContainedWorkspacePath;
7
+ exports.isRootWorkspacePath = isRootWorkspacePath;
8
+ exports.workspaceDocumentRootKey = workspaceDocumentRootKey;
9
+ const path_1 = __importDefault(require("path"));
10
+ function isAbsoluteOnSupportedPlatform(value) {
11
+ return path_1.default.isAbsolute(value) || path_1.default.posix.isAbsolute(value) || path_1.default.win32.isAbsolute(value);
12
+ }
13
+ function normalizeContainedWorkspacePath(value, label) {
14
+ const normalized = value.trim();
15
+ if (!normalized) {
16
+ throw new Error(`${label} cannot be empty`);
17
+ }
18
+ if (normalized.includes("\0")) {
19
+ throw new Error(`${label} cannot contain NUL bytes`);
20
+ }
21
+ if (isAbsoluteOnSupportedPlatform(normalized)) {
22
+ throw new Error(`${label} must be relative`);
23
+ }
24
+ if (normalized.split(/[\\/]+/).some((part) => part === "..")) {
25
+ throw new Error(`${label} cannot contain parent-directory components`);
26
+ }
27
+ return normalized;
28
+ }
29
+ function isRootWorkspacePath(value) {
30
+ const parts = value
31
+ .trim()
32
+ .split(/[\\/]+/)
33
+ .filter(Boolean);
34
+ return parts.length > 0 && parts.every((part) => part === ".");
35
+ }
36
+ function workspaceDocumentRootKey(workspacePath, mdkgDir) {
37
+ return [workspacePath, mdkgDir]
38
+ .flatMap((value) => value.trim().split(/[\\/]+/))
39
+ .filter((part) => part && part !== ".")
40
+ .join("/");
41
+ }