@shrkcrft/graph 0.1.0-alpha.17 → 0.1.0-alpha.19
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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/indexer/call-graph-support.d.ts +3 -0
- package/dist/indexer/call-graph-support.d.ts.map +1 -0
- package/dist/indexer/call-graph-support.js +32 -0
- package/dist/indexer/extract-ts-file.d.ts +26 -0
- package/dist/indexer/extract-ts-file.d.ts.map +1 -1
- package/dist/indexer/extract-ts-file.js +162 -20
- package/dist/indexer/incremental-updater.d.ts +21 -3
- package/dist/indexer/incremental-updater.d.ts.map +1 -1
- package/dist/indexer/incremental-updater.js +45 -10
- package/dist/indexer/index-builder.d.ts.map +1 -1
- package/dist/indexer/index-builder.js +12 -4
- package/dist/indexer/resolve-imports.d.ts +1 -0
- package/dist/indexer/resolve-imports.d.ts.map +1 -1
- package/dist/indexer/resolve-imports.js +26 -1
- package/dist/indexer/resolve-reexports.d.ts +32 -0
- package/dist/indexer/resolve-reexports.d.ts.map +1 -0
- package/dist/indexer/resolve-reexports.js +123 -0
- package/dist/query/graph-api-cache.d.ts +21 -0
- package/dist/query/graph-api-cache.d.ts.map +1 -0
- package/dist/query/graph-api-cache.js +54 -0
- package/dist/query/query-api.d.ts +126 -0
- package/dist/query/query-api.d.ts.map +1 -1
- package/dist/query/query-api.js +334 -0
- package/package.json +4 -4
package/dist/query/query-api.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
1
3
|
import { EdgeKind } from "../schema/edge-kind.js";
|
|
2
4
|
import { NodeKind } from "../schema/node-kind.js";
|
|
3
5
|
import { GraphStore } from "../store/graph-store.js";
|
|
6
|
+
import { fingerprintFile } from "../store/file-fingerprint.js";
|
|
4
7
|
import { findFileCycles } from "./cycle-detection.js";
|
|
8
|
+
/** Read the representative source line stored on a reference/call edge. */
|
|
9
|
+
function edgeLine(e) {
|
|
10
|
+
const v = e.data?.['line'];
|
|
11
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Forward "code uses code" edge kinds traversed by {@link GraphQueryApi.pathBetween}.
|
|
15
|
+
* Structural source-level dependencies only — package-aggregate edges
|
|
16
|
+
* (`belongs-to-package` / `package-depends-on`) and asset-bridge edges
|
|
17
|
+
* (rules / framework) are deliberately excluded so a path is always a real
|
|
18
|
+
* import/call/reference/heritage chain, not a routing through a package node.
|
|
19
|
+
*/
|
|
20
|
+
const CODE_PATH_EDGE_KINDS = new Set([
|
|
21
|
+
EdgeKind.ImportsFile,
|
|
22
|
+
EdgeKind.CallsSymbol,
|
|
23
|
+
EdgeKind.ReferencesSymbol,
|
|
24
|
+
EdgeKind.DeclaresSymbol,
|
|
25
|
+
EdgeKind.ReExportsSymbol,
|
|
26
|
+
EdgeKind.ExtendsSymbol,
|
|
27
|
+
EdgeKind.ImplementsSymbol,
|
|
28
|
+
]);
|
|
5
29
|
/**
|
|
6
30
|
* Read-only query layer over an in-memory graph snapshot.
|
|
7
31
|
*
|
|
@@ -187,6 +211,117 @@ export class GraphQueryApi {
|
|
|
187
211
|
}
|
|
188
212
|
return out;
|
|
189
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Symbols that EXTEND or IMPLEMENT the given symbol — the precise "who
|
|
216
|
+
* implements this interface / who subclasses this" answer that a generic
|
|
217
|
+
* reference cannot give (a reference might be a call, a type annotation, or
|
|
218
|
+
* a heritage clause; these are typed `extends-symbol`/`implements-symbol`
|
|
219
|
+
* edges only). Returns the subtype symbol nodes.
|
|
220
|
+
*/
|
|
221
|
+
subtypesOf(symbolNodeId) {
|
|
222
|
+
const edges = this.inByTo.get(symbolNodeId) ?? [];
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const e of edges) {
|
|
225
|
+
if (e.kind !== EdgeKind.ExtendsSymbol && e.kind !== EdgeKind.ImplementsSymbol)
|
|
226
|
+
continue;
|
|
227
|
+
const n = this.snap.nodes.get(e.from);
|
|
228
|
+
if (n)
|
|
229
|
+
out.push(n);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
/** Symbols the given symbol EXTENDS or IMPLEMENTS (its supertypes). */
|
|
234
|
+
supertypesOf(symbolNodeId) {
|
|
235
|
+
const edges = this.outByFrom.get(symbolNodeId) ?? [];
|
|
236
|
+
const out = [];
|
|
237
|
+
for (const e of edges) {
|
|
238
|
+
if (e.kind !== EdgeKind.ExtendsSymbol && e.kind !== EdgeKind.ImplementsSymbol)
|
|
239
|
+
continue;
|
|
240
|
+
const n = this.snap.nodes.get(e.to);
|
|
241
|
+
if (n)
|
|
242
|
+
out.push(n);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Like {@link callersOf}, but keeps the call-site line carried on each
|
|
248
|
+
* edge so a caller can render `path:line` (jump-straight-to-source)
|
|
249
|
+
* instead of just the file path — the difference between "grep, then grep
|
|
250
|
+
* again inside each file" and a direct hit.
|
|
251
|
+
*/
|
|
252
|
+
callerSitesOf(symbolNodeId) {
|
|
253
|
+
const edges = this.inByTo.get(symbolNodeId) ?? [];
|
|
254
|
+
const out = [];
|
|
255
|
+
for (const e of edges) {
|
|
256
|
+
if (e.kind !== EdgeKind.CallsSymbol)
|
|
257
|
+
continue;
|
|
258
|
+
const n = this.snap.nodes.get(e.from);
|
|
259
|
+
if (n)
|
|
260
|
+
out.push({ node: n, line: edgeLine(e) });
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
/** Like {@link referencesOf}, but keeps the representative use-site line. */
|
|
265
|
+
referenceSitesOf(symbolNodeId) {
|
|
266
|
+
const edges = this.inByTo.get(symbolNodeId) ?? [];
|
|
267
|
+
const out = [];
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
for (const e of edges) {
|
|
270
|
+
if (e.kind !== EdgeKind.ReferencesSymbol && e.kind !== EdgeKind.CallsSymbol)
|
|
271
|
+
continue;
|
|
272
|
+
if (seen.has(e.from))
|
|
273
|
+
continue;
|
|
274
|
+
seen.add(e.from);
|
|
275
|
+
const n = this.snap.nodes.get(e.from);
|
|
276
|
+
if (n)
|
|
277
|
+
out.push({ node: n, line: edgeLine(e) });
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Targeted, cheap staleness check over a handful of RESULT file paths (the
|
|
283
|
+
* declaring file + caller/dependent files of a query) — NOT a full tree
|
|
284
|
+
* walk. For each project-relative path that the index knows about: stat it
|
|
285
|
+
* (mtime+size gate, sha1 only on mismatch) and classify as `modified`
|
|
286
|
+
* (content changed → results may be wrong: stale lines / a removed caller
|
|
287
|
+
* still listed) or `deleted` (gone from disk → drop it). Lets a query flag
|
|
288
|
+
* or prune a silently-stale answer for a file the agent just edited without
|
|
289
|
+
* paying for a whole-repo freshness walk. `cwd` is passed explicitly: the
|
|
290
|
+
* snapshot's index-time absolute root is wrong if the repo moved.
|
|
291
|
+
*/
|
|
292
|
+
staleFilesAmong(cwd, paths) {
|
|
293
|
+
const modified = [];
|
|
294
|
+
const deleted = [];
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
for (const rel of paths) {
|
|
297
|
+
if (seen.has(rel))
|
|
298
|
+
continue;
|
|
299
|
+
seen.add(rel);
|
|
300
|
+
const fp = this.snap.files.get(rel);
|
|
301
|
+
if (!fp)
|
|
302
|
+
continue; // not an indexed file — nothing to compare against
|
|
303
|
+
const abs = nodePath.isAbsolute(rel) ? rel : nodePath.join(cwd, rel);
|
|
304
|
+
let st;
|
|
305
|
+
try {
|
|
306
|
+
st = statSync(abs);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
deleted.push(rel);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (!st.isFile()) {
|
|
313
|
+
deleted.push(rel);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (Math.floor(st.mtimeMs) === fp.mtime && st.size === fp.sizeBytes)
|
|
317
|
+
continue; // fresh
|
|
318
|
+
if (fingerprintFile(abs, cwd).sha1 !== fp.sha1)
|
|
319
|
+
modified.push(rel);
|
|
320
|
+
}
|
|
321
|
+
modified.sort((a, b) => a.localeCompare(b));
|
|
322
|
+
deleted.sort((a, b) => a.localeCompare(b));
|
|
323
|
+
return { modified, deleted };
|
|
324
|
+
}
|
|
190
325
|
/** Packages that this package depends on (PackageDependsOn). */
|
|
191
326
|
packageDeps(packageName) {
|
|
192
327
|
const edges = this.outByFrom.get(`package:${packageName}`) ?? [];
|
|
@@ -248,6 +383,205 @@ export class GraphQueryApi {
|
|
|
248
383
|
}),
|
|
249
384
|
};
|
|
250
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Shortest directed code path from `fromId` to `toId` over the
|
|
388
|
+
* "code uses code" edges (imports / calls / references / declares /
|
|
389
|
+
* re-exports / extends / implements). Answers "is A actually wired to B?"
|
|
390
|
+
* deterministically: a found path lists every hop with its edge kind (and
|
|
391
|
+
* call-site line where the edge carries one) so a caller sees HOW they are
|
|
392
|
+
* wired — a chain of `imports-file` hops reads very differently from a
|
|
393
|
+
* `calls-symbol` one. Breadth-first, so the returned path is minimal-hop;
|
|
394
|
+
* `explored` reports how many nodes were visited so a "no path" answer is
|
|
395
|
+
* honestly bounded rather than read as "definitely unrelated".
|
|
396
|
+
*/
|
|
397
|
+
pathBetween(fromId, toId, opts = {}) {
|
|
398
|
+
const maxDepth = opts.maxDepth && opts.maxDepth > 0 ? opts.maxDepth : 16;
|
|
399
|
+
const from = this.snap.nodes.get(fromId);
|
|
400
|
+
const to = this.snap.nodes.get(toId);
|
|
401
|
+
if (!from || !to) {
|
|
402
|
+
return {
|
|
403
|
+
found: false,
|
|
404
|
+
...(from ? { from } : {}),
|
|
405
|
+
...(to ? { to } : {}),
|
|
406
|
+
hops: [],
|
|
407
|
+
explored: 0,
|
|
408
|
+
reason: !from && !to
|
|
409
|
+
? 'neither endpoint is in the graph'
|
|
410
|
+
: !from
|
|
411
|
+
? 'source node is not in the graph'
|
|
412
|
+
: 'target node is not in the graph',
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (fromId === toId)
|
|
416
|
+
return { found: true, from, to, hops: [], explored: 1 };
|
|
417
|
+
// BFS. `parent` maps a discovered node id to the edge that first reached it.
|
|
418
|
+
const parent = new Map();
|
|
419
|
+
const depth = new Map([[fromId, 0]]);
|
|
420
|
+
const queue = [fromId];
|
|
421
|
+
let head = 0;
|
|
422
|
+
let explored = 0;
|
|
423
|
+
while (head < queue.length) {
|
|
424
|
+
const cur = queue[head++];
|
|
425
|
+
explored++;
|
|
426
|
+
const d = depth.get(cur);
|
|
427
|
+
if (d >= maxDepth)
|
|
428
|
+
continue;
|
|
429
|
+
for (const e of this.outByFrom.get(cur) ?? []) {
|
|
430
|
+
if (!CODE_PATH_EDGE_KINDS.has(e.kind))
|
|
431
|
+
continue;
|
|
432
|
+
if (e.to === cur || e.to === fromId || parent.has(e.to))
|
|
433
|
+
continue;
|
|
434
|
+
parent.set(e.to, e);
|
|
435
|
+
depth.set(e.to, d + 1);
|
|
436
|
+
if (e.to === toId) {
|
|
437
|
+
return { found: true, from, to, hops: this.reconstructPath(parent, fromId, toId), explored };
|
|
438
|
+
}
|
|
439
|
+
queue.push(e.to);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { found: false, from, to, hops: [], explored, reason: `no code path within ${maxDepth} hops` };
|
|
443
|
+
}
|
|
444
|
+
/** Walk the BFS `parent` map back from `toId` to `fromId` into ordered hops. */
|
|
445
|
+
reconstructPath(parent, fromId, toId) {
|
|
446
|
+
const chain = [];
|
|
447
|
+
let cur = toId;
|
|
448
|
+
while (cur !== fromId) {
|
|
449
|
+
const e = parent.get(cur);
|
|
450
|
+
if (!e)
|
|
451
|
+
break;
|
|
452
|
+
chain.push(e);
|
|
453
|
+
cur = e.from;
|
|
454
|
+
}
|
|
455
|
+
chain.reverse();
|
|
456
|
+
const hops = [];
|
|
457
|
+
for (const e of chain) {
|
|
458
|
+
const f = this.snap.nodes.get(e.from);
|
|
459
|
+
const t = this.snap.nodes.get(e.to);
|
|
460
|
+
if (!f || !t)
|
|
461
|
+
continue;
|
|
462
|
+
const line = edgeLine(e);
|
|
463
|
+
hops.push({ from: f, to: t, kind: e.kind, ...(line !== undefined ? { line } : {}) });
|
|
464
|
+
}
|
|
465
|
+
return hops;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* The most-depended-on code in the snapshot: symbols ranked by how many
|
|
469
|
+
* distinct files reference/call them and files ranked by how many distinct
|
|
470
|
+
* files import them. This is the "load-bearing code" view — the surface an
|
|
471
|
+
* agent should change most carefully and a human should understand first.
|
|
472
|
+
* In-degree counts DISTINCT dependent files (a file that calls a symbol
|
|
473
|
+
* ten times counts once), so the rank reflects blast radius, not call volume.
|
|
474
|
+
*/
|
|
475
|
+
/**
|
|
476
|
+
* The DIRECT dependents of a node — what breaks if you change it, one hop out.
|
|
477
|
+
* Kind-aware, because the edge that carries "depends on" differs by kind:
|
|
478
|
+
* - Symbol: the files that reference/call it, PLUS the files declaring its
|
|
479
|
+
* subtypes (a class that `import type`s an interface has no value-reference
|
|
480
|
+
* edge, so subtypes must be added explicitly or implementers are missed).
|
|
481
|
+
* Falls back to the importers of its declaring file when nothing references
|
|
482
|
+
* the symbol directly.
|
|
483
|
+
* - File: its importers (`imports-file`).
|
|
484
|
+
* - Package: the packages that depend on it.
|
|
485
|
+
* The shared building block for impact reverse-closures (CLI + MCP) — keep this
|
|
486
|
+
* the ONE source of truth so the two surfaces never disagree on a symbol's
|
|
487
|
+
* blast radius (they once did: an importers-only closure returned NOTHING for a
|
|
488
|
+
* symbol, a confidently-wrong "nothing breaks").
|
|
489
|
+
*/
|
|
490
|
+
directDependentsOf(anchor) {
|
|
491
|
+
if (anchor.kind === NodeKind.Symbol) {
|
|
492
|
+
const owner = this.declaringFileOf(anchor.id);
|
|
493
|
+
const out = new Map();
|
|
494
|
+
for (const n of this.referencesOf(anchor.id)) {
|
|
495
|
+
if (n.kind === NodeKind.File && n.id !== owner?.id)
|
|
496
|
+
out.set(n.id, n);
|
|
497
|
+
}
|
|
498
|
+
for (const n of this.callersOf(anchor.id)) {
|
|
499
|
+
if (n.kind === NodeKind.File && n.id !== owner?.id)
|
|
500
|
+
out.set(n.id, n);
|
|
501
|
+
}
|
|
502
|
+
for (const s of this.subtypesOf(anchor.id)) {
|
|
503
|
+
if (!s.path)
|
|
504
|
+
continue;
|
|
505
|
+
const fileNode = this.fileByPath.get(s.path);
|
|
506
|
+
if (fileNode && fileNode.id !== owner?.id)
|
|
507
|
+
out.set(fileNode.id, fileNode);
|
|
508
|
+
}
|
|
509
|
+
if (out.size > 0)
|
|
510
|
+
return [...out.values()];
|
|
511
|
+
return owner ? [...this.importersOf(owner.id)] : [];
|
|
512
|
+
}
|
|
513
|
+
if (anchor.kind === NodeKind.Package) {
|
|
514
|
+
return this.packageDependents(anchor.id.replace(/^package:/, ''));
|
|
515
|
+
}
|
|
516
|
+
return [...this.importersOf(anchor.id)];
|
|
517
|
+
}
|
|
518
|
+
/** The file that declares a symbol (the `declares-symbol` source), if known. */
|
|
519
|
+
declaringFileOf(symbolId) {
|
|
520
|
+
for (const e of this.inByTo.get(symbolId) ?? []) {
|
|
521
|
+
if (e.kind !== EdgeKind.DeclaresSymbol)
|
|
522
|
+
continue;
|
|
523
|
+
const n = this.snap.nodes.get(e.from);
|
|
524
|
+
if (n && n.kind === NodeKind.File)
|
|
525
|
+
return n;
|
|
526
|
+
}
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
topHubs(limit = 10, pathPrefix) {
|
|
530
|
+
// Optional path scope: rank only the load-bearing code WITHIN a subsystem
|
|
531
|
+
// (e.g. `packages/inspector`) — the global hubs are dominated by the biggest
|
|
532
|
+
// packages, but an agent working in one area wants that area's hubs. The
|
|
533
|
+
// in-degree still counts ALL dependents (anyone in the repo), answering
|
|
534
|
+
// "within this dir, what is most depended-on?".
|
|
535
|
+
const prefix = pathPrefix ? pathPrefix.replace(/\\/g, '/').replace(/\/+$/, '') : undefined;
|
|
536
|
+
const inScope = (path) => !prefix || (path !== undefined && (path === prefix || path.startsWith(prefix + '/')));
|
|
537
|
+
const symbolDeps = new Map();
|
|
538
|
+
const fileDeps = new Map();
|
|
539
|
+
for (const [toId, edges] of this.inByTo) {
|
|
540
|
+
const target = this.snap.nodes.get(toId);
|
|
541
|
+
if (!target)
|
|
542
|
+
continue;
|
|
543
|
+
if (!inScope(target.path))
|
|
544
|
+
continue;
|
|
545
|
+
if (target.kind === NodeKind.Symbol) {
|
|
546
|
+
const set = new Set();
|
|
547
|
+
for (const e of edges) {
|
|
548
|
+
if (e.kind === EdgeKind.ReferencesSymbol || e.kind === EdgeKind.CallsSymbol) {
|
|
549
|
+
set.add(e.from); // reference/call edges originate on a file node
|
|
550
|
+
}
|
|
551
|
+
else if (e.kind === EdgeKind.ExtendsSymbol || e.kind === EdgeKind.ImplementsSymbol) {
|
|
552
|
+
// Heritage edges originate on the SUBTYPE symbol — count its file so
|
|
553
|
+
// an interface implemented only via `import type` (no reference edge)
|
|
554
|
+
// still ranks as load-bearing, at the same file granularity.
|
|
555
|
+
const sub = this.snap.nodes.get(e.from);
|
|
556
|
+
set.add(sub?.path ? `file:${sub.path}` : e.from);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (set.size > 0)
|
|
560
|
+
symbolDeps.set(toId, set);
|
|
561
|
+
}
|
|
562
|
+
else if (target.kind === NodeKind.File) {
|
|
563
|
+
const set = new Set();
|
|
564
|
+
for (const e of edges) {
|
|
565
|
+
if (e.kind === EdgeKind.ImportsFile)
|
|
566
|
+
set.add(e.from);
|
|
567
|
+
}
|
|
568
|
+
if (set.size > 0)
|
|
569
|
+
fileDeps.set(toId, set);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const cap = Math.max(0, limit);
|
|
573
|
+
const toHubs = (m) => {
|
|
574
|
+
const arr = [];
|
|
575
|
+
for (const [id, set] of m) {
|
|
576
|
+
const node = this.snap.nodes.get(id);
|
|
577
|
+
if (node)
|
|
578
|
+
arr.push({ node, inDegree: set.size });
|
|
579
|
+
}
|
|
580
|
+
arr.sort((a, b) => b.inDegree - a.inDegree || a.node.label.localeCompare(b.node.label) || a.node.id.localeCompare(b.node.id));
|
|
581
|
+
return arr.slice(0, cap);
|
|
582
|
+
};
|
|
583
|
+
return { symbols: toHubs(symbolDeps), files: toHubs(fileDeps) };
|
|
584
|
+
}
|
|
251
585
|
}
|
|
252
586
|
function filterByPackage(list, pkg) {
|
|
253
587
|
if (!pkg)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/graph",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"description": "SharkCraft code intelligence graph: files, symbols, packages, imports and exports. Persistent JSONL store; on-disk index queried by every other code-intelligence package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SharkCraft contributors",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@shrkcrft/core": "^0.1.0-alpha.
|
|
49
|
-
"@shrkcrft/boundaries": "^0.1.0-alpha.
|
|
50
|
-
"@shrkcrft/inspector": "^0.1.0-alpha.
|
|
48
|
+
"@shrkcrft/core": "^0.1.0-alpha.19",
|
|
49
|
+
"@shrkcrft/boundaries": "^0.1.0-alpha.19",
|
|
50
|
+
"@shrkcrft/inspector": "^0.1.0-alpha.19",
|
|
51
51
|
"typescript": "^5.6.0"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|