mdkg 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +88 -15
  2. package/CLI_COMMAND_MATRIX.md +74 -5
  3. package/README.md +30 -2
  4. package/dist/cli.js +132 -5
  5. package/dist/command-contract.json +778 -16
  6. package/dist/commands/archive.js +196 -42
  7. package/dist/commands/bundle.js +180 -44
  8. package/dist/commands/capability.js +1 -0
  9. package/dist/commands/checkpoint.js +6 -5
  10. package/dist/commands/event_support.js +16 -7
  11. package/dist/commands/fix.js +11 -8
  12. package/dist/commands/git.js +41 -10
  13. package/dist/commands/goal.js +23 -23
  14. package/dist/commands/graph.js +64 -10
  15. package/dist/commands/handoff.js +4 -2
  16. package/dist/commands/init.js +28 -23
  17. package/dist/commands/loop.js +1668 -0
  18. package/dist/commands/loop_descriptors.js +219 -0
  19. package/dist/commands/mcp.js +101 -11
  20. package/dist/commands/new.js +49 -3
  21. package/dist/commands/pack.js +14 -7
  22. package/dist/commands/search.js +10 -1
  23. package/dist/commands/skill.js +12 -9
  24. package/dist/commands/skill_mirror.js +39 -31
  25. package/dist/commands/subgraph.js +59 -26
  26. package/dist/commands/task.js +77 -4
  27. package/dist/commands/upgrade.js +13 -15
  28. package/dist/commands/validate.js +20 -7
  29. package/dist/commands/work.js +44 -26
  30. package/dist/commands/workspace.js +10 -4
  31. package/dist/core/config.js +45 -17
  32. package/dist/core/filesystem_authority.js +281 -0
  33. package/dist/core/project_db_events.js +4 -0
  34. package/dist/core/project_db_materializer.js +2 -0
  35. package/dist/core/project_db_migrations.js +238 -181
  36. package/dist/core/project_db_snapshot.js +64 -14
  37. package/dist/core/workspace_path.js +7 -0
  38. package/dist/graph/archive_integrity.js +1 -1
  39. package/dist/graph/capabilities_index_cache.js +4 -1
  40. package/dist/graph/frontmatter.js +38 -0
  41. package/dist/graph/goal_scope.js +4 -1
  42. package/dist/graph/index_cache.js +37 -7
  43. package/dist/graph/loop_bindings.js +39 -0
  44. package/dist/graph/node.js +209 -1
  45. package/dist/graph/node_body.js +52 -6
  46. package/dist/graph/skills_index_cache.js +41 -6
  47. package/dist/graph/skills_indexer.js +5 -2
  48. package/dist/graph/sqlite_index.js +118 -105
  49. package/dist/graph/subgraphs.js +142 -5
  50. package/dist/graph/validate_graph.js +109 -13
  51. package/dist/graph/workspace_files.js +39 -9
  52. package/dist/init/AGENT_START.md +16 -0
  53. package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
  54. package/dist/init/config.json +7 -1
  55. package/dist/init/init-manifest.json +49 -4
  56. package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
  57. package/dist/init/templates/default/loop.md +160 -0
  58. package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
  59. package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
  60. package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
  61. package/dist/init/templates/loops/security-audit.loop.md +140 -0
  62. package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
  63. package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
  64. package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
  65. package/dist/pack/order.js +3 -1
  66. package/dist/pack/pack.js +139 -6
  67. package/dist/templates/loader.js +9 -3
  68. package/dist/util/lock.js +10 -9
  69. package/dist/util/zip.js +163 -9
  70. 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 buildPackNode(root, index, qid) {
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: (0, node_body_1.readNodeBody)(root, node),
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 nodes = included.map((qid) => buildPackNode(options.root, options.index, qid));
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,
@@ -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
- if (!fs_1.default.existsSync(templatePath) && templateSet !== undefined) {
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 = fs_1.default.existsSync(templatePath) ? "local" : "bundled";
21
+ const source = localExists ? "local" : "bundled";
18
22
  const resolvedPath = source === "local" ? templatePath : (0, builtin_1.requireBundledTemplatePath)(type);
19
- const content = fs_1.default.readFileSync(resolvedPath, "utf8");
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
- fs_1.default.mkdirSync(path_1.default.dirname(dir), { recursive: true });
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(dir);
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
- fs_1.default.writeFileSync(path_1.default.join(dir, "owner.json"), lockOwner(), "utf8");
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
- fs_1.default.rmSync(dir, { recursive: true, force: true });
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
- fs_1.default.rmSync(dir, { recursive: true, force: true });
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
- if (code !== "EEXIST") {
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
- const detailPath = path_1.default.join(dir, "owner.json");
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 readZipEntries(zip) {
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 + 4 <= zip.length) {
119
- const signature = zip.readUInt32LE(offset);
120
- if (signature === 0x02014b50 || signature === 0x06054b50) {
121
- break;
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
- const data = method === 8 ? zlib_1.default.inflateRawSync(compressed) : method === 0 ? compressed : undefined;
136
- if (!data) {
137
- throw new Error(`unsupported zip compression method: ${method}`);
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 = dataOffset + compressedSize;
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.4.2",
3
+ "version": "0.5.1",
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
- "prepack": "npm run build && node scripts/assert-publish-ready.js",
67
- "prepublishOnly": "npm run test && npm run cli:check && npm run cli:contract && npm run docs:check && node dist/cli.js validate && npm run smoke:consumer && 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",
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
  },