mdkg 0.5.0 → 0.5.2

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.
@@ -83,6 +83,22 @@ function normalizeArchiveId(raw) {
83
83
  }
84
84
  return normalized;
85
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
+ }
86
102
  function archiveIdFromInput(value) {
87
103
  return normalizeArchiveId((0, refs_1.archiveIdFromUri)(value) ?? value);
88
104
  }
@@ -133,9 +149,8 @@ function maybeReindex(root) {
133
149
  function resolveArchiveNode(root, id, ws) {
134
150
  const config = (0, config_1.loadConfig)(root);
135
151
  const { index } = (0, index_cache_1.loadIndex)({ root, config });
136
- const archiveId = archiveIdFromInput(id);
137
- const wsHint = normalizeWorkspace(ws);
138
- const qid = archiveId.includes(":") ? archiveId : `${wsHint}:${archiveId}`;
152
+ const target = archiveTargetFromInput(id, ws);
153
+ const qid = `${target.workspace}:${target.id}`;
139
154
  const node = index.nodes[qid];
140
155
  if (!node || node.type !== "archive") {
141
156
  throw new errors_1.NotFoundError(`archive not found: ${id}`);
@@ -143,6 +158,9 @@ function resolveArchiveNode(root, id, ws) {
143
158
  return node;
144
159
  }
145
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
+ }
146
164
  const sidecarPath = path_1.default.resolve(root, node.path);
147
165
  const sidecarDir = path_1.default.dirname(sidecarPath);
148
166
  return {
@@ -175,9 +193,12 @@ function stringAttribute(value) {
175
193
  return typeof value === "string" && value.trim().length > 0 ? value : undefined;
176
194
  }
177
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) {
178
199
  const lines = (0, frontmatter_1.formatFrontmatter)(frontmatter);
179
200
  const content = ["---", ...lines, "---", body.trimStart()].join("\n");
180
- (0, filesystem_authority_1.atomicReplaceContainedFile)({ root, relativePath: toPosixPath(path_1.default.relative(root, sidecarPath)) }, content.endsWith("\n") ? content : `${content}\n`);
201
+ return content.endsWith("\n") ? content : `${content}\n`;
181
202
  }
182
203
  function verifyArchiveSidecar(root, ws, sidecarPath) {
183
204
  const relativePath = toPosixPath(path_1.default.relative(root, sidecarPath));
@@ -436,42 +457,137 @@ function runArchiveVerifyCommand(options) {
436
457
  throw new errors_1.ValidationError("archive verification failed");
437
458
  }
438
459
  }
439
- function runArchiveCompressCommandLocked(options) {
440
- if (!options.all && !options.id) {
441
- throw new errors_1.UsageError("archive compress requires <id-or-archive-uri> or --all");
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);
442
477
  }
443
- const config = (0, config_1.loadConfig)(options.root);
444
- const { index } = (0, index_cache_1.loadIndex)({ root: options.root, config });
445
- const nodes = options.all
446
- ? Object.values(index.nodes).filter((node) => node.type === "archive")
447
- : [resolveArchiveNode(options.root, String(options.id), options.ws)];
448
- const updated = [];
449
- const today = (0, date_1.formatDate)(options.now ?? new Date());
450
- for (const node of nodes) {
451
- const { sidecarPath, rawPath, zipPath } = archiveNodePaths(options.root, node);
452
- const rawRelativePath = toPosixPath(path_1.default.relative(options.root, rawPath));
453
- const zipRelativePath = toPosixPath(path_1.default.relative(options.root, zipPath));
454
- const sidecarRelativePath = toPosixPath(path_1.default.relative(options.root, sidecarPath));
455
- for (const relativePath of [rawRelativePath, zipRelativePath, sidecarRelativePath]) {
456
- (0, filesystem_authority_1.withContainedPathSink)({ root: options.root, relativePath, operation: relativePath === rawRelativePath ? "read" : "replace" }, () => undefined);
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);
457
510
  }
458
- if (!(0, filesystem_authority_1.containedPathExists)({ root: options.root, relativePath: rawRelativePath })) {
459
- throw new errors_1.NotFoundError(`raw archive file missing for ${node.qid}: ${path_1.default.relative(options.root, rawPath)}`);
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
+ }
460
519
  }
461
- const rawData = (0, filesystem_authority_1.readContainedFile)({ root: options.root, relativePath: rawRelativePath }, null);
462
- const zipData = (0, zip_1.createDeterministicZip)(path_1.default.basename(rawPath), rawData);
463
- (0, filesystem_authority_1.atomicReplaceContainedFile)({ root: options.root, relativePath: zipRelativePath }, zipData);
464
- const parsed = (0, frontmatter_1.parseFrontmatter)((0, filesystem_authority_1.readContainedFile)({ root: options.root, relativePath: sidecarRelativePath }), sidecarPath);
465
- const nextFrontmatter = {
466
- ...parsed.frontmatter,
467
- byte_size: String(rawData.length),
468
- sha256: (0, archive_integrity_1.hashArchiveBuffer)(rawData),
469
- compressed_sha256: (0, archive_integrity_1.hashArchiveBuffer)(zipData),
470
- ingest_status: "compressed",
471
- updated: today,
472
- };
473
- writeArchiveSidecar(options.root, sidecarPath, nextFrontmatter, parsed.body);
474
- updated.push({
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: {
475
591
  workspace: node.ws,
476
592
  id: node.id,
477
593
  qid: node.qid,
@@ -482,14 +598,35 @@ function runArchiveCompressCommandLocked(options) {
482
598
  sha256: String(nextFrontmatter.sha256),
483
599
  compressed_sha256: String(nextFrontmatter.compressed_sha256),
484
600
  visibility: (0, visibility_1.normalizeVisibility)(typeof nextFrontmatter.visibility === "string" ? nextFrontmatter.visibility : undefined, "archive visibility"),
485
- });
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);
486
618
  }
487
619
  maybeReindex(options.root);
488
620
  if (options.json) {
489
- 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));
490
622
  return;
491
623
  }
492
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
+ }
493
630
  }
494
631
  function withArchiveLock(root, fn) {
495
632
  const config = (0, config_1.loadConfig)(root);