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.
- package/CHANGELOG.md +103 -15
- package/CLI_COMMAND_MATRIX.md +109 -2
- package/README.md +40 -2
- package/dist/cli.js +262 -2
- package/dist/command-contract.json +1902 -524
- 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 +624 -0
- 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/argparse.js +5 -0
- package/dist/util/lock.js +10 -9
- package/dist/util/zip.js +163 -9
- package/package.json +8 -4
package/dist/commands/work.js
CHANGED
|
@@ -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)({
|
|
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,
|
|
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,
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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)(
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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) {
|
package/dist/core/config.js
CHANGED
|
@@ -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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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,
|