mdkg 0.1.1 → 0.1.3

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 (73) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +124 -18
  3. package/dist/cli.js +567 -15
  4. package/dist/commands/archive.js +486 -0
  5. package/dist/commands/bundle.js +743 -0
  6. package/dist/commands/bundle_import.js +255 -0
  7. package/dist/commands/capability.js +162 -0
  8. package/dist/commands/checkpoint.js +31 -5
  9. package/dist/commands/doctor.js +269 -9
  10. package/dist/commands/format.js +38 -9
  11. package/dist/commands/index.js +12 -12
  12. package/dist/commands/init.js +194 -63
  13. package/dist/commands/init_manifest.js +19 -6
  14. package/dist/commands/list.js +5 -2
  15. package/dist/commands/new.js +36 -7
  16. package/dist/commands/next.js +7 -0
  17. package/dist/commands/node_card.js +4 -1
  18. package/dist/commands/pack.js +62 -2
  19. package/dist/commands/query_output.js +1 -0
  20. package/dist/commands/search.js +5 -2
  21. package/dist/commands/show.js +7 -14
  22. package/dist/commands/skill_mirror.js +22 -0
  23. package/dist/commands/task.js +23 -6
  24. package/dist/commands/upgrade.js +24 -1
  25. package/dist/commands/validate.js +20 -1
  26. package/dist/commands/work.js +397 -0
  27. package/dist/commands/workspace.js +12 -2
  28. package/dist/core/config.js +115 -1
  29. package/dist/graph/agent_file_types.js +78 -5
  30. package/dist/graph/archive_file.js +125 -0
  31. package/dist/graph/archive_integrity.js +66 -0
  32. package/dist/graph/bundle_imports.js +418 -0
  33. package/dist/graph/capabilities_index_cache.js +103 -0
  34. package/dist/graph/capabilities_indexer.js +231 -0
  35. package/dist/graph/frontmatter.js +19 -0
  36. package/dist/graph/index_cache.js +23 -6
  37. package/dist/graph/indexer.js +4 -1
  38. package/dist/graph/node.js +23 -4
  39. package/dist/graph/node_body.js +37 -0
  40. package/dist/graph/reindex.js +46 -0
  41. package/dist/graph/skills_index_cache.js +2 -2
  42. package/dist/graph/skills_indexer.js +8 -3
  43. package/dist/graph/sqlite_index.js +293 -0
  44. package/dist/graph/validate_graph.js +83 -7
  45. package/dist/graph/visibility.js +214 -0
  46. package/dist/graph/workspace_files.js +22 -0
  47. package/dist/init/AGENT_START.md +24 -0
  48. package/dist/init/CLI_COMMAND_MATRIX.md +61 -3
  49. package/dist/init/README.md +70 -4
  50. package/dist/init/config.json +18 -2
  51. package/dist/init/core/guide.md +6 -2
  52. package/dist/init/core/rule-1-mdkg-conventions.md +2 -1
  53. package/dist/init/core/rule-3-cli-contract.md +72 -4
  54. package/dist/init/core/rule-4-repo-safety-and-ignores.md +47 -11
  55. package/dist/init/core/rule-5-release-and-versioning.md +4 -3
  56. package/dist/init/core/rule-6-templates-and-schemas.md +7 -0
  57. package/dist/init/init-manifest.json +21 -16
  58. package/dist/init/skills/default/build-pack-and-execute-task/SKILL.md +2 -1
  59. package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +26 -0
  60. package/dist/init/templates/default/archive.md +33 -0
  61. package/dist/init/templates/default/receipt.md +15 -1
  62. package/dist/init/templates/default/work.md +6 -1
  63. package/dist/init/templates/default/work_order.md +15 -1
  64. package/dist/pack/export_md.js +3 -0
  65. package/dist/pack/export_xml.js +3 -0
  66. package/dist/pack/order.js +1 -0
  67. package/dist/pack/pack.js +3 -13
  68. package/dist/util/argparse.js +30 -0
  69. package/dist/util/atomic.js +44 -0
  70. package/dist/util/lock.js +72 -0
  71. package/dist/util/refs.js +40 -0
  72. package/dist/util/zip.js +153 -0
  73. package/package.json +14 -5
@@ -0,0 +1,293 @@
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.SQLITE_SCHEMA_VERSION = void 0;
7
+ exports.isSqliteBackend = isSqliteBackend;
8
+ exports.resolveSqlitePath = resolveSqlitePath;
9
+ exports.writeSqliteIndex = writeSqliteIndex;
10
+ exports.reserveSqliteNumericId = reserveSqliteNumericId;
11
+ exports.sqliteHealth = sqliteHealth;
12
+ const crypto_1 = __importDefault(require("crypto"));
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const staleness_1 = require("./staleness");
16
+ exports.SQLITE_SCHEMA_VERSION = 1;
17
+ function loadDatabaseCtor() {
18
+ try {
19
+ const loaded = require("node:sqlite");
20
+ if (!loaded.DatabaseSync) {
21
+ throw new Error("node:sqlite DatabaseSync is unavailable");
22
+ }
23
+ return loaded.DatabaseSync;
24
+ }
25
+ catch (err) {
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ throw new Error(`node:sqlite is required for mdkg SQLite index support: ${message}`);
28
+ }
29
+ }
30
+ function toPosixPath(value) {
31
+ return value.split(path_1.default.sep).join("/");
32
+ }
33
+ function sha256File(filePath) {
34
+ return `sha256:${crypto_1.default.createHash("sha256").update(fs_1.default.readFileSync(filePath)).digest("hex")}`;
35
+ }
36
+ function stripVolatileCacheFields(value) {
37
+ if (Array.isArray(value)) {
38
+ return value.map((item) => stripVolatileCacheFields(item));
39
+ }
40
+ if (value && typeof value === "object") {
41
+ const input = value;
42
+ const output = {};
43
+ for (const key of Object.keys(input).sort()) {
44
+ if (key === "generated_at" || key === "indexed_at") {
45
+ continue;
46
+ }
47
+ output[key] = stripVolatileCacheFields(input[key]);
48
+ }
49
+ return output;
50
+ }
51
+ return value;
52
+ }
53
+ function stableCacheJson(value) {
54
+ return JSON.stringify(stripVolatileCacheFields(value));
55
+ }
56
+ function isSqliteBackend(config) {
57
+ return config.index.backend === "sqlite";
58
+ }
59
+ function resolveSqlitePath(root, config) {
60
+ return path_1.default.resolve(root, config.index.sqlite_path);
61
+ }
62
+ function sqliteTempPath(sqlitePath) {
63
+ return path_1.default.join(path_1.default.dirname(sqlitePath), `.${path_1.default.basename(sqlitePath)}.${process.pid}.${Date.now()}.tmp`);
64
+ }
65
+ function createSchema(db) {
66
+ db.exec(`
67
+ PRAGMA journal_mode = DELETE;
68
+ PRAGMA synchronous = FULL;
69
+ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
70
+ CREATE TABLE nodes (
71
+ qid TEXT PRIMARY KEY,
72
+ id TEXT NOT NULL,
73
+ ws TEXT NOT NULL,
74
+ type TEXT NOT NULL,
75
+ title TEXT NOT NULL,
76
+ path TEXT NOT NULL,
77
+ status TEXT,
78
+ priority INTEGER,
79
+ updated TEXT,
80
+ source_hash TEXT,
81
+ json TEXT NOT NULL
82
+ );
83
+ CREATE TABLE edges (
84
+ source_qid TEXT NOT NULL,
85
+ kind TEXT NOT NULL,
86
+ target_qid TEXT NOT NULL,
87
+ PRIMARY KEY (source_qid, kind, target_qid)
88
+ );
89
+ CREATE TABLE skills (
90
+ qid TEXT PRIMARY KEY,
91
+ slug TEXT NOT NULL,
92
+ ws TEXT NOT NULL,
93
+ name TEXT NOT NULL,
94
+ path TEXT NOT NULL,
95
+ json TEXT NOT NULL
96
+ );
97
+ CREATE TABLE capabilities (
98
+ qid TEXT NOT NULL,
99
+ kind TEXT NOT NULL,
100
+ workspace TEXT NOT NULL,
101
+ visibility TEXT NOT NULL,
102
+ id TEXT NOT NULL,
103
+ path TEXT NOT NULL,
104
+ source_hash TEXT NOT NULL,
105
+ json TEXT NOT NULL,
106
+ PRIMARY KEY (qid, kind, path)
107
+ );
108
+ CREATE TABLE archives (
109
+ qid TEXT PRIMARY KEY,
110
+ visibility TEXT NOT NULL,
111
+ compressed_path TEXT,
112
+ compressed_sha256 TEXT,
113
+ json TEXT NOT NULL
114
+ );
115
+ CREATE TABLE bundle_imports (
116
+ alias TEXT PRIMARY KEY,
117
+ enabled INTEGER NOT NULL,
118
+ stale INTEGER NOT NULL,
119
+ error_count INTEGER NOT NULL,
120
+ warning_count INTEGER NOT NULL,
121
+ json TEXT NOT NULL
122
+ );
123
+ CREATE TABLE id_allocations (
124
+ ws TEXT NOT NULL,
125
+ prefix TEXT NOT NULL,
126
+ next_value INTEGER NOT NULL,
127
+ PRIMARY KEY (ws, prefix)
128
+ );
129
+ `);
130
+ }
131
+ function insertMeta(db, key, value) {
132
+ db.prepare("INSERT INTO meta (key, value) VALUES (?, ?)").run(key, value);
133
+ }
134
+ function nodeSourceHash(root, nodePath) {
135
+ const fullPath = path_1.default.resolve(root, nodePath);
136
+ if (!fs_1.default.existsSync(fullPath) || !fs_1.default.statSync(fullPath).isFile()) {
137
+ return undefined;
138
+ }
139
+ return sha256File(fullPath);
140
+ }
141
+ function buildSourceFingerprint(options) {
142
+ const payload = {
143
+ nodes: Object.values(options.nodeIndex.nodes)
144
+ .sort((a, b) => a.qid.localeCompare(b.qid))
145
+ .map((node) => ({
146
+ qid: node.qid,
147
+ path: node.path,
148
+ hash: options.nodeHashes.get(node.qid) ?? "",
149
+ })),
150
+ skills: Object.values(options.skillsIndex.skills).sort((a, b) => a.qid.localeCompare(b.qid)),
151
+ capabilities: options.capabilitiesIndex.records,
152
+ imports: options.importsIndex.imports,
153
+ };
154
+ return `sha256:${crypto_1.default.createHash("sha256").update(stableCacheJson(payload)).digest("hex")}`;
155
+ }
156
+ function writeSqliteIndex(options) {
157
+ const sqlitePath = resolveSqlitePath(options.root, options.config);
158
+ fs_1.default.mkdirSync(path_1.default.dirname(sqlitePath), { recursive: true });
159
+ const tempPath = sqliteTempPath(sqlitePath);
160
+ fs_1.default.rmSync(tempPath, { force: true });
161
+ const DatabaseSync = loadDatabaseCtor();
162
+ const db = new DatabaseSync(tempPath);
163
+ try {
164
+ const nodeHashes = new Map();
165
+ for (const node of Object.values(options.nodeIndex.nodes)) {
166
+ nodeHashes.set(node.qid, nodeSourceHash(options.root, node.path));
167
+ }
168
+ createSchema(db);
169
+ insertMeta(db, "tool", "mdkg");
170
+ insertMeta(db, "schema_version", String(exports.SQLITE_SCHEMA_VERSION));
171
+ insertMeta(db, "package_schema_version", String(options.config.schema_version));
172
+ insertMeta(db, "backend", options.config.index.backend);
173
+ insertMeta(db, "source_fingerprint", buildSourceFingerprint({ ...options, nodeHashes }));
174
+ insertMeta(db, "root", ".");
175
+ const insertNode = db.prepare("INSERT INTO nodes (qid, id, ws, type, title, path, status, priority, updated, source_hash, json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
176
+ const insertEdge = db.prepare("INSERT OR IGNORE INTO edges (source_qid, kind, target_qid) VALUES (?, ?, ?)");
177
+ for (const node of Object.values(options.nodeIndex.nodes).sort((a, b) => a.qid.localeCompare(b.qid))) {
178
+ insertNode.run(node.qid, node.id, node.ws, node.type, node.title, toPosixPath(node.path), node.status ?? null, node.priority ?? null, node.updated ?? null, nodeHashes.get(node.qid) ?? null, stableCacheJson(node));
179
+ for (const [kind, values] of [
180
+ ["relates", node.edges.relates],
181
+ ["blocked_by", node.edges.blocked_by],
182
+ ["blocks", node.edges.blocks],
183
+ ]) {
184
+ for (const target of values) {
185
+ insertEdge.run(node.qid, kind, target);
186
+ }
187
+ }
188
+ for (const [kind, target] of [
189
+ ["epic", node.edges.epic],
190
+ ["parent", node.edges.parent],
191
+ ["prev", node.edges.prev],
192
+ ["next", node.edges.next],
193
+ ]) {
194
+ if (target) {
195
+ insertEdge.run(node.qid, kind, target);
196
+ }
197
+ }
198
+ }
199
+ const insertSkill = db.prepare("INSERT INTO skills (qid, slug, ws, name, path, json) VALUES (?, ?, ?, ?, ?, ?)");
200
+ for (const skill of Object.values(options.skillsIndex.skills).sort((a, b) => a.qid.localeCompare(b.qid))) {
201
+ insertSkill.run(skill.qid, skill.slug, skill.ws, skill.name, skill.path, stableCacheJson(skill));
202
+ }
203
+ const insertCapability = db.prepare("INSERT INTO capabilities (qid, kind, workspace, visibility, id, path, source_hash, json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
204
+ for (const record of options.capabilitiesIndex.records) {
205
+ insertCapability.run(record.qid, record.kind, record.workspace, record.visibility, record.id, record.path, record.source_hash, stableCacheJson(record));
206
+ }
207
+ const insertArchive = db.prepare("INSERT INTO archives (qid, visibility, compressed_path, compressed_sha256, json) VALUES (?, ?, ?, ?, ?)");
208
+ for (const node of Object.values(options.nodeIndex.nodes).filter((item) => item.type === "archive")) {
209
+ insertArchive.run(node.qid, String(node.attributes.visibility ?? "private"), String(node.attributes.compressed_path ?? ""), String(node.attributes.compressed_sha256 ?? ""), stableCacheJson(node));
210
+ }
211
+ const insertImport = db.prepare("INSERT INTO bundle_imports (alias, enabled, stale, error_count, warning_count, json) VALUES (?, ?, ?, ?, ?, ?)");
212
+ for (const item of options.importsIndex.imports) {
213
+ insertImport.run(item.alias, item.enabled ? 1 : 0, item.stale ? 1 : 0, item.error_count, item.warning_count, stableCacheJson(item));
214
+ }
215
+ }
216
+ finally {
217
+ db.close();
218
+ }
219
+ fs_1.default.renameSync(tempPath, sqlitePath);
220
+ return sqlitePath;
221
+ }
222
+ function reserveSqliteNumericId(options) {
223
+ if (!isSqliteBackend(options.config)) {
224
+ return undefined;
225
+ }
226
+ const sqlitePath = resolveSqlitePath(options.root, options.config);
227
+ fs_1.default.mkdirSync(path_1.default.dirname(sqlitePath), { recursive: true });
228
+ const DatabaseSync = loadDatabaseCtor();
229
+ const db = new DatabaseSync(sqlitePath);
230
+ try {
231
+ db.exec("CREATE TABLE IF NOT EXISTS id_allocations (ws TEXT NOT NULL, prefix TEXT NOT NULL, next_value INTEGER NOT NULL, PRIMARY KEY (ws, prefix));");
232
+ db.exec("BEGIN IMMEDIATE");
233
+ const row = db
234
+ .prepare("SELECT next_value FROM id_allocations WHERE ws = ? AND prefix = ?")
235
+ .get(options.ws, options.prefix);
236
+ const existing = typeof row?.next_value === "number" ? row.next_value : undefined;
237
+ const nextValue = Math.max(existing ?? 1, options.currentMax + 1);
238
+ db.prepare("INSERT INTO id_allocations (ws, prefix, next_value) VALUES (?, ?, ?) ON CONFLICT(ws, prefix) DO UPDATE SET next_value = excluded.next_value").run(options.ws, options.prefix, nextValue + 1);
239
+ db.exec("COMMIT");
240
+ return `${options.prefix}-${nextValue}`;
241
+ }
242
+ catch (err) {
243
+ try {
244
+ db.exec("ROLLBACK");
245
+ }
246
+ catch {
247
+ // ignore rollback failures when no transaction is active
248
+ }
249
+ throw err;
250
+ }
251
+ finally {
252
+ db.close();
253
+ }
254
+ }
255
+ function sqliteHealth(root, config) {
256
+ const sqlitePath = resolveSqlitePath(root, config);
257
+ const warnings = [];
258
+ const errors = [];
259
+ if (!fs_1.default.existsSync(sqlitePath)) {
260
+ warnings.push(`SQLite cache missing at ${toPosixPath(path_1.default.relative(root, sqlitePath))}; run mdkg index`);
261
+ return { path: sqlitePath, exists: false, size: 0, warnings, errors };
262
+ }
263
+ const size = fs_1.default.statSync(sqlitePath).size;
264
+ if (config.index.sqlite_commit_warning_bytes > 0 && size > config.index.sqlite_commit_warning_bytes) {
265
+ warnings.push(`SQLite cache exceeds ${config.index.sqlite_commit_warning_bytes} bytes: ${toPosixPath(path_1.default.relative(root, sqlitePath))} (${size} bytes)`);
266
+ }
267
+ for (const suffix of ["-wal", "-shm", "-journal"]) {
268
+ const transient = `${sqlitePath}${suffix}`;
269
+ if (fs_1.default.existsSync(transient)) {
270
+ warnings.push(`transient SQLite file present: ${toPosixPath(path_1.default.relative(root, transient))}`);
271
+ }
272
+ }
273
+ try {
274
+ const DatabaseSync = loadDatabaseCtor();
275
+ const db = new DatabaseSync(sqlitePath);
276
+ try {
277
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
278
+ if (String(row?.value ?? "") !== String(exports.SQLITE_SCHEMA_VERSION)) {
279
+ errors.push(`SQLite schema mismatch; run mdkg index`);
280
+ }
281
+ }
282
+ finally {
283
+ db.close();
284
+ }
285
+ }
286
+ catch (err) {
287
+ errors.push(`failed to read SQLite cache: ${err instanceof Error ? err.message : String(err)}`);
288
+ }
289
+ if ((0, staleness_1.isIndexStale)(root, config)) {
290
+ warnings.push("SQLite cache may be stale because source graph files or config changed; run mdkg index");
291
+ }
292
+ return { path: sqlitePath, exists: true, size, warnings, errors };
293
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.collectGraphErrors = collectGraphErrors;
4
4
  exports.validateGraph = validateGraph;
5
+ const refs_1 = require("../util/refs");
5
6
  function pushError(errors, message) {
6
7
  if (errors) {
7
8
  errors.push(message);
@@ -9,7 +10,7 @@ function pushError(errors, message) {
9
10
  }
10
11
  throw new Error(message);
11
12
  }
12
- function validateEdgeTargets(index, allowMissing, knownSkillSlugs, errors) {
13
+ function validateEdgeTargets(index, allowMissing, knownSkillSlugs, externalWorkspaces, errors) {
13
14
  const nodes = index.nodes;
14
15
  for (const [qid, node] of Object.entries(nodes)) {
15
16
  const edges = node.edges;
@@ -33,6 +34,10 @@ function validateEdgeTargets(index, allowMissing, knownSkillSlugs, errors) {
33
34
  for (const [edgeKey, values] of edgeLists) {
34
35
  for (const value of values) {
35
36
  if (!nodes[value]) {
37
+ const [workspace] = value.split(":");
38
+ if (workspace && externalWorkspaces?.has(workspace)) {
39
+ continue;
40
+ }
36
41
  if (edgeKey === "relates" &&
37
42
  node.type === "proposal" &&
38
43
  node.attributes.proposal_kind === "skill_update" &&
@@ -320,7 +325,11 @@ function buildNodeIdsByWorkspace(index) {
320
325
  }
321
326
  return nodeIdsByWorkspace;
322
327
  }
323
- function validateAgentWorkflowNodeIdRef(qid, ws, field, value, nodeIdsByWorkspace, allowSkillRef, knownSkillSlugs, allowMissing, errors) {
328
+ function validateAgentWorkflowNodeIdRef(qid, ws, field, value, nodeIdsByWorkspace, allowSkillRef, knownSkillSlugs, externalWorkspaces, allowMissing, errors) {
329
+ const [workspace] = value.split(":");
330
+ if (workspace && value.includes(":") && externalWorkspaces?.has(workspace)) {
331
+ return;
332
+ }
324
333
  if (nodeIdsByWorkspace[ws]?.has(value)) {
325
334
  return;
326
335
  }
@@ -333,7 +342,7 @@ function validateAgentWorkflowNodeIdRef(qid, ws, field, value, nodeIdsByWorkspac
333
342
  }
334
343
  pushError(errors, `${qid}: ${field} references missing node ${value}`);
335
344
  }
336
- function validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSkillSlugs, errors) {
345
+ function validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSkillSlugs, externalWorkspaces, errors) {
337
346
  const nodeIdsByWorkspace = buildNodeIdsByWorkspace(index);
338
347
  for (const [qid, node] of Object.entries(index.nodes)) {
339
348
  if (node.type !== "feedback" && node.type !== "proposal") {
@@ -342,7 +351,7 @@ function validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSki
342
351
  const targetId = node.attributes.target_id;
343
352
  if (typeof targetId === "string") {
344
353
  const allowSkillTarget = node.type === "proposal" && node.attributes.proposal_kind === "skill_update";
345
- validateAgentWorkflowNodeIdRef(qid, node.ws, "target_id", targetId, nodeIdsByWorkspace, allowSkillTarget, knownSkillSlugs, allowMissing, errors);
354
+ validateAgentWorkflowNodeIdRef(qid, node.ws, "target_id", targetId, nodeIdsByWorkspace, allowSkillTarget, knownSkillSlugs, externalWorkspaces, allowMissing, errors);
346
355
  }
347
356
  if (node.type !== "proposal") {
348
357
  continue;
@@ -355,7 +364,72 @@ function validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSki
355
364
  if (typeof value !== "string") {
356
365
  continue;
357
366
  }
358
- validateAgentWorkflowNodeIdRef(qid, node.ws, `evidence_refs[${indexValue}]`, value, nodeIdsByWorkspace, true, knownSkillSlugs, allowMissing, errors);
367
+ validateAgentWorkflowNodeIdRef(qid, node.ws, `evidence_refs[${indexValue}]`, value, nodeIdsByWorkspace, true, knownSkillSlugs, externalWorkspaces, allowMissing, errors);
368
+ }
369
+ }
370
+ }
371
+ function buildArchiveIdsByWorkspace(index) {
372
+ const archiveIdsByWorkspace = {};
373
+ for (const node of Object.values(index.nodes)) {
374
+ if (node.type !== "archive") {
375
+ continue;
376
+ }
377
+ if (!archiveIdsByWorkspace[node.ws]) {
378
+ archiveIdsByWorkspace[node.ws] = new Set();
379
+ }
380
+ archiveIdsByWorkspace[node.ws].add(node.id);
381
+ }
382
+ return archiveIdsByWorkspace;
383
+ }
384
+ function validateArchiveUriValue(qid, ws, field, value, archiveIdsByWorkspace, allowMissing, errors) {
385
+ if (!value.startsWith("archive://")) {
386
+ return;
387
+ }
388
+ const archiveId = (0, refs_1.archiveIdFromUri)(value);
389
+ if (!archiveId) {
390
+ pushError(errors, `${qid}: ${field} has malformed archive ref ${value}`);
391
+ return;
392
+ }
393
+ if (archiveIdsByWorkspace[ws]?.has(archiveId)) {
394
+ return;
395
+ }
396
+ if (allowMissing) {
397
+ return;
398
+ }
399
+ pushError(errors, `${qid}: ${field} references missing archive ${value}`);
400
+ }
401
+ function validateArchiveUriRefs(index, allowMissing, errors) {
402
+ const archiveIdsByWorkspace = buildArchiveIdsByWorkspace(index);
403
+ const attributeListFields = [
404
+ "input_refs",
405
+ "constraint_refs",
406
+ "proof_refs",
407
+ "attestation_refs",
408
+ ];
409
+ const attributeScalarFields = ["request_ref", "cost_ref"];
410
+ for (const [qid, node] of Object.entries(index.nodes)) {
411
+ for (const [indexValue, value] of node.artifacts.entries()) {
412
+ validateArchiveUriValue(qid, node.ws, `artifacts[${indexValue}]`, value, archiveIdsByWorkspace, allowMissing, errors);
413
+ }
414
+ const attributes = node.attributes ?? {};
415
+ for (const field of attributeListFields) {
416
+ const values = attributes[field];
417
+ if (!Array.isArray(values)) {
418
+ continue;
419
+ }
420
+ for (const [indexValue, value] of values.entries()) {
421
+ if (typeof value !== "string") {
422
+ continue;
423
+ }
424
+ validateArchiveUriValue(qid, node.ws, `${field}[${indexValue}]`, value, archiveIdsByWorkspace, allowMissing, errors);
425
+ }
426
+ }
427
+ for (const field of attributeScalarFields) {
428
+ const value = attributes[field];
429
+ if (typeof value !== "string") {
430
+ continue;
431
+ }
432
+ validateArchiveUriValue(qid, node.ws, field, value, archiveIdsByWorkspace, allowMissing, errors);
359
433
  }
360
434
  }
361
435
  }
@@ -396,14 +470,16 @@ function collectGraphErrors(index, options = {}) {
396
470
  const errors = [];
397
471
  const allowMissing = options.allowMissing ?? false;
398
472
  const knownSkillSlugs = options.knownSkillSlugs;
399
- validateEdgeTargets(index, allowMissing, knownSkillSlugs, errors);
473
+ const externalWorkspaces = options.externalWorkspaces;
474
+ validateEdgeTargets(index, allowMissing, knownSkillSlugs, externalWorkspaces, errors);
400
475
  validatePrevNextSymmetry(index, allowMissing, errors);
401
476
  validateAgentWorkflowSpecWorkContracts(index, allowMissing, errors);
402
477
  validateAgentWorkflowWorkOrderWorkRefs(index, allowMissing, errors);
403
478
  validateAgentWorkflowReceiptWorkOrderRefs(index, allowMissing, errors);
404
479
  validateAgentWorkflowSubagentRefs(index, allowMissing, errors);
405
480
  validateAgentWorkflowDisputeRefs(index, allowMissing, errors);
406
- validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSkillSlugs, errors);
481
+ validateAgentWorkflowFeedbackProposalRefs(index, allowMissing, knownSkillSlugs, externalWorkspaces, errors);
482
+ validateArchiveUriRefs(index, allowMissing, errors);
407
483
  detectPrevNextCycles(index, errors);
408
484
  return errors;
409
485
  }
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VISIBILITY_VALUES = void 0;
4
+ exports.isVisibility = isVisibility;
5
+ exports.normalizeVisibility = normalizeVisibility;
6
+ exports.isVisibleAt = isVisibleAt;
7
+ exports.isLessVisibleThan = isLessVisibleThan;
8
+ exports.effectiveNodeVisibility = effectiveNodeVisibility;
9
+ exports.collectNodeVisibilityReferences = collectNodeVisibilityReferences;
10
+ exports.collectVisibilityViolations = collectVisibilityViolations;
11
+ exports.visibilityViolationMessages = visibilityViolationMessages;
12
+ const errors_1 = require("../util/errors");
13
+ const refs_1 = require("../util/refs");
14
+ exports.VISIBILITY_VALUES = ["public", "internal", "private"];
15
+ const VISIBILITY_RANK = {
16
+ public: 0,
17
+ internal: 1,
18
+ private: 2,
19
+ };
20
+ function isVisibility(value) {
21
+ return typeof value === "string" && exports.VISIBILITY_VALUES.includes(value);
22
+ }
23
+ function normalizeVisibility(value, label = "--visibility", fallback = "private") {
24
+ const normalized = (value ?? fallback).toLowerCase();
25
+ if (isVisibility(normalized)) {
26
+ return normalized;
27
+ }
28
+ throw new errors_1.UsageError(`${label} must be public, internal, or private`);
29
+ }
30
+ function isVisibleAt(recordVisibility, scope) {
31
+ return VISIBILITY_RANK[recordVisibility] <= VISIBILITY_RANK[scope];
32
+ }
33
+ function isLessVisibleThan(targetVisibility, sourceVisibility) {
34
+ return VISIBILITY_RANK[targetVisibility] > VISIBILITY_RANK[sourceVisibility];
35
+ }
36
+ function effectiveNodeVisibility(node, config) {
37
+ if (node.source?.imported && isVisibility(node.source.visibility)) {
38
+ return node.source.visibility;
39
+ }
40
+ const archiveVisibility = node.attributes.visibility;
41
+ if (node.type === "archive" && isVisibility(archiveVisibility)) {
42
+ return archiveVisibility;
43
+ }
44
+ const workspaceVisibility = config.workspaces[node.ws]?.visibility;
45
+ if (isVisibility(workspaceVisibility)) {
46
+ return workspaceVisibility;
47
+ }
48
+ const importVisibility = config.bundle_imports[node.ws]?.visibility;
49
+ if (isVisibility(importVisibility)) {
50
+ return importVisibility;
51
+ }
52
+ return "private";
53
+ }
54
+ function collectStringValues(value, prefix, out) {
55
+ if (typeof value === "string") {
56
+ out.push({ field: prefix, value });
57
+ return;
58
+ }
59
+ if (Array.isArray(value)) {
60
+ value.forEach((item, index) => collectStringValues(item, `${prefix}[${index}]`, out));
61
+ return;
62
+ }
63
+ if (typeof value === "object" && value !== null) {
64
+ for (const [key, child] of Object.entries(value)) {
65
+ collectStringValues(child, prefix ? `${prefix}.${key}` : key, out);
66
+ }
67
+ }
68
+ }
69
+ function collectNodeStringReferences(node) {
70
+ const values = [];
71
+ const edgeFields = [
72
+ ["epic", node.edges.epic],
73
+ ["parent", node.edges.parent],
74
+ ["prev", node.edges.prev],
75
+ ["next", node.edges.next],
76
+ ];
77
+ for (const [field, value] of edgeFields) {
78
+ if (value) {
79
+ values.push({ field, value });
80
+ }
81
+ }
82
+ for (const [index, value] of node.edges.relates.entries()) {
83
+ values.push({ field: `relates[${index}]`, value });
84
+ }
85
+ for (const [index, value] of node.edges.blocked_by.entries()) {
86
+ values.push({ field: `blocked_by[${index}]`, value });
87
+ }
88
+ for (const [index, value] of node.edges.blocks.entries()) {
89
+ values.push({ field: `blocks[${index}]`, value });
90
+ }
91
+ for (const [index, value] of node.links.entries()) {
92
+ values.push({ field: `links[${index}]`, value });
93
+ }
94
+ for (const [index, value] of node.artifacts.entries()) {
95
+ values.push({ field: `artifacts[${index}]`, value });
96
+ }
97
+ for (const [index, value] of node.refs.entries()) {
98
+ values.push({ field: `refs[${index}]`, value });
99
+ }
100
+ collectStringValues(node.attributes, "attributes", values);
101
+ return values;
102
+ }
103
+ function archiveNodeById(index, workspace, archiveId) {
104
+ const sameWorkspace = index.nodes[`${workspace}:${archiveId}`];
105
+ if (sameWorkspace?.type === "archive") {
106
+ return sameWorkspace;
107
+ }
108
+ const matches = Object.values(index.nodes).filter((candidate) => candidate.type === "archive" && candidate.id === archiveId);
109
+ return matches.length === 1 ? matches[0] : undefined;
110
+ }
111
+ function resolveReferenceTarget(index, source, value) {
112
+ const archiveId = (0, refs_1.archiveIdFromUri)(value);
113
+ if (archiveId) {
114
+ return archiveNodeById(index, source.ws, archiveId);
115
+ }
116
+ if ((0, refs_1.isUriRef)(value)) {
117
+ return undefined;
118
+ }
119
+ const exact = index.nodes[value];
120
+ if (exact) {
121
+ return exact;
122
+ }
123
+ if (!value.includes(":")) {
124
+ return index.nodes[`${source.ws}:${value}`];
125
+ }
126
+ return undefined;
127
+ }
128
+ function collectNodeVisibilityReferences(index, config, node) {
129
+ const references = [];
130
+ const seen = new Set();
131
+ for (const ref of collectNodeStringReferences(node)) {
132
+ const target = resolveReferenceTarget(index, node, ref.value);
133
+ if (!target || target.qid === node.qid) {
134
+ continue;
135
+ }
136
+ const key = `${ref.field}\0${ref.value}\0${target.qid}`;
137
+ if (seen.has(key)) {
138
+ continue;
139
+ }
140
+ seen.add(key);
141
+ references.push({
142
+ field: ref.field,
143
+ value: ref.value,
144
+ targetQid: target.qid,
145
+ targetVisibility: effectiveNodeVisibility(target, config),
146
+ });
147
+ }
148
+ return references;
149
+ }
150
+ function collectVisibilityViolations(index, config, options = {}) {
151
+ const violations = [];
152
+ const qids = options.includedQids
153
+ ? Object.keys(index.nodes).filter((qid) => options.includedQids?.has(qid))
154
+ : Object.keys(index.nodes);
155
+ for (const qid of qids.sort()) {
156
+ const node = index.nodes[qid];
157
+ if (!node) {
158
+ continue;
159
+ }
160
+ const sourceVisibility = effectiveNodeVisibility(node, config);
161
+ if (options.scope && !isVisibleAt(sourceVisibility, options.scope)) {
162
+ continue;
163
+ }
164
+ for (const ref of collectNodeVisibilityReferences(index, config, node)) {
165
+ if (options.includedQids && !options.includedQids.has(ref.targetQid)) {
166
+ const target = index.nodes[ref.targetQid];
167
+ if (!target) {
168
+ continue;
169
+ }
170
+ const targetVisibility = effectiveNodeVisibility(target, config);
171
+ if (options.scope && isVisibleAt(targetVisibility, options.scope)) {
172
+ continue;
173
+ }
174
+ violations.push({
175
+ qid: node.qid,
176
+ visibility: sourceVisibility,
177
+ field: ref.field,
178
+ value: ref.value,
179
+ target_qid: ref.targetQid,
180
+ target_visibility: targetVisibility,
181
+ message: `${node.qid} references ${targetVisibility} ${ref.targetQid} through ${ref.field}`,
182
+ });
183
+ continue;
184
+ }
185
+ if (isLessVisibleThan(ref.targetVisibility, sourceVisibility)) {
186
+ violations.push({
187
+ qid: node.qid,
188
+ visibility: sourceVisibility,
189
+ field: ref.field,
190
+ value: ref.value,
191
+ target_qid: ref.targetQid,
192
+ target_visibility: ref.targetVisibility,
193
+ message: `${node.qid} (${sourceVisibility}) references ${ref.targetVisibility} ${ref.targetQid} through ${ref.field}`,
194
+ });
195
+ continue;
196
+ }
197
+ if (options.scope && !isVisibleAt(ref.targetVisibility, options.scope)) {
198
+ violations.push({
199
+ qid: node.qid,
200
+ visibility: sourceVisibility,
201
+ field: ref.field,
202
+ value: ref.value,
203
+ target_qid: ref.targetQid,
204
+ target_visibility: ref.targetVisibility,
205
+ message: `${node.qid} references ${ref.targetVisibility} ${ref.targetQid} through ${ref.field}, which is excluded by ${options.scope} visibility`,
206
+ });
207
+ }
208
+ }
209
+ }
210
+ return violations;
211
+ }
212
+ function visibilityViolationMessages(violations) {
213
+ return Array.from(new Set(violations.map((violation) => violation.message))).sort();
214
+ }