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.
- package/CHANGELOG.md +88 -15
- package/CLI_COMMAND_MATRIX.md +74 -5
- package/README.md +30 -2
- package/dist/cli.js +132 -5
- package/dist/command-contract.json +778 -16
- package/dist/commands/archive.js +196 -42
- package/dist/commands/bundle.js +180 -44
- package/dist/commands/capability.js +1 -0
- package/dist/commands/checkpoint.js +6 -5
- package/dist/commands/event_support.js +16 -7
- package/dist/commands/fix.js +11 -8
- package/dist/commands/git.js +41 -10
- package/dist/commands/goal.js +23 -23
- package/dist/commands/graph.js +64 -10
- package/dist/commands/handoff.js +4 -2
- package/dist/commands/init.js +28 -23
- package/dist/commands/loop.js +1668 -0
- package/dist/commands/loop_descriptors.js +219 -0
- package/dist/commands/mcp.js +101 -11
- package/dist/commands/new.js +49 -3
- package/dist/commands/pack.js +14 -7
- package/dist/commands/search.js +10 -1
- package/dist/commands/skill.js +12 -9
- package/dist/commands/skill_mirror.js +39 -31
- package/dist/commands/subgraph.js +59 -26
- package/dist/commands/task.js +77 -4
- package/dist/commands/upgrade.js +13 -15
- package/dist/commands/validate.js +20 -7
- package/dist/commands/work.js +44 -26
- package/dist/commands/workspace.js +10 -4
- package/dist/core/config.js +45 -17
- package/dist/core/filesystem_authority.js +281 -0
- package/dist/core/project_db_events.js +4 -0
- package/dist/core/project_db_materializer.js +2 -0
- package/dist/core/project_db_migrations.js +238 -181
- package/dist/core/project_db_snapshot.js +64 -14
- package/dist/core/workspace_path.js +7 -0
- package/dist/graph/archive_integrity.js +1 -1
- package/dist/graph/capabilities_index_cache.js +4 -1
- package/dist/graph/frontmatter.js +38 -0
- package/dist/graph/goal_scope.js +4 -1
- package/dist/graph/index_cache.js +37 -7
- package/dist/graph/loop_bindings.js +39 -0
- package/dist/graph/node.js +209 -1
- package/dist/graph/node_body.js +52 -6
- package/dist/graph/skills_index_cache.js +41 -6
- package/dist/graph/skills_indexer.js +5 -2
- package/dist/graph/sqlite_index.js +118 -105
- package/dist/graph/subgraphs.js +142 -5
- package/dist/graph/validate_graph.js +109 -13
- package/dist/graph/workspace_files.js +39 -9
- package/dist/init/AGENT_START.md +16 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +14 -0
- package/dist/init/config.json +7 -1
- package/dist/init/init-manifest.json +49 -4
- package/dist/init/skills/default/pursue-mdkg-loop/SKILL.md +150 -0
- package/dist/init/templates/default/loop.md +160 -0
- package/dist/init/templates/loops/backend-api-cli-bloat-audit.loop.md +117 -0
- package/dist/init/templates/loops/design-frontend-ux-audit.loop.md +117 -0
- package/dist/init/templates/loops/duplicate-code-and-linting-audit.loop.md +114 -0
- package/dist/init/templates/loops/security-audit.loop.md +140 -0
- package/dist/init/templates/loops/tech-stack-best-practices-audit.loop.md +116 -0
- package/dist/init/templates/loops/test-ci-skill-infrastructure-audit.loop.md +116 -0
- package/dist/init/templates/loops/user-story-audit-and-recommendations.loop.md +116 -0
- package/dist/pack/order.js +3 -1
- package/dist/pack/pack.js +139 -6
- package/dist/templates/loader.js +9 -3
- package/dist/util/lock.js +10 -9
- package/dist/util/zip.js +163 -9
- package/package.json +8 -4
package/dist/commands/archive.js
CHANGED
|
@@ -11,6 +11,8 @@ exports.runArchiveCompressCommand = runArchiveCompressCommand;
|
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
13
|
const config_1 = require("../core/config");
|
|
14
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
15
|
+
const workspace_path_1 = require("../core/workspace_path");
|
|
14
16
|
const frontmatter_1 = require("../graph/frontmatter");
|
|
15
17
|
const archive_integrity_1 = require("../graph/archive_integrity");
|
|
16
18
|
const indexer_1 = require("../graph/indexer");
|
|
@@ -21,7 +23,6 @@ const date_1 = require("../util/date");
|
|
|
21
23
|
const errors_1 = require("../util/errors");
|
|
22
24
|
const id_1 = require("../util/id");
|
|
23
25
|
const refs_1 = require("../util/refs");
|
|
24
|
-
const atomic_1 = require("../util/atomic");
|
|
25
26
|
const lock_1 = require("../util/lock");
|
|
26
27
|
const zip_1 = require("../util/zip");
|
|
27
28
|
const event_support_1 = require("./event_support");
|
|
@@ -82,6 +83,22 @@ function normalizeArchiveId(raw) {
|
|
|
82
83
|
}
|
|
83
84
|
return normalized;
|
|
84
85
|
}
|
|
86
|
+
function archiveTargetFromInput(value, ws) {
|
|
87
|
+
const uriId = (0, refs_1.archiveIdFromUri)(value);
|
|
88
|
+
if (uriId) {
|
|
89
|
+
return { id: normalizeArchiveId(uriId), workspace: normalizeWorkspace(ws) };
|
|
90
|
+
}
|
|
91
|
+
const separator = value.indexOf(":");
|
|
92
|
+
if (separator > 0) {
|
|
93
|
+
const workspace = normalizeWorkspace(value.slice(0, separator));
|
|
94
|
+
const id = normalizeArchiveId(value.slice(separator + 1));
|
|
95
|
+
if (ws && normalizeWorkspace(ws) !== workspace) {
|
|
96
|
+
throw new errors_1.UsageError(`archive qid ${value} conflicts with --ws ${normalizeWorkspace(ws)}`);
|
|
97
|
+
}
|
|
98
|
+
return { id, workspace };
|
|
99
|
+
}
|
|
100
|
+
return { id: normalizeArchiveId(value), workspace: normalizeWorkspace(ws) };
|
|
101
|
+
}
|
|
85
102
|
function archiveIdFromInput(value) {
|
|
86
103
|
return normalizeArchiveId((0, refs_1.archiveIdFromUri)(value) ?? value);
|
|
87
104
|
}
|
|
@@ -132,9 +149,8 @@ function maybeReindex(root) {
|
|
|
132
149
|
function resolveArchiveNode(root, id, ws) {
|
|
133
150
|
const config = (0, config_1.loadConfig)(root);
|
|
134
151
|
const { index } = (0, index_cache_1.loadIndex)({ root, config });
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const qid = archiveId.includes(":") ? archiveId : `${wsHint}:${archiveId}`;
|
|
152
|
+
const target = archiveTargetFromInput(id, ws);
|
|
153
|
+
const qid = `${target.workspace}:${target.id}`;
|
|
138
154
|
const node = index.nodes[qid];
|
|
139
155
|
if (!node || node.type !== "archive") {
|
|
140
156
|
throw new errors_1.NotFoundError(`archive not found: ${id}`);
|
|
@@ -142,6 +158,9 @@ function resolveArchiveNode(root, id, ws) {
|
|
|
142
158
|
return node;
|
|
143
159
|
}
|
|
144
160
|
function archiveNodePaths(root, node) {
|
|
161
|
+
if (node.source?.imported || node.source?.read_only || node.path.includes("#")) {
|
|
162
|
+
throw new errors_1.ValidationError(`refusing to derive filesystem paths for read-only archive projection ${node.qid}`);
|
|
163
|
+
}
|
|
145
164
|
const sidecarPath = path_1.default.resolve(root, node.path);
|
|
146
165
|
const sidecarDir = path_1.default.dirname(sidecarPath);
|
|
147
166
|
return {
|
|
@@ -173,10 +192,13 @@ function walkArchiveSidecars(root) {
|
|
|
173
192
|
function stringAttribute(value) {
|
|
174
193
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
175
194
|
}
|
|
176
|
-
function writeArchiveSidecar(sidecarPath, frontmatter, body) {
|
|
195
|
+
function writeArchiveSidecar(root, sidecarPath, frontmatter, body) {
|
|
196
|
+
(0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: toPosixPath(path_1.default.relative(root, sidecarPath)) }, formatArchiveSidecar(frontmatter, body));
|
|
197
|
+
}
|
|
198
|
+
function formatArchiveSidecar(frontmatter, body) {
|
|
177
199
|
const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter);
|
|
178
200
|
const content = ["---", ...lines, "---", body.trimStart()].join("\n");
|
|
179
|
-
|
|
201
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
180
202
|
}
|
|
181
203
|
function verifyArchiveSidecar(root, ws, sidecarPath) {
|
|
182
204
|
const relativePath = toPosixPath(path_1.default.relative(root, sidecarPath));
|
|
@@ -302,18 +324,28 @@ function runArchiveAddCommandLocked(options) {
|
|
|
302
324
|
const visibility = (0, visibility_1.normalizeVisibility)(options.visibility);
|
|
303
325
|
const today = (0, date_1.formatDate)(options.now ?? new Date());
|
|
304
326
|
const archiveDir = path_1.default.resolve(options.root, workspace.path, workspace.mdkg_dir, "archive", id);
|
|
327
|
+
const archiveRelativeDir = (0, workspace_path_1.workspaceDocumentRelativePath)(workspace.path, workspace.mdkg_dir, "archive", id);
|
|
305
328
|
const rawDir = path_1.default.join(archiveDir, "source");
|
|
306
329
|
const rawPath = path_1.default.join(rawDir, basename);
|
|
307
330
|
const zipPath = path_1.default.join(archiveDir, `${basename}.zip`);
|
|
308
331
|
const sidecarPath = path_1.default.join(archiveDir, `${basename}.md`);
|
|
309
|
-
|
|
332
|
+
const sidecarRelativePath = `${archiveRelativeDir}/${basename}.md`;
|
|
333
|
+
const rawRelativePath = `${archiveRelativeDir}/source/${basename}`;
|
|
334
|
+
const zipRelativePath = `${archiveRelativeDir}/${basename}.zip`;
|
|
335
|
+
if ((0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: sidecarRelativePath })) {
|
|
310
336
|
throw new errors_1.UsageError(`archive sidecar already exists: ${path_1.default.relative(options.root, sidecarPath)}`);
|
|
311
337
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
338
|
+
for (const [relativePath, operation] of [
|
|
339
|
+
[rawRelativePath, "create"],
|
|
340
|
+
[zipRelativePath, "replace"],
|
|
341
|
+
[sidecarRelativePath, "create"],
|
|
342
|
+
]) {
|
|
343
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath, operation, createParents: true }, () => undefined);
|
|
344
|
+
}
|
|
345
|
+
const rawData = fs_1.default.readFileSync(sourcePath);
|
|
346
|
+
(0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: rawRelativePath }, rawData);
|
|
315
347
|
const zipData = (0, zip_1.createDeterministicZip)(basename, rawData);
|
|
316
|
-
(0,
|
|
348
|
+
(0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: zipRelativePath }, zipData);
|
|
317
349
|
const frontmatter = {
|
|
318
350
|
id,
|
|
319
351
|
type: "archive",
|
|
@@ -339,7 +371,7 @@ function runArchiveAddCommandLocked(options) {
|
|
|
339
371
|
created: today,
|
|
340
372
|
updated: today,
|
|
341
373
|
};
|
|
342
|
-
writeArchiveSidecar(sidecarPath, frontmatter, ["# Archive Entry", "", `Archived ${archiveKind}: ${basename}`, "", "# Provenance", "", "Copied from local workspace input."].join("\n"));
|
|
374
|
+
writeArchiveSidecar(options.root, sidecarPath, frontmatter, ["# Archive Entry", "", `Archived ${archiveKind}: ${basename}`, "", "# Provenance", "", "Copied from local workspace input."].join("\n"));
|
|
343
375
|
maybeReindex(options.root);
|
|
344
376
|
(0, event_support_1.appendAutomaticEvent)({
|
|
345
377
|
root: options.root,
|
|
@@ -425,36 +457,137 @@ function runArchiveVerifyCommand(options) {
|
|
|
425
457
|
throw new errors_1.ValidationError("archive verification failed");
|
|
426
458
|
}
|
|
427
459
|
}
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
460
|
+
function isReadOnlyArchiveProjection(config, node) {
|
|
461
|
+
return Boolean(node.source?.imported ||
|
|
462
|
+
node.source?.read_only ||
|
|
463
|
+
config.subgraphs[node.source?.subgraph_alias ?? node.ws]);
|
|
464
|
+
}
|
|
465
|
+
function readOnlyArchiveError(node) {
|
|
466
|
+
const alias = node.source?.subgraph_alias ?? node.ws;
|
|
467
|
+
return new errors_1.ValidationError(`cannot compress read-only archive ${node.qid} from imported workspace ${alias}; ` +
|
|
468
|
+
"run compression in the source workspace and refresh the subgraph bundle");
|
|
469
|
+
}
|
|
470
|
+
function readOnlyWorkspaceError(alias) {
|
|
471
|
+
return new errors_1.ValidationError(`cannot compress archives in read-only imported workspace ${alias}; ` +
|
|
472
|
+
"run compression in the source workspace and refresh the subgraph bundle");
|
|
473
|
+
}
|
|
474
|
+
function assertWritableArchiveOwner(config, node) {
|
|
475
|
+
if (isReadOnlyArchiveProjection(config, node)) {
|
|
476
|
+
throw readOnlyArchiveError(node);
|
|
431
477
|
}
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
478
|
+
const workspace = config.workspaces[node.ws];
|
|
479
|
+
if (!workspace) {
|
|
480
|
+
throw new errors_1.ValidationError(`archive ${node.qid} has no configured local workspace owner`);
|
|
481
|
+
}
|
|
482
|
+
if (!workspace.enabled) {
|
|
483
|
+
throw new errors_1.ValidationError(`archive workspace ${node.ws} is disabled and not writable`);
|
|
484
|
+
}
|
|
485
|
+
const archiveRoot = (0, workspace_path_1.workspaceDocumentRelativePath)(workspace.path, workspace.mdkg_dir, "archive");
|
|
486
|
+
const nodePath = toPosixPath(node.path);
|
|
487
|
+
if (!nodePath.startsWith(`${archiveRoot}/`)) {
|
|
488
|
+
throw new errors_1.ValidationError(`archive ${node.qid} path is outside its configured local archive root: ${archiveRoot}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function resolveArchiveNodeFromIndex(index, id, ws) {
|
|
492
|
+
const target = archiveTargetFromInput(id, ws);
|
|
493
|
+
const qid = `${target.workspace}:${target.id}`;
|
|
494
|
+
const node = index.nodes[qid];
|
|
495
|
+
if (!node || node.type !== "archive") {
|
|
496
|
+
throw new errors_1.NotFoundError(`archive not found: ${id}`);
|
|
497
|
+
}
|
|
498
|
+
return node;
|
|
499
|
+
}
|
|
500
|
+
function selectArchiveCompressionNodes(options, config, index) {
|
|
501
|
+
if (options.all && options.id) {
|
|
502
|
+
throw new errors_1.UsageError("archive compress accepts either <id-or-archive-uri-or-qid> or --all, not both");
|
|
503
|
+
}
|
|
504
|
+
const requestedWorkspace = options.ws ? normalizeWorkspace(options.ws) : null;
|
|
505
|
+
const excluded = [];
|
|
506
|
+
let nodes;
|
|
507
|
+
if (options.all) {
|
|
508
|
+
if (requestedWorkspace && config.subgraphs[requestedWorkspace]) {
|
|
509
|
+
throw readOnlyWorkspaceError(requestedWorkspace);
|
|
443
510
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
511
|
+
if (requestedWorkspace) {
|
|
512
|
+
const workspace = config.workspaces[requestedWorkspace];
|
|
513
|
+
if (!workspace) {
|
|
514
|
+
throw new errors_1.NotFoundError(`workspace not found: ${requestedWorkspace}`);
|
|
515
|
+
}
|
|
516
|
+
if (!workspace.enabled) {
|
|
517
|
+
throw new errors_1.ValidationError(`archive workspace ${requestedWorkspace} is disabled and not writable`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
nodes = Object.values(index.nodes)
|
|
521
|
+
.filter((node) => node.type === "archive")
|
|
522
|
+
.sort((a, b) => a.qid.localeCompare(b.qid))
|
|
523
|
+
.filter((node) => {
|
|
524
|
+
if (requestedWorkspace && node.ws !== requestedWorkspace) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (isReadOnlyArchiveProjection(config, node)) {
|
|
528
|
+
if (!requestedWorkspace) {
|
|
529
|
+
excluded.push({
|
|
530
|
+
workspace: node.ws,
|
|
531
|
+
qid: node.qid,
|
|
532
|
+
reason: "read_only_imported_subgraph",
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
assertWritableArchiveOwner(config, node);
|
|
538
|
+
return true;
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
const node = resolveArchiveNodeFromIndex(index, String(options.id), options.ws);
|
|
543
|
+
assertWritableArchiveOwner(config, node);
|
|
544
|
+
nodes = [node];
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
nodes,
|
|
548
|
+
selection: {
|
|
549
|
+
requested_workspace: requestedWorkspace,
|
|
550
|
+
selected_workspaces: Array.from(new Set(nodes.map((node) => node.ws))).sort(),
|
|
551
|
+
excluded_read_only: excluded.sort((a, b) => a.qid.localeCompare(b.qid)),
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function preflightArchiveCompression(root, node, today) {
|
|
556
|
+
for (const attribute of ["stored_path", "compressed_path"]) {
|
|
557
|
+
if (typeof node.attributes[attribute] !== "string" || !String(node.attributes[attribute]).trim()) {
|
|
558
|
+
throw new errors_1.ValidationError(`${node.qid}: ${attribute} is required before archive compression`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const { sidecarPath, rawPath, zipPath } = archiveNodePaths(root, node);
|
|
562
|
+
const rawRelativePath = toPosixPath(path_1.default.relative(root, rawPath));
|
|
563
|
+
const zipRelativePath = toPosixPath(path_1.default.relative(root, zipPath));
|
|
564
|
+
const sidecarRelativePath = toPosixPath(path_1.default.relative(root, sidecarPath));
|
|
565
|
+
for (const relativePath of [rawRelativePath, zipRelativePath, sidecarRelativePath]) {
|
|
566
|
+
(0, filesystem_authority_1.withContainedPathSink)({ root, relativePath, operation: relativePath === rawRelativePath ? "read" : "replace" }, () => undefined);
|
|
567
|
+
}
|
|
568
|
+
if (!(0, filesystem_authority_1.containedPathExists)({ root, relativePath: rawRelativePath })) {
|
|
569
|
+
throw new errors_1.NotFoundError(`raw archive file missing for ${node.qid}: ${path_1.default.relative(root, rawPath)}`);
|
|
570
|
+
}
|
|
571
|
+
const rawData = (0, filesystem_authority_1.readContainedFile)({ root, relativePath: rawRelativePath }, null);
|
|
572
|
+
const parsed = (0, frontmatter_1.parseFrontmatter)((0, filesystem_authority_1.readContainedFile)({ root, relativePath: sidecarRelativePath }), sidecarPath);
|
|
573
|
+
if (parsed.frontmatter.type !== "archive" || parsed.frontmatter.id !== node.id) {
|
|
574
|
+
throw new errors_1.ValidationError(`${node.qid}: archive sidecar identity changed before compression`);
|
|
575
|
+
}
|
|
576
|
+
const zipData = (0, zip_1.createDeterministicZip)(path_1.default.basename(rawPath), rawData);
|
|
577
|
+
const nextFrontmatter = {
|
|
578
|
+
...parsed.frontmatter,
|
|
579
|
+
byte_size: String(rawData.length),
|
|
580
|
+
sha256: (0, archive_integrity_1.hashArchiveBuffer)(rawData),
|
|
581
|
+
compressed_sha256: (0, archive_integrity_1.hashArchiveBuffer)(zipData),
|
|
582
|
+
ingest_status: "compressed",
|
|
583
|
+
updated: today,
|
|
584
|
+
};
|
|
585
|
+
return {
|
|
586
|
+
zipRelativePath,
|
|
587
|
+
sidecarRelativePath,
|
|
588
|
+
zipData,
|
|
589
|
+
sidecarContent: formatArchiveSidecar(nextFrontmatter, parsed.body),
|
|
590
|
+
receipt: {
|
|
458
591
|
workspace: node.ws,
|
|
459
592
|
id: node.id,
|
|
460
593
|
qid: node.qid,
|
|
@@ -465,14 +598,35 @@ function runArchiveCompressCommandLocked(options) {
|
|
|
465
598
|
sha256: String(nextFrontmatter.sha256),
|
|
466
599
|
compressed_sha256: String(nextFrontmatter.compressed_sha256),
|
|
467
600
|
visibility: (0, visibility_1.normalizeVisibility)(typeof nextFrontmatter.visibility === "string" ? nextFrontmatter.visibility : undefined, "archive visibility"),
|
|
468
|
-
}
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function runArchiveCompressCommandLocked(options) {
|
|
605
|
+
if (!options.all && !options.id) {
|
|
606
|
+
throw new errors_1.UsageError("archive compress requires <id-or-archive-uri-or-qid> or --all");
|
|
607
|
+
}
|
|
608
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
609
|
+
const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
|
|
610
|
+
const { nodes, selection } = selectArchiveCompressionNodes(options, config, index);
|
|
611
|
+
const today = (0, date_1.formatDate)(options.now ?? new Date());
|
|
612
|
+
const plans = nodes.map((node) => preflightArchiveCompression(options.root, node, today));
|
|
613
|
+
const updated = [];
|
|
614
|
+
for (const plan of plans) {
|
|
615
|
+
(0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: plan.zipRelativePath }, plan.zipData);
|
|
616
|
+
(0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: plan.sidecarRelativePath }, plan.sidecarContent);
|
|
617
|
+
updated.push(plan.receipt);
|
|
469
618
|
}
|
|
470
619
|
maybeReindex(options.root);
|
|
471
620
|
if (options.json) {
|
|
472
|
-
console.log(JSON.stringify({ action: "compressed", count: updated.length, archives: updated }, null, 2));
|
|
621
|
+
console.log(JSON.stringify({ action: "compressed", count: updated.length, archives: updated, selection }, null, 2));
|
|
473
622
|
return;
|
|
474
623
|
}
|
|
475
624
|
console.log(`archive compressed: ${updated.length}`);
|
|
625
|
+
console.log(`selected workspaces: ${selection.selected_workspaces.join(", ") || "none"}`);
|
|
626
|
+
console.log(`excluded read-only projections: ${selection.excluded_read_only.length}`);
|
|
627
|
+
for (const excluded of selection.excluded_read_only) {
|
|
628
|
+
console.log(` - ${excluded.qid} (${excluded.reason})`);
|
|
629
|
+
}
|
|
476
630
|
}
|
|
477
631
|
function withArchiveLock(root, fn) {
|
|
478
632
|
const config = (0, config_1.loadConfig)(root);
|
package/dist/commands/bundle.js
CHANGED
|
@@ -3,6 +3,8 @@ 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.validateBundleManifest = validateBundleManifest;
|
|
7
|
+
exports.bundlePayloadErrors = bundlePayloadErrors;
|
|
6
8
|
exports.sha256Buffer = sha256Buffer;
|
|
7
9
|
exports.buildBundle = buildBundle;
|
|
8
10
|
exports.parseBundle = parseBundle;
|
|
@@ -16,6 +18,8 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
16
18
|
const path_1 = __importDefault(require("path"));
|
|
17
19
|
const child_process_1 = require("child_process");
|
|
18
20
|
const config_1 = require("../core/config");
|
|
21
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
22
|
+
const atomic_1 = require("../util/atomic");
|
|
19
23
|
const capabilities_indexer_1 = require("../graph/capabilities_indexer");
|
|
20
24
|
const subgraphs_1 = require("../graph/subgraphs");
|
|
21
25
|
const indexer_1 = require("../graph/indexer");
|
|
@@ -31,6 +35,168 @@ const INDEX_ENTRY_PATHS = {
|
|
|
31
35
|
skills: ".mdkg/index/skills.json",
|
|
32
36
|
capabilities: ".mdkg/index/capabilities.json",
|
|
33
37
|
};
|
|
38
|
+
const REQUIRED_INDEX_PATHS = Object.values(INDEX_ENTRY_PATHS);
|
|
39
|
+
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function manifestError(message) {
|
|
44
|
+
throw new errors_1.ValidationError(`bundle manifest invalid: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
function manifestString(value, field) {
|
|
47
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
48
|
+
manifestError(`${field} must be a non-empty string`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function manifestHash(value, field) {
|
|
53
|
+
const hash = manifestString(value, field);
|
|
54
|
+
if (!SHA256_PATTERN.test(hash)) {
|
|
55
|
+
manifestError(`${field} must be a sha256 digest`);
|
|
56
|
+
}
|
|
57
|
+
return hash;
|
|
58
|
+
}
|
|
59
|
+
function safeManifestPath(value, field) {
|
|
60
|
+
const candidate = manifestString(value, field).replace(/\\/g, "/");
|
|
61
|
+
const parts = candidate.split("/");
|
|
62
|
+
if (path_1.default.posix.isAbsolute(candidate) || parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
63
|
+
manifestError(`${field} must be a safe relative path`);
|
|
64
|
+
}
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
function validateBundleManifest(value) {
|
|
68
|
+
if (!isRecord(value))
|
|
69
|
+
manifestError("root must be an object");
|
|
70
|
+
if (value.manifest_version !== 1)
|
|
71
|
+
manifestError("manifest_version must be 1");
|
|
72
|
+
if (value.tool !== "mdkg")
|
|
73
|
+
manifestError("tool must be mdkg");
|
|
74
|
+
const mdkgVersion = manifestString(value.mdkg_version, "mdkg_version");
|
|
75
|
+
if (value.profile !== "private" && value.profile !== "public") {
|
|
76
|
+
manifestError("profile must be private or public");
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(value.selected_workspaces) || value.selected_workspaces.length === 0) {
|
|
79
|
+
manifestError("selected_workspaces must be a non-empty string array");
|
|
80
|
+
}
|
|
81
|
+
const selectedWorkspaces = value.selected_workspaces.map((item, index) => manifestString(item, `selected_workspaces[${index}]`));
|
|
82
|
+
if (new Set(selectedWorkspaces).size !== selectedWorkspaces.length) {
|
|
83
|
+
manifestError("selected_workspaces must not contain duplicates");
|
|
84
|
+
}
|
|
85
|
+
if (!isRecord(value.source))
|
|
86
|
+
manifestError("source must be an object");
|
|
87
|
+
const repo = manifestString(value.source.repo, "source.repo");
|
|
88
|
+
const gitHead = value.source.git_head;
|
|
89
|
+
if (gitHead !== null && (typeof gitHead !== "string" || !/^[a-f0-9]{40}$/.test(gitHead))) {
|
|
90
|
+
manifestError("source.git_head must be null or a 40-character lowercase Git SHA");
|
|
91
|
+
}
|
|
92
|
+
if (typeof value.source.dirty !== "boolean")
|
|
93
|
+
manifestError("source.dirty must be boolean");
|
|
94
|
+
const sourceTreeHash = manifestHash(value.source_tree_hash, "source_tree_hash");
|
|
95
|
+
const bundleHash = manifestHash(value.bundle_hash, "bundle_hash");
|
|
96
|
+
if (!Number.isSafeInteger(value.file_count) || Number(value.file_count) < 0) {
|
|
97
|
+
manifestError("file_count must be a non-negative safe integer");
|
|
98
|
+
}
|
|
99
|
+
if (!isRecord(value.index_hashes))
|
|
100
|
+
manifestError("index_hashes must be an object");
|
|
101
|
+
const indexHashes = {};
|
|
102
|
+
for (const [rawPath, rawHash] of Object.entries(value.index_hashes)) {
|
|
103
|
+
const indexPath = safeManifestPath(rawPath, "index_hashes key");
|
|
104
|
+
indexHashes[indexPath] = manifestHash(rawHash, `index_hashes.${indexPath}`);
|
|
105
|
+
}
|
|
106
|
+
for (const requiredPath of REQUIRED_INDEX_PATHS) {
|
|
107
|
+
if (!indexHashes[requiredPath])
|
|
108
|
+
manifestError(`index_hashes missing required index ${requiredPath}`);
|
|
109
|
+
}
|
|
110
|
+
if (!Array.isArray(value.files))
|
|
111
|
+
manifestError("files must be an array");
|
|
112
|
+
const seenPaths = new Set();
|
|
113
|
+
const files = value.files.map((rawFile, index) => {
|
|
114
|
+
if (!isRecord(rawFile))
|
|
115
|
+
manifestError(`files[${index}] must be an object`);
|
|
116
|
+
const filePath = safeManifestPath(rawFile.path, `files[${index}].path`);
|
|
117
|
+
if (seenPaths.has(filePath))
|
|
118
|
+
manifestError(`duplicate file path: ${filePath}`);
|
|
119
|
+
seenPaths.add(filePath);
|
|
120
|
+
if (rawFile.kind !== "authored" && rawFile.kind !== "archive_cache" && rawFile.kind !== "generated_index") {
|
|
121
|
+
manifestError(`files[${index}].kind is invalid`);
|
|
122
|
+
}
|
|
123
|
+
if (!Number.isSafeInteger(rawFile.size) || Number(rawFile.size) < 0) {
|
|
124
|
+
manifestError(`files[${index}].size must be a non-negative safe integer`);
|
|
125
|
+
}
|
|
126
|
+
const workspace = rawFile.workspace === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: manifestString(rawFile.workspace, `files[${index}].workspace`);
|
|
129
|
+
const visibility = rawFile.visibility;
|
|
130
|
+
if (visibility !== undefined && visibility !== "private" && visibility !== "internal" && visibility !== "public") {
|
|
131
|
+
manifestError(`files[${index}].visibility is invalid`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
path: filePath,
|
|
135
|
+
kind: rawFile.kind,
|
|
136
|
+
...(workspace ? { workspace } : {}),
|
|
137
|
+
...(visibility ? { visibility } : {}),
|
|
138
|
+
size: Number(rawFile.size),
|
|
139
|
+
sha256: manifestHash(rawFile.sha256, `files[${index}].sha256`),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
if (Number(value.file_count) !== files.length)
|
|
143
|
+
manifestError("file_count does not match files length");
|
|
144
|
+
for (const requiredPath of REQUIRED_INDEX_PATHS) {
|
|
145
|
+
const row = files.find((file) => file.path === requiredPath);
|
|
146
|
+
if (!row || row.kind !== "generated_index") {
|
|
147
|
+
manifestError(`files missing required generated index ${requiredPath}`);
|
|
148
|
+
}
|
|
149
|
+
if (row.sha256 !== indexHashes[requiredPath]) {
|
|
150
|
+
manifestError(`index hash does not match file row for ${requiredPath}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
manifest_version: 1,
|
|
155
|
+
tool: "mdkg",
|
|
156
|
+
mdkg_version: mdkgVersion,
|
|
157
|
+
profile: value.profile,
|
|
158
|
+
selected_workspaces: selectedWorkspaces,
|
|
159
|
+
source: { repo, git_head: gitHead, dirty: value.source.dirty },
|
|
160
|
+
source_tree_hash: sourceTreeHash,
|
|
161
|
+
bundle_hash: bundleHash,
|
|
162
|
+
file_count: files.length,
|
|
163
|
+
index_hashes: indexHashes,
|
|
164
|
+
files,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function bundlePayloadErrors(entries, manifest) {
|
|
168
|
+
const errors = [];
|
|
169
|
+
const manifestPaths = new Set(manifest.files.map((file) => file.path));
|
|
170
|
+
for (const entryPath of entries.keys()) {
|
|
171
|
+
if (entryPath !== MANIFEST_ENTRY && !manifestPaths.has(entryPath))
|
|
172
|
+
errors.push(`unexpected bundle entry: ${entryPath}`);
|
|
173
|
+
}
|
|
174
|
+
for (const file of manifest.files) {
|
|
175
|
+
const data = entries.get(file.path);
|
|
176
|
+
if (!data) {
|
|
177
|
+
errors.push(`missing bundle entry: ${file.path}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (sha256Buffer(data) !== file.sha256)
|
|
181
|
+
errors.push(`hash mismatch for ${file.path}`);
|
|
182
|
+
if (data.length !== file.size)
|
|
183
|
+
errors.push(`size mismatch for ${file.path}`);
|
|
184
|
+
}
|
|
185
|
+
for (const requiredPath of REQUIRED_INDEX_PATHS) {
|
|
186
|
+
const data = entries.get(requiredPath);
|
|
187
|
+
if (!data)
|
|
188
|
+
continue;
|
|
189
|
+
if (sha256Buffer(data) !== manifest.index_hashes[requiredPath]) {
|
|
190
|
+
errors.push(`generated index hash mismatch: ${requiredPath}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (hashManifestFiles(manifest.files.filter((file) => file.kind !== "generated_index")) !== manifest.source_tree_hash) {
|
|
194
|
+
errors.push("source_tree_hash mismatch");
|
|
195
|
+
}
|
|
196
|
+
if (hashManifestFiles(manifest.files) !== manifest.bundle_hash)
|
|
197
|
+
errors.push("bundle_hash mismatch");
|
|
198
|
+
return Array.from(new Set(errors));
|
|
199
|
+
}
|
|
34
200
|
function toPosixPath(value) {
|
|
35
201
|
return value.split(path_1.default.sep).join("/");
|
|
36
202
|
}
|
|
@@ -519,9 +685,8 @@ function buildBundle(options) {
|
|
|
519
685
|
};
|
|
520
686
|
}
|
|
521
687
|
function parseBundle(bundlePath) {
|
|
522
|
-
const zip = fs_1.default.readFileSync(bundlePath);
|
|
523
688
|
const entries = new Map();
|
|
524
|
-
for (const entry of (0, zip_1.
|
|
689
|
+
for (const entry of (0, zip_1.readZipFileEntries)(bundlePath)) {
|
|
525
690
|
entries.set(entry.name, entry.data);
|
|
526
691
|
}
|
|
527
692
|
const manifestData = entries.get(MANIFEST_ENTRY);
|
|
@@ -529,10 +694,7 @@ function parseBundle(bundlePath) {
|
|
|
529
694
|
throw new errors_1.ValidationError("bundle manifest missing");
|
|
530
695
|
}
|
|
531
696
|
try {
|
|
532
|
-
return {
|
|
533
|
-
entries,
|
|
534
|
-
manifest: JSON.parse(manifestData.toString("utf8")),
|
|
535
|
-
};
|
|
697
|
+
return { entries, manifest: validateBundleManifest(JSON.parse(manifestData.toString("utf8"))) };
|
|
536
698
|
}
|
|
537
699
|
catch (err) {
|
|
538
700
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -562,25 +724,8 @@ function verifyBundle(root, bundlePath) {
|
|
|
562
724
|
stale_paths: [],
|
|
563
725
|
};
|
|
564
726
|
}
|
|
565
|
-
|
|
566
|
-
for (const entryPath of entries.keys()) {
|
|
567
|
-
if (entryPath !== MANIFEST_ENTRY && !manifestPaths.has(entryPath)) {
|
|
568
|
-
errors.push(`unexpected bundle entry: ${entryPath}`);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
727
|
+
errors.push(...bundlePayloadErrors(entries, manifest));
|
|
571
728
|
for (const file of manifest.files) {
|
|
572
|
-
const data = entries.get(file.path);
|
|
573
|
-
if (!data) {
|
|
574
|
-
errors.push(`missing bundle entry: ${file.path}`);
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
const hash = sha256Buffer(data);
|
|
578
|
-
if (hash !== file.sha256) {
|
|
579
|
-
errors.push(`hash mismatch for ${file.path}`);
|
|
580
|
-
}
|
|
581
|
-
if (data.length !== file.size) {
|
|
582
|
-
errors.push(`size mismatch for ${file.path}`);
|
|
583
|
-
}
|
|
584
729
|
if (file.kind !== "generated_index") {
|
|
585
730
|
const sourcePath = path_1.default.resolve(root, file.path);
|
|
586
731
|
if (!fs_1.default.existsSync(sourcePath)) {
|
|
@@ -592,24 +737,6 @@ function verifyBundle(root, bundlePath) {
|
|
|
592
737
|
}
|
|
593
738
|
}
|
|
594
739
|
}
|
|
595
|
-
for (const [indexPath, expectedHash] of Object.entries(manifest.index_hashes)) {
|
|
596
|
-
const data = entries.get(indexPath);
|
|
597
|
-
if (!data) {
|
|
598
|
-
errors.push(`missing generated index: ${indexPath}`);
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (sha256Buffer(data) !== expectedHash) {
|
|
602
|
-
errors.push(`generated index hash mismatch: ${indexPath}`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
const computedSourceHash = hashManifestFiles(manifest.files.filter((file) => file.kind !== "generated_index"));
|
|
606
|
-
if (computedSourceHash !== manifest.source_tree_hash) {
|
|
607
|
-
errors.push("source_tree_hash mismatch");
|
|
608
|
-
}
|
|
609
|
-
const computedBundleHash = hashManifestFiles(manifest.files);
|
|
610
|
-
if (computedBundleHash !== manifest.bundle_hash) {
|
|
611
|
-
errors.push("bundle_hash mismatch");
|
|
612
|
-
}
|
|
613
740
|
const currentHead = gitOutput(root, ["rev-parse", "HEAD"]);
|
|
614
741
|
if (manifest.source.git_head && currentHead && manifest.source.git_head !== currentHead) {
|
|
615
742
|
stalePaths.push("git:HEAD");
|
|
@@ -645,8 +772,17 @@ function bundleSummary(manifest, bundlePath, zipSha256) {
|
|
|
645
772
|
}
|
|
646
773
|
function runBundleCreateCommand(options) {
|
|
647
774
|
const result = buildBundle(options);
|
|
648
|
-
|
|
649
|
-
|
|
775
|
+
if (options.output && path_1.default.isAbsolute(options.output)) {
|
|
776
|
+
const external = (0, filesystem_authority_1.authorizeOperatorSelectedExternalPath)({
|
|
777
|
+
operation: "replace",
|
|
778
|
+
path: options.output,
|
|
779
|
+
operatorSelected: true,
|
|
780
|
+
});
|
|
781
|
+
(0, atomic_1.atomicWriteFile)(external.absolutePath, result.zip);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
(0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: path_1.default.relative(options.root, result.outputPath).split(path_1.default.sep).join("/") }, result.zip);
|
|
785
|
+
}
|
|
650
786
|
const receipt = {
|
|
651
787
|
action: "created",
|
|
652
788
|
...bundleSummary(result.manifest, path_1.default.relative(options.root, result.outputPath), result.zipSha256),
|
|
@@ -39,6 +39,7 @@ function loadCapabilityRecords(options) {
|
|
|
39
39
|
config,
|
|
40
40
|
useCache: !options.noCache,
|
|
41
41
|
allowReindex: !options.noReindex,
|
|
42
|
+
persistReindex: false,
|
|
42
43
|
});
|
|
43
44
|
if (stale && !rebuilt && !options.noCache) {
|
|
44
45
|
console.error("warning: capabilities index is stale; run mdkg index to refresh");
|
|
@@ -6,15 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.CHECKPOINT_KINDS = void 0;
|
|
7
7
|
exports.createCheckpoint = createCheckpoint;
|
|
8
8
|
exports.runCheckpointNewCommand = runCheckpointNewCommand;
|
|
9
|
-
const fs_1 = __importDefault(require("fs"));
|
|
10
9
|
const path_1 = __importDefault(require("path"));
|
|
11
10
|
const config_1 = require("../core/config");
|
|
11
|
+
const filesystem_authority_1 = require("../core/filesystem_authority");
|
|
12
|
+
const workspace_path_1 = require("../core/workspace_path");
|
|
12
13
|
const index_cache_1 = require("../graph/index_cache");
|
|
13
14
|
const loader_1 = require("../templates/loader");
|
|
14
15
|
const date_1 = require("../util/date");
|
|
15
16
|
const errors_1 = require("../util/errors");
|
|
16
17
|
const id_1 = require("../util/id");
|
|
17
|
-
const atomic_1 = require("../util/atomic");
|
|
18
18
|
const lock_1 = require("../util/lock");
|
|
19
19
|
const sqlite_index_1 = require("../graph/sqlite_index");
|
|
20
20
|
const event_support_1 = require("./event_support");
|
|
@@ -253,7 +253,8 @@ function createCheckpointLocked(options) {
|
|
|
253
253
|
const wsEntry = config.workspaces[ws];
|
|
254
254
|
const workDir = path_1.default.resolve(options.root, wsEntry.path, wsEntry.mdkg_dir, "work");
|
|
255
255
|
const filePath = path_1.default.join(workDir, fileName);
|
|
256
|
-
|
|
256
|
+
const relativeFilePath = (0, workspace_path_1.workspaceDocumentRelativePath)(wsEntry.path, wsEntry.mdkg_dir, "work", fileName);
|
|
257
|
+
if ((0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: relativeFilePath })) {
|
|
257
258
|
throw new errors_1.UsageError(`checkpoint file already exists: ${path_1.default.relative(options.root, filePath)}`);
|
|
258
259
|
}
|
|
259
260
|
const relates = parseCsvList(options.relates).map((value) => normalizeIdRef(value, "--relates"));
|
|
@@ -279,9 +280,9 @@ function createCheckpointLocked(options) {
|
|
|
279
280
|
relates,
|
|
280
281
|
scope,
|
|
281
282
|
});
|
|
282
|
-
const rendered = replaceRenderedBody(content, checkpointBody(kind));
|
|
283
|
+
const rendered = replaceRenderedBody(content, options.body ?? checkpointBody(kind));
|
|
283
284
|
try {
|
|
284
|
-
(0,
|
|
285
|
+
(0, filesystem_authority_1.writeContainedFileExclusive)({ root: options.root, relativePath: relativeFilePath }, rendered);
|
|
285
286
|
}
|
|
286
287
|
catch (err) {
|
|
287
288
|
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
|