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,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_FRONTMATTER_KEY_ORDER = void 0;
|
|
4
|
+
exports.parseFrontmatter = parseFrontmatter;
|
|
5
|
+
exports.formatFrontmatter = formatFrontmatter;
|
|
6
|
+
const KEY_RE = /^[a-z][a-z0-9_]*$/;
|
|
7
|
+
exports.DEFAULT_FRONTMATTER_KEY_ORDER = [
|
|
8
|
+
"id",
|
|
9
|
+
"type",
|
|
10
|
+
"title",
|
|
11
|
+
"status",
|
|
12
|
+
"priority",
|
|
13
|
+
"epic",
|
|
14
|
+
"parent",
|
|
15
|
+
"prev",
|
|
16
|
+
"next",
|
|
17
|
+
"supersedes",
|
|
18
|
+
"tags",
|
|
19
|
+
"owners",
|
|
20
|
+
"links",
|
|
21
|
+
"artifacts",
|
|
22
|
+
"relates",
|
|
23
|
+
"blocked_by",
|
|
24
|
+
"blocks",
|
|
25
|
+
"refs",
|
|
26
|
+
"aliases",
|
|
27
|
+
"cases",
|
|
28
|
+
"scope",
|
|
29
|
+
"created",
|
|
30
|
+
"updated",
|
|
31
|
+
];
|
|
32
|
+
function formatError(filePath, lineNumber, message) {
|
|
33
|
+
return new Error(`${filePath}:${lineNumber}: ${message}`);
|
|
34
|
+
}
|
|
35
|
+
function parseList(valueRaw, filePath, lineNumber) {
|
|
36
|
+
if (!valueRaw.startsWith("[") || !valueRaw.endsWith("]")) {
|
|
37
|
+
throw formatError(filePath, lineNumber, "list must be enclosed in [ ]");
|
|
38
|
+
}
|
|
39
|
+
const inner = valueRaw.slice(1, -1).trim();
|
|
40
|
+
if (inner.length === 0) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const parts = inner.split(",");
|
|
44
|
+
const items = [];
|
|
45
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
46
|
+
const item = parts[i].trim();
|
|
47
|
+
if (item.length === 0) {
|
|
48
|
+
throw formatError(filePath, lineNumber, "list items must be non-empty");
|
|
49
|
+
}
|
|
50
|
+
items.push(item);
|
|
51
|
+
}
|
|
52
|
+
return items;
|
|
53
|
+
}
|
|
54
|
+
function parseValue(valueRaw, filePath, lineNumber) {
|
|
55
|
+
if (valueRaw.length === 0) {
|
|
56
|
+
throw formatError(filePath, lineNumber, "value must not be empty");
|
|
57
|
+
}
|
|
58
|
+
if (valueRaw.startsWith("[")) {
|
|
59
|
+
return parseList(valueRaw, filePath, lineNumber);
|
|
60
|
+
}
|
|
61
|
+
if (valueRaw === "true") {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (valueRaw === "false") {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return valueRaw;
|
|
68
|
+
}
|
|
69
|
+
function parseFrontmatter(content, filePath) {
|
|
70
|
+
const lines = content.split(/\r?\n/);
|
|
71
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
72
|
+
throw formatError(filePath, 1, "frontmatter must start with ---");
|
|
73
|
+
}
|
|
74
|
+
let endIndex = -1;
|
|
75
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
76
|
+
if (lines[i].trim() === "---") {
|
|
77
|
+
endIndex = i;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (endIndex === -1) {
|
|
82
|
+
throw formatError(filePath, 1, "frontmatter closing --- not found");
|
|
83
|
+
}
|
|
84
|
+
const frontmatter = {};
|
|
85
|
+
for (let i = 1; i < endIndex; i += 1) {
|
|
86
|
+
const line = lines[i];
|
|
87
|
+
const lineNumber = i + 1;
|
|
88
|
+
if (line.trim().length === 0) {
|
|
89
|
+
throw formatError(filePath, lineNumber, "frontmatter lines must not be blank");
|
|
90
|
+
}
|
|
91
|
+
const colonIndex = line.indexOf(":");
|
|
92
|
+
if (colonIndex === -1) {
|
|
93
|
+
throw formatError(filePath, lineNumber, "frontmatter lines must be key: value");
|
|
94
|
+
}
|
|
95
|
+
const key = line.slice(0, colonIndex).trim();
|
|
96
|
+
const rawValue = line.slice(colonIndex + 1).trim();
|
|
97
|
+
if (!KEY_RE.test(key)) {
|
|
98
|
+
throw formatError(filePath, lineNumber, `invalid key: ${key}`);
|
|
99
|
+
}
|
|
100
|
+
if (Object.prototype.hasOwnProperty.call(frontmatter, key)) {
|
|
101
|
+
throw formatError(filePath, lineNumber, `duplicate key: ${key}`);
|
|
102
|
+
}
|
|
103
|
+
frontmatter[key] = parseValue(rawValue, filePath, lineNumber);
|
|
104
|
+
}
|
|
105
|
+
const body = lines.slice(endIndex + 1).join("\n");
|
|
106
|
+
return { frontmatter, body };
|
|
107
|
+
}
|
|
108
|
+
function formatValue(value) {
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
if (value.length === 0) {
|
|
111
|
+
return "[]";
|
|
112
|
+
}
|
|
113
|
+
return `[${value.join(", ")}]`;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === "boolean") {
|
|
116
|
+
return value ? "true" : "false";
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function formatFrontmatter(frontmatter, keyOrder = exports.DEFAULT_FRONTMATTER_KEY_ORDER) {
|
|
121
|
+
const keys = Object.keys(frontmatter);
|
|
122
|
+
const keySet = new Set(keys);
|
|
123
|
+
const ordered = [];
|
|
124
|
+
for (const key of keyOrder) {
|
|
125
|
+
if (keySet.has(key)) {
|
|
126
|
+
ordered.push(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const remaining = keys.filter((key) => !keyOrder.includes(key)).sort();
|
|
130
|
+
ordered.push(...remaining);
|
|
131
|
+
return ordered.map((key) => `${key}: ${formatValue(frontmatter[key])}`);
|
|
132
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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.writeIndex = writeIndex;
|
|
7
|
+
exports.loadIndex = loadIndex;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const sort_1 = require("../util/sort");
|
|
11
|
+
const indexer_1 = require("./indexer");
|
|
12
|
+
const staleness_1 = require("./staleness");
|
|
13
|
+
function readIndex(indexPath) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs_1.default.readFileSync(indexPath, "utf8");
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
20
|
+
throw new Error(`failed to read index: ${message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function writeIndex(indexPath, index) {
|
|
24
|
+
const sortedIndex = { ...index, nodes: (0, sort_1.sortIndexNodes)(index.nodes) };
|
|
25
|
+
fs_1.default.mkdirSync(path_1.default.dirname(indexPath), { recursive: true });
|
|
26
|
+
fs_1.default.writeFileSync(indexPath, JSON.stringify(sortedIndex, null, 2));
|
|
27
|
+
}
|
|
28
|
+
function loadIndex(options) {
|
|
29
|
+
const useCache = options.useCache ?? true;
|
|
30
|
+
const allowReindex = options.allowReindex ?? options.config.index.auto_reindex;
|
|
31
|
+
const tolerant = options.tolerant ?? options.config.index.tolerant;
|
|
32
|
+
const indexPath = path_1.default.resolve(options.root, options.config.index.global_index_path);
|
|
33
|
+
if (!useCache) {
|
|
34
|
+
const index = (0, indexer_1.buildIndex)(options.root, options.config, { tolerant });
|
|
35
|
+
return { index, rebuilt: true, stale: false };
|
|
36
|
+
}
|
|
37
|
+
const stale = (0, staleness_1.isIndexStale)(options.root, options.config);
|
|
38
|
+
if (fs_1.default.existsSync(indexPath) && !stale) {
|
|
39
|
+
return { index: readIndex(indexPath), rebuilt: false, stale: false };
|
|
40
|
+
}
|
|
41
|
+
if (allowReindex) {
|
|
42
|
+
const index = (0, indexer_1.buildIndex)(options.root, options.config, { tolerant });
|
|
43
|
+
writeIndex(indexPath, index);
|
|
44
|
+
return { index, rebuilt: true, stale };
|
|
45
|
+
}
|
|
46
|
+
if (fs_1.default.existsSync(indexPath)) {
|
|
47
|
+
return { index: readIndex(indexPath), rebuilt: false, stale: true };
|
|
48
|
+
}
|
|
49
|
+
throw new Error("index missing and auto-reindex is disabled");
|
|
50
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
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.buildIndex = buildIndex;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const node_1 = require("./node");
|
|
10
|
+
const workspace_files_1 = require("./workspace_files");
|
|
11
|
+
const validate_graph_1 = require("./validate_graph");
|
|
12
|
+
const template_schema_1 = require("./template_schema");
|
|
13
|
+
function normalizeEdgeTarget(value, ws) {
|
|
14
|
+
if (value.includes(":")) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
return `${ws}:${value}`;
|
|
18
|
+
}
|
|
19
|
+
function normalizeEdges(edges, ws) {
|
|
20
|
+
return {
|
|
21
|
+
epic: edges.epic ? normalizeEdgeTarget(edges.epic, ws) : undefined,
|
|
22
|
+
parent: edges.parent ? normalizeEdgeTarget(edges.parent, ws) : undefined,
|
|
23
|
+
prev: edges.prev ? normalizeEdgeTarget(edges.prev, ws) : undefined,
|
|
24
|
+
next: edges.next ? normalizeEdgeTarget(edges.next, ws) : undefined,
|
|
25
|
+
relates: edges.relates.map((value) => normalizeEdgeTarget(value, ws)),
|
|
26
|
+
blocked_by: edges.blocked_by.map((value) => normalizeEdgeTarget(value, ws)),
|
|
27
|
+
blocks: edges.blocks.map((value) => normalizeEdgeTarget(value, ws)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function addReverseEdge(reverse, edgeKey, target, source) {
|
|
31
|
+
if (!reverse[edgeKey]) {
|
|
32
|
+
reverse[edgeKey] = {};
|
|
33
|
+
}
|
|
34
|
+
if (!reverse[edgeKey][target]) {
|
|
35
|
+
reverse[edgeKey][target] = [];
|
|
36
|
+
}
|
|
37
|
+
reverse[edgeKey][target].push(source);
|
|
38
|
+
}
|
|
39
|
+
function buildIndex(root, config, options = {}) {
|
|
40
|
+
const tolerant = options.tolerant ?? config.index.tolerant;
|
|
41
|
+
const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(root, config, node_1.ALLOWED_TYPES);
|
|
42
|
+
const nodes = {};
|
|
43
|
+
const idsByWorkspace = {};
|
|
44
|
+
const docFilesByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(root, config);
|
|
45
|
+
const workspaceAliases = Object.keys(docFilesByAlias).sort();
|
|
46
|
+
for (const alias of workspaceAliases) {
|
|
47
|
+
idsByWorkspace[alias] = new Set();
|
|
48
|
+
const files = docFilesByAlias[alias];
|
|
49
|
+
for (const filePath of files) {
|
|
50
|
+
if (path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const content = fs_1.default.readFileSync(filePath, "utf8");
|
|
55
|
+
const node = (0, node_1.parseNode)(content, filePath, {
|
|
56
|
+
workStatusEnum: config.work.status_enum,
|
|
57
|
+
priorityMin: config.work.priority_min,
|
|
58
|
+
priorityMax: config.work.priority_max,
|
|
59
|
+
templateSchemas,
|
|
60
|
+
});
|
|
61
|
+
if (idsByWorkspace[alias].has(node.id)) {
|
|
62
|
+
throw new Error(`duplicate id ${node.id} in workspace ${alias}`);
|
|
63
|
+
}
|
|
64
|
+
idsByWorkspace[alias].add(node.id);
|
|
65
|
+
const qid = `${alias}:${node.id}`;
|
|
66
|
+
const relPath = path_1.default.relative(root, filePath);
|
|
67
|
+
const normalizedEdges = normalizeEdges(node.edges, alias);
|
|
68
|
+
nodes[qid] = {
|
|
69
|
+
id: node.id,
|
|
70
|
+
qid,
|
|
71
|
+
ws: alias,
|
|
72
|
+
type: node.type,
|
|
73
|
+
title: node.title,
|
|
74
|
+
status: node.status,
|
|
75
|
+
priority: node.priority,
|
|
76
|
+
created: node.created,
|
|
77
|
+
updated: node.updated,
|
|
78
|
+
tags: node.tags,
|
|
79
|
+
owners: node.owners,
|
|
80
|
+
links: node.links,
|
|
81
|
+
artifacts: node.artifacts,
|
|
82
|
+
refs: node.refs,
|
|
83
|
+
aliases: node.aliases,
|
|
84
|
+
path: relPath,
|
|
85
|
+
edges: normalizedEdges,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (!tolerant) {
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const reverse_edges = {};
|
|
96
|
+
for (const [qid, node] of Object.entries(nodes)) {
|
|
97
|
+
const edges = node.edges;
|
|
98
|
+
if (edges.epic) {
|
|
99
|
+
addReverseEdge(reverse_edges, "epic", edges.epic, qid);
|
|
100
|
+
}
|
|
101
|
+
if (edges.parent) {
|
|
102
|
+
addReverseEdge(reverse_edges, "parent", edges.parent, qid);
|
|
103
|
+
}
|
|
104
|
+
if (edges.prev) {
|
|
105
|
+
addReverseEdge(reverse_edges, "prev", edges.prev, qid);
|
|
106
|
+
}
|
|
107
|
+
if (edges.next) {
|
|
108
|
+
addReverseEdge(reverse_edges, "next", edges.next, qid);
|
|
109
|
+
}
|
|
110
|
+
for (const target of edges.relates) {
|
|
111
|
+
addReverseEdge(reverse_edges, "relates", target, qid);
|
|
112
|
+
}
|
|
113
|
+
for (const target of edges.blocked_by) {
|
|
114
|
+
addReverseEdge(reverse_edges, "blocked_by", target, qid);
|
|
115
|
+
}
|
|
116
|
+
for (const target of edges.blocks) {
|
|
117
|
+
addReverseEdge(reverse_edges, "blocks", target, qid);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const edgeKey of Object.keys(reverse_edges)) {
|
|
121
|
+
for (const target of Object.keys(reverse_edges[edgeKey])) {
|
|
122
|
+
reverse_edges[edgeKey][target].sort();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const workspaces = {};
|
|
126
|
+
for (const alias of Object.keys(config.workspaces).sort()) {
|
|
127
|
+
const entry = config.workspaces[alias];
|
|
128
|
+
workspaces[alias] = { path: entry.path, enabled: entry.enabled };
|
|
129
|
+
}
|
|
130
|
+
const index = {
|
|
131
|
+
meta: {
|
|
132
|
+
tool: config.tool,
|
|
133
|
+
schema_version: config.schema_version,
|
|
134
|
+
generated_at: new Date().toISOString(),
|
|
135
|
+
root,
|
|
136
|
+
workspaces: workspaceAliases,
|
|
137
|
+
},
|
|
138
|
+
workspaces,
|
|
139
|
+
nodes,
|
|
140
|
+
reverse_edges,
|
|
141
|
+
};
|
|
142
|
+
(0, validate_graph_1.validateGraph)(index, { allowMissing: tolerant });
|
|
143
|
+
return index;
|
|
144
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ALLOWED_TYPES = exports.DEC_TYPES = exports.WORK_TYPES = void 0;
|
|
4
|
+
exports.parseNode = parseNode;
|
|
5
|
+
const frontmatter_1 = require("./frontmatter");
|
|
6
|
+
const edges_1 = require("./edges");
|
|
7
|
+
const ID_RE = /^[a-z]+-[0-9]+$/;
|
|
8
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9
|
+
const DEC_ID_RE = /^dec-[0-9]+$/;
|
|
10
|
+
exports.WORK_TYPES = new Set(["epic", "feat", "task", "bug", "checkpoint", "test"]);
|
|
11
|
+
exports.DEC_TYPES = new Set(["dec"]);
|
|
12
|
+
exports.ALLOWED_TYPES = new Set([
|
|
13
|
+
"rule",
|
|
14
|
+
"prd",
|
|
15
|
+
"edd",
|
|
16
|
+
"dec",
|
|
17
|
+
"prop",
|
|
18
|
+
"epic",
|
|
19
|
+
"feat",
|
|
20
|
+
"task",
|
|
21
|
+
"bug",
|
|
22
|
+
"checkpoint",
|
|
23
|
+
"test",
|
|
24
|
+
]);
|
|
25
|
+
const DEC_STATUS = new Set(["proposed", "accepted", "rejected", "superseded"]);
|
|
26
|
+
function formatError(filePath, message) {
|
|
27
|
+
return new Error(`${filePath}: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
function expectString(frontmatter, key, filePath) {
|
|
30
|
+
const value = frontmatter[key];
|
|
31
|
+
if (typeof value !== "string") {
|
|
32
|
+
throw formatError(filePath, `${key} must be a string`);
|
|
33
|
+
}
|
|
34
|
+
if (value.trim().length === 0) {
|
|
35
|
+
throw formatError(filePath, `${key} must not be empty`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function optionalString(frontmatter, key, filePath) {
|
|
40
|
+
const value = frontmatter[key];
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value !== "string") {
|
|
45
|
+
throw formatError(filePath, `${key} must be a string`);
|
|
46
|
+
}
|
|
47
|
+
if (value.trim().length === 0) {
|
|
48
|
+
throw formatError(filePath, `${key} must not be empty`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function optionalList(frontmatter, key, filePath) {
|
|
53
|
+
const value = frontmatter[key];
|
|
54
|
+
if (value === undefined) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(value)) {
|
|
58
|
+
throw formatError(filePath, `${key} must be a list`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function requireLowercase(value, key, filePath) {
|
|
63
|
+
if (value !== value.toLowerCase()) {
|
|
64
|
+
throw formatError(filePath, `${key} must be lowercase`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function requireLowercaseList(values, key, filePath) {
|
|
69
|
+
return values.map((value, index) => {
|
|
70
|
+
if (value !== value.toLowerCase()) {
|
|
71
|
+
throw formatError(filePath, `${key}[${index}] must be lowercase`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function isValidId(value) {
|
|
77
|
+
return ID_RE.test(value) || value === "rule-guide";
|
|
78
|
+
}
|
|
79
|
+
function requireIdFormat(value, key, filePath) {
|
|
80
|
+
if (!isValidId(value)) {
|
|
81
|
+
throw formatError(filePath, `${key} must match <prefix>-<number>`);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function requireDate(value, key, filePath) {
|
|
86
|
+
if (!DATE_RE.test(value)) {
|
|
87
|
+
throw formatError(filePath, `${key} must be YYYY-MM-DD`);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
function parsePriority(value, filePath, min, max) {
|
|
92
|
+
const parsed = Number.parseInt(value, 10);
|
|
93
|
+
if (!Number.isInteger(parsed)) {
|
|
94
|
+
throw formatError(filePath, "priority must be an integer");
|
|
95
|
+
}
|
|
96
|
+
if (parsed < min || parsed > max) {
|
|
97
|
+
throw formatError(filePath, `priority must be between ${min} and ${max}`);
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
function normalizeIdList(values, key, filePath) {
|
|
102
|
+
return values.map((value) => {
|
|
103
|
+
if (value !== value.toLowerCase()) {
|
|
104
|
+
throw formatError(filePath, `${key} entries must be lowercase`);
|
|
105
|
+
}
|
|
106
|
+
if (!isValidId(value)) {
|
|
107
|
+
throw formatError(filePath, `${key} entries must match <prefix>-<number>`);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function requireTemplateSchema(type, templateSchemas, filePath) {
|
|
113
|
+
const schema = templateSchemas[type];
|
|
114
|
+
if (!schema) {
|
|
115
|
+
throw formatError(filePath, `template schema missing for type ${type}`);
|
|
116
|
+
}
|
|
117
|
+
return schema;
|
|
118
|
+
}
|
|
119
|
+
function validateTemplateKeys(frontmatter, schema, filePath) {
|
|
120
|
+
for (const key of Object.keys(frontmatter)) {
|
|
121
|
+
if (!schema.allowedKeys.has(key)) {
|
|
122
|
+
throw formatError(filePath, `unknown key: ${key}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
126
|
+
const expected = schema.keyKinds[key];
|
|
127
|
+
if (!expected) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const isList = Array.isArray(value);
|
|
131
|
+
const isBoolean = typeof value === "boolean";
|
|
132
|
+
if (expected === "list" && !isList) {
|
|
133
|
+
throw formatError(filePath, `${key} must be a list`);
|
|
134
|
+
}
|
|
135
|
+
if (expected === "boolean" && !isBoolean) {
|
|
136
|
+
throw formatError(filePath, `${key} must be a boolean`);
|
|
137
|
+
}
|
|
138
|
+
if (expected === "scalar" && (isList || isBoolean)) {
|
|
139
|
+
throw formatError(filePath, `${key} must be a string`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function parseNode(content, filePath, options) {
|
|
144
|
+
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
145
|
+
const type = requireLowercase(expectString(frontmatter, "type", filePath), "type", filePath);
|
|
146
|
+
if (!exports.ALLOWED_TYPES.has(type)) {
|
|
147
|
+
throw formatError(filePath, `type must be one of ${Array.from(exports.ALLOWED_TYPES).join(", ")}`);
|
|
148
|
+
}
|
|
149
|
+
const schema = requireTemplateSchema(type, options.templateSchemas, filePath);
|
|
150
|
+
validateTemplateKeys(frontmatter, schema, filePath);
|
|
151
|
+
const id = requireIdFormat(requireLowercase(expectString(frontmatter, "id", filePath), "id", filePath), "id", filePath);
|
|
152
|
+
const title = expectString(frontmatter, "title", filePath);
|
|
153
|
+
const created = requireDate(expectString(frontmatter, "created", filePath), "created", filePath);
|
|
154
|
+
const updated = requireDate(expectString(frontmatter, "updated", filePath), "updated", filePath);
|
|
155
|
+
const statusValue = optionalString(frontmatter, "status", filePath);
|
|
156
|
+
let status = undefined;
|
|
157
|
+
const workStatus = new Set(options.workStatusEnum.map((value) => value.toLowerCase()));
|
|
158
|
+
if (exports.WORK_TYPES.has(type)) {
|
|
159
|
+
if (!statusValue) {
|
|
160
|
+
throw formatError(filePath, "status is required for work items");
|
|
161
|
+
}
|
|
162
|
+
const normalized = requireLowercase(statusValue, "status", filePath);
|
|
163
|
+
if (!workStatus.has(normalized)) {
|
|
164
|
+
throw formatError(filePath, `status must be one of ${Array.from(workStatus).join(", ")}`);
|
|
165
|
+
}
|
|
166
|
+
status = normalized;
|
|
167
|
+
}
|
|
168
|
+
else if (exports.DEC_TYPES.has(type)) {
|
|
169
|
+
if (!statusValue) {
|
|
170
|
+
throw formatError(filePath, "status is required for decision records");
|
|
171
|
+
}
|
|
172
|
+
const normalized = requireLowercase(statusValue, "status", filePath);
|
|
173
|
+
if (!DEC_STATUS.has(normalized)) {
|
|
174
|
+
throw formatError(filePath, `status must be one of ${Array.from(DEC_STATUS).join(", ")}`);
|
|
175
|
+
}
|
|
176
|
+
status = normalized;
|
|
177
|
+
}
|
|
178
|
+
else if (statusValue) {
|
|
179
|
+
throw formatError(filePath, "status is not allowed for this type");
|
|
180
|
+
}
|
|
181
|
+
const priorityValue = optionalString(frontmatter, "priority", filePath);
|
|
182
|
+
let priority = undefined;
|
|
183
|
+
if (priorityValue !== undefined) {
|
|
184
|
+
if (!exports.WORK_TYPES.has(type)) {
|
|
185
|
+
throw formatError(filePath, "priority is only allowed for work items");
|
|
186
|
+
}
|
|
187
|
+
priority = parsePriority(priorityValue, filePath, options.priorityMin, options.priorityMax);
|
|
188
|
+
}
|
|
189
|
+
const tags = requireLowercaseList(optionalList(frontmatter, "tags", filePath), "tags", filePath);
|
|
190
|
+
const owners = requireLowercaseList(optionalList(frontmatter, "owners", filePath), "owners", filePath);
|
|
191
|
+
const links = optionalList(frontmatter, "links", filePath);
|
|
192
|
+
const artifacts = optionalList(frontmatter, "artifacts", filePath);
|
|
193
|
+
const refs = normalizeIdList(optionalList(frontmatter, "refs", filePath), "refs", filePath);
|
|
194
|
+
const aliases = requireLowercaseList(optionalList(frontmatter, "aliases", filePath), "aliases", filePath);
|
|
195
|
+
normalizeIdList(optionalList(frontmatter, "scope", filePath), "scope", filePath);
|
|
196
|
+
const supersedesValue = optionalString(frontmatter, "supersedes", filePath);
|
|
197
|
+
if (supersedesValue !== undefined) {
|
|
198
|
+
if (!exports.DEC_TYPES.has(type)) {
|
|
199
|
+
throw formatError(filePath, "supersedes is only allowed for decision records");
|
|
200
|
+
}
|
|
201
|
+
const normalized = requireLowercase(supersedesValue, "supersedes", filePath);
|
|
202
|
+
if (!DEC_ID_RE.test(normalized)) {
|
|
203
|
+
throw formatError(filePath, "supersedes must be a dec-# id");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const edges = (0, edges_1.extractEdges)(frontmatter, filePath);
|
|
207
|
+
return {
|
|
208
|
+
id,
|
|
209
|
+
type,
|
|
210
|
+
title,
|
|
211
|
+
created,
|
|
212
|
+
updated,
|
|
213
|
+
status,
|
|
214
|
+
priority,
|
|
215
|
+
tags,
|
|
216
|
+
owners,
|
|
217
|
+
links,
|
|
218
|
+
artifacts,
|
|
219
|
+
refs,
|
|
220
|
+
aliases,
|
|
221
|
+
edges,
|
|
222
|
+
body,
|
|
223
|
+
frontmatter,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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.isIndexStale = isIndexStale;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const paths_1 = require("../core/paths");
|
|
10
|
+
const workspace_files_1 = require("./workspace_files");
|
|
11
|
+
function mtimeMs(filePath) {
|
|
12
|
+
return fs_1.default.statSync(filePath).mtimeMs;
|
|
13
|
+
}
|
|
14
|
+
function isIndexStale(root, config) {
|
|
15
|
+
const indexPath = path_1.default.resolve(root, config.index.global_index_path);
|
|
16
|
+
if (!fs_1.default.existsSync(indexPath)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const indexMtime = mtimeMs(indexPath);
|
|
20
|
+
const cfgPath = (0, paths_1.configPath)(root);
|
|
21
|
+
if (fs_1.default.existsSync(cfgPath) && mtimeMs(cfgPath) > indexMtime) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const docs = (0, workspace_files_1.listWorkspaceDocFiles)(root, config);
|
|
25
|
+
for (const filePath of docs) {
|
|
26
|
+
if (mtimeMs(filePath) > indexMtime) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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.loadTemplateSchemas = loadTemplateSchemas;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const frontmatter_1 = require("./frontmatter");
|
|
10
|
+
function listMarkdownFiles(dir) {
|
|
11
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
files.push(...listMarkdownFiles(fullPath));
|
|
20
|
+
}
|
|
21
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
22
|
+
files.push(fullPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return files;
|
|
26
|
+
}
|
|
27
|
+
function getValueKind(value) {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return "list";
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "boolean") {
|
|
32
|
+
return "boolean";
|
|
33
|
+
}
|
|
34
|
+
return "scalar";
|
|
35
|
+
}
|
|
36
|
+
function addKeyToSchema(schema, key, kind, filePath) {
|
|
37
|
+
const existing = schema.keyKinds[key];
|
|
38
|
+
if (existing && existing !== kind) {
|
|
39
|
+
throw new Error(`template schema mismatch for ${schema.type}.${key}: ${existing} vs ${kind} (${filePath})`);
|
|
40
|
+
}
|
|
41
|
+
schema.keyKinds[key] = kind;
|
|
42
|
+
schema.allowedKeys.add(key);
|
|
43
|
+
if (kind === "list") {
|
|
44
|
+
schema.listKeys.add(key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function loadTemplateSchemas(root, config, requiredTypes) {
|
|
48
|
+
const templateRoot = path_1.default.resolve(root, config.templates.root_path, config.templates.default_set);
|
|
49
|
+
const files = listMarkdownFiles(templateRoot);
|
|
50
|
+
if (files.length === 0) {
|
|
51
|
+
throw new Error(`no templates found at ${templateRoot}`);
|
|
52
|
+
}
|
|
53
|
+
const schemas = {};
|
|
54
|
+
for (const filePath of files) {
|
|
55
|
+
const content = fs_1.default.readFileSync(filePath, "utf8");
|
|
56
|
+
const { frontmatter } = (0, frontmatter_1.parseFrontmatter)(content, filePath);
|
|
57
|
+
const typeValue = frontmatter.type;
|
|
58
|
+
if (typeof typeValue !== "string") {
|
|
59
|
+
throw new Error(`template missing type in ${filePath}`);
|
|
60
|
+
}
|
|
61
|
+
const normalizedType = typeValue.toLowerCase();
|
|
62
|
+
if (normalizedType !== typeValue) {
|
|
63
|
+
throw new Error(`template type must be lowercase in ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
const schema = schemas[normalizedType] ??
|
|
66
|
+
{
|
|
67
|
+
type: normalizedType,
|
|
68
|
+
allowedKeys: new Set(),
|
|
69
|
+
keyKinds: {},
|
|
70
|
+
listKeys: new Set(),
|
|
71
|
+
};
|
|
72
|
+
schemas[normalizedType] = schema;
|
|
73
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
74
|
+
const kind = getValueKind(value);
|
|
75
|
+
addKeyToSchema(schema, key, kind, filePath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (requiredTypes) {
|
|
79
|
+
const required = Array.from(requiredTypes, (value) => value.toLowerCase());
|
|
80
|
+
const missing = required.filter((value) => !schemas[value]);
|
|
81
|
+
if (missing.length > 0) {
|
|
82
|
+
throw new Error(`template schema missing for type(s): ${missing.join(", ")}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return schemas;
|
|
86
|
+
}
|