mdkg 0.4.1 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +103 -15
  2. package/CLI_COMMAND_MATRIX.md +109 -2
  3. package/README.md +40 -2
  4. package/dist/cli.js +262 -2
  5. package/dist/command-contract.json +1902 -524
  6. package/dist/commands/archive.js +31 -14
  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 +624 -0
  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/argparse.js +5 -0
  69. package/dist/util/lock.js +10 -9
  70. package/dist/util/zip.js +163 -9
  71. package/package.json +8 -4
@@ -13,10 +13,10 @@ exports.runWorkReceiptUpdateCommand = runWorkReceiptUpdateCommand;
13
13
  exports.runWorkReceiptVerifyCommand = runWorkReceiptVerifyCommand;
14
14
  exports.runWorkValidateCommand = runWorkValidateCommand;
15
15
  exports.runWorkArtifactAddCommand = runWorkArtifactAddCommand;
16
- const fs_1 = __importDefault(require("fs"));
17
16
  const path_1 = __importDefault(require("path"));
18
17
  const crypto_1 = __importDefault(require("crypto"));
19
18
  const config_1 = require("../core/config");
19
+ const filesystem_authority_1 = require("../core/filesystem_authority");
20
20
  const frontmatter_1 = require("../graph/frontmatter");
21
21
  const indexer_1 = require("../graph/indexer");
22
22
  const index_cache_1 = require("../graph/index_cache");
@@ -28,7 +28,6 @@ const errors_1 = require("../util/errors");
28
28
  const id_1 = require("../util/id");
29
29
  const refs_1 = require("../util/refs");
30
30
  const qid_1 = require("../util/qid");
31
- const atomic_1 = require("../util/atomic");
32
31
  const lock_1 = require("../util/lock");
33
32
  const event_support_1 = require("./event_support");
34
33
  const archive_1 = require("./archive");
@@ -323,7 +322,12 @@ function filterWorkflowMessages(messages, nodes, candidatePaths) {
323
322
  function buildWorkValidateReceipt(options) {
324
323
  const config = (0, config_1.loadConfig)(options.root);
325
324
  const ws = normalizeWorkspace(options.ws);
326
- const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config, tolerant: true });
325
+ const { index } = (0, index_cache_1.loadIndex)({
326
+ root: options.root,
327
+ config,
328
+ tolerant: true,
329
+ persistReindex: false,
330
+ });
327
331
  const type = normalizeWorkflowValidateType(options.type);
328
332
  let targets;
329
333
  let target;
@@ -636,10 +640,10 @@ function resolveTypedReadableNode(index, ws, idOrQid, type) {
636
640
  const node = index.nodes[resolved.qid];
637
641
  return node?.type === type ? node : undefined;
638
642
  }
639
- function writeFrontmatterFile(filePath, frontmatter, body) {
643
+ function writeFrontmatterFile(root, filePath, frontmatter, body) {
640
644
  const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter, frontmatter_1.DEFAULT_FRONTMATTER_KEY_ORDER);
641
645
  const content = ["---", ...lines, "---", body.trimStart()].join("\n");
642
- (0, atomic_1.atomicWriteFile)(filePath, content.endsWith("\n") ? content : `${content}\n`);
646
+ (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: path_1.default.relative(root, filePath).split(path_1.default.sep).join("/") }, content.endsWith("\n") ? content : `${content}\n`);
643
647
  }
644
648
  function mergeRenderedFrontmatter(content, filePath, additions) {
645
649
  const entries = Object.entries(additions).filter((entry) => entry[1] !== undefined);
@@ -679,8 +683,9 @@ function createAgentWorkflowNode(options) {
679
683
  ...options.overrides,
680
684
  });
681
685
  const content = mergeRenderedFrontmatter(renderedContent, filePath, options.overrides);
686
+ const relativeFilePath = path_1.default.relative(options.root, filePath).split(path_1.default.sep).join("/");
682
687
  try {
683
- (0, atomic_1.writeFileExclusive)(filePath, content);
688
+ (0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: relativeFilePath }, content);
684
689
  }
685
690
  catch (err) {
686
691
  const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
@@ -689,16 +694,28 @@ function createAgentWorkflowNode(options) {
689
694
  }
690
695
  throw err;
691
696
  }
692
- maybeReindex(options.root, config);
693
- (0, event_support_1.appendAutomaticEvent)({
694
- root: options.root,
695
- ws,
696
- kind: "WORK_NODE_CREATED",
697
- status: "ok",
698
- refs: [id],
699
- notes: `${options.type} semantic mirror created via mdkg work`,
700
- now: options.now,
701
- });
697
+ try {
698
+ maybeReindex(options.root, config);
699
+ (0, event_support_1.appendAutomaticEvent)({
700
+ root: options.root,
701
+ ws,
702
+ kind: "WORK_NODE_CREATED",
703
+ status: "ok",
704
+ refs: [id],
705
+ notes: `${options.type} semantic mirror created via mdkg work`,
706
+ now: options.now,
707
+ });
708
+ }
709
+ catch (error) {
710
+ (0, filesystem_authority_1.removeContainedPath)({ root: options.root, relativePath: relativeFilePath, force: true });
711
+ try {
712
+ maybeReindex(options.root, config);
713
+ }
714
+ catch {
715
+ // Canonical state is rolled back; a later explicit index rebuild repairs derived caches.
716
+ }
717
+ throw error;
718
+ }
702
719
  return {
703
720
  workspace: ws,
704
721
  id,
@@ -728,7 +745,7 @@ function loadMutableAgentNode(root, idOrQid, wsRaw, type) {
728
745
  const { index } = (0, index_cache_1.loadIndex)({ root, config });
729
746
  const node = resolveWorkNode(index, idOrQid, ws, new Set([type]), type);
730
747
  const filePath = path_1.default.resolve(root, node.path);
731
- const parsed = (0, frontmatter_1.parseFrontmatter)(fs_1.default.readFileSync(filePath, "utf8"), filePath);
748
+ const parsed = (0, frontmatter_1.parseFrontmatter)((0, filesystem_authority_1.readContainedFile)({ root, relativePath: node.path }), filePath);
732
749
  return { config, node, filePath, frontmatter: { ...parsed.frontmatter }, body: parsed.body };
733
750
  }
734
751
  function printReceipt(action, receipt, json) {
@@ -857,9 +874,10 @@ function runWorkTriggerCommandLocked(options) {
857
874
  const requester = options.requester ?? "user.local";
858
875
  const requestRef = "request.redacted";
859
876
  const triggerRef = "trigger.mdkg-work-trigger";
877
+ const workRef = workNode.source?.imported ? workNode.qid : workNode.id;
860
878
  const requestedOutputs = toStringList(workNode.attributes.outputs);
861
879
  const payloadHash = buildWorkOrderPayloadHash({
862
- workId: workNode.id,
880
+ workId: workRef,
863
881
  workVersion: String(workNode.attributes.version ?? "0.1.0"),
864
882
  requester,
865
883
  requestRef,
@@ -881,7 +899,7 @@ function runWorkTriggerCommandLocked(options) {
881
899
  ws,
882
900
  title,
883
901
  id,
884
- workId: workNode.id,
902
+ workId: workRef,
885
903
  workNode,
886
904
  requester,
887
905
  requestRef,
@@ -896,7 +914,7 @@ function runWorkTriggerCommandLocked(options) {
896
914
  kind: "mdkg.work_order.triggered",
897
915
  schema_version: 1,
898
916
  target_ref: options.targetRef,
899
- work_id: workNode.id,
917
+ work_id: workRef,
900
918
  work_qid: workNode.qid,
901
919
  work_order_id: created.receipt.id,
902
920
  work_order_qid: created.receipt.qid,
@@ -932,7 +950,7 @@ function runWorkTriggerCommandLocked(options) {
932
950
  ws,
933
951
  kind: queueDelivery ? "WORK_TRIGGER_ENQUEUED" : "WORK_TRIGGERED",
934
952
  status: "ok",
935
- refs: [created.receipt.id, workNode.id, ...(queueRef ? [queueRef] : [])],
953
+ refs: [created.receipt.id, workRef, ...(queueRef ? [queueRef] : [])],
936
954
  notes: queueDelivery
937
955
  ? `work trigger created order mirror and enqueued ${queueDelivery.message_id} on project DB queue ${queueDelivery.queue_name}; no work executed`
938
956
  : "work trigger created order mirror; no work executed",
@@ -980,14 +998,14 @@ function runWorkOrderUpdateCommandLocked(options) {
980
998
  loaded.frontmatter.queue_refs = appendUnique(toStringList(loaded.frontmatter.queue_refs), parseCsvList(options.addQueueRefs));
981
999
  loaded.frontmatter.artifacts = appendUnique(toStringList(loaded.frontmatter.artifacts), parseCsvList(options.addArtifacts));
982
1000
  loaded.frontmatter.updated = (0, date_1.formatDate)(options.now ?? new Date());
983
- writeFrontmatterFile(loaded.filePath, loaded.frontmatter, loaded.body);
1001
+ writeFrontmatterFile(options.root, loaded.filePath, loaded.frontmatter, loaded.body);
984
1002
  maybeReindex(options.root, loaded.config);
985
1003
  printReceipt("order updated", nodeReceipt(options.root, loaded.node), options.json);
986
1004
  }
987
1005
  function runWorkOrderStatusCommandLocked(options) {
988
1006
  const config = (0, config_1.loadConfig)(options.root);
989
1007
  const ws = normalizeWorkspace(options.ws);
990
- const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
1008
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config, persistReindex: false });
991
1009
  const order = resolveReadableWorkNode(index, options.id, ws, "work_order", "work order");
992
1010
  const receipt = buildWorkOrderStatusReceipt(index, order);
993
1011
  if (options.json) {
@@ -1043,14 +1061,14 @@ function runWorkReceiptUpdateCommandLocked(options) {
1043
1061
  loaded.frontmatter.attestation_refs = appendUnique(toStringList(loaded.frontmatter.attestation_refs), parseCsvList(options.addAttestationRefs));
1044
1062
  loaded.frontmatter.evidence_hashes = appendUnique(toStringList(loaded.frontmatter.evidence_hashes), normalizeSha256Refs(options.addEvidenceHashes, "--add-evidence-hashes"));
1045
1063
  loaded.frontmatter.updated = (0, date_1.formatDate)(options.now ?? new Date());
1046
- writeFrontmatterFile(loaded.filePath, loaded.frontmatter, loaded.body);
1064
+ writeFrontmatterFile(options.root, loaded.filePath, loaded.frontmatter, loaded.body);
1047
1065
  maybeReindex(options.root, loaded.config);
1048
1066
  printReceipt("receipt updated", nodeReceipt(options.root, loaded.node), options.json);
1049
1067
  }
1050
1068
  function runWorkReceiptVerifyCommandLocked(options) {
1051
1069
  const config = (0, config_1.loadConfig)(options.root);
1052
1070
  const ws = normalizeWorkspace(options.ws);
1053
- const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
1071
+ const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config, persistReindex: false });
1054
1072
  const receiptNode = resolveReadableWorkNode(index, options.id, ws, "receipt", "receipt");
1055
1073
  const receipt = buildWorkReceiptVerifyReceipt(index, receiptNode);
1056
1074
  if (options.json) {
@@ -1114,7 +1132,7 @@ function runWorkArtifactAddCommandLocked(options) {
1114
1132
  loaded.frontmatter.relates = appendUnique(toStringList(loaded.frontmatter.relates), [archivePayload.archive.id]);
1115
1133
  }
1116
1134
  loaded.frontmatter.updated = (0, date_1.formatDate)(options.now ?? new Date());
1117
- writeFrontmatterFile(loaded.filePath, loaded.frontmatter, loaded.body);
1135
+ writeFrontmatterFile(options.root, loaded.filePath, loaded.frontmatter, loaded.body);
1118
1136
  maybeReindex(options.root, loaded.config);
1119
1137
  if (options.json) {
1120
1138
  console.log(JSON.stringify({
@@ -12,6 +12,7 @@ const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
13
13
  const config_1 = require("../core/config");
14
14
  const migrate_1 = require("../core/migrate");
15
+ const filesystem_authority_1 = require("../core/filesystem_authority");
15
16
  const workspace_path_1 = require("../core/workspace_path");
16
17
  const atomic_1 = require("../util/atomic");
17
18
  const errors_1 = require("../util/errors");
@@ -67,6 +68,9 @@ function normalizeAlias(alias) {
67
68
  if (alias.toLowerCase() === "all") {
68
69
  throw new errors_1.UsageError("workspace alias cannot be 'all'");
69
70
  }
71
+ if (alias === "__proto__" || alias === "constructor" || alias === "prototype") {
72
+ throw new errors_1.UsageError(`workspace alias is reserved: ${alias}`);
73
+ }
70
74
  if (alias !== alias.toLowerCase() || !ALIAS_RE.test(alias)) {
71
75
  throw new errors_1.UsageError("workspace alias must be lowercase and use [a-z0-9_]");
72
76
  }
@@ -145,14 +149,16 @@ function runWorkspaceAddCommandLocked(options) {
145
149
  throw new errors_1.UsageError(`workspace document root already registered by ${existingAlias}`);
146
150
  }
147
151
  }
152
+ for (const folder of ["core", "design", "work"]) {
153
+ (0, filesystem_authority_1.ensureContainedDirectory)({
154
+ root: options.root,
155
+ relativePath: (0, workspace_path_1.workspaceDocumentRelativePath)(workspacePath, mdkgDir, folder),
156
+ });
157
+ }
148
158
  const workspace = { path: workspacePath, enabled: true, mdkg_dir: mdkgDir, visibility };
149
159
  workspaces[alias] = workspace;
150
160
  raw.workspaces = workspaces;
151
161
  writeRawConfig(configPath, raw);
152
- const wsRoot = path_1.default.resolve(options.root, workspacePath, mdkgDir);
153
- fs_1.default.mkdirSync(path_1.default.join(wsRoot, "core"), { recursive: true });
154
- fs_1.default.mkdirSync(path_1.default.join(wsRoot, "design"), { recursive: true });
155
- fs_1.default.mkdirSync(path_1.default.join(wsRoot, "work"), { recursive: true });
156
162
  printWorkspaceMutationReceipt("added", workspaceReceipt(alias, workspace), options.json);
157
163
  }
158
164
  function runWorkspaceRemoveCommandLocked(options) {
@@ -13,6 +13,7 @@ const migrate_1 = require("./migrate");
13
13
  const workspace_path_1 = require("./workspace_path");
14
14
  const project_db_1 = require("./project_db");
15
15
  const WORKSPACE_ALIAS_RE = /^[a-z][a-z0-9_]*$/;
16
+ const RESERVED_OBJECT_ALIASES = new Set(["__proto__", "constructor", "prototype"]);
16
17
  const PACK_EDGE_KEYS = new Set([
17
18
  "parent",
18
19
  "epic",
@@ -31,6 +32,12 @@ const SQL_IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
31
32
  const DEFAULT_ARCHIVE_LARGE_CACHE_WARNING_BYTES = 26214400;
32
33
  const DEFAULT_SQLITE_COMMIT_WARNING_BYTES = 52428800;
33
34
  const DEFAULT_LOCK_TIMEOUT_MS = 10000;
35
+ const DEFAULT_INDEX_LIMITS = {
36
+ max_files: 100000,
37
+ max_file_bytes: 8 * 1024 * 1024,
38
+ max_total_bytes: 512 * 1024 * 1024,
39
+ max_depth: 64,
40
+ };
34
41
  const DEFAULT_SUBGRAPH_MAX_STALE_SECONDS = 3600;
35
42
  exports.DEFAULT_SKILL_MIRROR_TARGETS = [".agents/skills", ".claude/skills"];
36
43
  const DEFAULT_PROJECT_DB_CONFIG = {
@@ -229,6 +236,10 @@ function validateWorkspaceAlias(alias, errors) {
229
236
  errors.push("workspaces.all alias is reserved");
230
237
  return;
231
238
  }
239
+ if (RESERVED_OBJECT_ALIASES.has(alias)) {
240
+ errors.push(`workspaces.${alias} alias is reserved`);
241
+ return;
242
+ }
232
243
  if (alias !== alias.toLowerCase() || !WORKSPACE_ALIAS_RE.test(alias)) {
233
244
  errors.push(`workspaces.${alias} alias must be lowercase and use [a-z0-9_]`);
234
245
  }
@@ -238,6 +249,10 @@ function validateSubgraphAlias(alias, workspaces, errors) {
238
249
  errors.push("subgraphs.all alias is reserved");
239
250
  return;
240
251
  }
252
+ if (RESERVED_OBJECT_ALIASES.has(alias)) {
253
+ errors.push(`subgraphs.${alias} alias is reserved`);
254
+ return;
255
+ }
241
256
  if (alias !== alias.toLowerCase() || !WORKSPACE_ALIAS_RE.test(alias)) {
242
257
  errors.push(`subgraphs.${alias} alias must be lowercase and use [a-z0-9_]`);
243
258
  }
@@ -364,23 +379,36 @@ function validateConfigSchema(raw) {
364
379
  }
365
380
  : undefined;
366
381
  const index = indexRaw
367
- ? {
368
- auto_reindex: requireBoolean(indexRaw.auto_reindex, "index.auto_reindex", errors),
369
- tolerant: requireBoolean(indexRaw.tolerant, "index.tolerant", errors),
370
- backend: indexRaw.backend === undefined
371
- ? "json"
372
- : requireStringInSet(indexRaw.backend, "index.backend", INDEX_BACKEND_VALUES, errors),
373
- global_index_path: requireContainedPath(indexRaw.global_index_path, "index.global_index_path", errors),
374
- sqlite_path: indexRaw.sqlite_path === undefined
375
- ? ".mdkg/index/mdkg.sqlite"
376
- : requireContainedPath(indexRaw.sqlite_path, "index.sqlite_path", errors),
377
- sqlite_commit_warning_bytes: indexRaw.sqlite_commit_warning_bytes === undefined
378
- ? DEFAULT_SQLITE_COMMIT_WARNING_BYTES
379
- : requireNonNegativeInteger(indexRaw.sqlite_commit_warning_bytes, "index.sqlite_commit_warning_bytes", errors),
380
- lock_timeout_ms: indexRaw.lock_timeout_ms === undefined
381
- ? DEFAULT_LOCK_TIMEOUT_MS
382
- : requirePositiveInteger(indexRaw.lock_timeout_ms, "index.lock_timeout_ms", errors),
383
- }
382
+ ? (() => {
383
+ const limitsRaw = indexRaw.limits === undefined
384
+ ? undefined
385
+ : requireObject(indexRaw.limits, "index.limits", errors);
386
+ return {
387
+ auto_reindex: requireBoolean(indexRaw.auto_reindex, "index.auto_reindex", errors),
388
+ tolerant: requireBoolean(indexRaw.tolerant, "index.tolerant", errors),
389
+ backend: indexRaw.backend === undefined
390
+ ? "json"
391
+ : requireStringInSet(indexRaw.backend, "index.backend", INDEX_BACKEND_VALUES, errors),
392
+ global_index_path: requireContainedPath(indexRaw.global_index_path, "index.global_index_path", errors),
393
+ sqlite_path: indexRaw.sqlite_path === undefined
394
+ ? ".mdkg/index/mdkg.sqlite"
395
+ : requireContainedPath(indexRaw.sqlite_path, "index.sqlite_path", errors),
396
+ sqlite_commit_warning_bytes: indexRaw.sqlite_commit_warning_bytes === undefined
397
+ ? DEFAULT_SQLITE_COMMIT_WARNING_BYTES
398
+ : requireNonNegativeInteger(indexRaw.sqlite_commit_warning_bytes, "index.sqlite_commit_warning_bytes", errors),
399
+ lock_timeout_ms: indexRaw.lock_timeout_ms === undefined
400
+ ? DEFAULT_LOCK_TIMEOUT_MS
401
+ : requirePositiveInteger(indexRaw.lock_timeout_ms, "index.lock_timeout_ms", errors),
402
+ limits: limitsRaw
403
+ ? {
404
+ max_files: requirePositiveInteger(limitsRaw.max_files, "index.limits.max_files", errors),
405
+ max_file_bytes: requirePositiveInteger(limitsRaw.max_file_bytes, "index.limits.max_file_bytes", errors),
406
+ max_total_bytes: requirePositiveInteger(limitsRaw.max_total_bytes, "index.limits.max_total_bytes", errors),
407
+ max_depth: requirePositiveInteger(limitsRaw.max_depth, "index.limits.max_depth", errors),
408
+ }
409
+ : { ...DEFAULT_INDEX_LIMITS },
410
+ };
411
+ })()
384
412
  : undefined;
385
413
  const capabilities = capabilitiesRaw
386
414
  ? {
@@ -0,0 +1,281 @@
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.ContainedPathError = void 0;
7
+ exports.withContainedPathSink = withContainedPathSink;
8
+ exports.withContainedTreeSink = withContainedTreeSink;
9
+ exports.containedPathExists = containedPathExists;
10
+ exports.ensureContainedDirectory = ensureContainedDirectory;
11
+ exports.readContainedDirectory = readContainedDirectory;
12
+ exports.readContainedFile = readContainedFile;
13
+ exports.writeContainedFileExclusive = writeContainedFileExclusive;
14
+ exports.appendContainedFile = appendContainedFile;
15
+ exports.atomicReplaceContainedFile = atomicReplaceContainedFile;
16
+ exports.removeContainedPath = removeContainedPath;
17
+ exports.authorizeOperatorSelectedExternalPath = authorizeOperatorSelectedExternalPath;
18
+ const crypto_1 = __importDefault(require("crypto"));
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ class ContainedPathError extends Error {
22
+ constructor(code, operation, relativePath, message) {
23
+ super(message);
24
+ this.name = "ContainedPathError";
25
+ this.code = code;
26
+ this.operation = operation;
27
+ this.relativePath = relativePath;
28
+ }
29
+ }
30
+ exports.ContainedPathError = ContainedPathError;
31
+ function fail(code, operation, relativePath, message) {
32
+ throw new ContainedPathError(code, operation, relativePath, message);
33
+ }
34
+ function isAbsoluteOnSupportedPlatform(value) {
35
+ return (path_1.default.isAbsolute(value) ||
36
+ path_1.default.posix.isAbsolute(value) ||
37
+ path_1.default.win32.isAbsolute(value) ||
38
+ path_1.default.win32.parse(value).root.length > 0);
39
+ }
40
+ function normalizeRelativePath(value, operation) {
41
+ if (!value) {
42
+ fail("ERR_CONTAINED_PATH_EMPTY", operation, value, "contained path cannot be empty");
43
+ }
44
+ if (value.includes("\0")) {
45
+ fail("ERR_CONTAINED_PATH_NUL", operation, value, "contained path cannot contain NUL bytes");
46
+ }
47
+ if (isAbsoluteOnSupportedPlatform(value)) {
48
+ fail("ERR_CONTAINED_PATH_ABSOLUTE", operation, value, "contained path must be relative");
49
+ }
50
+ const components = value.split(/[\\/]/);
51
+ if (components.some((component) => component === "" || component === "." || component === "..")) {
52
+ fail("ERR_CONTAINED_PATH_COMPONENT", operation, value, "contained path cannot contain empty, current-directory, or parent-directory components");
53
+ }
54
+ return components.join(path_1.default.sep);
55
+ }
56
+ function isInside(root, target) {
57
+ const relative = path_1.default.relative(root, target);
58
+ return relative === "" || (!relative.startsWith("..") && !path_1.default.isAbsolute(relative));
59
+ }
60
+ function lstatIfPresent(target) {
61
+ try {
62
+ return fs_1.default.lstatSync(target);
63
+ }
64
+ catch (error) {
65
+ if (error.code === "ENOENT") {
66
+ return undefined;
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+ function validatedRoot(root, operation, relativePath) {
72
+ const absoluteRoot = path_1.default.resolve(root);
73
+ const stat = lstatIfPresent(absoluteRoot);
74
+ if (!stat || !stat.isDirectory() || stat.isSymbolicLink()) {
75
+ fail("ERR_CONTAINED_PATH_ROOT", operation, relativePath, "contained path root must be an existing non-linked directory");
76
+ }
77
+ return {
78
+ absoluteRoot,
79
+ canonicalRoot: fs_1.default.realpathSync.native(absoluteRoot),
80
+ };
81
+ }
82
+ function inspectPath(root, relativePath, operation, createParents) {
83
+ // Node has no portable openat-style API. Keep validation and the sink in one
84
+ // authority, reject every visible link, and use O_NOFOLLOW for file opens.
85
+ const normalized = normalizeRelativePath(relativePath, operation);
86
+ const { absoluteRoot, canonicalRoot } = validatedRoot(root, operation, relativePath);
87
+ const components = normalized.split(path_1.default.sep);
88
+ let current = absoluteRoot;
89
+ let nearestExisting = absoluteRoot;
90
+ for (let index = 0; index < components.length; index += 1) {
91
+ current = path_1.default.join(current, components[index]);
92
+ const final = index === components.length - 1;
93
+ let stat = lstatIfPresent(current);
94
+ if (!stat && !final && createParents) {
95
+ try {
96
+ fs_1.default.mkdirSync(current);
97
+ }
98
+ catch (error) {
99
+ if (error.code !== "EEXIST") {
100
+ throw error;
101
+ }
102
+ }
103
+ stat = fs_1.default.lstatSync(current);
104
+ }
105
+ if (!stat) {
106
+ continue;
107
+ }
108
+ if (stat.isSymbolicLink()) {
109
+ fail("ERR_CONTAINED_PATH_LINK", operation, relativePath, final ? "contained path target cannot be a symbolic link" : "contained path has a linked ancestor");
110
+ }
111
+ if (!final && !stat.isDirectory()) {
112
+ fail("ERR_CONTAINED_PATH_NOT_DIRECTORY", operation, relativePath, "contained path ancestor is not a directory");
113
+ }
114
+ nearestExisting = current;
115
+ }
116
+ const absolutePath = path_1.default.resolve(absoluteRoot, normalized);
117
+ if (!isInside(absoluteRoot, absolutePath)) {
118
+ fail("ERR_CONTAINED_PATH_ESCAPE", operation, relativePath, "contained path resolves outside its root");
119
+ }
120
+ const canonicalExisting = fs_1.default.realpathSync.native(nearestExisting);
121
+ if (!isInside(canonicalRoot, canonicalExisting)) {
122
+ fail("ERR_CONTAINED_PATH_ESCAPE", operation, relativePath, "contained path resolves outside its canonical root");
123
+ }
124
+ return Object.freeze({
125
+ root: absoluteRoot,
126
+ operation,
127
+ relativePath: components.join("/"),
128
+ absolutePath,
129
+ });
130
+ }
131
+ function noFollowFlag() {
132
+ return typeof fs_1.default.constants.O_NOFOLLOW === "number" ? fs_1.default.constants.O_NOFOLLOW : 0;
133
+ }
134
+ function writeAndSync(filePath, data, flags) {
135
+ const handle = fs_1.default.openSync(filePath, flags, 0o666);
136
+ try {
137
+ fs_1.default.writeFileSync(handle, data, typeof data === "string" ? "utf8" : undefined);
138
+ fs_1.default.fsyncSync(handle);
139
+ }
140
+ finally {
141
+ fs_1.default.closeSync(handle);
142
+ }
143
+ }
144
+ function randomSuffix() {
145
+ return `${process.pid}-${Date.now()}-${crypto_1.default.randomBytes(6).toString("hex")}`;
146
+ }
147
+ function withContainedPathSink(input, sink) {
148
+ const descriptor = inspectPath(input.root, input.relativePath, input.operation, input.createParents ?? false);
149
+ return sink(descriptor);
150
+ }
151
+ function rejectLinkedTree(descriptor, currentPath) {
152
+ const entries = fs_1.default.readdirSync(currentPath, { withFileTypes: true });
153
+ for (const entry of entries) {
154
+ const child = path_1.default.join(currentPath, entry.name);
155
+ if (entry.isSymbolicLink()) {
156
+ fail("ERR_CONTAINED_PATH_LINK", descriptor.operation, descriptor.relativePath, "contained managed tree cannot contain symbolic links");
157
+ }
158
+ if (entry.isDirectory()) {
159
+ rejectLinkedTree(descriptor, child);
160
+ }
161
+ }
162
+ }
163
+ function withContainedTreeSink(input, sink) {
164
+ return withContainedPathSink(input, (descriptor) => {
165
+ const stat = lstatIfPresent(descriptor.absolutePath);
166
+ if (stat) {
167
+ if (!stat.isDirectory()) {
168
+ fail("ERR_CONTAINED_PATH_TYPE", descriptor.operation, descriptor.relativePath, "contained managed tree target must be a directory");
169
+ }
170
+ rejectLinkedTree(descriptor, descriptor.absolutePath);
171
+ }
172
+ return sink(descriptor);
173
+ });
174
+ }
175
+ function containedPathExists(input) {
176
+ return withContainedPathSink({ ...input, operation: "read" }, ({ absolutePath }) => lstatIfPresent(absolutePath) !== undefined);
177
+ }
178
+ function ensureContainedDirectory(input) {
179
+ let descriptor = inspectPath(input.root, input.relativePath, "create", true);
180
+ try {
181
+ fs_1.default.mkdirSync(descriptor.absolutePath);
182
+ }
183
+ catch (error) {
184
+ if (error.code !== "EEXIST") {
185
+ throw error;
186
+ }
187
+ }
188
+ descriptor = inspectPath(input.root, input.relativePath, "create", false);
189
+ if (!fs_1.default.lstatSync(descriptor.absolutePath).isDirectory()) {
190
+ fail("ERR_CONTAINED_PATH_TYPE", "create", input.relativePath, "contained directory target is not a directory");
191
+ }
192
+ return descriptor;
193
+ }
194
+ function readContainedDirectory(input) {
195
+ return withContainedPathSink({ ...input, operation: "read" }, ({ absolutePath }) => {
196
+ if (!fs_1.default.lstatSync(absolutePath).isDirectory()) {
197
+ fail("ERR_CONTAINED_PATH_TYPE", "read", input.relativePath, "contained read target must be a directory");
198
+ }
199
+ return fs_1.default.readdirSync(absolutePath, { withFileTypes: true });
200
+ });
201
+ }
202
+ function readContainedFile(input, encoding = "utf8") {
203
+ return withContainedPathSink({ ...input, operation: "read" }, ({ absolutePath }) => {
204
+ const handle = fs_1.default.openSync(absolutePath, fs_1.default.constants.O_RDONLY | noFollowFlag());
205
+ try {
206
+ const stat = fs_1.default.fstatSync(handle);
207
+ if (!stat.isFile()) {
208
+ fail("ERR_CONTAINED_PATH_TYPE", "read", input.relativePath, "contained read target must be a file");
209
+ }
210
+ if (input.maxBytes !== undefined && stat.size > input.maxBytes) {
211
+ fail("ERR_CONTAINED_PATH_TYPE", "read", input.relativePath, `contained read exceeds byte limit: ${input.maxBytes}`);
212
+ }
213
+ return encoding === null ? fs_1.default.readFileSync(handle) : fs_1.default.readFileSync(handle, encoding);
214
+ }
215
+ finally {
216
+ fs_1.default.closeSync(handle);
217
+ }
218
+ });
219
+ }
220
+ function writeContainedFileExclusive(input, data) {
221
+ const descriptor = inspectPath(input.root, input.relativePath, "create", true);
222
+ writeAndSync(descriptor.absolutePath, data, fs_1.default.constants.O_WRONLY | fs_1.default.constants.O_CREAT | fs_1.default.constants.O_EXCL | noFollowFlag());
223
+ return descriptor;
224
+ }
225
+ function appendContainedFile(input, data) {
226
+ const descriptor = inspectPath(input.root, input.relativePath, "replace", false);
227
+ const handle = fs_1.default.openSync(descriptor.absolutePath, fs_1.default.constants.O_WRONLY | fs_1.default.constants.O_APPEND | noFollowFlag());
228
+ try {
229
+ if (!fs_1.default.fstatSync(handle).isFile()) {
230
+ fail("ERR_CONTAINED_PATH_TYPE", "replace", input.relativePath, "contained append target must be a file");
231
+ }
232
+ fs_1.default.writeFileSync(handle, data, typeof data === "string" ? "utf8" : undefined);
233
+ fs_1.default.fsyncSync(handle);
234
+ }
235
+ finally {
236
+ fs_1.default.closeSync(handle);
237
+ }
238
+ return descriptor;
239
+ }
240
+ function atomicReplaceContainedFile(input, data) {
241
+ let descriptor = inspectPath(input.root, input.relativePath, "replace", true);
242
+ const tempRelative = `${descriptor.relativePath}.${randomSuffix()}.tmp`;
243
+ const temp = inspectPath(input.root, tempRelative, "create", false);
244
+ try {
245
+ writeAndSync(temp.absolutePath, data, fs_1.default.constants.O_WRONLY | fs_1.default.constants.O_CREAT | fs_1.default.constants.O_EXCL | noFollowFlag());
246
+ descriptor = inspectPath(input.root, input.relativePath, "replace", false);
247
+ inspectPath(input.root, tempRelative, "create", false);
248
+ fs_1.default.renameSync(temp.absolutePath, descriptor.absolutePath);
249
+ return descriptor;
250
+ }
251
+ catch (error) {
252
+ try {
253
+ const stat = lstatIfPresent(temp.absolutePath);
254
+ if (stat && !stat.isSymbolicLink()) {
255
+ fs_1.default.rmSync(temp.absolutePath, { force: true });
256
+ }
257
+ }
258
+ catch {
259
+ // Preserve the primary failure; any temp residue remains in the allowed root.
260
+ }
261
+ throw error;
262
+ }
263
+ }
264
+ function removeContainedPath(input) {
265
+ const descriptor = inspectPath(input.root, input.relativePath, "delete", false);
266
+ fs_1.default.rmSync(descriptor.absolutePath, {
267
+ recursive: input.recursive ?? false,
268
+ force: input.force ?? false,
269
+ });
270
+ return descriptor;
271
+ }
272
+ function authorizeOperatorSelectedExternalPath(input) {
273
+ if (!input.path || input.path.includes("\0")) {
274
+ throw new Error("operator-selected external path must be non-empty and cannot contain NUL bytes");
275
+ }
276
+ return Object.freeze({
277
+ operation: input.operation,
278
+ absolutePath: path_1.default.resolve(input.path),
279
+ operatorSelected: true,
280
+ });
281
+ }
@@ -351,6 +351,10 @@ function applyProjectDbReducer(databasePath, input) {
351
351
  if (!event) {
352
352
  throw new Error(`project DB event not found: ${input.event_id}`);
353
353
  }
354
+ if ((input.expected_project_id && event.project_id !== input.expected_project_id) ||
355
+ (input.expected_branch_id && event.branch_id !== input.expected_branch_id)) {
356
+ throw new Error(`project DB event scope mismatch: expected ${input.expected_project_id ?? event.project_id}/${input.expected_branch_id ?? event.branch_id}, found ${event.project_id}/${event.branch_id}`);
357
+ }
354
358
  if (event.status === "applied") {
355
359
  const receipt = writeReceiptWithDb(db, databasePath, {
356
360
  project_id: event.project_id,
@@ -339,6 +339,8 @@ function runNextProjectDbMaterializer(databasePath, input) {
339
339
  }
340
340
  const reducer = (0, project_db_events_1.applyProjectDbReducer)(databasePath, {
341
341
  event_id: payload.event_id,
342
+ expected_project_id: payload.project_id,
343
+ expected_branch_id: payload.branch_id,
342
344
  reducer_name: payload.reducer_name,
343
345
  reducer_version: payload.reducer_version,
344
346
  actor: input.lease_owner,