mdkg 0.0.1 → 0.0.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.
- package/README.md +20 -6
- package/dist/cli.js +667 -11
- package/dist/commands/checkpoint.js +133 -0
- package/dist/commands/format.js +297 -0
- package/dist/commands/guide.js +22 -0
- package/dist/commands/index.js +17 -0
- package/dist/commands/init.js +111 -0
- package/dist/commands/list.js +52 -0
- package/dist/commands/new.js +279 -0
- package/dist/commands/next.js +75 -0
- package/dist/commands/node_card.js +17 -0
- package/dist/commands/pack.js +105 -0
- package/dist/commands/search.js +70 -0
- package/dist/commands/show.js +95 -0
- package/dist/commands/validate.js +229 -0
- package/dist/commands/workspace.js +101 -0
- package/dist/core/config.js +162 -0
- package/dist/core/migrate.js +30 -0
- package/dist/core/paths.js +14 -0
- package/dist/graph/edges.js +64 -0
- package/dist/graph/frontmatter.js +132 -0
- package/dist/graph/index_cache.js +50 -0
- package/dist/graph/indexer.js +144 -0
- package/dist/graph/node.js +225 -0
- package/dist/graph/staleness.js +31 -0
- package/dist/graph/template_schema.js +86 -0
- package/dist/graph/validate_graph.js +115 -0
- package/dist/graph/workspace_files.js +64 -0
- package/dist/init/AGENTS.md +43 -0
- package/dist/init/CLAUDE.md +37 -0
- package/dist/init/config.json +67 -0
- package/dist/init/core/core.md +12 -0
- package/dist/init/core/guide.md +99 -0
- package/dist/init/core/rule-1-mdkg-conventions.md +232 -0
- package/dist/init/core/rule-2-context-pack-rules.md +186 -0
- package/dist/init/core/rule-3-cli-contract.md +177 -0
- package/dist/init/core/rule-4-repo-safety-and-ignores.md +97 -0
- package/dist/init/core/rule-5-release-and-versioning.md +82 -0
- package/dist/init/core/rule-6-templates-and-schemas.md +186 -0
- package/dist/init/templates/default/bug.md +54 -0
- package/dist/init/templates/default/chk.md +55 -0
- package/dist/init/templates/default/dec.md +38 -0
- package/dist/init/templates/default/edd.md +50 -0
- package/dist/init/templates/default/epic.md +46 -0
- package/dist/init/templates/default/feat.md +35 -0
- package/dist/init/templates/default/prd.md +59 -0
- package/dist/init/templates/default/prop.md +45 -0
- package/dist/init/templates/default/rule.md +33 -0
- package/dist/init/templates/default/task.md +53 -0
- package/dist/init/templates/default/test.md +49 -0
- package/dist/pack/export_json.js +38 -0
- package/dist/pack/export_md.js +93 -0
- package/dist/pack/export_toon.js +7 -0
- package/dist/pack/export_xml.js +73 -0
- package/dist/pack/order.js +162 -0
- package/dist/pack/pack.js +181 -0
- package/dist/pack/types.js +2 -0
- package/dist/pack/verbose_core.js +23 -0
- package/dist/templates/loader.js +82 -0
- package/dist/util/argparse.js +154 -0
- package/dist/util/date.js +9 -0
- package/dist/util/errors.js +12 -0
- package/dist/util/filter.js +26 -0
- package/dist/util/output.js +50 -0
- package/dist/util/qid.js +54 -0
- package/dist/util/sort.js +40 -0
- package/package.json +18 -2
|
@@ -0,0 +1,229 @@
|
|
|
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.runValidateCommand = runValidateCommand;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const config_1 = require("../core/config");
|
|
10
|
+
const template_schema_1 = require("../graph/template_schema");
|
|
11
|
+
const node_1 = require("../graph/node");
|
|
12
|
+
const workspace_files_1 = require("../graph/workspace_files");
|
|
13
|
+
const validate_graph_1 = require("../graph/validate_graph");
|
|
14
|
+
const errors_1 = require("../util/errors");
|
|
15
|
+
const RECOMMENDED_HEADINGS = {
|
|
16
|
+
task: [
|
|
17
|
+
"Overview",
|
|
18
|
+
"Acceptance Criteria",
|
|
19
|
+
"Files Affected",
|
|
20
|
+
"Implementation Notes",
|
|
21
|
+
"Test Plan",
|
|
22
|
+
"Links / Artifacts",
|
|
23
|
+
],
|
|
24
|
+
bug: [
|
|
25
|
+
"Overview",
|
|
26
|
+
"Reproduction Steps",
|
|
27
|
+
"Expected vs Actual",
|
|
28
|
+
"Suspected Cause",
|
|
29
|
+
"Fix Plan",
|
|
30
|
+
"Test Plan",
|
|
31
|
+
"Links / Artifacts",
|
|
32
|
+
],
|
|
33
|
+
feat: ["Overview", "Acceptance Criteria", "Notes"],
|
|
34
|
+
epic: ["Goal", "Scope", "Milestones", "Out of Scope", "Risks", "Links / Artifacts"],
|
|
35
|
+
checkpoint: [
|
|
36
|
+
"Summary",
|
|
37
|
+
"Scope Covered",
|
|
38
|
+
"Decisions Captured",
|
|
39
|
+
"Implementation Summary",
|
|
40
|
+
"Verification / Testing",
|
|
41
|
+
"Known Issues / Follow-ups",
|
|
42
|
+
"Links / Artifacts",
|
|
43
|
+
],
|
|
44
|
+
prd: [
|
|
45
|
+
"Problem",
|
|
46
|
+
"Goals",
|
|
47
|
+
"Non-goals",
|
|
48
|
+
"Requirements",
|
|
49
|
+
"Acceptance Criteria",
|
|
50
|
+
"Metrics / Success",
|
|
51
|
+
"Risks",
|
|
52
|
+
"Open Questions",
|
|
53
|
+
],
|
|
54
|
+
edd: [
|
|
55
|
+
"Overview",
|
|
56
|
+
"Architecture",
|
|
57
|
+
"Data model",
|
|
58
|
+
"APIs / interfaces",
|
|
59
|
+
"Failure modes",
|
|
60
|
+
"Observability",
|
|
61
|
+
"Security / privacy",
|
|
62
|
+
"Testing strategy",
|
|
63
|
+
"Rollout plan",
|
|
64
|
+
],
|
|
65
|
+
dec: ["Context", "Decision", "Alternatives considered", "Consequences", "Links / references"],
|
|
66
|
+
};
|
|
67
|
+
function normalizeHeading(value) {
|
|
68
|
+
return value.trim().toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
function extractHeadings(body) {
|
|
71
|
+
const headings = new Set();
|
|
72
|
+
const lines = body.split(/\r?\n/);
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const match = /^#+\s+(.*)$/.exec(line);
|
|
75
|
+
if (!match) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
headings.add(normalizeHeading(match[1] ?? ""));
|
|
79
|
+
}
|
|
80
|
+
return headings;
|
|
81
|
+
}
|
|
82
|
+
function isCoreListFile(filePath) {
|
|
83
|
+
return path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core";
|
|
84
|
+
}
|
|
85
|
+
function normalizeEdgeTarget(value, ws) {
|
|
86
|
+
if (value.includes(":")) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
return `${ws}:${value}`;
|
|
90
|
+
}
|
|
91
|
+
function normalizeEdges(edges, ws) {
|
|
92
|
+
return {
|
|
93
|
+
epic: edges.epic ? normalizeEdgeTarget(edges.epic, ws) : undefined,
|
|
94
|
+
parent: edges.parent ? normalizeEdgeTarget(edges.parent, ws) : undefined,
|
|
95
|
+
prev: edges.prev ? normalizeEdgeTarget(edges.prev, ws) : undefined,
|
|
96
|
+
next: edges.next ? normalizeEdgeTarget(edges.next, ws) : undefined,
|
|
97
|
+
relates: edges.relates.map((value) => normalizeEdgeTarget(value, ws)),
|
|
98
|
+
blocked_by: edges.blocked_by.map((value) => normalizeEdgeTarget(value, ws)),
|
|
99
|
+
blocks: edges.blocks.map((value) => normalizeEdgeTarget(value, ws)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function buildIndexNode(root, ws, filePath, node) {
|
|
103
|
+
return {
|
|
104
|
+
id: node.id,
|
|
105
|
+
qid: `${ws}:${node.id}`,
|
|
106
|
+
ws,
|
|
107
|
+
type: node.type,
|
|
108
|
+
title: node.title,
|
|
109
|
+
status: node.status,
|
|
110
|
+
priority: node.priority,
|
|
111
|
+
created: node.created,
|
|
112
|
+
updated: node.updated,
|
|
113
|
+
tags: node.tags,
|
|
114
|
+
owners: node.owners,
|
|
115
|
+
links: node.links,
|
|
116
|
+
artifacts: node.artifacts,
|
|
117
|
+
refs: node.refs,
|
|
118
|
+
aliases: node.aliases,
|
|
119
|
+
path: path_1.default.relative(root, filePath),
|
|
120
|
+
edges: normalizeEdges(node.edges, ws),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function buildWorkspaceMap(config) {
|
|
124
|
+
const workspaces = {};
|
|
125
|
+
for (const alias of Object.keys(config.workspaces).sort()) {
|
|
126
|
+
const entry = config.workspaces[alias];
|
|
127
|
+
workspaces[alias] = { path: entry.path, enabled: entry.enabled };
|
|
128
|
+
}
|
|
129
|
+
return workspaces;
|
|
130
|
+
}
|
|
131
|
+
function runValidateCommand(options) {
|
|
132
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
133
|
+
const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(options.root, config, node_1.ALLOWED_TYPES);
|
|
134
|
+
const filesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(options.root, config);
|
|
135
|
+
const errors = [];
|
|
136
|
+
const warnings = [];
|
|
137
|
+
const nodes = {};
|
|
138
|
+
const idsByWorkspace = {};
|
|
139
|
+
for (const [alias, files] of Object.entries(filesByAlias)) {
|
|
140
|
+
idsByWorkspace[alias] = new Map();
|
|
141
|
+
for (const filePath of files) {
|
|
142
|
+
if (isCoreListFile(filePath)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
let content = "";
|
|
146
|
+
try {
|
|
147
|
+
content = fs_1.default.readFileSync(filePath, "utf8");
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
151
|
+
errors.push(`${filePath}: failed to read file: ${message}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const node = (0, node_1.parseNode)(content, filePath, {
|
|
156
|
+
workStatusEnum: config.work.status_enum,
|
|
157
|
+
priorityMin: config.work.priority_min,
|
|
158
|
+
priorityMax: config.work.priority_max,
|
|
159
|
+
templateSchemas,
|
|
160
|
+
});
|
|
161
|
+
if (idsByWorkspace[alias].has(node.id)) {
|
|
162
|
+
const firstPath = idsByWorkspace[alias].get(node.id);
|
|
163
|
+
errors.push(`${filePath}: duplicate id ${node.id} in workspace ${alias} (also in ${firstPath})`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
idsByWorkspace[alias].set(node.id, filePath);
|
|
167
|
+
const qid = `${alias}:${node.id}`;
|
|
168
|
+
nodes[qid] = buildIndexNode(options.root, alias, filePath, node);
|
|
169
|
+
const recommended = RECOMMENDED_HEADINGS[node.type];
|
|
170
|
+
if (recommended) {
|
|
171
|
+
const headings = extractHeadings(node.body);
|
|
172
|
+
for (const heading of recommended) {
|
|
173
|
+
if (!headings.has(normalizeHeading(heading))) {
|
|
174
|
+
warnings.push(`${qid}: missing recommended heading "${heading}"`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
181
|
+
errors.push(message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const index = {
|
|
186
|
+
meta: {
|
|
187
|
+
tool: config.tool,
|
|
188
|
+
schema_version: config.schema_version,
|
|
189
|
+
generated_at: new Date().toISOString(),
|
|
190
|
+
root: options.root,
|
|
191
|
+
workspaces: Object.keys(filesByAlias).sort(),
|
|
192
|
+
},
|
|
193
|
+
workspaces: buildWorkspaceMap(config),
|
|
194
|
+
nodes,
|
|
195
|
+
reverse_edges: {},
|
|
196
|
+
};
|
|
197
|
+
const graphErrors = (0, validate_graph_1.collectGraphErrors)(index, { allowMissing: false });
|
|
198
|
+
errors.push(...graphErrors);
|
|
199
|
+
const reportLines = [
|
|
200
|
+
...warnings.map((warning) => `warning: ${warning}`),
|
|
201
|
+
...errors,
|
|
202
|
+
];
|
|
203
|
+
let outPath = undefined;
|
|
204
|
+
if (options.out) {
|
|
205
|
+
outPath = path_1.default.resolve(options.root, options.out);
|
|
206
|
+
fs_1.default.mkdirSync(path_1.default.dirname(outPath), { recursive: true });
|
|
207
|
+
fs_1.default.writeFileSync(outPath, reportLines.join("\n"), "utf8");
|
|
208
|
+
}
|
|
209
|
+
if (!options.quiet) {
|
|
210
|
+
for (const warning of warnings) {
|
|
211
|
+
console.error(`warning: ${warning}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (errors.length > 0) {
|
|
215
|
+
if (outPath) {
|
|
216
|
+
console.error(`validation failed: ${errors.length} error(s). details written to ${outPath}`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
for (const error of errors) {
|
|
220
|
+
console.error(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw new errors_1.ValidationError(`validation failed with ${errors.length} error(s)`);
|
|
224
|
+
}
|
|
225
|
+
if (outPath) {
|
|
226
|
+
console.log(`validation report written: ${outPath}`);
|
|
227
|
+
}
|
|
228
|
+
console.log("validation ok");
|
|
229
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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.runWorkspaceListCommand = runWorkspaceListCommand;
|
|
7
|
+
exports.runWorkspaceAddCommand = runWorkspaceAddCommand;
|
|
8
|
+
exports.runWorkspaceRemoveCommand = runWorkspaceRemoveCommand;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const config_1 = require("../core/config");
|
|
12
|
+
const errors_1 = require("../util/errors");
|
|
13
|
+
const ALIAS_RE = /^[a-z][a-z0-9_]*$/;
|
|
14
|
+
function readRawConfig(root) {
|
|
15
|
+
const configPath = path_1.default.join(root, ".mdkg", "config.json");
|
|
16
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
17
|
+
throw new errors_1.NotFoundError(`config not found at ${configPath}`);
|
|
18
|
+
}
|
|
19
|
+
let raw;
|
|
20
|
+
try {
|
|
21
|
+
raw = JSON.parse(fs_1.default.readFileSync(configPath, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
25
|
+
throw new errors_1.UsageError(`failed to read config: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof raw !== "object" || raw === null) {
|
|
28
|
+
throw new errors_1.UsageError("config must be a JSON object");
|
|
29
|
+
}
|
|
30
|
+
return { path: configPath, raw: raw };
|
|
31
|
+
}
|
|
32
|
+
function writeRawConfig(configPath, raw) {
|
|
33
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(raw, null, 2), "utf8");
|
|
34
|
+
}
|
|
35
|
+
function normalizeAlias(alias) {
|
|
36
|
+
const normalized = alias.toLowerCase();
|
|
37
|
+
if (normalized === "all") {
|
|
38
|
+
throw new errors_1.UsageError("workspace alias cannot be 'all'");
|
|
39
|
+
}
|
|
40
|
+
if (!ALIAS_RE.test(normalized)) {
|
|
41
|
+
throw new errors_1.UsageError("workspace alias must be lowercase and use [a-z0-9_]");
|
|
42
|
+
}
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
function runWorkspaceListCommand(options) {
|
|
46
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
47
|
+
const aliases = Object.keys(config.workspaces).sort();
|
|
48
|
+
if (aliases.length === 0) {
|
|
49
|
+
console.log("no workspaces registered");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const alias of aliases) {
|
|
53
|
+
const ws = config.workspaces[alias];
|
|
54
|
+
const status = ws.enabled ? "enabled" : "disabled";
|
|
55
|
+
console.log(`${alias} | ${status} | ${ws.path} | ${ws.mdkg_dir}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function runWorkspaceAddCommand(options) {
|
|
59
|
+
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");
|
|
63
|
+
}
|
|
64
|
+
const mdkgDir = options.mdkgDir?.trim() || ".mdkg";
|
|
65
|
+
const { path: configPath, raw } = readRawConfig(options.root);
|
|
66
|
+
const workspacesRaw = raw.workspaces;
|
|
67
|
+
if (typeof workspacesRaw !== "object" || workspacesRaw === null) {
|
|
68
|
+
throw new errors_1.UsageError("config.workspaces must be an object");
|
|
69
|
+
}
|
|
70
|
+
const workspaces = workspacesRaw;
|
|
71
|
+
if (workspaces[alias]) {
|
|
72
|
+
throw new errors_1.UsageError(`workspace already exists: ${alias}`);
|
|
73
|
+
}
|
|
74
|
+
workspaces[alias] = { path: workspacePath, enabled: true, mdkg_dir: mdkgDir };
|
|
75
|
+
raw.workspaces = workspaces;
|
|
76
|
+
writeRawConfig(configPath, raw);
|
|
77
|
+
const wsRoot = path_1.default.resolve(options.root, workspacePath, mdkgDir);
|
|
78
|
+
fs_1.default.mkdirSync(path_1.default.join(wsRoot, "core"), { recursive: true });
|
|
79
|
+
fs_1.default.mkdirSync(path_1.default.join(wsRoot, "design"), { recursive: true });
|
|
80
|
+
fs_1.default.mkdirSync(path_1.default.join(wsRoot, "work"), { recursive: true });
|
|
81
|
+
console.log(`workspace added: ${alias} (${workspacePath})`);
|
|
82
|
+
}
|
|
83
|
+
function runWorkspaceRemoveCommand(options) {
|
|
84
|
+
const alias = normalizeAlias(options.alias);
|
|
85
|
+
if (alias === "root") {
|
|
86
|
+
throw new errors_1.UsageError("cannot remove root workspace");
|
|
87
|
+
}
|
|
88
|
+
const { path: configPath, raw } = readRawConfig(options.root);
|
|
89
|
+
const workspacesRaw = raw.workspaces;
|
|
90
|
+
if (typeof workspacesRaw !== "object" || workspacesRaw === null) {
|
|
91
|
+
throw new errors_1.UsageError("config.workspaces must be an object");
|
|
92
|
+
}
|
|
93
|
+
const workspaces = workspacesRaw;
|
|
94
|
+
if (!workspaces[alias]) {
|
|
95
|
+
throw new errors_1.NotFoundError(`workspace not found: ${alias}`);
|
|
96
|
+
}
|
|
97
|
+
delete workspaces[alias];
|
|
98
|
+
raw.workspaces = workspaces;
|
|
99
|
+
writeRawConfig(configPath, raw);
|
|
100
|
+
console.log(`workspace removed: ${alias}`);
|
|
101
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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.loadConfig = loadConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const paths_1 = require("./paths");
|
|
9
|
+
const migrate_1 = require("./migrate");
|
|
10
|
+
function isObject(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function requireString(value, path, errors) {
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
errors.push(`${path} must be a string`);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function requireBoolean(value, path, errors) {
|
|
21
|
+
if (typeof value !== "boolean") {
|
|
22
|
+
errors.push(`${path} must be a boolean`);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function requireNumber(value, path, errors) {
|
|
28
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
29
|
+
errors.push(`${path} must be a number`);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function requireStringArray(value, path, errors) {
|
|
35
|
+
if (!Array.isArray(value)) {
|
|
36
|
+
errors.push(`${path} must be an array of strings`);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const items = [];
|
|
40
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
41
|
+
if (typeof value[i] !== "string") {
|
|
42
|
+
errors.push(`${path}[${i}] must be a string`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
items.push(value[i]);
|
|
46
|
+
}
|
|
47
|
+
return items;
|
|
48
|
+
}
|
|
49
|
+
function requireObject(value, path, errors) {
|
|
50
|
+
if (!isObject(value)) {
|
|
51
|
+
errors.push(`${path} must be an object`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function validateConfigSchema(raw) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
if (!isObject(raw)) {
|
|
59
|
+
throw new Error("config must be a JSON object");
|
|
60
|
+
}
|
|
61
|
+
const schema_version = requireNumber(raw.schema_version, "schema_version", errors);
|
|
62
|
+
const tool = requireString(raw.tool, "tool", errors);
|
|
63
|
+
const root_required = requireBoolean(raw.root_required, "root_required", errors);
|
|
64
|
+
const indexRaw = requireObject(raw.index, "index", errors);
|
|
65
|
+
const packRaw = requireObject(raw.pack, "pack", errors);
|
|
66
|
+
const templatesRaw = requireObject(raw.templates, "templates", errors);
|
|
67
|
+
const workRaw = requireObject(raw.work, "work", errors);
|
|
68
|
+
const workspacesRaw = requireObject(raw.workspaces, "workspaces", errors);
|
|
69
|
+
const index = indexRaw
|
|
70
|
+
? {
|
|
71
|
+
auto_reindex: requireBoolean(indexRaw.auto_reindex, "index.auto_reindex", errors),
|
|
72
|
+
tolerant: requireBoolean(indexRaw.tolerant, "index.tolerant", errors),
|
|
73
|
+
global_index_path: requireString(indexRaw.global_index_path, "index.global_index_path", errors),
|
|
74
|
+
}
|
|
75
|
+
: undefined;
|
|
76
|
+
const packLimitsRaw = packRaw ? requireObject(packRaw.limits, "pack.limits", errors) : undefined;
|
|
77
|
+
const pack = packRaw
|
|
78
|
+
? {
|
|
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),
|
|
82
|
+
limits: packLimitsRaw
|
|
83
|
+
? {
|
|
84
|
+
max_nodes: requireNumber(packLimitsRaw.max_nodes, "pack.limits.max_nodes", errors),
|
|
85
|
+
max_bytes: requireNumber(packLimitsRaw.max_bytes, "pack.limits.max_bytes", errors),
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
}
|
|
89
|
+
: undefined;
|
|
90
|
+
const templates = templatesRaw
|
|
91
|
+
? {
|
|
92
|
+
root_path: requireString(templatesRaw.root_path, "templates.root_path", errors),
|
|
93
|
+
default_set: requireString(templatesRaw.default_set, "templates.default_set", errors),
|
|
94
|
+
workspace_overrides_enabled: requireBoolean(templatesRaw.workspace_overrides_enabled, "templates.workspace_overrides_enabled", errors),
|
|
95
|
+
}
|
|
96
|
+
: undefined;
|
|
97
|
+
const workNextRaw = workRaw ? requireObject(workRaw.next, "work.next", errors) : undefined;
|
|
98
|
+
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),
|
|
107
|
+
}
|
|
108
|
+
: undefined,
|
|
109
|
+
}
|
|
110
|
+
: undefined;
|
|
111
|
+
const workspaces = {};
|
|
112
|
+
if (workspacesRaw) {
|
|
113
|
+
for (const [alias, entry] of Object.entries(workspacesRaw)) {
|
|
114
|
+
if (alias !== alias.toLowerCase()) {
|
|
115
|
+
errors.push(`workspaces.${alias} alias must be lowercase`);
|
|
116
|
+
}
|
|
117
|
+
const ws = requireObject(entry, `workspaces.${alias}`, errors);
|
|
118
|
+
if (!ws) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const wsPath = requireString(ws.path, `workspaces.${alias}.path`, errors);
|
|
122
|
+
const wsEnabled = requireBoolean(ws.enabled, `workspaces.${alias}.enabled`, errors);
|
|
123
|
+
const wsMdkgDir = requireString(ws.mdkg_dir, `workspaces.${alias}.mdkg_dir`, errors);
|
|
124
|
+
if (wsPath && wsEnabled !== undefined && wsMdkgDir) {
|
|
125
|
+
workspaces[alias] = {
|
|
126
|
+
path: wsPath,
|
|
127
|
+
enabled: wsEnabled,
|
|
128
|
+
mdkg_dir: wsMdkgDir,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (errors.length > 0) {
|
|
134
|
+
throw new Error(`config validation failed:\n${errors.join("\n")}`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
schema_version: schema_version,
|
|
138
|
+
tool: tool,
|
|
139
|
+
root_required: root_required,
|
|
140
|
+
index: index,
|
|
141
|
+
pack: pack,
|
|
142
|
+
templates: templates,
|
|
143
|
+
work: work,
|
|
144
|
+
workspaces,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function loadConfig(root) {
|
|
148
|
+
const path = (0, paths_1.configPath)(root);
|
|
149
|
+
if (!fs_1.default.existsSync(path)) {
|
|
150
|
+
throw new Error(`config not found at ${path}`);
|
|
151
|
+
}
|
|
152
|
+
let raw;
|
|
153
|
+
try {
|
|
154
|
+
raw = JSON.parse(fs_1.default.readFileSync(path, "utf8"));
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
158
|
+
throw new Error(`failed to read config: ${message}`);
|
|
159
|
+
}
|
|
160
|
+
const migrated = (0, migrate_1.migrateConfig)(raw);
|
|
161
|
+
return validateConfigSchema(migrated.config);
|
|
162
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LATEST_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.migrateConfig = migrateConfig;
|
|
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
|
+
}
|
|
11
|
+
const version = raw.schema_version;
|
|
12
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
13
|
+
throw new Error("config schema_version must be an integer");
|
|
14
|
+
}
|
|
15
|
+
if (version > exports.LATEST_SCHEMA_VERSION) {
|
|
16
|
+
throw new Error(`config schema_version ${version} is newer than supported ${exports.LATEST_SCHEMA_VERSION}`);
|
|
17
|
+
}
|
|
18
|
+
if (version === exports.LATEST_SCHEMA_VERSION) {
|
|
19
|
+
return { config: raw, from: version, to: version };
|
|
20
|
+
}
|
|
21
|
+
let current = raw;
|
|
22
|
+
for (let v = version; v < exports.LATEST_SCHEMA_VERSION; v += 1) {
|
|
23
|
+
const migrator = MIGRATIONS[v];
|
|
24
|
+
if (!migrator) {
|
|
25
|
+
throw new Error(`no migration available for schema_version ${v} -> ${v + 1}`);
|
|
26
|
+
}
|
|
27
|
+
current = migrator(current);
|
|
28
|
+
}
|
|
29
|
+
return { config: current, from: version, to: exports.LATEST_SCHEMA_VERSION };
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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.resolveRoot = resolveRoot;
|
|
7
|
+
exports.configPath = configPath;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function resolveRoot(rootArg) {
|
|
10
|
+
return rootArg ? path_1.default.resolve(rootArg) : process.cwd();
|
|
11
|
+
}
|
|
12
|
+
function configPath(root) {
|
|
13
|
+
return path_1.default.join(root, ".mdkg", "config.json");
|
|
14
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractEdges = extractEdges;
|
|
4
|
+
const ID_REF_RE = /^([a-z][a-z0-9_]*:)?[a-z]+-[0-9]+$/;
|
|
5
|
+
function formatError(filePath, key, message) {
|
|
6
|
+
return new Error(`${filePath}: ${key} ${message}`);
|
|
7
|
+
}
|
|
8
|
+
function normalizeIdRef(value, filePath, key) {
|
|
9
|
+
const normalized = value.toLowerCase();
|
|
10
|
+
if (normalized !== value) {
|
|
11
|
+
throw formatError(filePath, key, "must be lowercase");
|
|
12
|
+
}
|
|
13
|
+
if (!ID_REF_RE.test(normalized)) {
|
|
14
|
+
throw formatError(filePath, key, `invalid id reference: ${value}`);
|
|
15
|
+
}
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
function readString(fm, key) {
|
|
19
|
+
const value = fm[key];
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value !== "string") {
|
|
24
|
+
throw new Error(`${key} must be a string`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function readStringList(fm, key) {
|
|
29
|
+
const value = fm[key];
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(value)) {
|
|
34
|
+
throw new Error(`${key} must be a list`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function extractEdges(frontmatter, filePath) {
|
|
39
|
+
const epic = readString(frontmatter, "epic");
|
|
40
|
+
const parent = readString(frontmatter, "parent");
|
|
41
|
+
const prev = readString(frontmatter, "prev");
|
|
42
|
+
const next = readString(frontmatter, "next");
|
|
43
|
+
const relates = readStringList(frontmatter, "relates") ?? [];
|
|
44
|
+
const blocked_by = readStringList(frontmatter, "blocked_by") ?? [];
|
|
45
|
+
const blocks = readStringList(frontmatter, "blocks") ?? [];
|
|
46
|
+
const edges = {
|
|
47
|
+
relates: relates.map((value) => normalizeIdRef(value, filePath, "relates")),
|
|
48
|
+
blocked_by: blocked_by.map((value) => normalizeIdRef(value, filePath, "blocked_by")),
|
|
49
|
+
blocks: blocks.map((value) => normalizeIdRef(value, filePath, "blocks")),
|
|
50
|
+
};
|
|
51
|
+
if (epic) {
|
|
52
|
+
edges.epic = normalizeIdRef(epic, filePath, "epic");
|
|
53
|
+
}
|
|
54
|
+
if (parent) {
|
|
55
|
+
edges.parent = normalizeIdRef(parent, filePath, "parent");
|
|
56
|
+
}
|
|
57
|
+
if (prev) {
|
|
58
|
+
edges.prev = normalizeIdRef(prev, filePath, "prev");
|
|
59
|
+
}
|
|
60
|
+
if (next) {
|
|
61
|
+
edges.next = normalizeIdRef(next, filePath, "next");
|
|
62
|
+
}
|
|
63
|
+
return edges;
|
|
64
|
+
}
|