@aiwg/cli 2026.8.19 → 2026.8.20

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 (34) hide show
  1. package/THIRD_PARTY_NOTICES.md +12 -0
  2. package/dist/src/api/index.d.ts +2 -0
  3. package/dist/src/api/index.js +2 -0
  4. package/dist/src/artifacts/backend-runtime.js +26 -0
  5. package/dist/src/artifacts/backends/sqlite-backend.js +204 -28
  6. package/dist/src/artifacts/dep-graph.js +27 -5
  7. package/dist/src/artifacts/graph-backend.js +2 -2
  8. package/dist/src/artifacts/graph-query.js +21 -9
  9. package/dist/src/artifacts/index-builder.js +15 -0
  10. package/dist/src/artifacts/index-status.js +4 -1
  11. package/dist/src/artifacts/stats.js +4 -1
  12. package/dist/src/artifacts/types.js +13 -1
  13. package/dist/src/cli/handlers/help.js +2 -0
  14. package/dist/src/cli/handlers/index.js +6 -2
  15. package/dist/src/cli/handlers/mission.js +27 -0
  16. package/dist/src/cli/handlers/runtime-info.js +29 -0
  17. package/dist/src/cli/handlers/steward.js +12 -0
  18. package/dist/src/cli/handlers/uhp.js +88 -0
  19. package/dist/src/config/aiwg-config.js +10 -0
  20. package/dist/src/extensions/commands/definitions.js +38 -0
  21. package/dist/src/mission-protocol/codecs.js +265 -0
  22. package/dist/src/mission-protocol/index.js +3 -0
  23. package/dist/src/mission-protocol/types.js +2 -0
  24. package/dist/src/storage/backend-contract.js +64 -0
  25. package/dist/src/storage/index.js +2 -0
  26. package/dist/src/storage/migration-protocol.js +378 -0
  27. package/dist/src/uhp/client.js +374 -0
  28. package/dist/src/uhp/config.js +130 -0
  29. package/dist/src/uhp/errors.js +63 -0
  30. package/dist/src/uhp/index.js +7 -0
  31. package/dist/src/uhp/mission.js +111 -0
  32. package/dist/src/uhp/sse.js +76 -0
  33. package/dist/src/uhp/types.js +2 -0
  34. package/package.json +1 -1
@@ -28,6 +28,18 @@ npm ls @fortemi/core @bytecask/core
28
28
 
29
29
  Each installed dependency package includes its own `LICENSE` and `package.json`. To inspect the exact source reference for a different resolved version, run `npm view @fortemi/core@<version> repository license` or `npm view @bytecask/core@<version> repository license`.
30
30
 
31
+ ## Unified Harness Protocol schema
32
+
33
+ - Vendored file: `schemas/uhp/uhp-2026-08-11.schema.json`
34
+ - Upstream: <https://github.com/HarnessRouter/harnessrouter/blob/db78b957492766a5a5c76bc9981a9234f4546f5f/protocol/schema/uhp-2026-08-11.schema.json>
35
+ - Protocol version: `2026-08-11`
36
+ - Upstream license: Apache-2.0
37
+ - Vendored SHA-256: `c9a22b0e4752bfe407c08930dec9b4f1f6aaa862878c4e540a75274241b31c3a`
38
+
39
+ The schema is vendored unchanged to make UHP fixture and compatibility tests
40
+ offline and version-pinned. AIWG's experimental client does not imply UHP
41
+ server conformance.
42
+
31
43
  ## Distribution boundary
32
44
 
33
45
  The `aiwg` and `@aiwg/cli` npm archives do not copy Fortemi or Bytecask object code into their own tarballs. npm resolves those packages separately during installation. AIWG does, however, intentionally import Fortemi in-process at runtime, so separate archive delivery is not by itself a legal conclusion about whether execution forms a combined work.
@@ -15,5 +15,7 @@ export * from '../security/artifact-attestation.js';
15
15
  export * from '../providers/transformation-receipt.js';
16
16
  export * from '../providers/transformation-receipt-integration.js';
17
17
  export * from '../marketplace/artifact-attestation.js';
18
+ export * from '../uhp/index.js';
19
+ export * from '../mission-protocol/index.js';
18
20
  export { ARTIFACT_TRUST_ROOT_MEDIA_TYPE, ARTIFACT_TRUST_ROOT_SCHEMA_VERSION, ARTIFACT_TRUST_STATE_SCHEMA_VERSION, bootstrapTrustRoot, parseTrustRoot, parseTrustState, readTrustState, validateTrustRoot, validateTrustState, verifyRootTransition, writeTrustState, channelStateKey, canonicalJson, dssePae, publicKeyFingerprint, sha256, type ArtifactTrustRoot, type ArtifactTrustState, type RootBootstrapResult, type RootTransitionResult, type ArtifactTrustPolicySettings, type TrustedChannelState, } from '../security/artifact-trust.js';
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -15,5 +15,7 @@ export * from '../security/artifact-attestation.js';
15
15
  export * from '../providers/transformation-receipt.js';
16
16
  export * from '../providers/transformation-receipt-integration.js';
17
17
  export * from '../marketplace/artifact-attestation.js';
18
+ export * from '../uhp/index.js';
19
+ export * from '../mission-protocol/index.js';
18
20
  export { ARTIFACT_TRUST_ROOT_MEDIA_TYPE, ARTIFACT_TRUST_ROOT_SCHEMA_VERSION, ARTIFACT_TRUST_STATE_SCHEMA_VERSION, bootstrapTrustRoot, parseTrustRoot, parseTrustState, readTrustState, validateTrustRoot, validateTrustState, verifyRootTransition, writeTrustState, channelStateKey, canonicalJson, dssePae, publicKeyFingerprint, sha256, } from '../security/artifact-trust.js';
19
21
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { createGraphBackend } from './graph-backend.js';
4
+ import { loadGraphIndexFile } from './index-reader.js';
5
+ import { getGraphIndexDir, loadGlobalGraphConfigs, loadUserGraphConfigs, resolveGraphBackendType } from './types.js';
6
+ export function configuredGraphBackend(cwd, graph) {
7
+ loadUserGraphConfigs(cwd);
8
+ loadGlobalGraphConfigs();
9
+ return resolveGraphBackendType(graph);
10
+ }
11
+ export async function openGraphBackend(cwd, graph) {
12
+ const type = configuredGraphBackend(cwd, graph);
13
+ const persistentPath = type === 'sqlite' ? path.join(getGraphIndexDir(cwd, graph), 'graph.db') : undefined;
14
+ const legacy = loadGraphIndexFile(cwd, 'dependencies.json', graph);
15
+ if (!legacy && (!persistentPath || !fs.existsSync(persistentPath))) {
16
+ throw new Error(`No artifact index found for graph '${graph}'. Run \`aiwg index build --graph ${graph}\` first.`);
17
+ }
18
+ if (persistentPath)
19
+ fs.mkdirSync(path.dirname(persistentPath), { recursive: true });
20
+ const backend = await createGraphBackend(type, persistentPath);
21
+ if (legacy && (type !== 'sqlite' || backend.nodeCount() === 0))
22
+ backend.deserialize(legacy);
23
+ return { backend, type, persistentPath };
24
+ }
25
+ export async function closeGraphBackend(active) { await active?.backend.close?.(); }
26
+ //# sourceMappingURL=backend-runtime.js.map
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Optional implementation of GraphBackend using better-sqlite3.
5
5
  * Provides persistent on-disk storage, native SQL set operations
6
- * (INTERSECT/EXCEPT/UNION), recursive CTE traversal, and cross-graph
7
- * federation via ATTACH DATABASE.
6
+ * (INTERSECT/EXCEPT/UNION), recursive CTE traversal, and transactional
7
+ * row reconciliation. Cross-graph ATTACH federation is intentionally not
8
+ * exposed because its trust, lifecycle, and snapshot boundary is undefined.
8
9
  *
9
10
  * Enable with: aiwg features install sqlite
10
11
  *
@@ -14,6 +15,16 @@
14
15
  */
15
16
  import { normalizeEdges } from '../types.js';
16
17
  import { requireFeaturePackage } from '../../features/runtime.js';
18
+ const SCHEMA_VERSION = 1;
19
+ const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
20
+ export class SqliteBusyError extends Error {
21
+ code = 'AIWG_SQLITE_BUSY';
22
+ constructor(operation, cause) {
23
+ super(`sqlite backend remained busy while ${operation} after bounded waiting`);
24
+ this.name = 'SqliteBusyError';
25
+ this.cause = cause;
26
+ }
27
+ }
17
28
  /**
18
29
  * SQLite-backed graph with persistent storage and native SQL operations.
19
30
  *
@@ -23,24 +34,49 @@ import { requireFeaturePackage } from '../../features/runtime.js';
23
34
  export class SqliteGraphBackend {
24
35
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
36
  db;
37
+ busyTimeoutMs;
26
38
  /**
27
39
  * Create a new SQLite graph backend.
28
40
  *
29
41
  * @param dbPath - Path to the SQLite database file. Use ':memory:' for in-memory.
30
42
  */
31
- constructor(dbPath = ':memory:') {
43
+ constructor(dbPath = ':memory:', options = {}) {
44
+ this.busyTimeoutMs = options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;
32
45
  try {
33
46
  const Database = requireFeaturePackage('better-sqlite3');
34
- this.db = new Database(dbPath);
47
+ this.db = new Database(dbPath, { timeout: this.busyTimeoutMs });
35
48
  }
36
49
  catch {
37
50
  throw new Error('sqlite backend is unavailable; run `aiwg features install sqlite`');
38
51
  }
39
- this.db.pragma('journal_mode = WAL');
40
- this.initSchema();
52
+ this.assertSafeSqliteVersion();
53
+ const requestedJournal = dbPath === ':memory:' ? 'memory' : 'wal';
54
+ const actualJournal = String(this.db.pragma(`journal_mode = ${requestedJournal}`, { simple: true })).toLowerCase();
55
+ if (actualJournal !== requestedJournal) {
56
+ this.db.close();
57
+ throw new Error(`sqlite backend requested journal_mode=${requestedJournal} but received ${actualJournal}`);
58
+ }
59
+ this.db.pragma(`synchronous = ${options.synchronous ?? 'NORMAL'}`);
60
+ this.db.pragma('wal_autocheckpoint = 1000');
61
+ this.migrateSchema();
62
+ }
63
+ assertSafeSqliteVersion() {
64
+ const version = String(this.db.prepare('SELECT sqlite_version() AS version').get().version);
65
+ if (!isWalResetSafeVersion(version)) {
66
+ this.db.close();
67
+ throw new Error(`sqlite backend requires a WAL-reset-safe SQLite build (3.44.6, 3.50.7, or >=3.51.3); found ${version}`);
68
+ }
41
69
  }
42
- initSchema() {
43
- this.db.exec(`
70
+ migrateSchema() {
71
+ const current = Number(this.db.pragma('user_version', { simple: true }));
72
+ if (current > SCHEMA_VERSION) {
73
+ this.db.close();
74
+ throw new Error(`sqlite graph schema ${current} is newer than supported schema ${SCHEMA_VERSION}`);
75
+ }
76
+ if (current === SCHEMA_VERSION)
77
+ return;
78
+ this.db.transaction(() => {
79
+ this.db.exec(`
44
80
  CREATE TABLE IF NOT EXISTS nodes (
45
81
  id TEXT PRIMARY KEY,
46
82
  type TEXT,
@@ -61,23 +97,22 @@ export class SqliteGraphBackend {
61
97
 
62
98
  CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target, edge_type);
63
99
  CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source, edge_type);
64
- `);
100
+ CREATE INDEX IF NOT EXISTS idx_nodes_type_phase ON nodes(type, phase, id);
101
+ `);
102
+ this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
103
+ })();
65
104
  }
66
105
  // --- Mutation ---
67
106
  addNode(id, attrs) {
68
- const existing = this.db.prepare('SELECT attrs FROM nodes WHERE id = ?').get(id);
69
- if (!existing) {
70
- this.db.prepare('INSERT INTO nodes (id, attrs) VALUES (?, ?)').run(id, JSON.stringify(attrs ?? {}));
71
- }
72
- else if (attrs) {
73
- const merged = { ...JSON.parse(existing.attrs), ...attrs };
74
- this.db.prepare('UPDATE nodes SET attrs = ? WHERE id = ?').run(JSON.stringify(merged), id);
75
- }
107
+ this.withBusyContext('adding a node', () => this.upsertNode(id, attrs));
76
108
  }
77
109
  addEdge(source, target, type = 'depends-on', attrs) {
78
- this.addNode(source);
79
- this.addNode(target);
80
- this.db.prepare('INSERT OR IGNORE INTO edges (source, target, edge_type, attrs) VALUES (?, ?, ?, ?)').run(source, target, type, JSON.stringify(attrs ?? {}));
110
+ this.withBusyContext('adding an edge', () => this.db.transaction(() => {
111
+ this.upsertNode(source);
112
+ this.upsertNode(target);
113
+ this.db.prepare(`INSERT INTO edges (source, target, edge_type, attrs) VALUES (?, ?, ?, ?)
114
+ ON CONFLICT(source, target, edge_type) DO UPDATE SET attrs = excluded.attrs`).run(source, target, type, JSON.stringify(attrs ?? {}));
115
+ })());
81
116
  }
82
117
  // --- Query ---
83
118
  hasNode(id) {
@@ -98,6 +133,21 @@ export class SqliteGraphBackend {
98
133
  nodes() {
99
134
  return this.db.prepare('SELECT id FROM nodes').all().map((r) => r.id);
100
135
  }
136
+ queryNodes(filters) {
137
+ const clauses = [];
138
+ const values = [];
139
+ if (filters.type !== undefined) {
140
+ clauses.push('type = ?');
141
+ values.push(filters.type);
142
+ }
143
+ if (filters.phase !== undefined) {
144
+ clauses.push('phase = ?');
145
+ values.push(filters.phase);
146
+ }
147
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
148
+ return this.db.prepare(`SELECT id FROM nodes ${where} ORDER BY id`).all(...values)
149
+ .map((row) => row.id);
150
+ }
101
151
  // --- Traversal ---
102
152
  neighbors(nodeId, direction, edgeType) {
103
153
  const results = new Set();
@@ -127,15 +177,37 @@ export class SqliteGraphBackend {
127
177
  intersection(setA, setB) {
128
178
  if (setA.length === 0 || setB.length === 0)
129
179
  return [];
130
- const b = new Set(setB);
131
- return setA.filter(x => b.has(x));
180
+ return this.sqlSetOperation(setA, setB, 'INTERSECT');
132
181
  }
133
182
  difference(setA, setB) {
134
- const b = new Set(setB);
135
- return setA.filter(x => !b.has(x));
183
+ if (setA.length === 0)
184
+ return [];
185
+ return this.sqlSetOperation(setA, setB, 'EXCEPT');
136
186
  }
137
187
  union(setA, setB) {
138
- return [...new Set([...setA, ...setB])];
188
+ return this.sqlSetOperation(setA, setB, 'UNION');
189
+ }
190
+ /** Bounded recursive traversal with deterministic ordering and optional edge filtering. */
191
+ traverse(nodeId, direction, maxDepth, edgeType) {
192
+ if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 100) {
193
+ throw new Error('sqlite traversal maxDepth must be an integer from 1 through 100');
194
+ }
195
+ const from = direction === 'out' ? 'source' : 'target';
196
+ const to = direction === 'out' ? 'target' : 'source';
197
+ const rows = this.db.prepare(`
198
+ WITH RECURSIVE walk(id, depth, visited) AS (
199
+ SELECT ?, 0, char(31) || ? || char(31)
200
+ UNION ALL
201
+ SELECT e.${to}, walk.depth + 1, walk.visited || e.${to} || char(31)
202
+ FROM walk JOIN edges e ON e.${from} = walk.id
203
+ WHERE walk.depth < ?
204
+ AND (? IS NULL OR e.edge_type = ?)
205
+ AND instr(walk.visited, char(31) || e.${to} || char(31)) = 0
206
+ )
207
+ SELECT id, MIN(depth) AS depth FROM walk WHERE depth > 0
208
+ GROUP BY id ORDER BY depth, id
209
+ `).all(nodeId, nodeId, maxDepth, edgeType ?? null, edgeType ?? null);
210
+ return rows.map((row) => row);
139
211
  }
140
212
  // --- Persistence ---
141
213
  serialize() {
@@ -156,13 +228,18 @@ export class SqliteGraphBackend {
156
228
  return result;
157
229
  }
158
230
  deserialize(data) {
159
- // Clear existing data
160
- this.db.exec('DELETE FROM edges; DELETE FROM nodes;');
231
+ this.reconcile(data);
232
+ }
233
+ /** Transactionally applies a full desired graph and removes stale rows. */
234
+ reconcile(data) {
161
235
  const insertNode = this.db.prepare('INSERT OR IGNORE INTO nodes (id) VALUES (?)');
162
236
  const insertEdge = this.db.prepare('INSERT OR IGNORE INTO edges (source, target, edge_type) VALUES (?, ?, ?)');
163
237
  const runBatch = this.db.transaction(() => {
238
+ const desiredNodes = new Set();
239
+ const desiredEdges = new Set();
164
240
  // Add all nodes
165
241
  for (const id of Object.keys(data)) {
242
+ desiredNodes.add(id);
166
243
  insertNode.run(id);
167
244
  }
168
245
  // Add edges from upstream relationships
@@ -170,16 +247,53 @@ export class SqliteGraphBackend {
170
247
  const upEdges = normalizeEdges(node.upstream);
171
248
  for (const edge of upEdges) {
172
249
  insertNode.run(edge.path); // Ensure referenced nodes exist
250
+ desiredNodes.add(edge.path);
251
+ desiredEdges.add(`${edge.path}\0${id}\0${edge.type}`);
173
252
  insertEdge.run(edge.path, id, edge.type);
174
253
  }
175
254
  const downEdges = normalizeEdges(node.downstream);
176
255
  for (const edge of downEdges) {
177
256
  insertNode.run(edge.path);
257
+ desiredNodes.add(edge.path);
258
+ desiredEdges.add(`${id}\0${edge.path}\0${edge.type}`);
178
259
  insertEdge.run(id, edge.path, edge.type);
179
260
  }
180
261
  }
262
+ for (const edge of this.db.prepare('SELECT source, target, edge_type FROM edges').all()) {
263
+ if (!desiredEdges.has(`${edge.source}\0${edge.target}\0${edge.edge_type}`)) {
264
+ this.db.prepare('DELETE FROM edges WHERE source=? AND target=? AND edge_type=?')
265
+ .run(edge.source, edge.target, edge.edge_type);
266
+ }
267
+ }
268
+ for (const { id } of this.db.prepare('SELECT id FROM nodes').all()) {
269
+ if (!desiredNodes.has(id))
270
+ this.db.prepare('DELETE FROM nodes WHERE id=?').run(id);
271
+ }
181
272
  });
182
- runBatch();
273
+ this.withBusyContext('reconciling graph rows', runBatch);
274
+ }
275
+ schemaVersion() {
276
+ return Number(this.db.pragma('user_version', { simple: true }));
277
+ }
278
+ engineVersion() {
279
+ return String(this.db.prepare('SELECT sqlite_version() AS version').get().version);
280
+ }
281
+ journalMode() {
282
+ return String(this.db.pragma('journal_mode', { simple: true })).toLowerCase();
283
+ }
284
+ walMetrics() {
285
+ const row = this.db.pragma('wal_checkpoint(NOOP)')[0] ?? {};
286
+ return {
287
+ busy: Number(row.busy ?? 0),
288
+ logFrames: Number(row.log ?? 0),
289
+ checkpointedFrames: Number(row.checkpointed ?? 0),
290
+ };
291
+ }
292
+ checkpoint(mode = 'PASSIVE') {
293
+ this.withBusyContext('checkpointing the WAL', () => this.db.pragma(`wal_checkpoint(${mode})`));
294
+ }
295
+ async backup(destination) {
296
+ await this.db.backup(destination);
183
297
  }
184
298
  nodeCount() {
185
299
  return this.db.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
@@ -194,5 +308,67 @@ export class SqliteGraphBackend {
194
308
  close() {
195
309
  this.db.close();
196
310
  }
311
+ upsertNode(id, attrs) {
312
+ const existing = this.db.prepare('SELECT attrs FROM nodes WHERE id = ?').get(id);
313
+ const merged = { ...(existing ? JSON.parse(existing.attrs) : {}), ...(attrs ?? {}) };
314
+ this.db.prepare(`
315
+ INSERT INTO nodes (id, type, phase, title, summary, checksum, attrs)
316
+ VALUES (?, ?, ?, ?, ?, ?, ?)
317
+ ON CONFLICT(id) DO UPDATE SET
318
+ type=excluded.type, phase=excluded.phase, title=excluded.title,
319
+ summary=excluded.summary, checksum=excluded.checksum, attrs=excluded.attrs
320
+ `).run(id, stringAttr(merged, 'type'), stringAttr(merged, 'phase'), stringAttr(merged, 'title'), stringAttr(merged, 'summary'), stringAttr(merged, 'checksum'), JSON.stringify(merged));
321
+ }
322
+ sqlSetOperation(setA, setB, operation) {
323
+ const rows = this.db.prepare(`
324
+ SELECT value AS id FROM json_each(?)
325
+ ${operation}
326
+ SELECT value AS id FROM json_each(?)
327
+ ORDER BY id
328
+ `).all(JSON.stringify(setA), JSON.stringify(setB));
329
+ return rows.map((row) => row.id);
330
+ }
331
+ withBusyContext(operation, fn) {
332
+ const deadline = Date.now() + this.busyTimeoutMs;
333
+ let backoffMs = 2;
334
+ for (;;) {
335
+ try {
336
+ return fn();
337
+ }
338
+ catch (error) {
339
+ const code = error.code;
340
+ if (!code?.startsWith('SQLITE_BUSY') && !code?.startsWith('SQLITE_LOCKED'))
341
+ throw error;
342
+ const remaining = deadline - Date.now();
343
+ if (remaining <= 0)
344
+ throw new SqliteBusyError(operation, error);
345
+ const delay = Math.min(backoffMs, remaining, 100);
346
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
347
+ backoffMs = Math.min(backoffMs * 2, 100);
348
+ }
349
+ }
350
+ }
351
+ }
352
+ function stringAttr(attrs, key) {
353
+ return typeof attrs[key] === 'string' ? attrs[key] : null;
354
+ }
355
+ export function isWalResetSafeVersion(version) {
356
+ const parts = version.split('.').map(Number);
357
+ if (parts.length !== 3 || parts.some(part => !Number.isInteger(part) || part < 0))
358
+ return false;
359
+ const [major, minor, patch] = parts;
360
+ if (major !== 3)
361
+ return major > 3;
362
+ if (minor === 44)
363
+ return patch >= 6;
364
+ if (minor === 50)
365
+ return patch >= 7;
366
+ if (minor < 51)
367
+ return false;
368
+ if (minor === 51)
369
+ return patch >= 3;
370
+ if (minor === 52)
371
+ return false; // withdrawn upstream
372
+ return minor >= 53;
197
373
  }
198
374
  //# sourceMappingURL=sqlite-backend.js.map
@@ -9,8 +9,9 @@
9
9
  * @tests @test/unit/artifacts/dep-graph.test.ts
10
10
  */
11
11
  import { normalizeEdges } from './types.js';
12
- import { loadDependencyGraph, loadGraphIndexFile } from './index-reader.js';
12
+ import { loadDependencyGraph } from './index-reader.js';
13
13
  import { buildFortemiCoreDependencyGraph } from './fortemi-core-query-adapter.js';
14
+ import { closeGraphBackend, openGraphBackend } from './backend-runtime.js';
14
15
  /**
15
16
  * Traverse the dependency graph in one direction
16
17
  *
@@ -70,6 +71,7 @@ function flattenResults(results) {
70
71
  export async function showDeps(cwd, artifactPath, options = {}) {
71
72
  const { direction = 'both', depth = 3, json = false, graph: graphType, edgeType, backend = 'fortemi-core' } = options;
72
73
  let depGraph = null;
74
+ let graphBackend;
73
75
  if (backend === 'fortemi-core') {
74
76
  const loaded = buildFortemiCoreDependencyGraph(cwd, graphType ?? 'project');
75
77
  if (!loaded.graph) {
@@ -86,7 +88,14 @@ export async function showDeps(cwd, artifactPath, options = {}) {
86
88
  depGraph = loaded.graph;
87
89
  }
88
90
  else if (graphType) {
89
- depGraph = loadGraphIndexFile(cwd, 'dependencies.json', graphType);
91
+ const active = await openGraphBackend(cwd, graphType);
92
+ graphBackend = active.type;
93
+ try {
94
+ depGraph = active.backend.serialize();
95
+ }
96
+ finally {
97
+ await closeGraphBackend(active);
98
+ }
90
99
  if (!depGraph) {
91
100
  console.error(`Error: No artifact index found for graph '${graphType}'.`);
92
101
  console.log("Run 'aiwg index build' first to create the index.");
@@ -98,9 +107,21 @@ export async function showDeps(cwd, artifactPath, options = {}) {
98
107
  const graphTypes = ['project', 'codebase'];
99
108
  const merged = {};
100
109
  for (const g of graphTypes) {
101
- const partial = loadGraphIndexFile(cwd, 'dependencies.json', g);
102
- if (partial)
103
- Object.assign(merged, partial);
110
+ try {
111
+ const active = await openGraphBackend(cwd, g);
112
+ try {
113
+ Object.assign(merged, active.backend.serialize());
114
+ }
115
+ finally {
116
+ await closeGraphBackend(active);
117
+ }
118
+ }
119
+ catch (error) {
120
+ // Missing optional graph data is ignorable; unavailable configured
121
+ // backends are actionable and must not silently fall back to JSON.
122
+ if (error.message.includes('backend is unavailable'))
123
+ throw error;
124
+ }
104
125
  }
105
126
  if (Object.keys(merged).length > 0) {
106
127
  depGraph = merged;
@@ -132,6 +153,7 @@ export async function showDeps(cwd, artifactPath, options = {}) {
132
153
  console.log(JSON.stringify({
133
154
  artifact: artifactPath,
134
155
  backend,
156
+ ...(graphBackend ? { graphBackend } : {}),
135
157
  direction,
136
158
  depth,
137
159
  upstream: flattenResults(upstreamResults),
@@ -20,7 +20,7 @@
20
20
  * @returns A new GraphBackend instance
21
21
  * @throws Error if the requested backend's dependencies are not installed
22
22
  */
23
- export async function createGraphBackend(type = 'json') {
23
+ export async function createGraphBackend(type = 'json', persistentPath) {
24
24
  switch (type) {
25
25
  case 'json': {
26
26
  const { JsonGraphBackend } = await import('./backends/json-backend.js');
@@ -38,7 +38,7 @@ export async function createGraphBackend(type = 'json') {
38
38
  case 'sqlite': {
39
39
  try {
40
40
  const { SqliteGraphBackend } = await import('./backends/sqlite-backend.js');
41
- return new SqliteGraphBackend();
41
+ return new SqliteGraphBackend(persistentPath);
42
42
  }
43
43
  catch {
44
44
  throw new Error('sqlite backend is unavailable; run `aiwg features install sqlite`');
@@ -9,8 +9,8 @@
9
9
  * @tests @test/unit/artifacts/graph-query.test.ts
10
10
  */
11
11
  import { normalizeEdges } from './types.js';
12
- import { loadGraphIndexFile } from './index-reader.js';
13
12
  import { buildFortemiCoreDependencyGraph } from './fortemi-core-query-adapter.js';
13
+ import { closeGraphBackend, openGraphBackend } from './backend-runtime.js';
14
14
  /**
15
15
  * Get neighbors of a node in a dependency graph.
16
16
  *
@@ -84,18 +84,13 @@ export function setDifference(a, b) {
84
84
  const setB = new Set(b);
85
85
  return a.filter(x => !setB.has(x));
86
86
  }
87
- /**
88
- * Load a dependency graph for a given graph type
89
- */
90
- function loadGraph(cwd, graphType) {
91
- return loadGraphIndexFile(cwd, 'dependencies.json', graphType);
92
- }
93
87
  /**
94
88
  * Execute the `neighbors` subcommand
95
89
  */
96
90
  export async function showNeighbors(cwd, options) {
97
91
  const { graph: graphType, node, direction = 'both', edgeType, json = false, backend = 'fortemi-core' } = options;
98
92
  let graph = null;
93
+ let graphBackend;
99
94
  if (backend === 'fortemi-core') {
100
95
  const loaded = buildFortemiCoreDependencyGraph(cwd, graphType);
101
96
  if (!loaded.graph) {
@@ -112,7 +107,14 @@ export async function showNeighbors(cwd, options) {
112
107
  graph = loaded.graph;
113
108
  }
114
109
  else {
115
- graph = loadGraph(cwd, graphType);
110
+ const active = await openGraphBackend(cwd, graphType);
111
+ graphBackend = active.type;
112
+ try {
113
+ graph = active.backend.serialize();
114
+ }
115
+ finally {
116
+ await closeGraphBackend(active);
117
+ }
116
118
  }
117
119
  if (!graph) {
118
120
  console.error(`Error: No index found for graph '${graphType}'.`);
@@ -137,6 +139,7 @@ export async function showNeighbors(cwd, options) {
137
139
  console.log(JSON.stringify({
138
140
  graph: graphType,
139
141
  backend,
142
+ ...(graphBackend ? { graphBackend } : {}),
140
143
  node: resolved,
141
144
  direction,
142
145
  edgeType: edgeType ?? null,
@@ -165,6 +168,7 @@ export async function showNeighbors(cwd, options) {
165
168
  export async function executeSetQuery(cwd, options) {
166
169
  const { graph: graphType, op, nodeA, nodeB, direction = 'in', edgeType, json = false, backend = 'fortemi-core' } = options;
167
170
  let graph = null;
171
+ let graphBackend;
168
172
  if (backend === 'fortemi-core') {
169
173
  const loaded = buildFortemiCoreDependencyGraph(cwd, graphType);
170
174
  if (!loaded.graph) {
@@ -181,7 +185,14 @@ export async function executeSetQuery(cwd, options) {
181
185
  graph = loaded.graph;
182
186
  }
183
187
  else {
184
- graph = loadGraph(cwd, graphType);
188
+ const active = await openGraphBackend(cwd, graphType);
189
+ graphBackend = active.type;
190
+ try {
191
+ graph = active.backend.serialize();
192
+ }
193
+ finally {
194
+ await closeGraphBackend(active);
195
+ }
185
196
  }
186
197
  if (!graph) {
187
198
  console.error(`Error: No index found for graph '${graphType}'.`);
@@ -215,6 +226,7 @@ export async function executeSetQuery(cwd, options) {
215
226
  console.log(JSON.stringify({
216
227
  graph: graphType,
217
228
  backend,
229
+ ...(graphBackend ? { graphBackend } : {}),
218
230
  op,
219
231
  nodeA: resolvedA,
220
232
  nodeB: resolvedB,
@@ -1077,6 +1077,20 @@ export async function buildIndex(cwd, options = {}) {
1077
1077
  writeIndexFile(effectiveOutputCwd, 'metadata.json', index, indexOutputDir);
1078
1078
  writeIndexFile(effectiveOutputCwd, 'tags.json', tagIndex, indexOutputDir);
1079
1079
  writeIndexFile(effectiveOutputCwd, 'dependencies.json', depGraph, indexOutputDir);
1080
+ // Materialize the configured backend and always retain dependencies.json as
1081
+ // the stable compatibility/export contract.
1082
+ const { createGraphBackend } = await import('./graph-backend.js');
1083
+ const { resolveGraphBackendType } = await import('./types.js');
1084
+ const backendType = resolveGraphBackendType(graph);
1085
+ const persistentPath = backendType === 'sqlite' ? path.join(indexOutputDir, 'graph.db') : undefined;
1086
+ let selectedBackend;
1087
+ try {
1088
+ selectedBackend = await createGraphBackend(backendType, persistentPath);
1089
+ selectedBackend.deserialize(depGraph);
1090
+ }
1091
+ finally {
1092
+ await selectedBackend?.close?.();
1093
+ }
1080
1094
  // Update and persist the checksum manifest for faster future builds (#794).
1081
1095
  // The next manifest contains entries for every file we processed this build.
1082
1096
  // Files that disappeared from disk are pruned; the resulting manifest is
@@ -1118,6 +1132,7 @@ export async function buildIndex(cwd, options = {}) {
1118
1132
  byType,
1119
1133
  tagDistribution: tagDist,
1120
1134
  graphMetrics: {
1135
+ backend: backendType,
1121
1136
  totalEdges,
1122
1137
  markdownLinkEdges: markdownLinkEdgeCount,
1123
1138
  ...(citationMetrics ? {
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
- import { GRAPH_CONFIGS, BUILTIN_GRAPH_CONFIGS, getGraphIndexDir, getProjectIndexRoot, loadGlobalGraphConfigs, loadUserGraphConfigs, } from './types.js';
21
+ import { GRAPH_CONFIGS, BUILTIN_GRAPH_CONFIGS, getGraphIndexDir, getProjectIndexRoot, loadGlobalGraphConfigs, loadUserGraphConfigs, resolveGraphBackendType, } from './types.js';
22
22
  import { getFortemiCoreSyncStatus, } from './fortemi-core-sync.js';
23
23
  function readBuiltMeta(indexDir) {
24
24
  try {
@@ -62,6 +62,7 @@ export function collectIndexStatus(cwd, nowMs) {
62
62
  }
63
63
  graphs.push({
64
64
  name,
65
+ backend: resolveGraphBackendType(name),
65
66
  origin: name in BUILTIN_GRAPH_CONFIGS ? 'builtin' : 'registered',
66
67
  shared: config.shared,
67
68
  defaultBuild: config.defaultBuild,
@@ -131,6 +132,7 @@ export async function showIndexStatus(cwd, opts = {}) {
131
132
  'GRAPH'.padEnd(22) +
132
133
  'ORIGIN'.padEnd(12) +
133
134
  'STATE'.padEnd(10) +
135
+ 'BACKEND'.padEnd(12) +
134
136
  'ENTRIES'.padEnd(9) +
135
137
  'AGE'.padEnd(10) +
136
138
  'LOCATION');
@@ -145,6 +147,7 @@ export async function showIndexStatus(cwd, opts = {}) {
145
147
  g.name.padEnd(22) +
146
148
  g.origin.padEnd(12) +
147
149
  state.padEnd(10) +
150
+ g.backend.padEnd(12) +
148
151
  String(g.entries ?? '—').padEnd(9) +
149
152
  age.padEnd(10) +
150
153
  shortenPath(g.location, cwd));
@@ -7,7 +7,7 @@
7
7
  * @source @src/artifacts/types.ts
8
8
  * @tests @test/unit/artifacts/stats.test.ts
9
9
  */
10
- import { GRAPH_CONFIGS, loadGlobalGraphConfigs, loadUserGraphConfigs } from './types.js';
10
+ import { GRAPH_CONFIGS, loadGlobalGraphConfigs, loadUserGraphConfigs, resolveGraphBackendType } from './types.js';
11
11
  import { loadIndexStats, loadGraphIndexFile } from './index-reader.js';
12
12
  import { collectGraphIndexFiles, indexPathFor } from './index-files.js';
13
13
  /** Calculate coverage over the same current file set used by the index builder. */
@@ -71,6 +71,7 @@ export async function showStats(cwd, options = {}) {
71
71
  const coverage = await calculateCoverage(cwd, s, type);
72
72
  combined[type] = {
73
73
  ...s,
74
+ backend: resolveGraphBackendType(type),
74
75
  coverage,
75
76
  };
76
77
  }
@@ -91,6 +92,7 @@ async function renderStats(cwd, stats, options, graphType) {
91
92
  const coverage = await calculateCoverage(cwd, stats, graphType);
92
93
  console.log(JSON.stringify({
93
94
  ...stats,
95
+ backend: resolveGraphBackendType(graphType),
94
96
  coverage,
95
97
  }, null, 2));
96
98
  return;
@@ -101,6 +103,7 @@ async function renderStats(cwd, stats, options, graphType) {
101
103
  console.log(`Index version: ${stats.version}`);
102
104
  console.log(`Last built: ${stats.builtAt}`);
103
105
  console.log(`Build time: ${stats.buildTimeMs}ms`);
106
+ console.log(`Graph backend: ${resolveGraphBackendType(graphType)}`);
104
107
  console.log('');
105
108
  // By phase
106
109
  console.log('Artifacts by Phase:');