mdkg 0.4.2 → 0.5.0
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/CHANGELOG.md +71 -15
- package/CLI_COMMAND_MATRIX.md +59 -2
- package/README.md +20 -2
- package/dist/cli.js +124 -2
- package/dist/command-contract.json +756 -2
- package/dist/commands/archive.js +31 -14
- package/dist/commands/bundle.js +180 -44
- package/dist/commands/capability.js +1 -0
- package/dist/commands/checkpoint.js +6 -5
- package/dist/commands/event_support.js +16 -7
- package/dist/commands/fix.js +11 -8
- package/dist/commands/git.js +41 -10
- package/dist/commands/goal.js +23 -23
- package/dist/commands/graph.js +64 -10
- package/dist/commands/handoff.js +4 -2
- package/dist/commands/init.js +28 -23
- package/dist/commands/loop.js +1668 -0
- package/dist/commands/loop_descriptors.js +219 -0
- package/dist/commands/mcp.js +101 -11
- package/dist/commands/new.js +49 -3
- package/dist/commands/pack.js +14 -7
- package/dist/commands/search.js +10 -1
- package/dist/commands/skill.js +12 -9
- package/dist/commands/skill_mirror.js +39 -31
- package/dist/commands/subgraph.js +59 -26
- package/dist/commands/task.js +77 -4
- package/dist/commands/upgrade.js +13 -15
- package/dist/commands/validate.js +20 -7
- package/dist/commands/work.js +44 -26
- package/dist/commands/workspace.js +10 -4
- package/dist/core/config.js +45 -17
- package/dist/core/filesystem_authority.js +281 -0
- package/dist/core/project_db_events.js +4 -0
- package/dist/core/project_db_materializer.js +2 -0
- package/dist/core/project_db_migrations.js +238 -181
- package/dist/core/project_db_snapshot.js +64 -14
- package/dist/core/workspace_path.js +7 -0
- package/dist/graph/archive_integrity.js +1 -1
- package/dist/graph/capabilities_index_cache.js +4 -1
- package/dist/graph/frontmatter.js +38 -0
- package/dist/graph/goal_scope.js +4 -1
- package/dist/graph/index_cache.js +37 -7
- package/dist/graph/loop_bindings.js +39 -0
- package/dist/graph/node.js +209 -1
- package/dist/graph/node_body.js +52 -6
- package/dist/graph/skills_index_cache.js +41 -6
- package/dist/graph/skills_indexer.js +5 -2
- package/dist/graph/sqlite_index.js +118 -105
- package/dist/graph/subgraphs.js +142 -5
- package/dist/graph/validate_graph.js +109 -13
- package/dist/graph/workspace_files.js +39 -9
- package/dist/init/AGENT_START.md +16 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
- package/dist/init/config.json +7 -1
- package/dist/init/init-manifest.json +49 -4
- package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
- package/dist/init/templates/default/loop.md +160 -0
- package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
- package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
- package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
- package/dist/init/templates/loops/security-audit.loop.md +140 -0
- package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
- package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
- package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
- package/dist/pack/order.js +3 -1
- package/dist/pack/pack.js +139 -6
- package/dist/templates/loader.js +9 -3
- package/dist/util/lock.js +10 -9
- package/dist/util/zip.js +163 -9
- package/package.json +8 -4
package/dist/pack/pack.js
CHANGED
|
@@ -7,6 +7,17 @@ const qid_1 = require("../util/qid");
|
|
|
7
7
|
const order_1 = require("./order");
|
|
8
8
|
const verbose_core_1 = require("./verbose_core");
|
|
9
9
|
const EDGE_KEYS = ["parent", "epic", "relates", "blocked_by", "blocks", "prev", "next", "context_refs", "evidence_refs"];
|
|
10
|
+
const LOOP_REF_FIELDS = [
|
|
11
|
+
"scope_refs",
|
|
12
|
+
"child_refs",
|
|
13
|
+
"lane_waiver_refs",
|
|
14
|
+
"run_refs",
|
|
15
|
+
"decision_refs",
|
|
16
|
+
"output_refs",
|
|
17
|
+
"approval_refs",
|
|
18
|
+
"evaluation_refs",
|
|
19
|
+
];
|
|
20
|
+
const LOOP_REVERSE_CHILD_EDGES = ["parent", "epic", "relates"];
|
|
10
21
|
function normalizeEdgeList(edges) {
|
|
11
22
|
const seen = new Set();
|
|
12
23
|
const result = [];
|
|
@@ -71,7 +82,7 @@ function getNeighbors(index, qid, edges) {
|
|
|
71
82
|
}
|
|
72
83
|
return neighbors;
|
|
73
84
|
}
|
|
74
|
-
function collectNodes(index, rootQid, depth, edges) {
|
|
85
|
+
function collectNodes(index, rootQid, depth, edges, maxNodes) {
|
|
75
86
|
const selectedEdges = normalizeEdgeList(edges);
|
|
76
87
|
const visited = new Set();
|
|
77
88
|
const depths = new Map();
|
|
@@ -95,6 +106,9 @@ function collectNodes(index, rootQid, depth, edges) {
|
|
|
95
106
|
if (visited.has(neighbor)) {
|
|
96
107
|
continue;
|
|
97
108
|
}
|
|
109
|
+
if (visited.size >= maxNodes) {
|
|
110
|
+
throw new Error(`pack graph traversal exceeds node limit: ${maxNodes}`);
|
|
111
|
+
}
|
|
98
112
|
visited.add(neighbor);
|
|
99
113
|
depths.set(neighbor, current.depth + 1);
|
|
100
114
|
queue.push({ qid: neighbor, depth: current.depth + 1 });
|
|
@@ -102,7 +116,121 @@ function collectNodes(index, rootQid, depth, edges) {
|
|
|
102
116
|
}
|
|
103
117
|
return { qids: visited, depths };
|
|
104
118
|
}
|
|
105
|
-
function
|
|
119
|
+
function toStringList(value) {
|
|
120
|
+
if (!Array.isArray(value)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
return value.filter((item) => typeof item === "string");
|
|
124
|
+
}
|
|
125
|
+
function isUriRef(value) {
|
|
126
|
+
return value.includes("://");
|
|
127
|
+
}
|
|
128
|
+
function resolveLoopGraphRef(index, loopQid, field, ref, wsHint, warnings) {
|
|
129
|
+
if (isUriRef(ref)) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const resolved = (0, qid_1.resolveQid)(index, ref, wsHint);
|
|
133
|
+
if (resolved.status === "ok") {
|
|
134
|
+
return resolved.qid;
|
|
135
|
+
}
|
|
136
|
+
if (resolved.status === "ambiguous") {
|
|
137
|
+
mergeWarnings(warnings, `loop scope ref ambiguous: ${loopQid} ${field} ${ref} (${resolved.candidates.join(", ")})`);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
mergeWarnings(warnings, `loop scope ref missing: ${loopQid} ${field} ${ref}`);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
function addLoopPackClosure(index, rootLoopQid, qids, depths, warnings, maxNodes) {
|
|
144
|
+
const visited = new Set();
|
|
145
|
+
const queued = new Set();
|
|
146
|
+
const queue = [];
|
|
147
|
+
function enqueue(qid, depth) {
|
|
148
|
+
if (queued.has(qid)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (queued.size >= maxNodes) {
|
|
152
|
+
throw new Error(`loop pack closure exceeds node limit: ${maxNodes}`);
|
|
153
|
+
}
|
|
154
|
+
queued.add(qid);
|
|
155
|
+
queue.push({ qid, depth });
|
|
156
|
+
}
|
|
157
|
+
function addDeclaredLoopRefs(loopQid, depth) {
|
|
158
|
+
const loop = index.nodes[loopQid];
|
|
159
|
+
if (!loop) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const field of LOOP_REF_FIELDS) {
|
|
163
|
+
for (const ref of toStringList(loop.attributes[field])) {
|
|
164
|
+
const resolved = resolveLoopGraphRef(index, loop.qid, field, ref, loop.ws, warnings);
|
|
165
|
+
if (resolved) {
|
|
166
|
+
enqueue(resolved, depth);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function addReverseChildren(qid, depth) {
|
|
172
|
+
for (const edge of LOOP_REVERSE_CHILD_EDGES) {
|
|
173
|
+
for (const childQid of index.reverse_edges[edge]?.[qid] ?? []) {
|
|
174
|
+
enqueue(childQid, depth);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function addSemanticEdges(loopQid, depth) {
|
|
179
|
+
const loop = index.nodes[loopQid];
|
|
180
|
+
if (!loop) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const field of ["context_refs", "evidence_refs"]) {
|
|
184
|
+
for (const ref of loop.edges[field] ?? []) {
|
|
185
|
+
if (index.nodes[ref]) {
|
|
186
|
+
enqueue(ref, depth);
|
|
187
|
+
}
|
|
188
|
+
else if (!isUriRef(ref)) {
|
|
189
|
+
mergeWarnings(warnings, `loop scope ref missing: ${loop.qid} ${field} ${ref}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
addDeclaredLoopRefs(rootLoopQid, 1);
|
|
195
|
+
addSemanticEdges(rootLoopQid, 1);
|
|
196
|
+
addReverseChildren(rootLoopQid, 1);
|
|
197
|
+
while (queue.length > 0) {
|
|
198
|
+
const current = queue.shift();
|
|
199
|
+
if (!current || visited.has(current.qid)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
visited.add(current.qid);
|
|
203
|
+
const node = index.nodes[current.qid];
|
|
204
|
+
if (!node) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
qids.add(current.qid);
|
|
208
|
+
if (!depths.has(current.qid)) {
|
|
209
|
+
depths.set(current.qid, current.depth);
|
|
210
|
+
}
|
|
211
|
+
if (node.type === "goal") {
|
|
212
|
+
const scoped = (0, goal_scope_1.collectGoalScope)(index, node, { maxNodes });
|
|
213
|
+
for (const scopedQid of scoped.qids) {
|
|
214
|
+
enqueue(scopedQid, current.depth + 1);
|
|
215
|
+
}
|
|
216
|
+
for (const missing of scoped.missingRefs) {
|
|
217
|
+
mergeWarnings(warnings, `loop goal scope ref missing: ${node.qid} ${missing}`);
|
|
218
|
+
}
|
|
219
|
+
for (const invalid of scoped.invalidRefs) {
|
|
220
|
+
mergeWarnings(warnings, `loop goal scope ref unsupported: ${node.qid} ${invalid}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (node.type === "loop") {
|
|
224
|
+
addDeclaredLoopRefs(node.qid, current.depth + 1);
|
|
225
|
+
addSemanticEdges(node.qid, current.depth + 1);
|
|
226
|
+
addReverseChildren(node.qid, current.depth + 1);
|
|
227
|
+
}
|
|
228
|
+
if (node.type === "epic" || node.type === "feat") {
|
|
229
|
+
addReverseChildren(node.qid, current.depth + 1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function buildPackNode(index, qid, readBody) {
|
|
106
234
|
const node = index.nodes[qid];
|
|
107
235
|
if (!node) {
|
|
108
236
|
throw new Error(`node not found: ${qid}`);
|
|
@@ -124,7 +252,7 @@ function buildPackNode(root, index, qid) {
|
|
|
124
252
|
aliases: node.aliases,
|
|
125
253
|
attributes: node.attributes ?? {},
|
|
126
254
|
...(node.source ? { source: node.source } : {}),
|
|
127
|
-
body: (
|
|
255
|
+
body: readBody(node),
|
|
128
256
|
};
|
|
129
257
|
}
|
|
130
258
|
function mergeWarnings(warnings, message) {
|
|
@@ -160,11 +288,12 @@ function applyMaxNodes(orderedQids, maxNodes, truncation) {
|
|
|
160
288
|
}
|
|
161
289
|
function buildPack(options) {
|
|
162
290
|
const warnings = [];
|
|
291
|
+
const maxTraversalNodes = options.maxTraversalNodes ?? Math.max(1000, options.maxNodes * 10);
|
|
163
292
|
const includeLatestCheckpoint = options.includeLatestCheckpoint ?? true;
|
|
164
|
-
const { qids, depths } = collectNodes(options.index, options.rootQid, options.depth, options.edges);
|
|
293
|
+
const { qids, depths } = collectNodes(options.index, options.rootQid, options.depth, options.edges, maxTraversalNodes);
|
|
165
294
|
const rootNode = options.index.nodes[options.rootQid];
|
|
166
295
|
if (rootNode?.type === "goal") {
|
|
167
|
-
const scoped = (0, goal_scope_1.collectGoalScope)(options.index, rootNode);
|
|
296
|
+
const scoped = (0, goal_scope_1.collectGoalScope)(options.index, rootNode, { maxNodes: maxTraversalNodes });
|
|
168
297
|
for (const qid of scoped.qids) {
|
|
169
298
|
qids.add(qid);
|
|
170
299
|
if (!depths.has(qid)) {
|
|
@@ -178,6 +307,9 @@ function buildPack(options) {
|
|
|
178
307
|
mergeWarnings(warnings, `goal scope ref unsupported: ${invalid}`);
|
|
179
308
|
}
|
|
180
309
|
}
|
|
310
|
+
if (rootNode?.type === "loop") {
|
|
311
|
+
addLoopPackClosure(options.index, rootNode.qid, qids, depths, warnings, maxTraversalNodes);
|
|
312
|
+
}
|
|
181
313
|
const workspace = checkpointWorkspaceFromQid(options.rootQid);
|
|
182
314
|
const latestCheckpointHint = options.index.meta.latest_checkpoint_qid?.[workspace];
|
|
183
315
|
const latestCheckpointResolved = includeLatestCheckpoint
|
|
@@ -214,7 +346,8 @@ function buildPack(options) {
|
|
|
214
346
|
dropped: [],
|
|
215
347
|
};
|
|
216
348
|
const { included } = applyMaxNodes(ordered, options.maxNodes, truncation);
|
|
217
|
-
const
|
|
349
|
+
const readBody = (0, node_body_1.createNodeBodyReader)(options.root, options.maxBodyBytes);
|
|
350
|
+
const nodes = included.map((qid) => buildPackNode(options.index, qid, readBody));
|
|
218
351
|
const pack = {
|
|
219
352
|
meta: {
|
|
220
353
|
root: options.rootQid,
|
package/dist/templates/loader.js
CHANGED
|
@@ -6,17 +6,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.loadTemplate = loadTemplate;
|
|
7
7
|
exports.renderTemplate = renderTemplate;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
9
11
|
const frontmatter_1 = require("../graph/frontmatter");
|
|
10
12
|
const errors_1 = require("../util/errors");
|
|
11
13
|
const builtin_1 = require("./builtin");
|
|
12
14
|
function loadTemplate(root, config, type, templateSet) {
|
|
13
15
|
const templatePath = (0, builtin_1.resolveLocalTemplatePath)(root, config, type, templateSet);
|
|
14
|
-
|
|
16
|
+
const templateRelativePath = path_1.default.relative(root, templatePath).split(path_1.default.sep).join("/");
|
|
17
|
+
const localExists = (0, filesystem_authority_1.containedPathExists)({ root, relativePath: templateRelativePath });
|
|
18
|
+
if (!localExists && templateSet !== undefined) {
|
|
15
19
|
throw new errors_1.NotFoundError(`template not found: ${templateSet.toLowerCase()}/${(0, builtin_1.templateNameForType)(type)}.md`);
|
|
16
20
|
}
|
|
17
|
-
const source =
|
|
21
|
+
const source = localExists ? "local" : "bundled";
|
|
18
22
|
const resolvedPath = source === "local" ? templatePath : (0, builtin_1.requireBundledTemplatePath)(type);
|
|
19
|
-
const content =
|
|
23
|
+
const content = source === "local"
|
|
24
|
+
? (0, filesystem_authority_1.readContainedFile)({ root, relativePath: templateRelativePath })
|
|
25
|
+
: fs_1.default.readFileSync(resolvedPath, "utf8");
|
|
20
26
|
const { frontmatter, body } = (0, frontmatter_1.parseFrontmatter)(content, resolvedPath);
|
|
21
27
|
return { templatePath: resolvedPath, source, frontmatter, body };
|
|
22
28
|
}
|
package/dist/util/lock.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.withMutationLock = withMutationLock;
|
|
|
7
7
|
exports.lockTimeoutFromConfig = lockTimeoutFromConfig;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
10
11
|
const HELD_LOCKS = new Set();
|
|
11
12
|
function sleepSync(ms) {
|
|
12
13
|
const shared = new SharedArrayBuffer(4);
|
|
@@ -28,15 +29,15 @@ function withMutationLock(root, timeoutMs, fn) {
|
|
|
28
29
|
if (HELD_LOCKS.has(dir)) {
|
|
29
30
|
return fn();
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
(0, filesystem_authority_1.ensureContainedDirectory)({ root, relativePath: ".mdkg/index" });
|
|
32
33
|
const started = Date.now();
|
|
33
34
|
let lastError;
|
|
34
35
|
while (Date.now() - started <= timeoutMs) {
|
|
35
36
|
try {
|
|
36
|
-
fs_1.default.mkdirSync(
|
|
37
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root, relativePath: ".mdkg/index/write.lock", operation: "create" }, ({ absolutePath }) => fs_1.default.mkdirSync(absolutePath));
|
|
37
38
|
let acquired = false;
|
|
38
39
|
try {
|
|
39
|
-
|
|
40
|
+
(0, filesystem_authority_1.writeContainedFileExclusive)({ root, relativePath: ".mdkg/index/write.lock/owner.json" }, lockOwner());
|
|
40
41
|
HELD_LOCKS.add(dir);
|
|
41
42
|
acquired = true;
|
|
42
43
|
try {
|
|
@@ -44,28 +45,28 @@ function withMutationLock(root, timeoutMs, fn) {
|
|
|
44
45
|
}
|
|
45
46
|
finally {
|
|
46
47
|
HELD_LOCKS.delete(dir);
|
|
47
|
-
|
|
48
|
+
(0, filesystem_authority_1.removeContainedPath)({ root, relativePath: ".mdkg/index/write.lock", recursive: true, force: true });
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
catch (err) {
|
|
51
52
|
if (!acquired) {
|
|
52
|
-
|
|
53
|
+
(0, filesystem_authority_1.removeContainedPath)({ root, relativePath: ".mdkg/index/write.lock", recursive: true, force: true });
|
|
53
54
|
}
|
|
54
55
|
throw err;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
59
|
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
|
|
59
|
-
|
|
60
|
+
// A competing writer can release the lock between authority inspection
|
|
61
|
+
// and realpath resolution. Treat that transient ENOENT like contention.
|
|
62
|
+
if (code !== "EEXIST" && code !== "ENOENT") {
|
|
60
63
|
throw err;
|
|
61
64
|
}
|
|
62
65
|
lastError = err;
|
|
63
66
|
sleepSync(25);
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
|
|
67
|
-
const owner = fs_1.default.existsSync(detailPath) ? fs_1.default.readFileSync(detailPath, "utf8").trim() : "unknown owner";
|
|
68
|
-
throw new Error(`timed out waiting for mdkg mutation lock at ${path_1.default.relative(root, dir)}; owner: ${owner || "unknown"}`);
|
|
69
|
+
throw new Error(`timed out waiting for mdkg mutation lock at ${path_1.default.relative(root, dir)}; owner details withheld`);
|
|
69
70
|
}
|
|
70
71
|
function lockTimeoutFromConfig(config) {
|
|
71
72
|
return config.index.lock_timeout_ms;
|
package/dist/util/zip.js
CHANGED
|
@@ -3,10 +3,14 @@ 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.DEFAULT_ZIP_READ_LIMITS = void 0;
|
|
6
7
|
exports.createDeterministicZip = createDeterministicZip;
|
|
7
8
|
exports.createDeterministicZipFromEntries = createDeterministicZipFromEntries;
|
|
8
9
|
exports.readZipEntries = readZipEntries;
|
|
10
|
+
exports.readZipFileEntries = readZipFileEntries;
|
|
9
11
|
exports.readSingleFileZip = readSingleFileZip;
|
|
12
|
+
exports.readSingleFileZipPath = readSingleFileZipPath;
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
14
|
const zlib_1 = __importDefault(require("zlib"));
|
|
11
15
|
const CRC_TABLE = new Uint32Array(256);
|
|
12
16
|
for (let i = 0; i < 256; i += 1) {
|
|
@@ -35,6 +39,14 @@ function headerBuffer(size) {
|
|
|
35
39
|
function createDeterministicZip(entryName, data) {
|
|
36
40
|
return createDeterministicZipFromEntries([{ name: entryName, data }]);
|
|
37
41
|
}
|
|
42
|
+
exports.DEFAULT_ZIP_READ_LIMITS = {
|
|
43
|
+
maxArchiveBytes: 128 * 1024 * 1024,
|
|
44
|
+
maxEntries: 10000,
|
|
45
|
+
maxEntryNameBytes: 4 * 1024,
|
|
46
|
+
maxEntryUncompressedBytes: 64 * 1024 * 1024,
|
|
47
|
+
maxTotalUncompressedBytes: 256 * 1024 * 1024,
|
|
48
|
+
maxExpansionRatio: 1000,
|
|
49
|
+
};
|
|
38
50
|
function normalizeEntryName(entryName) {
|
|
39
51
|
const normalizedName = entryName.split(/[\\/]/).filter(Boolean).join("/");
|
|
40
52
|
if (!normalizedName || normalizedName.includes("..")) {
|
|
@@ -112,38 +124,173 @@ function createDeterministicZipFromEntries(entries) {
|
|
|
112
124
|
end.writeUInt16LE(0, 20);
|
|
113
125
|
return Buffer.concat([localPayload, centralPayload, end]);
|
|
114
126
|
}
|
|
115
|
-
function
|
|
127
|
+
function resolveZipReadLimits(overrides) {
|
|
128
|
+
const limits = { ...exports.DEFAULT_ZIP_READ_LIMITS, ...overrides };
|
|
129
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
130
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
131
|
+
throw new Error(`zip read limit must be positive: ${key}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (limits.maxExpansionRatio < 1) {
|
|
135
|
+
throw new Error("zip read limit maxExpansionRatio must be at least 1");
|
|
136
|
+
}
|
|
137
|
+
return limits;
|
|
138
|
+
}
|
|
139
|
+
function readCentralDirectoryBounds(zip, limits) {
|
|
140
|
+
if (zip.length < 22) {
|
|
141
|
+
throw new Error("zip end of central directory missing or truncated");
|
|
142
|
+
}
|
|
143
|
+
const minimumOffset = Math.max(0, zip.length - (22 + 0xffff));
|
|
144
|
+
for (let offset = zip.length - 22; offset >= minimumOffset; offset -= 1) {
|
|
145
|
+
if (zip.readUInt32LE(offset) !== 0x06054b50) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const commentLength = zip.readUInt16LE(offset + 20);
|
|
149
|
+
if (offset + 22 + commentLength !== zip.length) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const diskNumber = zip.readUInt16LE(offset + 4);
|
|
153
|
+
const centralDisk = zip.readUInt16LE(offset + 6);
|
|
154
|
+
const diskEntries = zip.readUInt16LE(offset + 8);
|
|
155
|
+
const entryCount = zip.readUInt16LE(offset + 10);
|
|
156
|
+
const centralSize = zip.readUInt32LE(offset + 12);
|
|
157
|
+
const centralOffset = zip.readUInt32LE(offset + 16);
|
|
158
|
+
if (diskNumber !== 0 ||
|
|
159
|
+
centralDisk !== 0 ||
|
|
160
|
+
diskEntries !== entryCount ||
|
|
161
|
+
entryCount === 0xffff ||
|
|
162
|
+
centralSize === 0xffffffff ||
|
|
163
|
+
centralOffset === 0xffffffff) {
|
|
164
|
+
throw new Error("multi-disk and ZIP64 archives are not supported");
|
|
165
|
+
}
|
|
166
|
+
if (entryCount > limits.maxEntries) {
|
|
167
|
+
throw new Error(`zip entry count exceeds configured limit: ${limits.maxEntries}`);
|
|
168
|
+
}
|
|
169
|
+
if (centralOffset + centralSize > offset || centralOffset > zip.length) {
|
|
170
|
+
throw new Error("zip central directory bounds are invalid");
|
|
171
|
+
}
|
|
172
|
+
return { offset: centralOffset, entryCount };
|
|
173
|
+
}
|
|
174
|
+
throw new Error("zip end of central directory missing or truncated");
|
|
175
|
+
}
|
|
176
|
+
function exceedsExpansionRatio(uncompressedSize, compressedSize, limit) {
|
|
177
|
+
if (uncompressedSize === 0) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return compressedSize === 0 || uncompressedSize / compressedSize > limit;
|
|
181
|
+
}
|
|
182
|
+
function readZipEntries(zip, overrides = {}) {
|
|
183
|
+
const limits = resolveZipReadLimits(overrides);
|
|
184
|
+
if (zip.length > limits.maxArchiveBytes) {
|
|
185
|
+
throw new Error(`zip archive exceeds configured byte limit: ${limits.maxArchiveBytes}`);
|
|
186
|
+
}
|
|
187
|
+
const centralDirectory = readCentralDirectoryBounds(zip, limits);
|
|
116
188
|
const entries = [];
|
|
189
|
+
const seenNames = new Set();
|
|
190
|
+
let totalUncompressedBytes = 0;
|
|
191
|
+
let localEntryCount = 0;
|
|
117
192
|
let offset = 0;
|
|
118
|
-
while (offset
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
193
|
+
while (offset < centralDirectory.offset) {
|
|
194
|
+
localEntryCount += 1;
|
|
195
|
+
if (localEntryCount > limits.maxEntries || localEntryCount > centralDirectory.entryCount) {
|
|
196
|
+
throw new Error(`zip local entry count exceeds declared or configured limit: ${limits.maxEntries}`);
|
|
197
|
+
}
|
|
198
|
+
if (offset + 30 > centralDirectory.offset) {
|
|
199
|
+
throw new Error("zip local header is truncated");
|
|
122
200
|
}
|
|
201
|
+
const signature = zip.readUInt32LE(offset);
|
|
123
202
|
if (signature !== 0x04034b50) {
|
|
124
203
|
throw new Error("zip local header missing");
|
|
125
204
|
}
|
|
205
|
+
const flags = zip.readUInt16LE(offset + 6);
|
|
126
206
|
const method = zip.readUInt16LE(offset + 8);
|
|
127
207
|
const compressedSize = zip.readUInt32LE(offset + 18);
|
|
128
208
|
const uncompressedSize = zip.readUInt32LE(offset + 22);
|
|
129
209
|
const nameLength = zip.readUInt16LE(offset + 26);
|
|
130
210
|
const extraLength = zip.readUInt16LE(offset + 28);
|
|
211
|
+
if ((flags & ~0x0806) !== 0) {
|
|
212
|
+
throw new Error(`unsupported zip general-purpose flags: ${flags}`);
|
|
213
|
+
}
|
|
214
|
+
if (method !== 0 && method !== 8) {
|
|
215
|
+
throw new Error(`unsupported zip compression method: ${method}`);
|
|
216
|
+
}
|
|
217
|
+
if (nameLength === 0 || nameLength > limits.maxEntryNameBytes) {
|
|
218
|
+
throw new Error(`zip entry name exceeds configured limit: ${limits.maxEntryNameBytes}`);
|
|
219
|
+
}
|
|
131
220
|
const nameStart = offset + 30;
|
|
132
221
|
const dataOffset = nameStart + nameLength + extraLength;
|
|
222
|
+
const dataEnd = dataOffset + compressedSize;
|
|
223
|
+
if (dataOffset > centralDirectory.offset || dataEnd > centralDirectory.offset) {
|
|
224
|
+
throw new Error("zip entry data is truncated");
|
|
225
|
+
}
|
|
133
226
|
const entryName = zip.slice(nameStart, nameStart + nameLength).toString("utf8");
|
|
227
|
+
if (seenNames.has(entryName)) {
|
|
228
|
+
throw new Error(`duplicate zip entry: ${entryName}`);
|
|
229
|
+
}
|
|
230
|
+
if (uncompressedSize > limits.maxEntryUncompressedBytes) {
|
|
231
|
+
throw new Error(`zip entry exceeds configured uncompressed byte limit: ${entryName} (${limits.maxEntryUncompressedBytes})`);
|
|
232
|
+
}
|
|
233
|
+
if (totalUncompressedBytes + uncompressedSize > limits.maxTotalUncompressedBytes) {
|
|
234
|
+
throw new Error(`zip total uncompressed bytes exceed configured limit: ${limits.maxTotalUncompressedBytes}`);
|
|
235
|
+
}
|
|
236
|
+
if (exceedsExpansionRatio(uncompressedSize, compressedSize, limits.maxExpansionRatio)) {
|
|
237
|
+
throw new Error(`zip entry exceeds configured expansion ratio: ${entryName}`);
|
|
238
|
+
}
|
|
134
239
|
const compressed = zip.slice(dataOffset, dataOffset + compressedSize);
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
240
|
+
let data;
|
|
241
|
+
if (method === 0) {
|
|
242
|
+
data = compressed;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const remainingTotal = limits.maxTotalUncompressedBytes - totalUncompressedBytes;
|
|
246
|
+
const maxOutputLength = Math.max(1, Math.min(limits.maxEntryUncompressedBytes, remainingTotal));
|
|
247
|
+
try {
|
|
248
|
+
data = zlib_1.default.inflateRawSync(compressed, { maxOutputLength });
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
const code = err.code;
|
|
252
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
253
|
+
if (code === "ERR_BUFFER_TOO_LARGE" || /maxOutputLength|larger than/i.test(message)) {
|
|
254
|
+
throw new Error(`zip entry output exceeds configured limit: ${entryName}`);
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`zip entry inflation failed for ${entryName}: ${message}`);
|
|
257
|
+
}
|
|
138
258
|
}
|
|
139
259
|
if (data.length !== uncompressedSize) {
|
|
140
260
|
throw new Error("zip uncompressed size mismatch");
|
|
141
261
|
}
|
|
262
|
+
if (data.length > limits.maxEntryUncompressedBytes) {
|
|
263
|
+
throw new Error(`zip entry output exceeds configured limit: ${entryName}`);
|
|
264
|
+
}
|
|
265
|
+
if (totalUncompressedBytes + data.length > limits.maxTotalUncompressedBytes) {
|
|
266
|
+
throw new Error(`zip total uncompressed bytes exceed configured limit: ${limits.maxTotalUncompressedBytes}`);
|
|
267
|
+
}
|
|
268
|
+
if (exceedsExpansionRatio(data.length, compressedSize, limits.maxExpansionRatio)) {
|
|
269
|
+
throw new Error(`zip entry exceeds configured expansion ratio: ${entryName}`);
|
|
270
|
+
}
|
|
271
|
+
seenNames.add(entryName);
|
|
272
|
+
totalUncompressedBytes += data.length;
|
|
142
273
|
entries.push({ name: entryName, data });
|
|
143
|
-
offset =
|
|
274
|
+
offset = dataEnd;
|
|
275
|
+
}
|
|
276
|
+
if (offset !== centralDirectory.offset ||
|
|
277
|
+
localEntryCount !== centralDirectory.entryCount ||
|
|
278
|
+
entries.length !== centralDirectory.entryCount) {
|
|
279
|
+
throw new Error("zip local and central directory entry counts do not match");
|
|
144
280
|
}
|
|
145
281
|
return entries;
|
|
146
282
|
}
|
|
283
|
+
function readZipFileEntries(filePath, overrides = {}) {
|
|
284
|
+
const limits = resolveZipReadLimits(overrides);
|
|
285
|
+
const stat = fs_1.default.statSync(filePath);
|
|
286
|
+
if (!stat.isFile()) {
|
|
287
|
+
throw new Error(`zip path is not a regular file: ${filePath}`);
|
|
288
|
+
}
|
|
289
|
+
if (stat.size > limits.maxArchiveBytes) {
|
|
290
|
+
throw new Error(`zip archive exceeds configured byte limit: ${limits.maxArchiveBytes}`);
|
|
291
|
+
}
|
|
292
|
+
return readZipEntries(fs_1.default.readFileSync(filePath), limits);
|
|
293
|
+
}
|
|
147
294
|
function readSingleFileZip(zip) {
|
|
148
295
|
const entries = readZipEntries(zip);
|
|
149
296
|
if (entries.length !== 1) {
|
|
@@ -151,3 +298,10 @@ function readSingleFileZip(zip) {
|
|
|
151
298
|
}
|
|
152
299
|
return { entryName: entries[0].name, data: entries[0].data };
|
|
153
300
|
}
|
|
301
|
+
function readSingleFileZipPath(filePath) {
|
|
302
|
+
const entries = readZipFileEntries(filePath);
|
|
303
|
+
if (entries.length !== 1) {
|
|
304
|
+
throw new Error("zip must contain exactly one file");
|
|
305
|
+
}
|
|
306
|
+
return { entryName: entries[0].name, data: entries[0].data };
|
|
307
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdkg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Git-native project memory for AI coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "node scripts/clean-build-output.js && tsc -p tsconfig.build.json && node scripts/add-shebang.js && node scripts/copy-init-assets.js && node scripts/generate-command-contract.js --write",
|
|
11
11
|
"build:test": "node scripts/clean-build-output.js tests && tsc -p tsconfig.test.json",
|
|
12
|
-
"test": "npm run build && npm run build:test && node --test dist/tests/**/*.test.js",
|
|
12
|
+
"test": "npm run build && npm run build:test && node --test dist/tests/**/*.test.js && npm run test:public-release",
|
|
13
|
+
"test:public-release": "node --test tests/public-release.test.mjs tests/security-remediation.test.mjs",
|
|
14
|
+
"security:verify": "node scripts/verify-security-remediation.js",
|
|
13
15
|
"test:coverage": "npm run build && npm run build:test && node --test --experimental-test-coverage dist/tests/**/*.test.js",
|
|
14
16
|
"smoke:consumer": "npm run build && node scripts/smoke-consumer.js",
|
|
17
|
+
"smoke:loop": "npm run build && node scripts/smoke-loop.js",
|
|
15
18
|
"smoke:matrix": "npm run build && node scripts/smoke-command-matrix.js",
|
|
16
19
|
"smoke:upgrade": "npm run build && node scripts/smoke-upgrade.js",
|
|
17
20
|
"smoke:init": "npm run build && node scripts/smoke-init.js",
|
|
@@ -63,8 +66,9 @@
|
|
|
63
66
|
"cli:snapshot": "npm run build && node scripts/cli_help_snapshot.js",
|
|
64
67
|
"cli:check": "npm run build && node scripts/cli_help_snapshot.js --check",
|
|
65
68
|
"cli:contract": "npm run build && node scripts/generate-command-contract.js --check",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
69
|
+
"ci:release": "npm run test && npm run cli:check && npm run cli:contract && npm run docs:check && npm run smoke:loop && npm run security:verify && node scripts/assert-publish-ready.js",
|
|
70
|
+
"prepack": "npm run build && npm run security:verify && node scripts/assert-publish-ready.js",
|
|
71
|
+
"prepublishOnly": "npm run test && npm run cli:check && npm run cli:contract && npm run docs:check && node dist/cli.js validate && npm run security:verify && npm run smoke:consumer && npm run smoke:loop && npm run smoke:matrix && npm run smoke:upgrade && npm run smoke:init && npm run smoke:capabilities && npm run smoke:db && npm run smoke:db-queue && npm run smoke:db-queue-cli && npm run smoke:db-events && npm run smoke:db-materializer && npm run smoke:db-snapshot && npm run smoke:archive-work && npm run smoke:work-invocation && npm run smoke:cli-ux-polish && npm run smoke:operator-health && npm run smoke:fix-plan && npm run smoke:branch-conflicts && npm run smoke:id-repair && npm run smoke:command-docs && npm run smoke:spike && npm run smoke:goal-lifecycle && npm run smoke:semantic-refs && npm run smoke:checkpoint-templates && npm run smoke:handoff && npm run smoke:warning-ux && npm run smoke:integration-ux && npm run smoke:mdkg-dev && npm run smoke:mdkg-dev-docs && npm run smoke:mdkg-dev-seo && npm run smoke:mdkg-dev-polish-pass2 && npm run smoke:mdkg-dev-polish-pass3 && npm run smoke:mdkg-dev-polish-pass4 && npm run smoke:mdkg-dev-polish-pass5 && npm run smoke:mdkg-dev-a11y && npm run smoke:mdkg-dev-perf && npm run smoke:demo-graph && npm run smoke:bundle && npm run smoke:graph-clone && npm run smoke:mcp && npm run smoke:subgraph && npm run smoke:visibility && npm run smoke:sqlite && npm run smoke:parallel && npm run smoke:goal && node scripts/assert-publish-ready.js",
|
|
68
72
|
"postinstall": "node scripts/postinstall.js",
|
|
69
73
|
"smoke:subgraph": "npm run build && node scripts/smoke-subgraph.js"
|
|
70
74
|
},
|