@shrkcrft/graph 0.1.0-alpha.17 → 0.1.0-alpha.18

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
@@ -25,6 +25,9 @@ export * from './indexer/resolve-imports.js';
25
25
  export * from './indexer/index-builder.js';
26
26
  export * from './indexer/incremental-updater.js';
27
27
  export * from './indexer/unresolved-imports.js';
28
+ export * from './indexer/call-graph-support.js';
29
+ export * from './indexer/resolve-reexports.js';
28
30
  export * from './query/query-api.js';
31
+ export * from './query/graph-api-cache.js';
29
32
  export * from './query/cycle-detection.js';
30
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAE3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAEhD,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAE3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,kCAAkC,CAAC;AACjD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,gCAAgC,CAAC;AAE/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -28,5 +28,8 @@ export * from "./indexer/resolve-imports.js";
28
28
  export * from "./indexer/index-builder.js";
29
29
  export * from "./indexer/incremental-updater.js";
30
30
  export * from "./indexer/unresolved-imports.js";
31
+ export * from "./indexer/call-graph-support.js";
32
+ export * from "./indexer/resolve-reexports.js";
31
33
  export * from "./query/query-api.js";
34
+ export * from "./query/graph-api-cache.js";
32
35
  export * from "./query/cycle-detection.js";
@@ -0,0 +1,3 @@
1
+ /** True when call/reference edges are extracted for this file language. */
2
+ export declare function hasCallGraphReferences(language: string | undefined): boolean;
3
+ //# sourceMappingURL=call-graph-support.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call-graph-support.d.ts","sourceRoot":"","sources":["../../src/indexer/call-graph-support.ts"],"names":[],"mappings":"AA6BA,2EAA2E;AAC3E,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE5E"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Languages whose dedicated extractor does NOT emit call/reference edges
3
+ * (`CallsSymbol` / `ReferencesSymbol`). Only the TS-family extractor
4
+ * (`extractTsFile`, for ts/tsx/js/jsx) walks an AST to build the call graph;
5
+ * the per-language extractors (Go, Python, Java, …) and the single-file
6
+ * component / SDL formats produce symbol + import nodes but no reference edges.
7
+ *
8
+ * Consumers (`graph callers`, `code_find_usages`) use this to tell an agent
9
+ * that an EMPTY caller list for a symbol in one of these files means
10
+ * "not tracked", not "nothing calls it" — so it doesn't read silence as proof.
11
+ */
12
+ const NON_CALL_GRAPH_LANGUAGES = new Set([
13
+ 'python',
14
+ 'go',
15
+ 'java',
16
+ 'rust',
17
+ 'kotlin',
18
+ 'ruby',
19
+ 'csharp',
20
+ 'elixir',
21
+ 'php',
22
+ 'dart',
23
+ 'swift',
24
+ 'vue',
25
+ 'svelte',
26
+ 'astro',
27
+ 'graphql',
28
+ ]);
29
+ /** True when call/reference edges are extracted for this file language. */
30
+ export function hasCallGraphReferences(language) {
31
+ return !language || !NON_CALL_GRAPH_LANGUAGES.has(language);
32
+ }
@@ -22,6 +22,24 @@ export interface IExtractedFile {
22
22
  * symbols and emits `references-symbol` / `calls-symbol` edges.
23
23
  */
24
24
  identifierReferences: readonly IIdentifierReference[];
25
+ /**
26
+ * `class C extends B` / `class C implements I` / `interface I extends J`
27
+ * heritage relationships. The post-pass resolves each base name (like a
28
+ * reference) and emits a typed `extends-symbol` / `implements-symbol` edge
29
+ * FROM the subject symbol TO the base — richer than the generic reference
30
+ * edge the base identifier also produces, so "who implements I" / "is C a
31
+ * subtype of B" are first-class graph queries. Optional: only the TS/TSX/JS
32
+ * extractor populates it — the per-language extractors omit it.
33
+ */
34
+ heritageReferences?: readonly IHeritageReference[];
35
+ }
36
+ export interface IHeritageReference {
37
+ /** The declaring class/interface name (resolves to `symbol:<path>#<subject>`). */
38
+ subjectName: string;
39
+ /** The referenced base type name (resolved like an identifier reference). */
40
+ baseName: string;
41
+ kind: 'extends' | 'implements';
42
+ line: number;
25
43
  }
26
44
  export interface IRawImportSpecifier {
27
45
  specifier: string;
@@ -37,6 +55,14 @@ export interface IImportBinding {
37
55
  specifier: string;
38
56
  isDefault: boolean;
39
57
  line: number;
58
+ /**
59
+ * `import type { X }` / `import { type X }` — a type-only binding. These are
60
+ * NOT used to resolve value references (a type-only import is not a runtime
61
+ * use), but ARE used to resolve heritage clauses (`implements X` where `X` is
62
+ * an interface, near-always imported `import type`). Without this an
63
+ * interface implemented across files would never connect to its implementers.
64
+ */
65
+ isTypeOnly?: boolean;
40
66
  }
41
67
  export interface IIdentifierReference {
42
68
  /** Identifier text at the use site. */
@@ -1 +1 @@
1
- {"version":3,"file":"extract-ts-file.d.ts","sourceRoot":"","sources":["../../src/indexer/extract-ts-file.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAc3D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,SAAS,KAAK,EAAE,CAAC;IAC9B,KAAK,EAAE,SAAS,KAAK,EAAE,CAAC;IACxB,uEAAuE;IACvE,mBAAmB,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACpD;;;;;;OAMG;IACH,cAAc,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C;;;;OAIG;IACH,oBAAoB,EAAE,SAAS,oBAAoB,EAAE,CAAC;CACvD;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,gBAAgB,EAC7B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,cAAc,CA+EhB;AA6ID;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,iDAAiD;IACjD,uBAAuB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACjE,yEAAyE;IACzE,0BAA0B,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzD,GAAG,SAAS,KAAK,EAAE,CA+BnB"}
1
+ {"version":3,"file":"extract-ts-file.d.ts","sourceRoot":"","sources":["../../src/indexer/extract-ts-file.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG/C,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAqB3D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,KAAK,CAAC;IAChB,WAAW,EAAE,SAAS,KAAK,EAAE,CAAC;IAC9B,KAAK,EAAE,SAAS,KAAK,EAAE,CAAC;IACxB,uEAAuE;IACvE,mBAAmB,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACpD;;;;;;OAMG;IACH,cAAc,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C;;;;OAIG;IACH,oBAAoB,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACtD;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACpD;AAED,MAAM,WAAW,kBAAkB;IACjC,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,YAAY,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,gBAAgB,EAC7B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,cAAc,CA+EhB;AAgQD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACtD,iDAAiD;IACjD,uBAAuB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACjE,yEAAyE;IACzE,0BAA0B,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzD,GAAG,SAAS,KAAK,EAAE,CAkDnB"}
@@ -7,11 +7,18 @@ import { EdgeKind } from "../schema/edge-kind.js";
7
7
  import { NodeKind } from "../schema/node-kind.js";
8
8
  export const EXTRACT_TS_FILE_SOURCE = 'extract-ts-file@v1';
9
9
  /**
10
- * Per-file import scan. The boundaries package owns the project-wide
11
- * `scanImports`; for the graph extractor we need a per-file pass that
12
- * runs against a buffer we already have in hand. The regexes mirror
13
- * `boundaries/scan-imports.ts` deliberately — keeping them in sync is a
14
- * test obligation, not a runtime one.
10
+ * Per-file import scan for NON-TS single-file-component formats
11
+ * (Vue/Svelte/Astro `<script>` blocks) where no TypeScript AST is
12
+ * available.
13
+ *
14
+ * TS/TSX/JS/JSX files derive their import specifiers from the AST in
15
+ * `walkAst` instead — that is comment- and string-literal-safe, so an
16
+ * `import ... from '...'` that appears inside a comment, JSDoc block, a
17
+ * template-literal body (e.g. a generator template), rule documentation,
18
+ * or a test fixture string is never mistaken for a real import (the
19
+ * regexes below would, and that produced phantom `unresolved:` edges and
20
+ * phantom arch violations). The regexes still mirror
21
+ * `boundaries/scan-imports.ts` for the SFC formats that need them.
15
22
  */
16
23
  const IMPORT_RE = /(?:^|\s)(?:import|export)\s+[^'"`]*?from\s+['"]([^'"`]+)['"]/g;
17
24
  const SIDE_EFFECT_IMPORT_RE = /(?:^|\s)import\s+['"]([^'"`]+)['"]/g;
@@ -79,8 +86,7 @@ export function extractTsFile(fingerprint, absPath, content) {
79
86
  line: re.line,
80
87
  }));
81
88
  }
82
- const rawImportSpecifiers = scanFileImports(text);
83
- const { importBindings, identifierReferences } = walkAst(absPath, text);
89
+ const { importBindings, identifierReferences, rawImportSpecifiers, heritageReferences } = walkAst(absPath, text);
84
90
  return {
85
91
  fileNode,
86
92
  symbolNodes,
@@ -88,6 +94,7 @@ export function extractTsFile(fingerprint, absPath, content) {
88
94
  rawImportSpecifiers,
89
95
  importBindings,
90
96
  identifierReferences,
97
+ heritageReferences,
91
98
  };
92
99
  }
93
100
  function pickScriptKind(ext) {
@@ -116,10 +123,52 @@ function walkAst(absPath, text) {
116
123
  sf = ts.createSourceFile(absPath, text, ts.ScriptTarget.Latest, true, pickScriptKind(ext));
117
124
  }
118
125
  catch {
119
- return { importBindings: [], identifierReferences: [] };
126
+ // AST unavailable fall back to the raw-text regex so we don't lose
127
+ // import edges entirely (rare; createSourceFile is very lenient).
128
+ return {
129
+ importBindings: [],
130
+ identifierReferences: [],
131
+ rawImportSpecifiers: scanFileImports(text),
132
+ heritageReferences: [],
133
+ };
120
134
  }
121
135
  const bindings = [];
122
136
  const refs = [];
137
+ const rawSpecs = [];
138
+ const heritage = [];
139
+ // AST-sourced import specifiers (comment- and string-literal-safe). Every
140
+ // real import shape the regex caught must be reproduced here or a genuine
141
+ // unresolved/cross-package import would stop being flagged: static + side
142
+ // effect + type-only imports, `export ... from`, `import x = require()`,
143
+ // and dynamic `import()` / `require()` calls (the last two collected in
144
+ // the visitor below).
145
+ for (const stmt of sf.statements) {
146
+ if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier)) {
147
+ rawSpecs.push({
148
+ specifier: stmt.moduleSpecifier.text,
149
+ line: lineOf(sf, stmt),
150
+ kind: stmt.importClause ? 'static' : 'side-effect',
151
+ });
152
+ }
153
+ else if (ts.isExportDeclaration(stmt) &&
154
+ stmt.moduleSpecifier &&
155
+ ts.isStringLiteral(stmt.moduleSpecifier)) {
156
+ rawSpecs.push({
157
+ specifier: stmt.moduleSpecifier.text,
158
+ line: lineOf(sf, stmt),
159
+ kind: 'static',
160
+ });
161
+ }
162
+ else if (ts.isImportEqualsDeclaration(stmt) &&
163
+ ts.isExternalModuleReference(stmt.moduleReference) &&
164
+ ts.isStringLiteral(stmt.moduleReference.expression)) {
165
+ rawSpecs.push({
166
+ specifier: stmt.moduleReference.expression.text,
167
+ line: lineOf(sf, stmt),
168
+ kind: 'require',
169
+ });
170
+ }
171
+ }
123
172
  for (const stmt of sf.statements) {
124
173
  if (!ts.isImportDeclaration(stmt))
125
174
  continue;
@@ -129,8 +178,9 @@ function walkAst(absPath, text) {
129
178
  const clause = stmt.importClause;
130
179
  if (!clause)
131
180
  continue;
132
- if (clause.isTypeOnly)
133
- continue;
181
+ // Type-only imports are TAGGED (not skipped) so heritage resolution can use
182
+ // them; the value-reference pass still ignores them (see stitchPerFileReferences).
183
+ const clauseTypeOnly = clause.isTypeOnly;
134
184
  if (clause.name) {
135
185
  bindings.push({
136
186
  localName: clause.name.text,
@@ -138,18 +188,19 @@ function walkAst(absPath, text) {
138
188
  specifier,
139
189
  isDefault: true,
140
190
  line: lineOf(sf, clause.name),
191
+ ...(clauseTypeOnly ? { isTypeOnly: true } : {}),
141
192
  });
142
193
  }
143
194
  if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
144
195
  for (const elem of clause.namedBindings.elements) {
145
- if (elem.isTypeOnly)
146
- continue;
196
+ const typeOnly = clauseTypeOnly || elem.isTypeOnly;
147
197
  bindings.push({
148
198
  localName: elem.name.text,
149
199
  importedName: elem.propertyName ? elem.propertyName.text : elem.name.text,
150
200
  specifier,
151
201
  isDefault: false,
152
202
  line: lineOf(sf, elem),
203
+ ...(typeOnly ? { isTypeOnly: true } : {}),
153
204
  });
154
205
  }
155
206
  }
@@ -159,6 +210,36 @@ function walkAst(absPath, text) {
159
210
  // Skip the declaration sites we already harvested.
160
211
  if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node))
161
212
  return;
213
+ // Dynamic `import('...')` and `require('...')` calls — real call nodes,
214
+ // so text inside comments/strings never matches.
215
+ if (ts.isCallExpression(node)) {
216
+ const arg0 = node.arguments[0];
217
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
218
+ if (arg0 && ts.isStringLiteral(arg0)) {
219
+ rawSpecs.push({ specifier: arg0.text, line: lineOf(sf, node), kind: 'dynamic' });
220
+ }
221
+ }
222
+ else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
223
+ if (arg0 && ts.isStringLiteral(arg0)) {
224
+ rawSpecs.push({ specifier: arg0.text, line: lineOf(sf, node), kind: 'require' });
225
+ }
226
+ }
227
+ }
228
+ // `class C extends B implements I` / `interface I extends J` — record the
229
+ // typed heritage relationship. The base identifier is ALSO captured as a
230
+ // generic reference below (kept, so reference-based queries still see it);
231
+ // the post-pass adds the richer extends/implements edge on top.
232
+ if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.name && node.heritageClauses) {
233
+ const subjectName = node.name.text;
234
+ for (const clause of node.heritageClauses) {
235
+ const hkind = clause.token === ts.SyntaxKind.ExtendsKeyword ? 'extends' : 'implements';
236
+ for (const t of clause.types) {
237
+ const baseName = headIdentifierName(t.expression);
238
+ if (baseName)
239
+ heritage.push({ subjectName, baseName, kind: hkind, line: lineOf(sf, t.expression) });
240
+ }
241
+ }
242
+ }
162
243
  if (ts.isIdentifier(node) && !isDeclarationName(node)) {
163
244
  refs.push({
164
245
  name: node.text,
@@ -180,7 +261,46 @@ function walkAst(absPath, text) {
180
261
  seen.add(k);
181
262
  dedupedRefs.push(r);
182
263
  }
183
- return { importBindings: bindings, identifierReferences: dedupedRefs };
264
+ // De-dupe identical (kind, specifier, line) — same contract scanFileImports
265
+ // applied to the regex output.
266
+ const seenSpecs = new Set();
267
+ const dedupedSpecs = [];
268
+ for (const s of rawSpecs) {
269
+ const k = `${s.kind}|${s.specifier}|${s.line}`;
270
+ if (seenSpecs.has(k))
271
+ continue;
272
+ seenSpecs.add(k);
273
+ dedupedSpecs.push(s);
274
+ }
275
+ // De-dupe identical (subject, base, kind) heritage triples.
276
+ const seenHeritage = new Set();
277
+ const dedupedHeritage = [];
278
+ for (const h of heritage) {
279
+ const k = `${h.subjectName}|${h.baseName}|${h.kind}`;
280
+ if (seenHeritage.has(k))
281
+ continue;
282
+ seenHeritage.add(k);
283
+ dedupedHeritage.push(h);
284
+ }
285
+ return {
286
+ importBindings: bindings,
287
+ identifierReferences: dedupedRefs,
288
+ rawImportSpecifiers: dedupedSpecs,
289
+ heritageReferences: dedupedHeritage,
290
+ };
291
+ }
292
+ /**
293
+ * The leftmost identifier name of a heritage base expression: `Base` for
294
+ * `Base`, `Base<T>` (already unwrapped — `t.expression` is the head), and the
295
+ * leftmost for a qualified `ns.Base` (returns `ns`, matching how the generic
296
+ * reference walk resolves `ns` — namespace-qualified bases stay best-effort).
297
+ */
298
+ function headIdentifierName(expr) {
299
+ if (ts.isIdentifier(expr))
300
+ return expr.text;
301
+ if (ts.isPropertyAccessExpression(expr))
302
+ return headIdentifierName(expr.expression);
303
+ return undefined;
184
304
  }
185
305
  function isDeclarationName(id) {
186
306
  const parent = id.parent;
@@ -247,18 +367,21 @@ function lineOf(sf, node) {
247
367
  */
248
368
  export function stitchPerFileReferences(input) {
249
369
  const { fileNodeId, extracted, resolvedSpec, defaultExportNameByPath, localSymbolNamesInThisFile } = input;
370
+ // `bindings` resolves VALUE references (type-only imports excluded — a type
371
+ // import is not a runtime use). `heritageBindings` additionally includes
372
+ // type-only imports, used only for `implements`/`extends` resolution.
250
373
  const bindings = new Map();
374
+ const heritageBindings = new Map();
251
375
  for (const b of extracted.importBindings) {
252
376
  const targetPath = resolvedSpec.get(b.specifier);
253
377
  if (!targetPath)
254
378
  continue;
255
- if (b.isDefault) {
256
- const defName = defaultExportNameByPath.get(targetPath);
257
- bindings.set(b.localName, `symbol:${targetPath}#${defName ?? 'default'}`);
258
- }
259
- else {
260
- bindings.set(b.localName, `symbol:${targetPath}#${b.importedName}`);
261
- }
379
+ const id = b.isDefault
380
+ ? `symbol:${targetPath}#${defaultExportNameByPath.get(targetPath) ?? 'default'}`
381
+ : `symbol:${targetPath}#${b.importedName}`;
382
+ heritageBindings.set(b.localName, id);
383
+ if (!b.isTypeOnly)
384
+ bindings.set(b.localName, id);
262
385
  }
263
386
  const out = [];
264
387
  const seen = new Set();
@@ -281,6 +404,25 @@ export function stitchPerFileReferences(input) {
281
404
  seen.add(edgeKey);
282
405
  out.push(buildEdge(fileNodeId, target, kind, { line: r.line }));
283
406
  }
407
+ // Typed heritage edges, FROM the subject symbol TO the resolved base symbol.
408
+ // Resolved like a reference (import binding or local symbol); a base in an
409
+ // external lib (`extends Error`) resolves to nothing and is skipped, so only
410
+ // intra-project subtype relationships become edges.
411
+ const filePath = fileNodeId.replace(/^file:/, '');
412
+ for (const h of extracted.heritageReferences ?? []) {
413
+ const target = heritageBindings.get(h.baseName) ?? localSymbolNamesInThisFile.get(h.baseName);
414
+ if (!target)
415
+ continue;
416
+ const subjectId = `symbol:${filePath}#${h.subjectName}`;
417
+ if (subjectId === target)
418
+ continue; // ignore self-reference
419
+ const kind = h.kind === 'extends' ? EdgeKind.ExtendsSymbol : EdgeKind.ImplementsSymbol;
420
+ const edgeKey = `${subjectId}|${target}|${kind}`;
421
+ if (seen.has(edgeKey))
422
+ continue;
423
+ seen.add(edgeKey);
424
+ out.push(buildEdge(subjectId, target, kind, { line: h.line }));
425
+ }
284
426
  return out;
285
427
  }
286
428
  function makeFileNodeForNonTs(fp) {
@@ -24,10 +24,28 @@ export interface IIncrementalUpdateResult {
24
24
  * append/compact is the optimisation when the cold-rewrite cost is felt.
25
25
  */
26
26
  export declare function updateChanged(options: IIncrementalUpdateOptions): IIncrementalUpdateResult;
27
+ export interface IGraphFreshness {
28
+ hasIndex: boolean;
29
+ lastIndexedAt?: string;
30
+ /** Indexed files whose on-disk content changed since the index was built. */
31
+ modified: readonly string[];
32
+ /** Source files on disk that are not in the index yet. */
33
+ added: readonly string[];
34
+ /** Indexed files that no longer exist on disk. */
35
+ deleted: readonly string[];
36
+ }
37
+ /**
38
+ * Walk the project and categorise every source file against the stored
39
+ * snapshot: `modified` (indexed but content changed), `added` (on disk, not
40
+ * indexed), `deleted` (indexed, gone from disk). This is the per-file truth
41
+ * behind honest `graph status` freshness and the targeted per-query staleness
42
+ * check — so an agent never gets a silently-stale answer for a file it just
43
+ * edited. Outputs are sorted for determinism.
44
+ */
45
+ export declare function detectGraphFreshness(projectRoot: string): IGraphFreshness;
27
46
  /**
28
- * Helper: detect changed files by walking the project and comparing
29
- * fingerprints against the stored snapshot. Used by
30
- * `shrk graph index --changed` when no explicit file list is provided.
47
+ * Back-compat adapter for `shrk graph index --changed`: `changed` is the
48
+ * union of modified + added (both need re-extraction); `deleted` unchanged.
31
49
  */
32
50
  export declare function detectChangedAndDeleted(projectRoot: string): {
33
51
  changed: readonly string[];
@@ -1 +1 @@
1
- {"version":3,"file":"incremental-updater.d.ts","sourceRoot":"","sources":["../../src/indexer/incremental-updater.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAyC5D,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,2CAA2C;IAC3C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,oCAAoC;IACpC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,qEAAqE;IACrE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,yBAAyB,GACjC,wBAAwB,CAwM1B;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG;IAC5D,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,CAqEA;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAcrF"}
1
+ {"version":3,"file":"incremental-updater.d.ts","sourceRoot":"","sources":["../../src/indexer/incremental-updater.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA0C5D,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,2CAA2C;IAC3C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,oCAAoC;IACpC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,qEAAqE;IACrE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,yBAAyB,GACjC,wBAAwB,CAiN1B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,0DAA0D;IAC1D,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,kDAAkD;IAClD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,CA+EzE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG;IAC5D,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,CAMA;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAcrF"}
@@ -8,6 +8,7 @@ import { fingerprintFile } from "../store/file-fingerprint.js";
8
8
  import { GraphStore } from "../store/graph-store.js";
9
9
  import { summarizeCycles } from "../query/cycle-detection.js";
10
10
  import { summarizeUnresolvedImports } from "./unresolved-imports.js";
11
+ import { resolveReExportedReferenceEdges } from "./resolve-reexports.js";
11
12
  import { detectWorkspacePackages, } from "./detect-workspace.js";
12
13
  import { EXTRACT_TS_FILE_SOURCE, extractTsFile, stitchPerFileReferences, } from "./extract-ts-file.js";
13
14
  import { extractPythonFile } from "./extract-python-file.js";
@@ -179,7 +180,17 @@ export function updateChanged(options) {
179
180
  edges.set(e.id, e);
180
181
  }
181
182
  const nodeList = [...nodes.values()];
182
- const edgeList = [...edges.values()];
183
+ // Resolve barrel re-export chains (rewrites phantom cross-package
184
+ // reference/call targets to the real symbol), then de-dupe since a rewrite
185
+ // can collide a rewritten edge id with an existing one.
186
+ const seenEdge = new Set();
187
+ const edgeList = [];
188
+ for (const e of resolveReExportedReferenceEdges(nodeList, [...edges.values()])) {
189
+ if (seenEdge.has(e.id))
190
+ continue;
191
+ seenEdge.add(e.id);
192
+ edgeList.push(e);
193
+ }
183
194
  const cycles = summarizeCycles(nodeList, edgeList);
184
195
  const unresolved = summarizeUnresolvedImports(edgeList);
185
196
  const manifest = store.writeSnapshot(nodeList, edgeList, [...files.values()], {
@@ -206,18 +217,22 @@ export function updateChanged(options) {
206
217
  };
207
218
  }
208
219
  /**
209
- * Helper: detect changed files by walking the project and comparing
210
- * fingerprints against the stored snapshot. Used by
211
- * `shrk graph index --changed` when no explicit file list is provided.
220
+ * Walk the project and categorise every source file against the stored
221
+ * snapshot: `modified` (indexed but content changed), `added` (on disk, not
222
+ * indexed), `deleted` (indexed, gone from disk). This is the per-file truth
223
+ * behind honest `graph status` freshness and the targeted per-query staleness
224
+ * check — so an agent never gets a silently-stale answer for a file it just
225
+ * edited. Outputs are sorted for determinism.
212
226
  */
213
- export function detectChangedAndDeleted(projectRoot) {
227
+ export function detectGraphFreshness(projectRoot) {
214
228
  const store = new GraphStore(projectRoot);
215
229
  if (!store.exists()) {
216
- return { changed: [], deleted: [] };
230
+ return { hasIndex: false, modified: [], added: [], deleted: [] };
217
231
  }
218
232
  const snap = store.loadSnapshot();
219
233
  const seen = new Set();
220
- const changed = [];
234
+ const modified = [];
235
+ const added = [];
221
236
  const fsStack = [projectRoot];
222
237
  const skip = new Set([
223
238
  'node_modules',
@@ -269,7 +284,7 @@ export function detectChangedAndDeleted(projectRoot) {
269
284
  seen.add(rel);
270
285
  const oldFp = snap.files.get(rel);
271
286
  if (!oldFp) {
272
- changed.push(rel);
287
+ added.push(rel);
273
288
  continue;
274
289
  }
275
290
  // Cheap check: mtime first. If equal, trust it (assumes mtime
@@ -279,7 +294,7 @@ export function detectChangedAndDeleted(projectRoot) {
279
294
  continue;
280
295
  const newFp = fingerprintFile(full, projectRoot);
281
296
  if (newFp.sha1 !== oldFp.sha1)
282
- changed.push(rel);
297
+ modified.push(rel);
283
298
  }
284
299
  }
285
300
  const deleted = [];
@@ -287,7 +302,27 @@ export function detectChangedAndDeleted(projectRoot) {
287
302
  if (!seen.has(path))
288
303
  deleted.push(path);
289
304
  }
290
- return { changed, deleted };
305
+ modified.sort((a, b) => a.localeCompare(b));
306
+ added.sort((a, b) => a.localeCompare(b));
307
+ deleted.sort((a, b) => a.localeCompare(b));
308
+ return {
309
+ hasIndex: true,
310
+ lastIndexedAt: snap.manifest.lastIndexedAt,
311
+ modified,
312
+ added,
313
+ deleted,
314
+ };
315
+ }
316
+ /**
317
+ * Back-compat adapter for `shrk graph index --changed`: `changed` is the
318
+ * union of modified + added (both need re-extraction); `deleted` unchanged.
319
+ */
320
+ export function detectChangedAndDeleted(projectRoot) {
321
+ const f = detectGraphFreshness(projectRoot);
322
+ return {
323
+ changed: [...f.modified, ...f.added].sort((a, b) => a.localeCompare(b)),
324
+ deleted: f.deleted,
325
+ };
291
326
  }
292
327
  /**
293
328
  * Get the list of files changed since a git ref (e.g. `main`, `HEAD~5`,
@@ -1 +1 @@
1
- {"version":3,"file":"index-builder.d.ts","sourceRoot":"","sources":["../../src/indexer/index-builder.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA6D5D,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CA0I9E"}
1
+ {"version":3,"file":"index-builder.d.ts","sourceRoot":"","sources":["../../src/indexer/index-builder.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AA8D5D,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,gBAAgB,CAkJ9E"}
@@ -21,6 +21,7 @@ import { extractPhpFile } from "./extract-php-file.js";
21
21
  import { extractDartFile } from "./extract-dart-file.js";
22
22
  import { extractSwiftFile } from "./extract-swift-file.js";
23
23
  import { createImportResolverContext, ImportResolution, resolveImport, } from "./resolve-imports.js";
24
+ import { resolveReExportedReferenceEdges } from "./resolve-reexports.js";
24
25
  const SOURCE_EXTS = new Set([
25
26
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
26
27
  // Web component formats — parsed by framework-scanners; the TS-AST
@@ -122,7 +123,9 @@ export function buildFullIndex(options) {
122
123
  else {
123
124
  const externalId = r.kind === ImportResolution.External
124
125
  ? `external:${r.specifier}`
125
- : `unresolved:${r.specifier}`;
126
+ : r.kind === ImportResolution.Asset
127
+ ? `asset:${r.specifier}`
128
+ : `unresolved:${r.specifier}`;
126
129
  edges.push(buildEdge(fp.nodeId, externalId, EdgeKind.ImportsFile, EXTRACT_TS_FILE_SOURCE, data));
127
130
  }
128
131
  }
@@ -144,15 +147,20 @@ export function buildFullIndex(options) {
144
147
  for (const e of refEdges)
145
148
  edges.push(e);
146
149
  }
150
+ // Resolve barrel re-export chains so reference/call edges that point at a
151
+ // phantom `symbol:<barrel>#name` are rewritten to the real declaring
152
+ // symbol — otherwise cross-package consumers (which import from a package
153
+ // barrel) never show up in `graph callers` / impact.
154
+ const resolvedEdges = resolveReExportedReferenceEdges(nodes, edges);
147
155
  // PackageDependsOn aggregates: collapse internal ImportsFile edges to
148
156
  // their owning package on both sides.
149
- collectPackageDependsOn(edges, packageDirIndex);
157
+ collectPackageDependsOn(resolvedEdges, packageDirIndex);
150
158
  // Drop duplicate edges (extractor may emit identical edges for `export
151
159
  // { foo } from './foo'` and an `import` re-using the same line — same
152
- // hashed id, last write wins). Edge dedupe by id.
160
+ // hashed id, last write wins; a re-export rewrite can also collide ids).
153
161
  const seen = new Set();
154
162
  const dedupedEdges = [];
155
- for (const e of edges) {
163
+ for (const e of resolvedEdges) {
156
164
  if (seen.has(e.id))
157
165
  continue;
158
166
  seen.add(e.id);
@@ -5,6 +5,7 @@ export declare enum ImportResolution {
5
5
  Alias = "alias",
6
6
  Workspace = "workspace",
7
7
  External = "external",
8
+ Asset = "asset",
8
9
  Unresolved = "unresolved"
9
10
  }
10
11
  export interface IResolvedImport {
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-imports.d.ts","sourceRoot":"","sources":["../../src/indexer/resolve-imports.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAI/D,oBAAY,gBAAgB;IAC1B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,QAAQ,aAAa;IACrB,UAAU,eAAe;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAED,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,GAC9C,sBAAsB,CAMxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,sBAAsB,GAC1B,eAAe,CAwCjB"}
1
+ {"version":3,"file":"resolve-imports.d.ts","sourceRoot":"","sources":["../../src/indexer/resolve-imports.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAmB/D,oBAAY,gBAAgB;IAC1B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,UAAU,eAAe;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAChD,aAAa,EAAE,iBAAiB,CAAC;CAClC;AAED,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,SAAS,iBAAiB,EAAE,GAC9C,sBAAsB,CAMxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,sBAAsB,GAC1B,eAAe,CAkDjB"}
@@ -2,12 +2,27 @@ import { existsSync, statSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
3
  import { loadTsconfigPaths, resolveAliasCandidates, } from '@shrkcrft/boundaries';
4
4
  const PROBE_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
5
+ /**
6
+ * Extensions for non-code assets a TS/JS file can legitimately import
7
+ * (CSS modules, JSON, images, fonts, wasm). The graph only indexes source
8
+ * files, so these never resolve to a file node — but an asset that EXISTS
9
+ * on disk is NOT an unresolved import (the bundler handles it), whereas a
10
+ * missing one still is a real broken reference.
11
+ */
12
+ const NON_CODE_ASSET_EXTS = new Set([
13
+ '.css', '.scss', '.sass', '.less', '.styl',
14
+ '.json', '.json5',
15
+ '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.bmp',
16
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
17
+ '.wasm',
18
+ ]);
5
19
  export var ImportResolution;
6
20
  (function (ImportResolution) {
7
21
  ImportResolution["Relative"] = "relative";
8
22
  ImportResolution["Alias"] = "alias";
9
23
  ImportResolution["Workspace"] = "workspace";
10
24
  ImportResolution["External"] = "external";
25
+ ImportResolution["Asset"] = "asset";
11
26
  ImportResolution["Unresolved"] = "unresolved";
12
27
  })(ImportResolution || (ImportResolution = {}));
13
28
  export function createImportResolverContext(projectRoot, workspacePackages) {
@@ -32,7 +47,17 @@ export function createImportResolverContext(projectRoot, workspacePackages) {
32
47
  export function resolveImport(specifier, fromAbsPath, ctx) {
33
48
  if (specifier.startsWith('.')) {
34
49
  const dir = nodePath.dirname(fromAbsPath);
35
- const probe = probeCandidate(nodePath.resolve(dir, specifier));
50
+ const abs = nodePath.resolve(dir, specifier);
51
+ const ext = nodePath.extname(specifier).toLowerCase();
52
+ if (NON_CODE_ASSET_EXTS.has(ext)) {
53
+ // Existing asset → resolved-enough (not counted as unresolved); a
54
+ // missing asset stays Unresolved so a real broken reference is caught.
55
+ if (existsSafe(abs) && isFile(abs)) {
56
+ return { specifier, kind: ImportResolution.Asset };
57
+ }
58
+ return { specifier, kind: ImportResolution.Unresolved };
59
+ }
60
+ const probe = probeCandidate(abs);
36
61
  if (probe) {
37
62
  return {
38
63
  specifier,