@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.
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.js +2 -0
- package/dist/src/artifacts/backend-runtime.js +26 -0
- package/dist/src/artifacts/backends/sqlite-backend.js +204 -28
- package/dist/src/artifacts/dep-graph.js +27 -5
- package/dist/src/artifacts/graph-backend.js +2 -2
- package/dist/src/artifacts/graph-query.js +21 -9
- package/dist/src/artifacts/index-builder.js +15 -0
- package/dist/src/artifacts/index-status.js +4 -1
- package/dist/src/artifacts/stats.js +4 -1
- package/dist/src/artifacts/types.js +13 -1
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +6 -2
- package/dist/src/cli/handlers/mission.js +27 -0
- package/dist/src/cli/handlers/runtime-info.js +29 -0
- package/dist/src/cli/handlers/steward.js +12 -0
- package/dist/src/cli/handlers/uhp.js +88 -0
- package/dist/src/config/aiwg-config.js +10 -0
- package/dist/src/extensions/commands/definitions.js +38 -0
- package/dist/src/mission-protocol/codecs.js +265 -0
- package/dist/src/mission-protocol/index.js +3 -0
- package/dist/src/mission-protocol/types.js +2 -0
- package/dist/src/storage/backend-contract.js +64 -0
- package/dist/src/storage/index.js +2 -0
- package/dist/src/storage/migration-protocol.js +378 -0
- package/dist/src/uhp/client.js +374 -0
- package/dist/src/uhp/config.js +130 -0
- package/dist/src/uhp/errors.js +63 -0
- package/dist/src/uhp/index.js +7 -0
- package/dist/src/uhp/mission.js +111 -0
- package/dist/src/uhp/sse.js +76 -0
- package/dist/src/uhp/types.js +2 -0
- package/package.json +1 -1
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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.
|
package/dist/src/api/index.d.ts
CHANGED
|
@@ -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
|
package/dist/src/api/index.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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.
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
this.db.
|
|
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
|
-
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
131
|
-
return setA.filter(x => b.has(x));
|
|
180
|
+
return this.sqlSetOperation(setA, setB, 'INTERSECT');
|
|
132
181
|
}
|
|
133
182
|
difference(setA, setB) {
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
if (setA.length === 0)
|
|
184
|
+
return [];
|
|
185
|
+
return this.sqlSetOperation(setA, setB, 'EXCEPT');
|
|
136
186
|
}
|
|
137
187
|
union(setA, setB) {
|
|
138
|
-
return
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:');
|