weavatrix 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +90 -21
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +108 -43
  6. package/src/analysis/allowed-test-runner.js +20 -9
  7. package/src/analysis/architecture/contract-graph.js +119 -0
  8. package/src/analysis/architecture/contract-schema.js +110 -0
  9. package/src/analysis/architecture/contract-storage.js +35 -0
  10. package/src/analysis/architecture/contract-verification.js +168 -0
  11. package/src/analysis/architecture-contract.js +16 -343
  12. package/src/analysis/change-classification/diff-parser.js +103 -0
  13. package/src/analysis/change-classification/options.js +40 -0
  14. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  15. package/src/analysis/change-classification.js +120 -519
  16. package/src/analysis/dead-code-review/policy.js +23 -0
  17. package/src/analysis/dead-code-review.js +9 -19
  18. package/src/analysis/dep-check.js +10 -57
  19. package/src/analysis/dep-rules.js +10 -303
  20. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  21. package/src/analysis/endpoints/common.js +62 -0
  22. package/src/analysis/endpoints/extract.js +107 -0
  23. package/src/analysis/endpoints/inventory.js +84 -0
  24. package/src/analysis/endpoints/mounts.js +129 -0
  25. package/src/analysis/endpoints-java.js +80 -3
  26. package/src/analysis/endpoints.js +3 -448
  27. package/src/analysis/git-history/analytics.js +177 -0
  28. package/src/analysis/git-history/collector.js +109 -0
  29. package/src/analysis/git-history/options.js +68 -0
  30. package/src/analysis/git-history.js +128 -577
  31. package/src/analysis/health-capabilities.js +82 -0
  32. package/src/analysis/http-contracts/analysis.js +169 -0
  33. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  34. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  35. package/src/analysis/http-contracts/graph-context.js +143 -0
  36. package/src/analysis/http-contracts/matching.js +61 -0
  37. package/src/analysis/http-contracts/shared.js +86 -0
  38. package/src/analysis/http-contracts.js +6 -822
  39. package/src/analysis/internal-audit/dependency-health.js +111 -0
  40. package/src/analysis/internal-audit/python-manifests.js +65 -0
  41. package/src/analysis/internal-audit/repo-files.js +55 -0
  42. package/src/analysis/internal-audit/supply-chain.js +132 -0
  43. package/src/analysis/internal-audit.collect.js +5 -106
  44. package/src/analysis/internal-audit.run.js +113 -200
  45. package/src/analysis/jvm-dependency-evidence.js +69 -0
  46. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  47. package/src/analysis/source-correctness.js +225 -0
  48. package/src/analysis/structure/dependency-graph.js +156 -0
  49. package/src/analysis/structure/findings.js +142 -0
  50. package/src/graph/builder/go-receiver-resolution.js +112 -0
  51. package/src/graph/builder/js/queries.js +48 -0
  52. package/src/graph/builder/lang-go.js +89 -4
  53. package/src/graph/builder/lang-js.js +4 -44
  54. package/src/graph/builder/pass2-resolution.js +68 -0
  55. package/src/graph/freshness-probe.js +2 -1
  56. package/src/graph/incremental-refresh.js +3 -2
  57. package/src/graph/internal-builder.build.js +22 -183
  58. package/src/graph/internal-builder.pass2.js +161 -0
  59. package/src/graph/internal-builder.resolvers.js +2 -105
  60. package/src/graph/resolvers/rust.js +117 -0
  61. package/src/mcp/actions/advisories.mjs +18 -0
  62. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  63. package/src/mcp/actions/graph-sync.mjs +195 -0
  64. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  65. package/src/mcp/architecture-bootstrap.mjs +168 -0
  66. package/src/mcp/architecture-starter.mjs +234 -0
  67. package/src/mcp/catalog.mjs +22 -6
  68. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  69. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  70. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  71. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  72. package/src/mcp/graph/context-core.mjs +111 -0
  73. package/src/mcp/graph/context-seeds.mjs +208 -0
  74. package/src/mcp/graph/context-state.mjs +86 -0
  75. package/src/mcp/graph/tools-core.mjs +143 -0
  76. package/src/mcp/graph/tools-query.mjs +223 -0
  77. package/src/mcp/graph-context.mjs +4 -496
  78. package/src/mcp/health/audit-format.mjs +172 -0
  79. package/src/mcp/health/audit.mjs +171 -0
  80. package/src/mcp/health/dead-code.mjs +110 -0
  81. package/src/mcp/health/duplicates.mjs +83 -0
  82. package/src/mcp/health/endpoints.mjs +42 -0
  83. package/src/mcp/health/structure.mjs +219 -0
  84. package/src/mcp/runtime-version.mjs +32 -0
  85. package/src/mcp/server/auto-refresh.mjs +66 -0
  86. package/src/mcp/server/runtime-config.mjs +43 -0
  87. package/src/mcp/server/shutdown.mjs +40 -0
  88. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  89. package/src/mcp/sync/evidence-common.mjs +88 -0
  90. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  91. package/src/mcp/sync/evidence-health.mjs +56 -0
  92. package/src/mcp/sync/evidence-packages.mjs +95 -0
  93. package/src/mcp/sync/payload-common.mjs +97 -0
  94. package/src/mcp/sync/payload-v2.mjs +53 -0
  95. package/src/mcp/sync/payload-v3.mjs +79 -0
  96. package/src/mcp/sync-evidence.mjs +7 -402
  97. package/src/mcp/sync-payload.mjs +10 -244
  98. package/src/mcp/tools-actions.mjs +18 -414
  99. package/src/mcp/tools-architecture.mjs +18 -70
  100. package/src/mcp/tools-context.mjs +56 -15
  101. package/src/mcp/tools-endpoints.mjs +4 -1
  102. package/src/mcp/tools-graph.mjs +17 -330
  103. package/src/mcp/tools-health.mjs +24 -705
  104. package/src/mcp/tools-verified-change.mjs +12 -4
  105. package/src/mcp-server.mjs +49 -146
  106. package/src/precision/lsp-client/constants.js +12 -0
  107. package/src/precision/lsp-client/environment.js +20 -0
  108. package/src/precision/lsp-client/errors.js +15 -0
  109. package/src/precision/lsp-client/lifecycle.js +127 -0
  110. package/src/precision/lsp-client/message-parser.js +81 -0
  111. package/src/precision/lsp-client/protocol.js +212 -0
  112. package/src/precision/lsp-client/registry.js +58 -0
  113. package/src/precision/lsp-client/repo-uri.js +60 -0
  114. package/src/precision/lsp-client/stdio-client.js +151 -0
  115. package/src/precision/lsp-client.js +10 -682
  116. package/src/precision/lsp-overlay/build.js +238 -0
  117. package/src/precision/lsp-overlay/contract.js +61 -0
  118. package/src/precision/lsp-overlay/reference-results.js +108 -0
  119. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  120. package/src/precision/lsp-overlay/source-session.js +134 -0
  121. package/src/precision/lsp-overlay/store.js +150 -0
  122. package/src/precision/lsp-overlay/target-index.js +147 -0
  123. package/src/precision/lsp-overlay/target-query.js +55 -0
  124. package/src/precision/lsp-overlay.js +11 -868
  125. package/src/precision/typescript-lsp-provider.js +12 -682
  126. package/src/precision/typescript-provider/client.js +69 -0
  127. package/src/precision/typescript-provider/discovery.js +124 -0
  128. package/src/precision/typescript-provider/project-host.js +249 -0
  129. package/src/precision/typescript-provider/project-safety.js +161 -0
  130. package/src/precision/typescript-provider/reference-usage.js +53 -0
  131. package/src/version.js +7 -0
  132. package/docs/releases/v0.2.12.md +0 -45
@@ -0,0 +1,48 @@
1
+ export const CALLABLE = /arrow_function|function|function_expression|generator_function/
2
+
3
+ export const FUNCS = `
4
+ (function_declaration name: (identifier) @fn)
5
+ (generator_function_declaration name: (identifier) @fn)
6
+ (class_declaration name: (_) @class)
7
+ (method_definition name: (_) @method)`
8
+
9
+ export const TOPVARS = `
10
+ (program (lexical_declaration (variable_declarator) @decl))
11
+ (program (variable_declaration (variable_declarator) @decl))
12
+ (program (export_statement (lexical_declaration (variable_declarator) @decl)))
13
+ (program (export_statement (variable_declaration (variable_declarator) @decl)))`
14
+
15
+ export const TYPES = `
16
+ (interface_declaration name: (_) @interface)
17
+ (type_alias_declaration name: (_) @type)
18
+ (enum_declaration name: (_) @enum)`
19
+
20
+ export const REQUIRE = `(variable_declarator name: (_) @lhs value: (call_expression function: (identifier) @req arguments: (arguments (string (string_fragment) @src))))`
21
+
22
+ export function parseExportSpecifiers(raw) {
23
+ return String(raw || '').split(',').map((part) => {
24
+ const text = part.trim()
25
+ const typeOnly = /^type\s+/.test(text)
26
+ const clean = text.replace(/^type\s+/, '').trim()
27
+ const match = clean.match(/^([A-Za-z_$][\w$]*|default)(?:\s+as\s+([A-Za-z_$][\w$]*|default))?$/)
28
+ return match ? {imported: match[1], exported: match[2] || match[1], typeOnly} : null
29
+ }).filter(Boolean)
30
+ }
31
+
32
+ export function importTypeOnly(node) {
33
+ if (/^\s*import\s+type\b/.test(node.text)) return true
34
+ const clause = node.namedChildren.find((child) => child.type === 'import_clause')
35
+ if (!clause) return false
36
+ const parts = clause.namedChildren
37
+ if (parts.some((child) => child.type === 'identifier' || child.type === 'namespace_import')) return false
38
+ const named = parts.find((child) => child.type === 'named_imports')
39
+ const specifiers = named?.namedChildren.filter((child) => child.type === 'import_specifier') || []
40
+ return specifiers.length > 0 && specifiers.every((specifier) => /^\s*type\b/.test(specifier.text))
41
+ }
42
+
43
+ export function reexportTypeOnly(node) {
44
+ if (/^\s*export\s+type\b/.test(node.text)) return true
45
+ const clause = node.namedChildren.find((child) => child.type === 'export_clause')
46
+ const specifiers = clause?.namedChildren.filter((child) => child.type === 'export_specifier') || []
47
+ return specifiers.length > 0 && specifiers.every((specifier) => /^\s*type\b/.test(specifier.text))
48
+ }
@@ -5,6 +5,61 @@
5
5
  // in the orchestrator; cross-pkg `pkg.Func()` is a selector — see resolveGoPackage/pass2). No inheritance.
6
6
  import { goSpecToPkg } from "./spec-pkg.js";
7
7
 
8
+ // Return the named Go type behind pointers/generics/parentheses. Qualified names are
9
+ // kept (`speaker.Speaker`) so pass 2 can route them through the import alias table.
10
+ export function goTypeName(node) {
11
+ if (!node) return null;
12
+ if (node.type === "type_identifier") return node.text;
13
+ if (node.type === "qualified_type") return node.text.replace(/\s+/g, "");
14
+ if (node.type === "generic_type") return goTypeName(node.childForFieldName?.("type") || node.namedChildren?.[0]);
15
+ if (["pointer_type", "parenthesized_type", "unary_expression"].includes(node.type)) {
16
+ return goTypeName(node.childForFieldName?.("type") || node.childForFieldName?.("operand") || node.namedChildren?.[0]);
17
+ }
18
+ if ((node.namedChildren?.length || 0) === 1) return goTypeName(node.namedChildren[0]);
19
+ return null;
20
+ }
21
+
22
+ const namedFieldChildren = (node, name) => {
23
+ const children = node?.childrenForFieldName?.(name);
24
+ if (Array.isArray(children) && children.length) return children;
25
+ const child = node?.childForFieldName?.(name);
26
+ return child ? [child] : [];
27
+ };
28
+
29
+ function goStructFields(typeNode) {
30
+ if (typeNode?.type !== "struct_type") return null;
31
+ const fields = {};
32
+ const visit = (node) => {
33
+ if (!node) return;
34
+ if (node.type === "field_declaration") {
35
+ const type = goTypeName(node.childForFieldName?.("type"));
36
+ if (!type) return;
37
+ const names = namedFieldChildren(node, "name");
38
+ if (names.length) for (const name of names) fields[name.text] = type;
39
+ else {
40
+ // An embedded field is addressed by the base type name (`h.Speaker`).
41
+ const embedded = type.split(".").pop();
42
+ if (embedded) fields[embedded] = type;
43
+ }
44
+ return;
45
+ }
46
+ for (const child of node.namedChildren || []) visit(child);
47
+ };
48
+ visit(typeNode);
49
+ return fields;
50
+ }
51
+
52
+ function goResultType(declaration) {
53
+ const result = declaration?.childForFieldName?.("result");
54
+ if (!result) return null;
55
+ if (result.type !== "parameter_list") return goTypeName(result);
56
+ const declarations = (result.namedChildren || []).filter((child) => child.type === "parameter_declaration");
57
+ // A call used in `value, err := New()` contributes its first result to the
58
+ // first binding. Preserve that receiver type instead of discarding every
59
+ // multi-result signature just because a trailing error is present.
60
+ return goTypeName(declarations[0]?.childForFieldName?.("type"));
61
+ }
62
+
8
63
  export default {
9
64
  family: "go",
10
65
  grammars: ["go"],
@@ -20,10 +75,40 @@ export default {
20
75
  const { grammar, tree, fileRel, caps, addSym, addImportEdge, addExternalImport, imports, resolveGoImport, dirFiles, goModule, goRequires } = ctx;
21
76
 
22
77
  // ---- symbols ----
23
- for (const cap of caps(grammar, `(function_declaration name: (identifier) @fn) (method_declaration name: (field_identifier) @method)`, tree.rootNode))
24
- addSym(cap.node.text, cap.node.startPosition.row + 1, true, { sourceNode: cap.node.parent });
25
- for (const cap of caps(grammar, `(source_file (type_declaration (type_spec name: (type_identifier) @type)))`, tree.rootNode))
26
- addSym(cap.node.text, cap.node.startPosition.row + 1, false, { sourceNode: cap.node.parent });
78
+ for (const cap of caps(grammar, `(function_declaration name: (identifier) @fn)`, tree.rootNode)) {
79
+ const declaration = cap.node.parent;
80
+ addSym(cap.node.text, cap.node.startPosition.row + 1, true, {
81
+ sourceNode: declaration,
82
+ selectionNode: cap.node,
83
+ symbolKind: "function",
84
+ returnType: goResultType(declaration),
85
+ });
86
+ }
87
+ for (const cap of caps(grammar, `(method_declaration name: (field_identifier) @method)`, tree.rootNode)) {
88
+ const declaration = cap.node.parent;
89
+ const receiver = declaration.childForFieldName?.("receiver");
90
+ const receiverDeclaration = receiver?.namedChildren?.find((child) => child.type === "parameter_declaration");
91
+ const receiverType = goTypeName(receiverDeclaration?.childForFieldName?.("type"));
92
+ addSym(cap.node.text, cap.node.startPosition.row + 1, true, {
93
+ sourceNode: declaration,
94
+ selectionNode: cap.node,
95
+ symbolKind: "method",
96
+ memberOf: receiverType || undefined,
97
+ receiverType: receiverType || undefined,
98
+ returnType: goResultType(declaration),
99
+ });
100
+ }
101
+ for (const cap of caps(grammar, `(source_file (type_declaration (type_spec name: (type_identifier) @type)))`, tree.rootNode)) {
102
+ const specification = cap.node.parent;
103
+ const typeNode = specification.childForFieldName?.("type");
104
+ addSym(cap.node.text, cap.node.startPosition.row + 1, false, {
105
+ sourceNode: specification,
106
+ selectionNode: cap.node,
107
+ symbolKind: typeNode?.type === "interface_type" ? "interface" : "type",
108
+ symbolSpace: "type",
109
+ fieldTypes: goStructFields(typeNode),
110
+ });
111
+ }
27
112
  for (const cap of caps(grammar, `(source_file (var_declaration (var_spec name: (identifier) @var))) (source_file (const_declaration (const_spec name: (identifier) @const)))`, tree.rootNode))
28
113
  addSym(cap.node.text, cap.node.startPosition.row + 1, false, { sourceNode: cap.node.parent });
29
114
 
@@ -2,33 +2,10 @@
2
2
  // Symbols: functions, generators, classes, methods, top-level const/let/var (data + arrow/fn consts).
3
3
  // Imports: ESM `import`, CJS `require`, path-aliases (tsconfig/vite), and barrel re-exports (`export … from`).
4
4
  // Calls: bare `foo()`. Heritage: `class X extends Y`. Everything runs through the shared ctx (see internal-builder.js).
5
- const CALLABLE = /arrow_function|function|function_expression|generator_function/;
6
-
7
- const FUNCS = `
8
- (function_declaration name: (identifier) @fn)
9
- (generator_function_declaration name: (identifier) @fn)
10
- (class_declaration name: (_) @class)
11
- (method_definition name: (_) @method)`;
12
- const TOPVARS = `
13
- (program (lexical_declaration (variable_declarator) @decl))
14
- (program (variable_declaration (variable_declarator) @decl))
15
- (program (export_statement (lexical_declaration (variable_declarator) @decl)))
16
- (program (export_statement (variable_declaration (variable_declarator) @decl)))`;
17
- const TYPES = `
18
- (interface_declaration name: (_) @interface)
19
- (type_alias_declaration name: (_) @type)
20
- (enum_declaration name: (_) @enum)`;
21
- const REQUIRE = `(variable_declarator name: (_) @lhs value: (call_expression function: (identifier) @req arguments: (arguments (string (string_fragment) @src))))`;
22
-
23
- function parseExportSpecifiers(raw) {
24
- return String(raw || "").split(",").map((part) => {
25
- const text = part.trim();
26
- const typeOnly = /^type\s+/.test(text);
27
- const clean = text.replace(/^type\s+/, "").trim();
28
- const match = clean.match(/^([A-Za-z_$][\w$]*|default)(?:\s+as\s+([A-Za-z_$][\w$]*|default))?$/);
29
- return match ? { imported: match[1], exported: match[2] || match[1], typeOnly } : null;
30
- }).filter(Boolean);
31
- }
5
+ import {
6
+ CALLABLE, FUNCS, TOPVARS, TYPES, REQUIRE, parseExportSpecifiers,
7
+ importTypeOnly, reexportTypeOnly,
8
+ } from './js/queries.js';
32
9
 
33
10
  export default {
34
11
  family: "js",
@@ -162,23 +139,6 @@ export default {
162
139
  });
163
140
  }
164
141
 
165
- const importTypeOnly = (node) => {
166
- if (/^\s*import\s+type\b/.test(node.text)) return true;
167
- const clause = node.namedChildren.find((c) => c.type === "import_clause");
168
- if (!clause) return false; // side-effect import executes the target module
169
- const parts = clause.namedChildren;
170
- if (parts.some((c) => c.type === "identifier" || c.type === "namespace_import")) return false;
171
- const named = parts.find((c) => c.type === "named_imports");
172
- const specs = named?.namedChildren.filter((c) => c.type === "import_specifier") || [];
173
- return specs.length > 0 && specs.every((s) => /^\s*type\b/.test(s.text));
174
- };
175
- const reexportTypeOnly = (node) => {
176
- if (/^\s*export\s+type\b/.test(node.text)) return true;
177
- const clause = node.namedChildren.find((c) => c.type === "export_clause");
178
- const specs = clause?.namedChildren.filter((c) => c.type === "export_specifier") || [];
179
- return specs.length > 0 && specs.every((s) => /^\s*type\b/.test(s.text));
180
- };
181
-
182
142
  // ---- ESM imports ----
183
143
  for (const cap of caps(grammar, `(import_statement) @imp`, tree.rootNode)) {
184
144
  const node = cap.node; const srcNode = field(node, "source");
@@ -0,0 +1,68 @@
1
+ export function createPass2Resolution({symIdsByFileName, nodeById, importedLocals, symByFileName}) {
2
+ const dirSymbols = new Map(), dirMethods = new Map(), dirMethodsByName = new Map(), dirTypes = new Map()
3
+ const sharesDirScope = (file) => file.endsWith('.go') || file.endsWith('.cs') || file.endsWith('.rs')
4
+ for (const [file, names] of symIdsByFileName) {
5
+ if (!sharesDirScope(file)) continue
6
+ const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : ''
7
+ let symbols = dirSymbols.get(dir)
8
+ if (!symbols) dirSymbols.set(dir, (symbols = new Map()))
9
+ for (const [name, ids] of names) for (const id of ids) {
10
+ const node = nodeById.get(id)
11
+ if (file.endsWith('.go') && node?.member_of && node?.symbol_kind === 'method') {
12
+ let receivers = dirMethods.get(dir)
13
+ if (!receivers) dirMethods.set(dir, (receivers = new Map()))
14
+ let methods = receivers.get(node.member_of)
15
+ if (!methods) receivers.set(node.member_of, (methods = new Map()))
16
+ const exact = methods.get(name) || []
17
+ exact.push(id); methods.set(name, exact)
18
+ let byName = dirMethodsByName.get(dir)
19
+ if (!byName) dirMethodsByName.set(dir, (byName = new Map()))
20
+ const candidates = byName.get(name) || []
21
+ candidates.push(id); byName.set(name, candidates)
22
+ continue
23
+ }
24
+ if (!symbols.has(name)) symbols.set(name, id)
25
+ if (file.endsWith('.go') && node?.symbol_space === 'type') {
26
+ let types = dirTypes.get(dir)
27
+ if (!types) dirTypes.set(dir, (types = new Map()))
28
+ if (!types.has(name)) types.set(name, id)
29
+ }
30
+ }
31
+ }
32
+ const symbolSpace = (node) => {
33
+ const explicit = String(node?.symbol_space || '')
34
+ if (['value', 'type', 'both'].includes(explicit)) return explicit
35
+ const kind = String(node?.symbol_kind || '').toLowerCase()
36
+ if (['interface', 'type'].includes(kind)) return 'type'
37
+ return ['class', 'enum'].includes(kind) ? 'both' : 'value'
38
+ }
39
+ const resolveNamedSymbol = (file, name, space = 'value') => {
40
+ const ids = symIdsByFileName.get(file)?.get(name) || []
41
+ const exact = ids.find((id) => symbolSpace(nodeById.get(id)) === space)
42
+ return exact || ids.find((id) => ['both', space].includes(symbolSpace(nodeById.get(id)))) || null
43
+ }
44
+ const resolveCall = (name, file) => {
45
+ const local = resolveNamedSymbol(file, name, 'value')
46
+ if (local) return local
47
+ if (sharesDirScope(file)) {
48
+ const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : ''
49
+ const symbols = dirSymbols.get(dir)
50
+ if (symbols?.has(name)) return symbols.get(name)
51
+ }
52
+ const imported = importedLocals.get(file)?.get(name)
53
+ if (!imported?.targetFile) return null
54
+ return resolveNamedSymbol(imported.originFile || imported.targetFile, imported.originName || imported.imported, 'value')
55
+ }
56
+ const javaTypeKinds = new Set(['class', 'interface', 'enum', 'record', 'annotation'])
57
+ const resolveJavaType = (name, file) => {
58
+ const imported = importedLocals.get(file)?.get(name)
59
+ if (imported?.targetFile) {
60
+ const symbols = symByFileName.get(imported.targetFile)
61
+ const target = symbols?.get(imported.imported) || symbols?.get(name)
62
+ if (target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind)) return target
63
+ }
64
+ const target = symByFileName.get(file)?.get(name)
65
+ return target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind) ? target : null
66
+ }
67
+ return {dirSymbols, dirMethods, dirMethodsByName, dirTypes, resolveNamedSymbol, resolveCall, resolveJavaType}
68
+ }
@@ -9,7 +9,7 @@ import {childProcessEnv} from '../child-env.js'
9
9
  import {createRepoBoundary} from '../repo-path.js'
10
10
 
11
11
  export const REPOSITORY_FRESHNESS_PROBE_V = 1
12
- export const GRAPH_BUILDER_SCHEMA_V = 5
12
+ export const GRAPH_BUILDER_SCHEMA_V = 6
13
13
  export const GRAPH_BUILDER_VERSION = (() => {
14
14
  try { return String(createRequire(import.meta.url)('../../package.json').version) }
15
15
  catch { return '0.0.0' }
@@ -23,6 +23,7 @@ const CURRENT_GRAPH_SCHEMA = Object.freeze({
23
23
  edgeTypesV: 2,
24
24
  edgeProvenanceV: 1,
25
25
  complexityV: 2,
26
+ physicalFileLocV: 1,
26
27
  repoBoundaryV: 1,
27
28
  barrelResolutionV: 1,
28
29
  reExportOccurrencesV: 1,
@@ -163,7 +163,7 @@ function mergeScopedGraph(base, scoped, affected, snapshot) {
163
163
  controlHashes: snapshot.controlHashes,
164
164
  graphRevision: snapshot.revision,
165
165
  };
166
- for (const key of ["extImportsV", "edgeTypesV", "edgeProvenanceV", "complexityV", "repoBoundaryV", "barrelResolutionV", "reExportOccurrencesV", "symbolSpacesV", "extractorSchemaV"]) {
166
+ for (const key of ["extImportsV", "edgeTypesV", "edgeProvenanceV", "complexityV", "physicalFileLocV", "repoBoundaryV", "barrelResolutionV", "reExportOccurrencesV", "symbolSpacesV", "extractorSchemaV"]) {
167
167
  merged[key] = Math.max(Number(base[key]) || 0, Number(scoped[key]) || 0);
168
168
  }
169
169
  return merged;
@@ -184,7 +184,8 @@ export async function refreshGraphIncrementally(repoDir, existingGraph, {
184
184
 
185
185
  if (!existingGraph || !existingGraph.fileHashes || !existingGraph.fileExportSignatures
186
186
  || !existingGraph.controlHashes || !existingGraph.jsExportRecords || Number(existingGraph.barrelResolutionV) < 1
187
- || Number(existingGraph.extractorSchemaV) < 5 || Number(existingGraph.reExportOccurrencesV) < 1
187
+ || Number(existingGraph.extractorSchemaV) < 5 || Number(existingGraph.physicalFileLocV) < 1
188
+ || Number(existingGraph.reExportOccurrencesV) < 1
188
189
  || Number(existingGraph.symbolSpacesV) < 1 || Number(existingGraph.edgeProvenanceV) < 1) {
189
190
  return full("incremental-baseline-unavailable");
190
191
  }
@@ -8,11 +8,17 @@ import { specToPkg } from "./builder/spec-pkg.js";
8
8
  import { analyzeSyntaxComplexity } from "../analysis/source-complexity.js";
9
9
  import { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk } from "./internal-builder.langs.js";
10
10
  import { buildResolvers } from "./internal-builder.resolvers.js";
11
- import { addJavaReferences } from "./internal-builder.java.js";
12
11
  import { assignDeterministicCommunities } from "./community.js";
13
- import { resolveJsBarrels } from "./internal-builder.barrels.js";
14
12
  import { snapshotRepository } from "./incremental-refresh.js";
15
13
  import { EDGE_PROVENANCE_V, stampEdgeProvenance } from "./edge-provenance.js";
14
+ import {runInternalGraphPass2} from './internal-builder.pass2.js'
15
+
16
+ function physicalLineCount(text) {
17
+ if (!text.length) return 0;
18
+ let lines = 1;
19
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) lines++;
20
+ return text.endsWith("\n") ? lines - 1 : lines;
21
+ }
16
22
 
17
23
  // Parse a repo directory into a graph-builder-compatible { nodes, links } graph.
18
24
  export async function buildInternalGraph(repoDir, opts = {}) {
@@ -78,10 +84,13 @@ export async function buildInternalGraph(repoDir, opts = {}) {
78
84
  let _parsed = 0;
79
85
  for (const abs of files) {
80
86
  const fileRel = rel(abs); const ext = extname(abs);
81
- addNode({ id: fileRel, label: fileRel.split("/").pop(), file_type: "code", source_file: fileRel, source_location: "L1" });
82
- if (isDataFile(fileRel) || isDocFile(fileRel)) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; } // config/infra/docs file-only node
87
+ const fileNode = { id: fileRel, label: fileRel.split("/").pop(), file_type: "code", source_file: fileRel, source_location: "L1" };
88
+ if (isDataFile(fileRel) || isDocFile(fileRel)) {
89
+ addNode(fileNode); perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue;
90
+ } // config/infra/docs file-only node
91
+ let code; try { code = readFileSync(abs, "utf8"); } catch { addNode(fileNode); continue; }
92
+ addNode({ ...fileNode, physical_loc: physicalLineCount(code) });
83
93
  const grammar = EXT_LANG[ext]; const lang = LANGS[FAMILY[grammar]]; if (!lang || !langs[grammar]) continue;
84
- let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
85
94
  // giant / generated / minified file → keep the file NODE but don't parse symbols (avoids a wedged parse)
86
95
  if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; }
87
96
  if ((++_parsed % 24) === 0) await new Promise((r) => setImmediate(r)); // breathe: let the UI paint between chunks
@@ -144,7 +153,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
144
153
  ...(extra && extra.symbolSpace ? { symbol_space: extra.symbolSpace } : {}),
145
154
  ...(extra && extra.memberOf ? { member_of: extra.memberOf } : {}),
146
155
  ...(extra && extra.visibility ? { visibility: extra.visibility } : {}),
147
- ...(Number.isInteger(extra?.parameterCount) ? { parameter_count: extra.parameterCount } : {})
156
+ ...(Number.isInteger(extra?.parameterCount) ? { parameter_count: extra.parameterCount } : {}),
157
+ ...(extra?.receiverType ? { receiver_type: extra.receiverType } : {}),
158
+ ...(extra?.returnType ? { return_type: extra.returnType } : {}),
159
+ ...(extra?.fieldTypes && Object.keys(extra.fieldTypes).length ? { field_types: extra.fieldTypes } : {})
148
160
  });
149
161
  links.push({ source: fileRel, target: id, relation: "contains", confidence: "EXTRACTED" });
150
162
  syms.push({
@@ -216,184 +228,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
216
228
  // ---- pass 2: scope-aware calls + inheritance (Go package = whole dir → same-dir symbols share scope;
217
229
  // C# gets the same treatment: one folder ≈ one namespace by convention, and `using` names namespaces,
218
230
  // not files, so the folder map is the only reliable cross-file resolver) ----
219
- const goDirSymbols = new Map();
220
- const sharesDirScope = (fr) => fr.endsWith(".go") || fr.endsWith(".cs") || fr.endsWith(".rs");
221
- for (const [fr, m] of symByFileName) {
222
- if (!sharesDirScope(fr)) continue;
223
- const d = fr.includes("/") ? fr.slice(0, fr.lastIndexOf("/")) : "";
224
- let dm = goDirSymbols.get(d); if (!dm) goDirSymbols.set(d, (dm = new Map()));
225
- for (const [n, id] of m) if (!dm.has(n)) dm.set(n, id);
226
- }
227
- const inferredSymbolSpace = (node) => {
228
- const explicit = String(node?.symbol_space || "");
229
- if (["value", "type", "both"].includes(explicit)) return explicit;
230
- const kind = String(node?.symbol_kind || "").toLowerCase();
231
- if (["interface", "type"].includes(kind)) return "type";
232
- if (["class", "enum"].includes(kind)) return "both";
233
- return "value";
234
- };
235
- const resolveNamedSymbol = (file, name, space = "value") => {
236
- const ids = symIdsByFileName.get(file)?.get(name) || [];
237
- const accepts = (id) => {
238
- const symbolSpace = inferredSymbolSpace(nodeById.get(id));
239
- return symbolSpace === "both" || symbolSpace === space;
240
- };
241
- const exact = ids.find((id) => inferredSymbolSpace(nodeById.get(id)) === space);
242
- return exact || ids.find(accepts) || null;
243
- };
244
- const { resolveNamespaceMember, reExportOccurrences } = resolveJsBarrels({
245
- jsExports, importedLocals, links,
246
- resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? "type" : "value"),
231
+ const {reExportOccurrences} = runInternalGraphPass2({
232
+ files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
233
+ symIdsByFileName, importedLocals, jsExports,
247
234
  });
248
- // Exact source ranges can overlap (a named function nested inside another function). Attribute a call
249
- // to the innermost matching symbol, not whichever outer declaration happened to be added first.
250
- const enclosing = (fileRel, line) => {
251
- let best = null;
252
- for (const s of perFileSymbols.get(fileRel) || []) {
253
- if (line < s.start || line > s.end) continue;
254
- if (!best || s.start > best.start || (s.start === best.start && s.end < best.end)) best = s;
255
- }
256
- return best;
257
- };
258
- const resolveCall = (name, fileRel) => {
259
- const local = resolveNamedSymbol(fileRel, name, "value"); if (local) return local;
260
- if (sharesDirScope(fileRel)) { const d = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : ""; const dm = goDirSymbols.get(d); if (dm && dm.has(name)) return dm.get(name); }
261
- const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(name);
262
- if (imp && imp.targetFile) {
263
- const targetFile = imp.originFile || imp.targetFile;
264
- const importedName = imp.originName || imp.imported;
265
- return resolveNamedSymbol(targetFile, importedName, "value");
266
- }
267
- return null;
268
- };
269
- const javaTypeKinds = new Set(["class", "interface", "enum", "record", "annotation"]);
270
- const resolveJavaType = (name, fileRel) => {
271
- const imp = importedLocals.get(fileRel)?.get(name);
272
- if (imp?.targetFile) {
273
- const symbols = symByFileName.get(imp.targetFile);
274
- const target = symbols?.get(imp.imported) || symbols?.get(name);
275
- if (target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind)) return target;
276
- }
277
- const target = symByFileName.get(fileRel)?.get(name);
278
- return target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind) ? target : null;
279
- };
280
- for (const abs of files) {
281
- const fileRel = rel(abs); const grammar = EXT_LANG[extname(abs)]; if (!grammar) continue;
282
- const lang = LANGS[FAMILY[grammar]]; if (!lang || lang.isWeb || !langs[grammar]) continue;
283
- let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
284
- const parser = new Parser(); parser.setLanguage(langs[grammar]);
285
- let tree; try { tree = parser.parse(code); } catch { continue; }
286
- if (!lang.customCalls) for (const cap of caps(grammar, lang.calls, tree.rootNode)) {
287
- const caller = enclosing(fileRel, cap.node.startPosition.row + 1); if (!caller) continue;
288
- const target = resolveCall(cap.node.text, fileRel); if (!target || target === caller.id) continue;
289
- links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
290
- }
291
- if (typeof lang.pass2 === "function") {
292
- try {
293
- lang.pass2({
294
- grammar, tree, fileRel, code, caps, field, enclosing, links, nodeById,
295
- perFileSymbols, symByFileName, symIdsByFileName, importedLocals, resolveCall, resolveJavaType,
296
- });
297
- } catch (e) { /* one language-specific resolver never sinks the graph */ void e; }
298
- }
299
- // qualified/selector calls (Go): `pkg.Func()` → the imported package's dir; else `receiver.Method()` → the
300
- // SAME package (heuristic by method name — connects lifecycle methods like peer.Enable() that need type info).
301
- if (lang.selectorCall) for (const cap of caps(grammar, lang.selectorCall, tree.rootNode)) {
302
- const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
303
- if (!operand || operand.type !== "identifier" || !fld) continue;
304
- const caller = enclosing(fileRel, sel.startPosition.row + 1); if (!caller) continue;
305
- const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text);
306
- const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
307
- const dm = goDirSymbols.get(imp && imp.targetDir ? imp.targetDir : dir);
308
- const target = dm && dm.get(fld.text);
309
- if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
310
- }
311
- for (const heritageSpec of lang.heritage || []) {
312
- const query = typeof heritageSpec === "string" ? heritageSpec : heritageSpec.query;
313
- const relation = typeof heritageSpec === "string" ? "inherits" : (heritageSpec.relation || "inherits");
314
- for (const cap of caps(grammar, query, tree.rootNode)) {
315
- const cls = enclosing(fileRel, cap.node.startPosition.row + 1); if (!cls) continue;
316
- const target = FAMILY[grammar] === "java" ? resolveJavaType(cap.node.text, fileRel) : resolveCall(cap.node.text, fileRel);
317
- if (target && target !== cls.id) links.push({ source: cls.id, target, relation, confidence: "INFERRED" });
318
- }
319
- }
320
- // JSX is a real symbol use even though it is not a call_expression. Resolve imported components to their
321
- // declaration so component fan-in and unused-export checks do not claim `<SettingsView />` is unreferenced.
322
- if (FAMILY[grammar] === "js") {
323
- // Namespace calls (`ui.run()`) need the member name before an export-star facade can be resolved.
324
- for (const cap of caps(grammar, `(call_expression function: (member_expression) @memberCall)`, tree.rootNode)) {
325
- const object = field(cap.node, "object"), property = field(cap.node, "property");
326
- if (!object || object.type !== "identifier" || !property) continue;
327
- const imp = importedLocals.get(fileRel)?.get(object.text);
328
- if (!imp || !["*", "default"].includes(imp.imported) || imp.typeOnly) continue;
329
- const origin = resolveNamespaceMember(fileRel, imp, property.text, "call");
330
- if (origin.status !== "resolved") continue;
331
- const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, "value");
332
- const caller = enclosing(fileRel, cap.node.startPosition.row + 1);
333
- if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
334
- }
335
- for (const cap of caps(grammar, `[
336
- (jsx_opening_element name: (_) @jsx)
337
- (jsx_self_closing_element name: (_) @jsx)
338
- ]`, tree.rootNode)) {
339
- const jsxName = cap.node.text;
340
- const parts = jsxName.split(".");
341
- const localName = parts[0];
342
- if (parts.length === 1 && !/^[A-Z_$]/.test(localName)) continue; // undotted lowercase tags are platform/intrinsic elements
343
- const imp = importedLocals.get(fileRel)?.get(localName);
344
- if (!imp || !imp.targetFile || imp.typeOnly) continue;
345
- let targetFile = imp.originFile || imp.targetFile;
346
- let importedName = imp.originName || imp.imported;
347
- if (imp.imported === "*" && parts.length > 1) {
348
- const origin = resolveNamespaceMember(fileRel, imp, parts[parts.length - 1], "jsx");
349
- if (origin.status === "resolved") { targetFile = origin.origin.file; importedName = origin.origin.name; }
350
- }
351
- const target = resolveNamedSymbol(targetFile, importedName, "value") || resolveNamedSymbol(targetFile, localName, "value");
352
- if (!target) continue;
353
- const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
354
- links.push({ source: owner?.id || fileRel, target, relation: "references", confidence: "INFERRED", usage: "jsx", line: cap.node.startPosition.row + 1 });
355
- }
356
- // Type identifiers are a separate namespace in TypeScript. Resolve them to type/both-space
357
- // declarations without creating runtime calls or keeping a same-named value symbol alive.
358
- if (grammar !== "javascript") for (const cap of caps(grammar, `(type_identifier) @typeRef`, tree.rootNode)) {
359
- const name = cap.node.text;
360
- const imp = importedLocals.get(fileRel)?.get(name);
361
- const targetFile = imp?.originFile || imp?.targetFile || fileRel;
362
- const targetName = imp?.originName || imp?.imported || name;
363
- const target = resolveNamedSymbol(targetFile, targetName, "type");
364
- if (!target || String(target).startsWith(`${fileRel}#${name}@${cap.node.startPosition.row + 1}`)) continue;
365
- const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
366
- const source = owner?.id || fileRel;
367
- if (source === target) continue;
368
- links.push({source, target, relation: "references", confidence: "EXTRACTED", provenance: "RESOLVED", typeOnly: true, line: cap.node.startPosition.row + 1, usage: "type"});
369
- }
370
- }
371
- // Go value references: a top-level const/var/type/func used BY NAME (bare `X`, or cross-package `pkg.X`) in
372
- // another file/scope → a `references` edge, so used-but-never-called symbols (message-type consts, etc.) are
373
- // not falsely DEAD. (Same-file usage is already covered by graph-builder-analysis localRefs.) Go-only for now.
374
- if (FAMILY[grammar] === "go") {
375
- const refSeen = new Set();
376
- const emitRef = (src, target) => { if (!target || target === src) return; const k = src + ">" + target; if (refSeen.has(k)) return; refSeen.add(k); links.push({ source: src, target, relation: "references", confidence: "INFERRED" }); };
377
- const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
378
- const dm = goDirSymbols.get(dir);
379
- for (const cap of caps(grammar, `[(identifier) (type_identifier)] @id`, tree.rootNode)) { // type_identifier → struct/type usage
380
- const target = dm && dm.get(cap.node.text); if (!target || target.slice(0, target.indexOf("#")) === fileRel) continue;
381
- const caller = enclosing(fileRel, cap.node.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
382
- }
383
- for (const cap of caps(grammar, `(selector_expression) @sel`, tree.rootNode)) {
384
- const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
385
- if (!operand || operand.type !== "identifier" || !fld) continue;
386
- const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text); if (!imp || !imp.targetDir) continue;
387
- const tdm = goDirSymbols.get(imp.targetDir); const target = tdm && tdm.get(fld.text); if (!target) continue;
388
- const caller = enclosing(fileRel, sel.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
389
- }
390
- }
391
- if (FAMILY[grammar] === "java") {
392
- addJavaReferences({ grammar, tree, fileRel, caps, resolveJavaType, enclosing, links });
393
- }
394
- tree.delete();
395
- }
396
-
397
235
  // HTML class/id usage → the CSS file(s) defining that selector: file-level reference edges (deduped per pair).
398
236
  const htmlRefSeen = new Set();
399
237
  for (const u of htmlUsages) {
@@ -422,6 +260,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
422
260
  edgeTypesV: 2,
423
261
  edgeProvenanceV: EDGE_PROVENANCE_V,
424
262
  complexityV: 2,
263
+ physicalFileLocV: 1,
425
264
  repoBoundaryV: 1,
426
265
  barrelResolutionV: 1,
427
266
  reExportOccurrencesV: 1,