@dreamtree-org/graphify 1.0.0 → 1.1.1

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 CHANGED
@@ -51,15 +51,22 @@ interface ExportOptions {
51
51
  */
52
52
  declare function collectFiles(root: string): FileManifest;
53
53
 
54
- type Extractor = (path: string) => Promise<ExtractionResult>;
54
+ /** `displayPath`, when given, is used for node ids/sourceFile instead of the read path — see extract(). */
55
+ type Extractor = (path: string, displayPath?: string) => Promise<ExtractionResult>;
55
56
  /** File extension -> extractor module. Register new languages here. */
56
57
  declare const EXTRACTOR_REGISTRY: Record<string, Extractor>;
57
58
  /**
58
59
  * Dispatch a single file to its language extractor. Returns an empty
59
60
  * extraction (not a thrown error) for unregistered extensions so detect's
60
61
  * file list and extract's coverage can diverge without crashing a run.
62
+ *
63
+ * `displayPath` decouples where the file is read from (relative to the
64
+ * process cwd) from the path used in node ids and sourceFile attributes —
65
+ * the pipeline passes the project-root-relative path so ids stay stable
66
+ * and portable no matter which directory the pipeline was launched from
67
+ * (this is what makes cross-project merging's namespacing meaningful).
61
68
  */
62
- declare function extract(filePath: string): Promise<ExtractionResult>;
69
+ declare function extract(filePath: string, displayPath?: string): Promise<ExtractionResult>;
63
70
 
64
71
  /**
65
72
  * Merge per-file extraction results into one graphology graph.
@@ -83,6 +90,18 @@ declare function extract(filePath: string): Promise<ExtractionResult>;
83
90
  */
84
91
  declare function buildGraph(extractions: ExtractionResult[]): Graph;
85
92
 
93
+ interface ResolveStats {
94
+ /** Edge endpoints re-pointed from a guessed id to a real node id. */
95
+ resolvedEndpoints: number;
96
+ /** Placeholder module nodes dropped because the real file node replaced them. */
97
+ droppedPlaceholders: number;
98
+ }
99
+ /**
100
+ * Mutates `extractions` in place: re-points guessed edge endpoints at real
101
+ * node ids and drops the placeholder module nodes those guesses created.
102
+ */
103
+ declare function resolveCrossFileReferences(extractions: ExtractionResult[]): ResolveStats;
104
+
86
105
  type ClusterAlgorithm = 'louvain' | 'leiden';
87
106
  interface ClusterOptions {
88
107
  /**
@@ -155,6 +174,17 @@ interface PipelineOptions {
155
174
  algorithm?: ClusterAlgorithm;
156
175
  /** Leiden-only: cap on outer iterations for very large graphs. */
157
176
  maxIterations?: number;
177
+ /**
178
+ * Pre-computed extractions from non-filesystem sources (e.g. a MySQL
179
+ * schema via extractMysql()) merged into the same graph as the code.
180
+ */
181
+ extraExtractions?: ExtractionResult[];
182
+ /**
183
+ * Incremental mode: reuse cached per-file extractions (keyed by content
184
+ * hash) and re-parse only changed files. The cache is warmed on every
185
+ * run either way — `update` only controls whether it's read.
186
+ */
187
+ update?: boolean;
158
188
  /** Called with a short progress message after each stage (e.g. for CLI stderr output). */
159
189
  onProgress?: (message: string) => void;
160
190
  }
@@ -186,7 +216,18 @@ interface NodeMatch {
186
216
  label: string;
187
217
  score: number;
188
218
  }
189
- /** Score every node by how many query tokens appear in its label or id. */
219
+ /**
220
+ * Score every node against the query's tokens. Identifier-aware and
221
+ * typo-tolerant, in three tiers per token (best tier wins):
222
+ *
223
+ * 1. exact subtoken match (camelCase/snake_case-split word of the label
224
+ * or id) — "storage" matches `fileStorage.js`;
225
+ * 2. plain substring of the label/id (the original v1 behavior);
226
+ * 3. fuzzy (Damerau-Levenshtein) match against a subtoken — "sorce"
227
+ * still finds `scoreNodes`.
228
+ *
229
+ * Still fully lexical and deterministic — no LLM, no randomness.
230
+ */
190
231
  declare function scoreNodes(graph: Graph, query: string): NodeMatch[];
191
232
  /**
192
233
  * Best-effort single-node resolution: the highest-scoring lexical match,
@@ -216,8 +257,10 @@ interface QueryResult {
216
257
  }
217
258
  interface QueryOptions {
218
259
  dfs?: boolean;
219
- /** Approximate cap on how many nodes to include (a stand-in for a true token budget see docs). */
260
+ /** Hard cap on how many nodes to include. Defaults to a generous ceiling derived from `tokenBudget`. */
220
261
  budget?: number;
262
+ /** Approximate cap, in tokens (~4 chars each), on the serialized size of the result. Default 2000. */
263
+ tokenBudget?: number;
221
264
  maxDepth?: number;
222
265
  maxSeeds?: number;
223
266
  }
@@ -251,6 +294,291 @@ interface ExplainResult {
251
294
  /** Plain-language-ready explanation of the node that best matches `query`. */
252
295
  declare function explainNode(graph: Graph, query: string): ExplainResult | null;
253
296
 
297
+ interface AffectedNode {
298
+ id: string;
299
+ label: string;
300
+ sourceFile: string;
301
+ sourceLocation: string;
302
+ /** 1 = depends on the target directly, 2 = one step removed, ... */
303
+ depth: number;
304
+ /** The dependency edge that pulled this node in (this node -> something already affected). */
305
+ via: {
306
+ relation: Relation;
307
+ confidence: Confidence;
308
+ /** The already-affected node this one depends on. */
309
+ dependsOn: string;
310
+ };
311
+ }
312
+ interface ImpactResult {
313
+ target: NodeMatch;
314
+ affected: AffectedNode[];
315
+ /** How many affected nodes there are per confidence tier of the edge that pulled each one in. */
316
+ byConfidence: Record<Confidence, number>;
317
+ maxDepth: number;
318
+ truncated: boolean;
319
+ }
320
+ interface ImpactOptions {
321
+ /** How many reverse hops to follow (default 3). */
322
+ maxDepth?: number;
323
+ /** Hard cap on affected nodes reported (default 200). */
324
+ limit?: number;
325
+ }
326
+ /**
327
+ * Resolve `nodeQuery` to its best-matching node, then reverse-BFS over
328
+ * incoming dependency edges. Returns null when nothing in the graph matches
329
+ * the query at all (same contract as explainNode()).
330
+ */
331
+ declare function affectedBy(graph: Graph, nodeQuery: string, options?: ImpactOptions): ImpactResult | null;
332
+
333
+ /**
334
+ * Cross-project graph merging — combine several already-built graphs (one
335
+ * per repo/project) into a single queryable global graph.
336
+ *
337
+ * Node ids inside one project are relative-path-based (`src/a.ts::foo`), so
338
+ * two projects' ids would collide. Every node id and sourceFile is therefore
339
+ * prefixed with `<project>/` — EXCEPT `module:*` (bare package imports) and
340
+ * `external:*` (unresolved reference targets) nodes, which deliberately stay
341
+ * unprefixed and shared: two repos importing the same package meet at the
342
+ * same node, so cross-repo bridges emerge in the merged graph instead of
343
+ * each repo floating as a disconnected island.
344
+ */
345
+ interface MergeEntry {
346
+ /** Project name used as the id/sourceFile namespace prefix. */
347
+ name: string;
348
+ graph: Graph;
349
+ }
350
+ /**
351
+ * Merge the entries into one directed graph, applying the same
352
+ * deterministic-ordering and duplicate-edge rules as buildGraph(): entries
353
+ * are processed in name order, duplicate edges (possible via shared
354
+ * external nodes) keep the strongest confidence, and every node carries a
355
+ * `project` attribute (shared external nodes get `project: '(shared)'`
356
+ * once more than one project touches them).
357
+ */
358
+ declare function mergeGraphs(entries: MergeEntry[]): Graph;
359
+
360
+ /**
361
+ * MySQL schema extraction — introspect information_schema over a
362
+ * user-supplied DSN and map the schema onto the same ExtractionResult shape
363
+ * every code extractor produces, so tables/columns/foreign keys land in the
364
+ * same graph as the code that talks to them.
365
+ *
366
+ * Only ever surfaces the credential-free DSN rendering (see
367
+ * security.validateDsn()) in node attributes; the password never leaves the
368
+ * connection config.
369
+ */
370
+ /** One `SELECT` against the live connection. Injectable so tests run on canned rows, mirroring security.ts's LookupFn pattern. */
371
+ type DsnQueryFn = (sql: string, params: ReadonlyArray<string>) => Promise<Array<Record<string, unknown>>>;
372
+ /**
373
+ * Extract the schema graph for the database named in `dsn`.
374
+ *
375
+ * Nodes: the schema, every table/view, every column.
376
+ * Edges: schema `contains` table, table `contains` column, FK column
377
+ * `references` the referenced table (EXTRACTED), view `references` its base
378
+ * tables (EXTRACTED via VIEW_TABLE_USAGE on MySQL >= 8.0.13, INFERRED via a
379
+ * VIEW_DEFINITION text match on older servers).
380
+ */
381
+ declare function extractMysql(dsn: string, queryFn?: DsnQueryFn): Promise<ExtractionResult>;
382
+
383
+ /**
384
+ * The graph feedback loop — `graphify save-result` logs which graph nodes
385
+ * actually helped answer a question (or led nowhere), and `graphify
386
+ * reflect` aggregates those logs into a recency-weighted LESSONS.md an
387
+ * agent can read at session start. This is what turns the graph from a
388
+ * static map into one that learns which landmarks matter.
389
+ */
390
+ type ResultOutcome = 'useful' | 'dead_end' | 'corrected';
391
+ type ResultType = 'query' | 'path' | 'explain' | 'affected';
392
+ interface SavedResult {
393
+ question: string;
394
+ answer: string;
395
+ type: ResultType;
396
+ outcome: ResultOutcome;
397
+ /** Node ids/labels cited in the answer. */
398
+ nodes: string[];
399
+ /** What the right answer was — pairs with outcome 'corrected'. */
400
+ correction?: string;
401
+ savedAt: string;
402
+ }
403
+ /** Append one result to `<memoryDir>/result-<stamp>-<hash>.json`. Returns the file path. */
404
+ declare function saveResult(entry: Omit<SavedResult, 'savedAt'>, memoryDir: string, now?: Date): Promise<string>;
405
+ interface ReflectOptions {
406
+ /** A result's weight halves every this many days (default 30). */
407
+ halfLifeDays?: number;
408
+ /** Injectable clock for deterministic tests. */
409
+ now?: Date;
410
+ /** Only list nodes cited usefully at least this many distinct times (default 2). */
411
+ minCorroboration?: number;
412
+ }
413
+ /** Read every result JSON under `memoryDir` (missing dir = no results). */
414
+ declare function loadResults(memoryDir: string): Promise<SavedResult[]>;
415
+ /** Aggregate saved results into a LESSONS.md string. Deterministic given results + options.now. */
416
+ declare function renderLessons(results: SavedResult[], options?: ReflectOptions): string;
417
+
418
+ /**
419
+ * `graphify context` — the working-set pack. Instead of returning node
420
+ * names for the agent to chase (query -> then read whole files anyway),
421
+ * this fuses the two steps: the graph knows every entity's file and line,
422
+ * so rank the nodes relevant to the task, read just the relevant line
423
+ * ranges, and pack the actual code into a token budget. One call returns
424
+ * the code an agent needs to start a task.
425
+ */
426
+ interface ContextSnippet {
427
+ nodeId: string;
428
+ label: string;
429
+ file: string;
430
+ startLine: number;
431
+ endLine: number;
432
+ code: string;
433
+ /** Why this snippet is in the pack (lexical match / neighbor-of relation). */
434
+ reason: string;
435
+ score: number;
436
+ }
437
+ interface ContextPack {
438
+ task: string;
439
+ snippets: ContextSnippet[];
440
+ /** Approximate tokens used out of the budget. */
441
+ tokens: number;
442
+ tokenBudget: number;
443
+ /** Ranked nodes that didn't fit the budget — the agent can pull them explicitly. */
444
+ overflow: Array<{
445
+ nodeId: string;
446
+ file: string;
447
+ }>;
448
+ }
449
+ interface ContextOptions {
450
+ /** Approximate token cap for the pack (default 4000). */
451
+ tokenBudget?: number;
452
+ maxSeeds?: number;
453
+ /** Cap on snippet length in lines (default 60). */
454
+ maxSnippetLines?: number;
455
+ }
456
+ type FileReader = (path: string) => Promise<string>;
457
+ interface RankedNode {
458
+ id: string;
459
+ score: number;
460
+ reason: string;
461
+ }
462
+ /**
463
+ * Rank nodes for the task: lexical seeds first (their match score), then
464
+ * their graph neighbors at a decayed score, labeled with the relation that
465
+ * connects them. Deterministic (score desc, id asc).
466
+ */
467
+ declare function rankForTask(graph: Graph, task: string, maxSeeds?: number): RankedNode[];
468
+ declare function buildContextPack(graph: Graph, task: string, readFile: FileReader, options?: ContextOptions): Promise<ContextPack>;
469
+ /** Render a pack as agent-ready markdown. */
470
+ declare function renderContextPack(pack: ContextPack): string;
471
+
472
+ declare function isTestFile(path: string): boolean;
473
+ interface TestSelection {
474
+ /** What the selection was computed for (resolved node id, or the changed files). */
475
+ target: string;
476
+ /** Unique test files, most directly affected first. */
477
+ testFiles: Array<{
478
+ file: string;
479
+ depth: number;
480
+ via: string;
481
+ }>;
482
+ /** How many non-test nodes were in the blast radius (context for confidence). */
483
+ affectedNodeCount: number;
484
+ }
485
+ /** Test files reachable (reverse) from the node best matching `nodeQuery`. Null if nothing matches. */
486
+ declare function testsForNode(graph: Graph, nodeQuery: string): TestSelection | null;
487
+ /**
488
+ * Test files reachable from any of the given changed files (as reported by
489
+ * `git diff --name-only`). Changed test files select themselves. Unknown
490
+ * files (not in the graph) are skipped.
491
+ */
492
+ declare function testsForChangedFiles(graph: Graph, changedFiles: string[]): TestSelection;
493
+
494
+ /**
495
+ * `graphify review` — semantic graph diff between two git revisions.
496
+ * Full structural extraction is sub-second, so instead of parsing text
497
+ * diffs we build the graph at both revs and diff the graphs: which symbols
498
+ * appeared, disappeared, or got rewired — each with its blast radius and
499
+ * the test files worth running. One command answers both "what changed
500
+ * since I was last here" and "review this PR structurally".
501
+ */
502
+ interface ChangedSymbol {
503
+ id: string;
504
+ label: string;
505
+ sourceFile: string;
506
+ /** Everything that depends on this symbol (computed in the graph where it exists). */
507
+ blastRadius: number;
508
+ /** Test files reachable from it. */
509
+ tests: string[];
510
+ }
511
+ interface RewiredSymbol extends ChangedSymbol {
512
+ gainedEdges: string[];
513
+ lostEdges: string[];
514
+ }
515
+ interface GraphDiff {
516
+ base: string;
517
+ head: string;
518
+ added: ChangedSymbol[];
519
+ removed: ChangedSymbol[];
520
+ rewired: RewiredSymbol[];
521
+ }
522
+ /** Extract a graph from a directory without exporting anything. */
523
+ declare function graphForDirectory(root: string): Promise<Graph>;
524
+ declare function diffGraphs(baseGraph: Graph, headGraph: Graph, base: string, head: string): GraphDiff;
525
+ /** Diff two revisions of the repo at `repoRoot`. `head` defaults to the working head via a second archive of HEAD. */
526
+ declare function reviewRevisions(repoRoot: string, base: string, head?: string): Promise<GraphDiff>;
527
+ declare function renderReview(diff: GraphDiff): string;
528
+
529
+ /**
530
+ * `graphify check` — architecture guardrails. Declarative dependency rules
531
+ * checked against the graph's edges, with CI-friendly results: an agent (or
532
+ * a pipeline) can verify an edit didn't violate the architecture before it
533
+ * lands. Cross-language, because the graph already is.
534
+ */
535
+ interface DependencyRule {
536
+ /** Short rule name shown in violations. */
537
+ name: string;
538
+ /** Glob over the depending file (edge source's sourceFile). */
539
+ from: string;
540
+ /** Globs the dependency target's file must NOT match. */
541
+ disallow: string[];
542
+ /** Optional human explanation echoed with violations. */
543
+ reason?: string;
544
+ }
545
+ interface RulesConfig {
546
+ rules: DependencyRule[];
547
+ }
548
+ interface Violation {
549
+ rule: string;
550
+ reason?: string;
551
+ fromFile: string;
552
+ fromNode: string;
553
+ toFile: string;
554
+ toNode: string;
555
+ relation: string;
556
+ }
557
+ /**
558
+ * Minimal glob: `**` crosses directories, `*` stays within one segment.
559
+ * Anchored on both ends. Enough for path rules without a dependency.
560
+ */
561
+ declare function globToRegExp(glob: string): RegExp;
562
+ declare function validateRules(value: unknown): RulesConfig;
563
+ /** Check every dependency edge in the graph against the rules. Deterministic ordering. */
564
+ declare function checkRules(graph: Graph, config: RulesConfig): Violation[];
565
+
566
+ /**
567
+ * Per-file extraction cache — what makes `--update` (and every rebuild
568
+ * behind a git hook or --watch) incremental. Entries are keyed by a hash of
569
+ * the file's CONTENT plus its root-relative path: content because that's
570
+ * what extraction depends on, path because node ids embed it. A stale entry
571
+ * for content that changed simply never matches again; the whole directory
572
+ * can always be deleted safely.
573
+ */
574
+ declare class ExtractionCache {
575
+ private readonly dir;
576
+ constructor(outDir: string);
577
+ key(relFile: string, content: Buffer | string): string;
578
+ get(key: string): Promise<ExtractionResult | null>;
579
+ put(key: string, extraction: ExtractionResult): Promise<void>;
580
+ }
581
+
254
582
  declare const ExtractionResultSchema: z.ZodObject<{
255
583
  nodes: z.ZodArray<z.ZodObject<{
256
584
  id: z.ZodString;
@@ -298,4 +626,4 @@ declare class ExtractionValidationError extends Error {
298
626
  */
299
627
  declare function validateExtraction(value: unknown, context?: string): ExtractionResult;
300
628
 
301
- export { type Analysis, type ClusterAlgorithm, type ClusterOptions, type Confidence, EXTRACTOR_REGISTRY, type ExplainResult, type ExportOptions, type ExtractionResult, ExtractionResultSchema, ExtractionValidationError, type ExtractionValidationIssue, type FileCategory, type FileManifest, type GraphEdge, type GraphNode, type NodeMatch, type PathResult, type PipelineOptions, type PipelineResult, type QueryOptions, type QueryResult, type Relation, type TraversalEdge, type VisitedNode, analyze, buildGraph, cluster, collectFiles, explainNode, exportGraph, extract, loadGraph, queryGraph, renderReport, resolveNode, runPipeline, scoreNodes, shortestPath, validateExtraction };
629
+ export { type AffectedNode, type Analysis, type ChangedSymbol, type ClusterAlgorithm, type ClusterOptions, type Confidence, type ContextOptions, type ContextPack, type ContextSnippet, type DependencyRule, type DsnQueryFn, EXTRACTOR_REGISTRY, type ExplainResult, type ExportOptions, ExtractionCache, type ExtractionResult, ExtractionResultSchema, ExtractionValidationError, type ExtractionValidationIssue, type FileCategory, type FileManifest, type GraphDiff, type GraphEdge, type GraphNode, type ImpactOptions, type ImpactResult, type MergeEntry, type NodeMatch, type PathResult, type PipelineOptions, type PipelineResult, type QueryOptions, type QueryResult, type ReflectOptions, type Relation, type ResolveStats, type ResultOutcome, type ResultType, type RewiredSymbol, type RulesConfig, type SavedResult, type TestSelection, type TraversalEdge, type Violation, type VisitedNode, affectedBy, analyze, buildContextPack, buildGraph, checkRules, cluster, collectFiles, diffGraphs, explainNode, exportGraph, extract, extractMysql, globToRegExp, graphForDirectory, isTestFile, loadGraph, loadResults, mergeGraphs, queryGraph, rankForTask, renderContextPack, renderLessons, renderReport, renderReview, resolveCrossFileReferences, resolveNode, reviewRevisions, runPipeline, saveResult, scoreNodes, shortestPath, testsForChangedFiles, testsForNode, validateExtraction, validateRules };
package/dist/index.js CHANGED
@@ -1,43 +1,88 @@
1
1
  import {
2
2
  EXTRACTOR_REGISTRY,
3
+ ExtractionCache,
3
4
  ExtractionResultSchema,
4
5
  ExtractionValidationError,
5
6
  analyze,
6
7
  buildGraph,
8
+ checkRules,
7
9
  cluster,
8
10
  collectFiles,
11
+ diffGraphs,
9
12
  exportGraph,
10
13
  extract,
14
+ globToRegExp,
15
+ graphForDirectory,
16
+ loadResults,
17
+ mergeGraphs,
18
+ renderLessons,
11
19
  renderReport,
20
+ renderReview,
21
+ resolveCrossFileReferences,
22
+ reviewRevisions,
12
23
  runPipeline,
13
- validateExtraction
14
- } from "./chunk-DG5FECXV.js";
24
+ saveResult,
25
+ validateExtraction,
26
+ validateRules
27
+ } from "./chunk-EW23BLJC.js";
15
28
  import {
29
+ affectedBy,
30
+ buildContextPack,
16
31
  explainNode,
32
+ isTestFile,
17
33
  loadGraph,
18
34
  queryGraph,
35
+ rankForTask,
36
+ renderContextPack,
19
37
  resolveNode,
20
38
  scoreNodes,
21
- shortestPath
22
- } from "./chunk-5ANIDX3G.js";
39
+ shortestPath,
40
+ testsForChangedFiles,
41
+ testsForNode
42
+ } from "./chunk-YT7B6DOD.js";
43
+ import {
44
+ extractMysql
45
+ } from "./chunk-ZPB37LLQ.js";
46
+ import "./chunk-6JLEILYF.js";
23
47
  export {
24
48
  EXTRACTOR_REGISTRY,
49
+ ExtractionCache,
25
50
  ExtractionResultSchema,
26
51
  ExtractionValidationError,
52
+ affectedBy,
27
53
  analyze,
54
+ buildContextPack,
28
55
  buildGraph,
56
+ checkRules,
29
57
  cluster,
30
58
  collectFiles,
59
+ diffGraphs,
31
60
  explainNode,
32
61
  exportGraph,
33
62
  extract,
63
+ extractMysql,
64
+ globToRegExp,
65
+ graphForDirectory,
66
+ isTestFile,
34
67
  loadGraph,
68
+ loadResults,
69
+ mergeGraphs,
35
70
  queryGraph,
71
+ rankForTask,
72
+ renderContextPack,
73
+ renderLessons,
36
74
  renderReport,
75
+ renderReview,
76
+ resolveCrossFileReferences,
37
77
  resolveNode,
78
+ reviewRevisions,
38
79
  runPipeline,
80
+ saveResult,
39
81
  scoreNodes,
40
82
  shortestPath,
41
- validateExtraction
83
+ testsForChangedFiles,
84
+ testsForNode,
85
+ validateExtraction,
86
+ validateRules
42
87
  };
43
88
  //# sourceMappingURL=index.js.map