gitnexus 1.6.12-rc.2 → 1.6.12-rc.21

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 (188) hide show
  1. package/dist/_shared/graph/types.d.ts +1 -1
  2. package/dist/_shared/graph/types.d.ts.map +1 -1
  3. package/dist/_shared/language-detection.d.ts.map +1 -1
  4. package/dist/_shared/language-detection.js +2 -0
  5. package/dist/_shared/language-detection.js.map +1 -1
  6. package/dist/_shared/languages.d.ts +1 -0
  7. package/dist/_shared/languages.d.ts.map +1 -1
  8. package/dist/_shared/languages.js +1 -0
  9. package/dist/_shared/languages.js.map +1 -1
  10. package/dist/_shared/lbug/schema-constants.d.ts +1 -1
  11. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  12. package/dist/_shared/lbug/schema-constants.js +2 -0
  13. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  14. package/dist/_shared/scope-resolution/language-classification.d.ts +2 -1
  15. package/dist/_shared/scope-resolution/language-classification.d.ts.map +1 -1
  16. package/dist/_shared/scope-resolution/language-classification.js +3 -1
  17. package/dist/_shared/scope-resolution/language-classification.js.map +1 -1
  18. package/dist/_shared/scope-resolution/registries/class-registry.d.ts +3 -3
  19. package/dist/_shared/scope-resolution/registries/class-registry.js +3 -3
  20. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
  21. package/dist/_shared/scope-resolution/registries/context.js +2 -0
  22. package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
  23. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
  24. package/dist/_shared/scope-resolution/resolve-type-ref.js +2 -0
  25. package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
  26. package/dist/cli/analyze-config.d.ts +2 -2
  27. package/dist/cli/analyze-config.js +10 -33
  28. package/dist/cli/doctor.js +6 -4
  29. package/dist/cli/eval-server.js +13 -1
  30. package/dist/cli/i18n/en.d.ts +4 -2
  31. package/dist/cli/i18n/en.js +4 -2
  32. package/dist/cli/i18n/resources.d.ts +6 -2
  33. package/dist/cli/i18n/zh-CN.d.ts +2 -0
  34. package/dist/cli/i18n/zh-CN.js +4 -2
  35. package/dist/core/analysis-feature-registry.d.ts +1 -1
  36. package/dist/core/analysis-feature-registry.js +2 -0
  37. package/dist/core/auto-sync/starter.js +2 -2
  38. package/dist/core/embeddings/ast-utils.js +8 -3
  39. package/dist/core/embeddings/chunker.js +67 -10
  40. package/dist/core/embeddings/embedding-pipeline.d.ts +1 -1
  41. package/dist/core/embeddings/embedding-pipeline.js +1 -1
  42. package/dist/core/embeddings/structural-extractor.js +2 -2
  43. package/dist/core/embeddings/types.d.ts +4 -2
  44. package/dist/core/embeddings/types.js +18 -0
  45. package/dist/core/git-ref.d.ts +22 -0
  46. package/dist/core/git-ref.js +114 -0
  47. package/dist/core/git-staleness.js +14 -0
  48. package/dist/core/group/extractors/graphql-extractor.d.ts +1 -1
  49. package/dist/core/group/extractors/graphql-extractor.js +149 -13
  50. package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
  51. package/dist/core/group/extractors/manifest-extractor.js +2 -2
  52. package/dist/core/ingestion/call-extractors/zig-static-gating.d.ts +5 -8
  53. package/dist/core/ingestion/content-language-classification.d.ts +10 -0
  54. package/dist/core/ingestion/content-language-classification.js +30 -0
  55. package/dist/core/ingestion/filesystem-walker.d.ts +1 -0
  56. package/dist/core/ingestion/filesystem-walker.js +1 -1
  57. package/dist/core/ingestion/import-resolvers/node-workspace-packages.js +3 -2
  58. package/dist/core/ingestion/language-config.d.ts +82 -13
  59. package/dist/core/ingestion/language-config.js +217 -13
  60. package/dist/core/ingestion/language-provider.d.ts +88 -1
  61. package/dist/core/ingestion/languages/index.d.ts +7 -0
  62. package/dist/core/ingestion/languages/index.js +30 -1
  63. package/dist/core/ingestion/languages/objective-c/analysis-features.d.ts +10 -0
  64. package/dist/core/ingestion/languages/objective-c/analysis-features.js +19 -0
  65. package/dist/core/ingestion/languages/objective-c/compilation-unit-siblings.d.ts +13 -0
  66. package/dist/core/ingestion/languages/objective-c/compilation-unit-siblings.js +124 -0
  67. package/dist/core/ingestion/languages/objective-c/facts.d.ts +148 -0
  68. package/dist/core/ingestion/languages/objective-c/facts.js +1150 -0
  69. package/dist/core/ingestion/languages/objective-c/import-target.d.ts +11 -0
  70. package/dist/core/ingestion/languages/objective-c/import-target.js +122 -0
  71. package/dist/core/ingestion/languages/objective-c/macro-marker-preprocess.d.ts +24 -0
  72. package/dist/core/ingestion/languages/objective-c/macro-marker-preprocess.js +261 -0
  73. package/dist/core/ingestion/languages/objective-c/resolution-config.d.ts +41 -0
  74. package/dist/core/ingestion/languages/objective-c/resolution-config.js +282 -0
  75. package/dist/core/ingestion/languages/objective-c/scope-resolver.d.ts +2 -0
  76. package/dist/core/ingestion/languages/objective-c/scope-resolver.js +503 -0
  77. package/dist/core/ingestion/languages/objective-c.d.ts +2 -0
  78. package/dist/core/ingestion/languages/objective-c.js +300 -0
  79. package/dist/core/ingestion/languages/typescript/query.d.ts +31 -0
  80. package/dist/core/ingestion/languages/typescript/query.js +7 -1
  81. package/dist/core/ingestion/languages/typescript/tsconfig.js +3 -2
  82. package/dist/core/ingestion/languages/typescript.js +1 -1
  83. package/dist/core/ingestion/languages/zig/query.d.ts +26 -0
  84. package/dist/core/ingestion/languages/zig/query.js +61 -1
  85. package/dist/core/ingestion/languages/zig/range-binding.d.ts +8 -0
  86. package/dist/core/ingestion/languages/zig/range-binding.js +24 -0
  87. package/dist/core/ingestion/languages/zig/scope-resolver.d.ts +3 -2
  88. package/dist/core/ingestion/languages/zig/scope-resolver.js +18 -5
  89. package/dist/core/ingestion/languages/zig/this-alias-bindings.d.ts +58 -0
  90. package/dist/core/ingestion/languages/zig/this-alias-bindings.js +176 -0
  91. package/dist/core/ingestion/languages/zig/workspace-static-gating.d.ts +8 -0
  92. package/dist/core/ingestion/languages/zig/workspace-static-gating.js +82 -0
  93. package/dist/core/ingestion/model/registration-table.d.ts +3 -3
  94. package/dist/core/ingestion/model/registration-table.js +9 -5
  95. package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
  96. package/dist/core/ingestion/model/symbol-table.js +2 -0
  97. package/dist/core/ingestion/model/type-registry.d.ts +1 -1
  98. package/dist/core/ingestion/parsing-processor.d.ts +22 -6
  99. package/dist/core/ingestion/parsing-processor.js +39 -20
  100. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +9 -1
  101. package/dist/core/ingestion/pipeline-phases/parse-impl.js +307 -96
  102. package/dist/core/ingestion/pipeline-phases/parse-round-budget.d.ts +42 -0
  103. package/dist/core/ingestion/pipeline-phases/parse-round-budget.js +50 -0
  104. package/dist/core/ingestion/pipeline-phases/parse.d.ts +7 -1
  105. package/dist/core/ingestion/scope-extractor.js +4 -0
  106. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +13 -0
  107. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +2 -0
  108. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +2 -0
  109. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +78 -5
  110. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +262 -7
  111. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +5 -2
  112. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  113. package/dist/core/ingestion/scope-resolution/pipeline/run.js +10 -1
  114. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +39 -3
  115. package/dist/core/ingestion/scope-resolution/scope/walkers.js +82 -3
  116. package/dist/core/ingestion/scope-resolution/value-ref-edges.d.ts +14 -0
  117. package/dist/core/ingestion/scope-resolution/value-ref-edges.js +14 -0
  118. package/dist/core/ingestion/tree-sitter-queries.js +2 -0
  119. package/dist/core/ingestion/utils/symbol-labels.js +2 -0
  120. package/dist/core/ingestion/workers/parse-worker.d.ts +1 -1
  121. package/dist/core/ingestion/workers/parse-worker.js +63 -3
  122. package/dist/core/ingestion/workers/worker-pool.d.ts +56 -2
  123. package/dist/core/ingestion/workers/worker-pool.js +117 -26
  124. package/dist/core/lbug/csv-generator.js +2 -0
  125. package/dist/core/lbug/schema.d.ts +3 -1
  126. package/dist/core/lbug/schema.js +27 -0
  127. package/dist/core/search/fts-indexes.js +13 -4
  128. package/dist/core/search/fts-schema.js +2 -0
  129. package/dist/core/tree-sitter/parser-loader.js +10 -0
  130. package/dist/core/tree-sitter/vendored-grammars.d.ts +1 -1
  131. package/dist/core/tree-sitter/vendored-grammars.js +2 -1
  132. package/dist/mcp/local/local-backend.d.ts +33 -0
  133. package/dist/mcp/local/local-backend.js +166 -5
  134. package/dist/mcp/tools.js +6 -4
  135. package/dist/server/analyze-job.d.ts +19 -1
  136. package/dist/server/analyze-job.js +14 -3
  137. package/dist/server/analyze-launch.d.ts +14 -0
  138. package/dist/server/analyze-launch.js +59 -14
  139. package/dist/server/analyze-worker-ipc.d.ts +6 -5
  140. package/dist/server/analyze-worker-ipc.js +4 -0
  141. package/dist/server/api.js +68 -21
  142. package/dist/server/git-clone.d.ts +38 -5
  143. package/dist/server/git-clone.js +216 -38
  144. package/dist/server/repo-projection.d.ts +88 -0
  145. package/dist/server/repo-projection.js +53 -0
  146. package/dist/storage/file-lock.js +5 -3
  147. package/dist/storage/parse-cache.js +32 -1
  148. package/dist/storage/parsedfile-store.js +53 -1
  149. package/dist/storage/v8-sidecar.d.ts +11 -0
  150. package/dist/storage/v8-sidecar.js +15 -7
  151. package/dist/utils/process-identity.d.ts +13 -0
  152. package/dist/utils/process-identity.js +18 -0
  153. package/package.json +1 -1
  154. package/scripts/build-tree-sitter-grammars.cjs +4 -3
  155. package/vendor/tree-sitter-objc/LICENSE +21 -0
  156. package/vendor/tree-sitter-objc/README.md +18 -0
  157. package/vendor/tree-sitter-objc/binding.gyp +35 -0
  158. package/vendor/tree-sitter-objc/bindings/node/binding.cc +19 -0
  159. package/vendor/tree-sitter-objc/bindings/node/binding_test.js +9 -0
  160. package/vendor/tree-sitter-objc/bindings/node/index.d.ts +27 -0
  161. package/vendor/tree-sitter-objc/bindings/node/index.js +11 -0
  162. package/vendor/tree-sitter-objc/grammar.js +1286 -0
  163. package/vendor/tree-sitter-objc/package.json +56 -0
  164. package/vendor/tree-sitter-objc/prebuilds/SHA256SUMS +6 -0
  165. package/vendor/tree-sitter-objc/prebuilds/darwin-arm64/tree-sitter-objc.node +0 -0
  166. package/vendor/tree-sitter-objc/prebuilds/darwin-x64/tree-sitter-objc.node +0 -0
  167. package/vendor/tree-sitter-objc/prebuilds/linux-arm64/tree-sitter-objc.node +0 -0
  168. package/vendor/tree-sitter-objc/prebuilds/linux-x64/tree-sitter-objc.node +0 -0
  169. package/vendor/tree-sitter-objc/prebuilds/win32-arm64/tree-sitter-objc.node +0 -0
  170. package/vendor/tree-sitter-objc/prebuilds/win32-x64/tree-sitter-objc.node +0 -0
  171. package/vendor/tree-sitter-objc/queries/folds.scm +20 -0
  172. package/vendor/tree-sitter-objc/queries/highlights.scm +216 -0
  173. package/vendor/tree-sitter-objc/queries/indents.scm +1 -0
  174. package/vendor/tree-sitter-objc/queries/injections.scm +10 -0
  175. package/vendor/tree-sitter-objc/queries/locals.scm +1 -0
  176. package/vendor/tree-sitter-objc/src/grammar.json +16292 -0
  177. package/vendor/tree-sitter-objc/src/node-types.json +7524 -0
  178. package/vendor/tree-sitter-objc/src/parser.c +684023 -0
  179. package/vendor/tree-sitter-objc/src/tree_sitter/alloc.h +54 -0
  180. package/vendor/tree-sitter-objc/src/tree_sitter/array.h +291 -0
  181. package/vendor/tree-sitter-objc/src/tree_sitter/parser.h +266 -0
  182. package/vendor/tree-sitter-objc/tree-sitter-objc.wasm +0 -0
  183. package/vendor/tree-sitter-objc/tree-sitter.json +38 -0
  184. package/web/assets/{agent-BMRqXorA.js → agent-C2n33ANd.js} +1 -1
  185. package/web/assets/{index-Dj3vQOHK.js → index-CHJoW_P1.js} +18 -18
  186. package/web/assets/src-jc8Ffy3T.js +1 -0
  187. package/web/index.html +2 -2
  188. package/web/assets/src-Df5C1Nz4.js +0 -1
@@ -60,6 +60,28 @@ CREATE NODE TABLE Class (
60
60
  frameworkAnnotations STRING[],
61
61
  PRIMARY KEY (id)
62
62
  )`;
63
+ export const PROTOCOL_SCHEMA = `
64
+ CREATE NODE TABLE Protocol (
65
+ id STRING,
66
+ name STRING,
67
+ filePath STRING,
68
+ startLine INT64,
69
+ endLine INT64,
70
+ content STRING,
71
+ description STRING,
72
+ PRIMARY KEY (id)
73
+ )`;
74
+ export const CATEGORY_SCHEMA = `
75
+ CREATE NODE TABLE Category (
76
+ id STRING,
77
+ name STRING,
78
+ filePath STRING,
79
+ startLine INT64,
80
+ endLine INT64,
81
+ content STRING,
82
+ description STRING,
83
+ PRIMARY KEY (id)
84
+ )`;
63
85
  export const INTERFACE_SCHEMA = `
64
86
  CREATE NODE TABLE Interface (
65
87
  id STRING,
@@ -550,6 +572,9 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
550
572
  FROM \`Module\` TO \`Namespace\`,
551
573
  FROM \`Namespace\` TO Function,
552
574
  FROM CodeElement TO CodeElement,
575
+ FROM CodeElement TO Class,
576
+ FROM CodeElement TO Category,
577
+ FROM CodeElement TO Method,
553
578
  FROM CodeElement TO \`Module\`,
554
579
  FROM CodeElement TO \`Property\`,
555
580
  FROM Section TO Section,
@@ -674,6 +699,8 @@ export const NODE_SCHEMA_QUERIES = [
674
699
  FOLDER_SCHEMA,
675
700
  FUNCTION_SCHEMA,
676
701
  CLASS_SCHEMA,
702
+ PROTOCOL_SCHEMA,
703
+ CATEGORY_SCHEMA,
677
704
  INTERFACE_SCHEMA,
678
705
  METHOD_SCHEMA,
679
706
  CODE_ELEMENT_SCHEMA,
@@ -267,10 +267,19 @@ export async function createSearchFTSIndexes(options) {
267
267
  // the old name+content index would silently persist. `dropFTSIndex` no-ops
268
268
  // when the index is absent (first-ever analyze) and clears the per-connection
269
269
  // memo so the create below actually runs.
270
- // ponytail: this rebuilds every FTS index on every analyze instead of
271
- // skipping when present; FTS build is proportional to symbol-table size and
272
- // runs inside the existing FTS phase. Gate on a stored schema fingerprint if
273
- // this rebuild cost ever shows up in analyze profiles.
270
+ // The cost DID show up in analyze profiles — 7.5s of a 31.7s edit loop on a
271
+ // 5350-file repo, `bench/analyze-phase-breakdown.md` — and a "skip when the
272
+ // index is already present" gate is NOT the answer, so don't reach for it.
273
+ // Every caller that reaches this loop has already dropped the indexes it
274
+ // passes in `tables`: the incremental writeback drops them because Ladybug
275
+ // cannot DML a table with a live FTS index (#2589), and a full rebuild
276
+ // builds into a fresh staging DB that never had one. A presence gate would
277
+ // therefore never fire. The cost is inherent — Ladybug's FTS is not
278
+ // incremental, so one changed row means re-tokenizing the whole table, and
279
+ // `File` alone is ~33MB of file content at ~10MB/s. The measured floor and
280
+ // the four exits that were tried and closed (narrow further, build
281
+ // concurrently, raise the connection thread count, drop `content`) are in
282
+ // that document.
274
283
  try {
275
284
  await dropFTSIndex(table, indexName);
276
285
  await createFTSIndex(table, indexName, [...properties], stemmer);
@@ -18,6 +18,8 @@ export const FTS_INDEXES = [
18
18
  // Original 5 (minus File) gain `description`.
19
19
  { table: 'Function', indexName: 'function_fts', properties: FTS_PROPERTIES },
20
20
  { table: 'Class', indexName: 'class_fts', properties: FTS_PROPERTIES },
21
+ { table: 'Protocol', indexName: 'protocol_fts', properties: FTS_PROPERTIES },
22
+ { table: 'Category', indexName: 'category_fts', properties: FTS_PROPERTIES },
21
23
  { table: 'Method', indexName: 'method_fts', properties: FTS_PROPERTIES },
22
24
  { table: 'Interface', indexName: 'interface_fts', properties: FTS_PROPERTIES },
23
25
  // Remaining EMBEDDABLE_LABELS symbol tables — all CODE_ELEMENT_BASE-shaped
@@ -41,6 +41,16 @@ const SOURCES = {
41
41
  load: () => _require('tree-sitter-cpp'),
42
42
  unavailableNote: 'C++ parsing requires `tree-sitter-cpp`. Check the install and native binding.',
43
43
  },
44
+ [SupportedLanguages.ObjectiveC]: {
45
+ load: () => requireVendoredGrammar('tree-sitter-objc'),
46
+ optional: true,
47
+ severity: 'error',
48
+ unavailableNote: 'Objective-C parsing disabled: vendored `tree-sitter-objc` (under ' +
49
+ '`gitnexus/vendor/tree-sitter-objc`) could not be loaded. GitNexus ships ' +
50
+ 'prebuilt binaries for supported macOS/Linux runner architectures; this usually ' +
51
+ 'indicates a corrupted install or native ABI mismatch with the bundled ' +
52
+ 'tree-sitter@0.21.1 runtime.',
53
+ },
44
54
  [SupportedLanguages.Go]: {
45
55
  load: () => _require('tree-sitter-go'),
46
56
  unavailableNote: 'Go parsing requires `tree-sitter-go`. Check the install and native binding.',
@@ -20,7 +20,7 @@ export declare const vendoredGrammarDir: (packageName: string) => string;
20
20
  /**
21
21
  * Load a vendored tree-sitter grammar by its absolute path under `vendor/`.
22
22
  *
23
- * GitNexus vendors six grammars (c/dart/proto/swift/kotlin/zig) inside its own
23
+ * GitNexus vendors seven grammars (c/dart/proto/swift/kotlin/objc/zig) inside its own
24
24
  * package under `vendor/`, each shipping committed per-platform prebuilds. They
25
25
  * are deliberately NOT npm dependencies and must NEVER be copied into
26
26
  * `node_modules`: an undeclared package under `node_modules` is "extraneous" to
@@ -24,6 +24,7 @@ export const VENDORED_GRAMMAR_PACKAGES = new Set([
24
24
  'tree-sitter-proto',
25
25
  'tree-sitter-swift',
26
26
  'tree-sitter-kotlin',
27
+ 'tree-sitter-objc',
27
28
  'tree-sitter-zig',
28
29
  ]);
29
30
  /** Absolute directory of a vendored grammar package under `vendor/`. */
@@ -31,7 +32,7 @@ export const vendoredGrammarDir = (packageName) => path.join(VENDOR_ROOT, packag
31
32
  /**
32
33
  * Load a vendored tree-sitter grammar by its absolute path under `vendor/`.
33
34
  *
34
- * GitNexus vendors six grammars (c/dart/proto/swift/kotlin/zig) inside its own
35
+ * GitNexus vendors seven grammars (c/dart/proto/swift/kotlin/objc/zig) inside its own
35
36
  * package under `vendor/`, each shipping committed per-platform prebuilds. They
36
37
  * are deliberately NOT npm dependencies and must NEVER be copied into
37
38
  * `node_modules`: an undeclared package under `node_modules` is "extraneous" to
@@ -142,6 +142,39 @@ export interface EpistemicCauses {
142
142
  * "nothing was undecided", and a re-index is what tells the two apart.
143
143
  */
144
144
  readonly undecidedSatisfaction: number;
145
+ /**
146
+ * Symbols that name this callable in VALUE position rather than calling it
147
+ * (#3399) — a registration table (`bridge.accessor(Element.getNamespaceUri,
148
+ * …)`), a callback argument, a function pointer stored in a field.
149
+ *
150
+ * Unit: SYMBOLS — distinct referrers, the same unit and the same reason as
151
+ * `dispatchBoundary`: the reference edge is per-site but the walk's question
152
+ * is "who else might reach this", and a referrer that names the callable
153
+ * twice is still one place the value escapes from.
154
+ *
155
+ * Kept separate from `dispatchBoundary` even though both describe dispatch
156
+ * the walk cannot follow. That slot counts implementations and
157
+ * interface-level consumers found by the heritage probe; these are neither,
158
+ * and folding them in would tell a consumer branching on the numbers that an
159
+ * interface boundary exists where there is none. The distinction is also the
160
+ * actionable one: a dispatch boundary is irreducible, whereas a callable
161
+ * value CAN often be followed once the language models the store/load that
162
+ * carries it.
163
+ *
164
+ * The reference itself IS modelled — that is what makes it countable. What is
165
+ * missing is the invocation through the value: it happens later, through a
166
+ * struct field, a registry lookup, or comptime reflection, and no CALLS edge
167
+ * connects the eventual call site back to this symbol.
168
+ *
169
+ * Zero when the property-dispatch pass DID synthesize that invocation
170
+ * (`x.<key>()` through a registered object-literal key): the walk followed
171
+ * the registration, so nothing was missed and the result stays `exact`.
172
+ *
173
+ * Also zero — WITH a boundary note — when the probe itself could not run.
174
+ * The note is the signal there; the count is not, which is why a reader must
175
+ * branch on `epistemic` first and read the causes as explanation.
176
+ */
177
+ readonly callableValueReferences: number;
145
178
  }
146
179
  interface RepoHandle {
147
180
  id: string;
@@ -61,6 +61,7 @@ import { compareCodeUnits } from '../../lib/utils.js';
61
61
  import { lookupExternalCallCount, lookupUnresolvedCallCount, } from '../../core/ingestion/scope-resolution/unresolved-receivers.js';
62
62
  import { scopeExtractionFailureTotal } from '../../core/ingestion/scope-resolution/scope-extraction-failures.js';
63
63
  import { lookupCount } from '../../core/ingestion/scope-resolution/summary-maps.js';
64
+ import { VALUE_REF_EDGE_REASON } from '../../core/ingestion/scope-resolution/value-ref-edges.js';
64
65
  import { DEFERRED_IMPORT_REASON_SUFFIX, TYPE_ONLY_IMPORT_REASON_SUFFIX, } from '../../core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js';
65
66
  import { fnLineOf, isPdgDegradedLayerStatus, makePdgImpactErrorResult, makePdgLayerDegradedResult, pdgLayerStatus, pdgStampForMode, runImpactPDG, validateImpactMode, pdgBridgeEvidenceForImpact, betterBridgeEvidence, composeUnifiedPdgImpactResult, splitCalleeIds, } from './pdg-impact.js';
66
67
  /**
@@ -164,7 +165,13 @@ function normalizeToolParams(method, params) {
164
165
  // Internal CLI callers materialize omitted optional flags as undefined.
165
166
  if (value === undefined)
166
167
  continue;
167
- if (typeof value !== 'string' || !value.trim()) {
168
+ // Strict OpenAI/Anthropic adapters also materialize omitted optional
169
+ // string aliases as "". Treat those exactly like undefined. Required
170
+ // canonical values are still rejected by the method-specific check
171
+ // below, and non-string aliases remain invalid.
172
+ if (typeof value === 'string' && !value.trim())
173
+ continue;
174
+ if (typeof value !== 'string') {
168
175
  return { error: `MCP parameter ${method}.${key} must be a non-empty string.` };
169
176
  }
170
177
  supplied.push({ key, value: value.trim() });
@@ -177,8 +184,8 @@ function normalizeToolParams(method, params) {
177
184
  .join(', ')} must agree.`,
178
185
  };
179
186
  }
180
- for (const alias of aliases)
181
- delete normalized[alias];
187
+ for (const key of keys)
188
+ delete normalized[key];
182
189
  if (supplied.length > 0)
183
190
  normalized[canonical] = supplied[0].value;
184
191
  }
@@ -199,6 +206,8 @@ export const VALID_NODE_LABELS = new Set([
199
206
  'Folder',
200
207
  'Function',
201
208
  'Class',
209
+ 'Protocol',
210
+ 'Category',
202
211
  'Interface',
203
212
  'Method',
204
213
  'CodeElement',
@@ -460,6 +469,7 @@ function epistemicFrom(dropped) {
460
469
  dispatchBoundary: dropped.dispatch,
461
470
  externalBoundary: dropped.external,
462
471
  undecidedSatisfaction: 0,
472
+ callableValueReferences: dropped.callableValueReferences,
463
473
  },
464
474
  }
465
475
  : { epistemic: 'exact' }
@@ -476,6 +486,7 @@ function epistemicFrom(dropped) {
476
486
  dispatchBoundary: dropped.dispatch,
477
487
  externalBoundary: dropped.external,
478
488
  undecidedSatisfaction: dropped.undecided,
489
+ callableValueReferences: dropped.callableValueReferences,
479
490
  },
480
491
  };
481
492
  }
@@ -561,6 +572,138 @@ function undecidedSatisfactionBoundaries(summary, names) {
561
572
  }
562
573
  return { notes, undecided };
563
574
  }
575
+ /**
576
+ * Boundary evidence for callables named in VALUE position (#3399).
577
+ *
578
+ * `bridge.accessor(Element.getNamespaceUri, null, .{})`, `{ onClick: handler }`,
579
+ * `qsort(xs, n, sz, compareItems)` — each REGISTERS a function somewhere
580
+ * instead of calling it. The registration is modelled (`value-ref` → a USES
581
+ * edge, Kythe `ref` / Joern `METHOD_REF`); the invocation through the stored
582
+ * value is not, because it happens later through a struct field, a registry
583
+ * lookup or comptime reflection.
584
+ *
585
+ * That gap is precisely the first half of `tools.ts`'s definition of
586
+ * `lower-bound` — the walk provably missed callers — and it was previously
587
+ * reported as `exact`. (The second half, a probe that could not run, is what
588
+ * the failure branch below publishes; the contract states both because this
589
+ * function can produce either.) A
590
+ * public DOM accessor bound into a JS bridge table came back LOW/exact with two
591
+ * internal callers, which is worse than no answer: `lower-bound` invites the
592
+ * reader to look further, `exact` tells them not to bother.
593
+ *
594
+ * Counted as DISTINCT REFERRERS rather than sites: the question the count
595
+ * serves is "how many places does this value escape from", and a table that
596
+ * registers the same callable twice is still one table.
597
+ *
598
+ * NOT every value reference is a gap. Where the property-dispatch pass
599
+ * synthesized the invocation side, the walk followed it and the answer stays
600
+ * `exact` — see the second probe below.
601
+ *
602
+ * Three failure modes, three different answers, none of them silence:
603
+ * - the query cannot run → hedge, count 0 (a probe that did not answer
604
+ * is not evidence of completeness);
605
+ * - the query returns nothing → no hedge (a real, measured zero);
606
+ * - the reference was followed → no hedge (nothing was missed).
607
+ *
608
+ * The probe reads the edge's `reason`, which is why writer and reader share
609
+ * {@link VALUE_REF_EDGE_REASON}. Language-neutral by construction — every
610
+ * provider that emits a `value-ref` capture participates, and one that emits
611
+ * none simply gets no rows.
612
+ */
613
+ async function callableValueReferenceBoundaries(lbugPath, symId) {
614
+ // `COUNT(DISTINCT …)`, not a capped row list. A `LIMIT n` here would make the
615
+ // published cause silently understate a target with more than n
616
+ // registrations — and this number is documented as "how many symbols", so a
617
+ // reader comparing its magnitude against `receiverTyping` would be comparing
618
+ // a truth to a ceiling. Aggregating in the database keeps the work bounded
619
+ // without capping the answer; scalar `sym.id` equality plus an implicit
620
+ // group-by is the shape `countByType` below already relies on.
621
+ //
622
+ // `null`, not `[]`, on failure: see below — an empty result set and an
623
+ // unanswerable query must not be the same value.
624
+ const rows = await executeParameterized(lbugPath, `MATCH (other)-[r:CodeRelation]->(sym)
625
+ WHERE sym.id = $symId AND r.type = 'USES' AND r.reason = $reason
626
+ RETURN COUNT(DISTINCT other.id) AS cnt`, { symId, reason: VALUE_REF_EDGE_REASON }).catch(() => null);
627
+ // A probe that could not run must never read as certainty — the same rule the
628
+ // `loadMeta` read above states, and the whole reason this function exists.
629
+ // Returning zero here would publish `exact` on the strength of a query that
630
+ // never answered.
631
+ if (rows === null) {
632
+ return {
633
+ referrers: 0,
634
+ notes: [
635
+ 'The callable-value-reference probe could not be run against this index, so whether ' +
636
+ 'this symbol is registered somewhere as a value is unknown. Treat the caller list as ' +
637
+ 'incomplete until it can be re-checked.',
638
+ ],
639
+ };
640
+ }
641
+ const referrers = rows.length > 0 ? Number(rows[0].cnt ?? rows[0][0] ?? 0) : 0;
642
+ if (!Number.isFinite(referrers) || referrers <= 0)
643
+ return { notes: [], referrers: 0 };
644
+ // Registrations whose invocation side the analyzer ALREADY synthesized are
645
+ // not a gap. `emitPropertyDispatchCalls` sweep 2 connects `x.<key>()` member
646
+ // calls to every function registered under `<key>` and stamps those edges
647
+ // `property-dispatch`; where that happened, the walk did not "provably miss"
648
+ // the caller and `lower-bound` would be noise sprayed over an answer the
649
+ // analyzer actually computed. Zig — the case this was built for — never sets
650
+ // a property key (no object-literal key to dispatch through), so it is never
651
+ // excluded here; the exclusion exists to keep TypeScript/JavaScript hook
652
+ // tables that ARE followed from being downgraded.
653
+ //
654
+ // SYMBOL-LEVEL, NOT PER-EDGE, and that is only sound because of an invariant
655
+ // that lives nowhere near this line. The graph does not record which
656
+ // registration produced which synthesized call, so if one symbol could carry
657
+ // both a followed and an unfollowed registration, this would zero the note
658
+ // over a gap the analyzer provably did not close — #3399 returning through a
659
+ // side door. Today no symbol can:
660
+ //
661
+ // - sweep 2 synthesizes CALLS only for a registration whose site carried a
662
+ // `propertyKey` (sweep 1 skips the index when it is undefined);
663
+ // - every JS/TS `@reference.value-ref` rule also captures
664
+ // `@reference.property-key` — both are object-literal shapes;
665
+ // - no Zig `@reference.value-ref` rule captures one.
666
+ //
667
+ // So a dispatchable registration is always a JS/TS one, an undispatchable
668
+ // registration is always a Zig one, and the two never meet on one symbol.
669
+ // `test/unit/scope-resolution/value-ref-dispatchability.test.ts` FAILS the day
670
+ // that stops holding — a JS/TS rule for a bare callback argument
671
+ // (`register(handler)`), a Zig rule that grows a key. When it does, the
672
+ // choice to make here is between (a) splitting the edge `reason` into
673
+ // dispatchable / undispatchable so this probe can count them apart, and
674
+ // (b) hedging any symbol with an undispatchable registration regardless of
675
+ // dispatch. (a) is precise and costs a graph-content change; (b) is cheap and
676
+ // over-hedges. What is NOT acceptable is leaving this as-is, because a signal
677
+ // that quietly stops firing is the defect this whole feature removes.
678
+ //
679
+ // Given the invariant, the residual today is only the coarseness of the
680
+ // exclusion within JS/TS, and hedging every property-value registration in
681
+ // every JS/TS codebase is worse: a signal that fires on everything stops
682
+ // carrying information, and the fan-out cap warning still sits behind it.
683
+ const dispatched = await executeParameterized(lbugPath, `MATCH (other)-[r:CodeRelation]->(sym)
684
+ WHERE sym.id = $symId AND r.type = 'CALLS' AND r.reason = 'property-dispatch'
685
+ RETURN COUNT(r) AS cnt`, { symId }).catch(() => null);
686
+ // Failure here is NOT a reason to skip the hedge: we already know a value
687
+ // reference exists, and being unable to prove it was followed leaves the
688
+ // conservative answer standing.
689
+ const dispatchedCount = dispatched === null || dispatched.length === 0
690
+ ? 0
691
+ : Number(dispatched[0].cnt ?? dispatched[0][0] ?? 0);
692
+ if (Number.isFinite(dispatchedCount) && dispatchedCount > 0) {
693
+ return { notes: [], referrers: 0 };
694
+ }
695
+ const one = referrers === 1;
696
+ return {
697
+ referrers,
698
+ notes: [
699
+ `${referrers} ${one ? 'symbol references' : 'symbols reference'} this callable as a VALUE ` +
700
+ `rather than calling it (a registration table, a callback argument, a stored function ` +
701
+ `pointer). The reference is recorded, but the call made THROUGH that value is not: it is ` +
702
+ `dispatched later from wherever the value is stored. Callers reached that way are absent ` +
703
+ `from this result — actual impact may be higher.`,
704
+ ],
705
+ };
706
+ }
564
707
  /** Resolve symlinks for path comparison; falls back to path.resolve on error.
565
708
  * Uses `realpathSync.native` (not the pure-JS `realpathSync`) so that Windows
566
709
  * 8.3 short names (e.g. RUNNER~1 → runneradmin) are expanded to long form,
@@ -3338,7 +3481,8 @@ export class LocalBackend {
3338
3481
  const [incomingRows, incomingAdvisedRows] = await Promise.all([
3339
3482
  executeParameterized(repo.lbugPath, `
3340
3483
  MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
3341
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
3484
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'MEMBER_OF', 'USES', 'DECLARES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
3485
+ AND (r.type <> 'MEMBER_OF' OR labels(n) <> 'Community')
3342
3486
  RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
3343
3487
  ORDER BY uid, relType
3344
3488
  LIMIT 30
@@ -3461,7 +3605,8 @@ export class LocalBackend {
3461
3605
  const [outgoingRows, outgoingAdvisedRows] = await Promise.all([
3462
3606
  executeParameterized(repo.lbugPath, `
3463
3607
  MATCH (n {id: $symId})-[r:CodeRelation]->(target)
3464
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
3608
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'MEMBER_OF', 'USES', 'DECLARES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
3609
+ AND (r.type <> 'MEMBER_OF' OR labels(target) <> 'Community')
3465
3610
  RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
3466
3611
  ORDER BY uid, relType
3467
3612
  LIMIT 30
@@ -5685,6 +5830,18 @@ export class LocalBackend {
5685
5830
  const convexDispatchPromise = direction === 'downstream'
5686
5831
  ? Promise.resolve(undefined)
5687
5832
  : queryConvexDispatchMetadata(repo.lbugPath, symId, symName, symType);
5833
+ // #3399 — callables named in value position. Upstream only: the question
5834
+ // "who can reach this symbol" is the one a registration makes unanswerable.
5835
+ // A downstream walk asks what THIS symbol reaches, which a reference INTO
5836
+ // it does not affect.
5837
+ //
5838
+ // Issued alongside the heritage probe rather than after it, and read into
5839
+ // `droppedBoundaries` below, so it hedges even when that probe finds
5840
+ // nothing AND when it throws — a value reference is an independent reason
5841
+ // a count is short, exactly as the receiver drops above are.
5842
+ const valueRefPromise = direction === 'downstream'
5843
+ ? Promise.resolve({ notes: [], referrers: 0 })
5844
+ : callableValueReferenceBoundaries(repo.lbugPath, symId);
5688
5845
  const interfaceRowsPromise = executeParameterized(repo.lbugPath, `MATCH (x)-[r:CodeRelation]->(iface)
5689
5846
  WHERE x.id = $symId AND r.type IN $heritage
5690
5847
  RETURN DISTINCT iface.id AS id, iface.name AS name, labels(iface)[0] AS label
@@ -5700,12 +5857,14 @@ export class LocalBackend {
5700
5857
  : []),
5701
5858
  ]);
5702
5859
  const convexDispatch = await convexDispatchPromise;
5860
+ const valueRefDrops = await valueRefPromise;
5703
5861
  const droppedBoundaries = {
5704
5862
  ...receiverDrops,
5705
5863
  notes: [
5706
5864
  ...receiverDrops.notes,
5707
5865
  ...scopeExtractionDrops.notes,
5708
5866
  ...undecidedDrops.notes,
5867
+ ...valueRefDrops.notes,
5709
5868
  ...(convexDispatch === undefined ? [] : [convexDispatch.boundary]),
5710
5869
  ],
5711
5870
  undecided: undecidedDrops.undecided,
@@ -5714,6 +5873,7 @@ export class LocalBackend {
5714
5873
  // inventing one from the presence of a note.
5715
5874
  dispatch: 0,
5716
5875
  scopeExtraction: scopeExtractionDrops.files,
5876
+ callableValueReferences: valueRefDrops.referrers,
5717
5877
  };
5718
5878
  try {
5719
5879
  // Discover the interface / abstract supertypes on the target's boundary.
@@ -5794,6 +5954,7 @@ export class LocalBackend {
5794
5954
  dispatchBoundary: droppedBoundaries.dispatch + dispatchBoundarySymbols,
5795
5955
  externalBoundary: droppedBoundaries.external,
5796
5956
  undecidedSatisfaction: droppedBoundaries.undecided,
5957
+ callableValueReferences: droppedBoundaries.callableValueReferences,
5797
5958
  },
5798
5959
  };
5799
5960
  }
package/dist/mcp/tools.js CHANGED
@@ -250,14 +250,15 @@ Handles disambiguation: if multiple symbols share the same name, returns ranked
250
250
  NOTE: ACCESSES edges (field read/write tracking) are included in context results with reason 'read' or 'write'. CALLS edges resolve through field access chains and method-call chains (e.g., user.address.getCity().save() produces CALLS edges at each step).
251
251
 
252
252
  COMPLETENESS OF incoming: alongside symbol/incoming/outgoing the result carries the same epistemic envelope impact() returns:
253
- - epistemic: 'exact' | 'lower-bound' — 'lower-bound' means callers exist that this view provably does not list.
253
+ - epistemic: 'exact' | 'lower-bound' — 'lower-bound' means incoming is a FLOOR: either the walk provably missed callers, or a probe that would have established completeness could not run. Do not read it as proof that an omitted caller exists — read boundaries for which of the two it is.
254
254
  - boundaries: string[] — one plain-language sentence per reason. Prose for humans; branch on causes instead.
255
- - causes: { scopeExtractionFiles, receiverTyping, dispatchBoundary, externalBoundary, undecidedSatisfaction } — machine-readable WHY. Every field counts MISSING THINGS, never sentences:
255
+ - causes: { scopeExtractionFiles, receiverTyping, dispatchBoundary, externalBoundary, undecidedSatisfaction, callableValueReferences } — machine-readable WHY. Every field counts MISSING THINGS, never sentences:
256
256
  - causes.scopeExtractionFiles (unit: files) > 0 — scope extraction still failed after the fallback pass, so scope-resolution edges from those files are absent. A value of 0 does not prove completeness when epistemic is 'lower-bound' because an older or unverified index has no measured file count. Re-run \`gitnexus analyze --force\`; if the reason persists, inspect the extraction warnings.
257
257
  - causes.receiverTyping (unit: call sites) > 0 — RESOLVER GAP: the analyzer dropped that many call sites on this name because it could not type the receiver, so they are missing from incoming. Do not read an absent caller as proof none exists.
258
258
  - causes.externalBoundary (unit: call sites) > 0 — the calls left the indexed program (System.out.println, fetch(...)). NOT a defect: no in-graph node could have been reached. An epistemic:'exact' result can carry this.
259
259
  - causes.dispatchBoundary (unit: symbols) > 0 — DI or interface dispatch: that many symbols sit on or beyond a boundary static analysis cannot cross. Irreducible. A symbol count, not a site count — per-site multiplicity is not retained for these edges — so compare its magnitude with receiverTyping, not its exact value. A framework runtime-proxy boundary can make epistemic lower-bound while this value remains 0 because endpoint metadata proves the gap but cannot count omitted symbols.
260
260
  - causes.undecidedSatisfaction (unit: unjudged interface/type pairs) > 0 — the analyzer could not decide whether a type satisfies an interface, so no IMPLEMENTS edge exists and no dispatch boundary was left for the walk to notice. Usually fixable by making the missing dependency available to analysis.
261
+ - causes.callableValueReferences (unit: symbols) > 0 — that many symbols name this callable as a VALUE instead of calling it (a Zig registration table or const initialiser, a JS/TS object-literal property value). A bare callback argument in JS/TS is not captured today and is not counted, so a 0 does not rule that shape out; nor does it, on an index built before the language emitted these captures — re-analyze first. The reference is in the graph as a USES edge; the call made THROUGH the value is not, because it is dispatched later from wherever the value was stored. incoming.calls is therefore a floor. Follow the USES edges to find the registration, then the code that reads it. It is 0 when the analyzer DID synthesize the dispatch through a registered property key. That exclusion is per SYMBOL, not per registration: a target with BOTH a followed registration and an unfollowed escape reads 0 here, so a 0 means 'no unfollowed registration was proven', not 'this symbol escapes nowhere'. A 0 alongside epistemic 'lower-bound' can also mean the probe itself could not run — read boundaries for which.
261
262
 
262
263
  REQUIRES RE-INDEX: causes.scopeExtractionFiles, causes.receiverTyping, causes.externalBoundary, causes.undecidedSatisfaction, and framework runtime-proxy boundary detection depend on index-time metadata that only a current analyzer writes. Against an older index the metadata can be absent, which is indistinguishable from "nothing was dropped" unless the schema probe detects the stale index — re-run \`gitnexus analyze\` before trusting a zero or an apparently exact result.
263
264
 
@@ -443,15 +444,16 @@ Output includes:
443
444
  - affected_processes: which execution flows break and at which step
444
445
  - affected_modules: which functional areas are hit (direct vs indirect; classification-unavailable when that secondary query fails)
445
446
  - byDepth: affected symbols grouped by traversal depth (paginated by limit/offset; omitted when summaryOnly:true — use byDepthCounts for totals per depth, pagination object when truncated). Each item includes a processes:[{id,label,processType,step}] field listing the execution flows that symbol participates in. Empty when the symbol has no process membership. Can ALSO be empty when partial:true is set — either the process-aggregation pass hit its cap before detecting affected processes, or per-symbol enrichment was capped on a very large page. When partial:true, do NOT treat processes:[] as proof of no participation; cross-check the top-level affected_processes list. An item carries staticGated:true only when the edge that reached it is provably unreachable at compile time from the indexed source (today: Zig calls inside an 'if (CONST_FALSE)' body or the else of 'if (CONST_TRUE)'); the field is absent when the edge is live or the language does not model it. Traversal and risk do NOT filter or rank on it: it is metadata for the caller to weigh.
446
- - epistemic: 'exact' | 'lower-bound' — whether impactedCount is the whole story. 'lower-bound' means the walk provably missed callers, so the count is a floor. Absent only on skipped probes (ambiguous-candidate lists, group fan-out).
447
+ - epistemic: 'exact' | 'lower-bound' — whether impactedCount is the whole story. 'lower-bound' means the count is a FLOOR: either the walk provably missed callers, or a probe that would have established completeness could not run (a failed callable-value-reference query says so in boundaries). It is not itself proof that an omitted caller exists — branch on causes and read boundaries. Absent only on skipped probes (ambiguous-candidate lists, group fan-out).
447
448
  - boundaries: string[] — one plain-language sentence per reason the count is short. Prose for humans; branch on causes instead.
448
- - causes: { scopeExtractionFiles, receiverTyping, dispatchBoundary, externalBoundary, undecidedSatisfaction } — the machine-readable split of WHY, so an agent gating its own edits can tell a fixable analyzer gap from an irreducible one. Every field counts MISSING THINGS, never sentences:
449
+ - causes: { scopeExtractionFiles, receiverTyping, dispatchBoundary, externalBoundary, undecidedSatisfaction, callableValueReferences } — the machine-readable split of WHY, so an agent gating its own edits can tell a fixable analyzer gap from an irreducible one. Every field counts MISSING THINGS, never sentences:
449
450
  - causes.scopeExtractionFiles (unit: files) > 0 — scope extraction still failed after the fallback pass, so scope-resolution edges from those files are absent. A value of 0 does not prove completeness when epistemic is 'lower-bound' because an older or unverified index has no measured file count. Re-run \`gitnexus analyze --force\`; if the reason persists, inspect the extraction warnings.
450
451
  - causes.receiverTyping (unit: call sites) > 0 — the RESOLVER GAP signal: the analyzer dropped that many call sites because it could not establish the receiver's type (unresolved constructor, factory, chained expression). Those callers are absent from byDepth. Treat the result as incomplete: grep the symbol name before deleting or renaming.
451
452
  - causes.externalBoundary (unit: call sites) > 0 — those calls left the indexed program (System.out.println, fetch(...), os.environ.*). NOT a defect and NOT a reason the count is short: there is no in-graph node any edge could have reached. An epistemic:'exact' result can carry this.
452
453
  - causes.dispatchBoundary (unit: symbols) > 0 — DI or interface dispatch: that many symbols sit on or beyond a boundary a static walk cannot cross. Irreducible. A symbol count, not a site count — per-site multiplicity is not retained for these edges — so compare its magnitude with receiverTyping, not its exact value. A framework runtime-proxy boundary can make epistemic lower-bound while this value remains 0 because endpoint metadata proves the gap but cannot count omitted symbols.
453
454
 
454
455
  - causes.undecidedSatisfaction (unit: unjudged interface/type pairs) > 0 — the analyzer could not DECIDE whether a type satisfies an interface (a type in a required signature named a package it could not resolve), so no IMPLEMENTS edge exists and no dispatch boundary was left for the walk to notice. Distinct from every cause above, which count decided facts that could not be attributed; this one counts questions never answered. It is the only cause that shortens a result WITHOUT leaving a trace in the graph, so an unhedged zero on a symbol reached only through such an interface would otherwise read as 'nobody calls this'. Usually fixable: it most often means a dependency is missing from the analyzed tree.
456
+ - causes.callableValueReferences (unit: symbols) > 0 — that many symbols name this callable as a VALUE rather than calling it: 'bridge.accessor(Element.getNamespaceUri, ...)' and 'pub const h = onReset;' in Zig, '{ onClick: handler }' in JS/TS. Those are the shapes actually captured today — a bare callback argument in JS/TS ('qsort'-style, 'setTimeout(tick)') is NOT one of them and is not counted, so a 0 here does not rule that shape out. The registration IS modelled (a USES edge); the invocation through the stored value is NOT, because it happens later via a struct field, a registry lookup or comptime reflection. So impactedCount is a floor and a LOW risk verdict on such a symbol is a floor too. Unlike dispatchBoundary this is often reducible — it usually means the language provider does not yet follow that store/load — but until it is, do NOT read an empty or small caller set as 'safe to change'. It is an exact count, not a capped sample. It is 0 when the analyzer DID synthesize the dispatch through a registered property key, and epistemic stays 'exact' on that account. That exclusion is symbol-level, not edge-level — the graph does not record which registration produced which synthesized call — so a symbol with a mix of followed and unfollowed registrations also reads 0: treat a 0 as 'no unfollowed registration was proven', not as proof the value escapes nowhere. A 0 alongside epistemic 'lower-bound' can instead mean the probe could not run at all, so read boundaries to tell those apart. Read from the graph, so it needs no index-time metadata BEYOND the edges being there: an index built by an analyzer that did not yet emit this language's value-ref captures has none, and reports 0. Re-analyze before reading a 0 as measured.
455
457
 
456
458
  REQUIRES RE-INDEX: causes.scopeExtractionFiles, causes.receiverTyping, causes.externalBoundary, causes.undecidedSatisfaction, and framework runtime-proxy boundary detection depend on index-time metadata that only a current analyzer writes. Against an older index the metadata can be absent, which is indistinguishable from "nothing was dropped" unless the schema probe detects the stale index — re-run \`gitnexus analyze\` before trusting a zero or an apparently exact result.
457
459
 
@@ -53,6 +53,13 @@ export interface AnalyzeJob {
53
53
  repoUrl?: string;
54
54
  repoPath?: string;
55
55
  repoName?: string;
56
+ /**
57
+ * Index-branch selector this job was started with, part of the job's dedup
58
+ * identity. A repo is not "the same repo" for reuse purposes when a different
59
+ * branch was asked for — reusing across branches would hand the caller a 202
60
+ * for a job indexing something else.
61
+ */
62
+ branch?: string;
56
63
  progress: AnalyzeJobProgress;
57
64
  error?: string;
58
65
  /** Set only when a terminal `failed` job still persisted usable work. */
@@ -70,10 +77,21 @@ export declare class JobManager {
70
77
  private emitter;
71
78
  private cleanupTimer;
72
79
  constructor();
73
- /** Create a new job, or return existing active job for the same repo. */
80
+ /**
81
+ * Create a new job, or return the existing active job for the same repo AND
82
+ * the same branch.
83
+ *
84
+ * Branch is part of the identity deliberately. Deduping on repo alone would
85
+ * return the in-flight job for branch A to a caller that asked for branch B,
86
+ * and that caller would read the resulting 202/`complete` as "B is indexed"
87
+ * — the same silent wrong-branch outcome that made `branch` worth honoring in
88
+ * the first place. Falling through instead lets the single-slot guard below
89
+ * reject the request outright, which is a truthful answer.
90
+ */
74
91
  createJob(params: {
75
92
  repoUrl?: string;
76
93
  repoPath?: string;
94
+ branch?: string;
77
95
  }): AnalyzeJob;
78
96
  getJob(id: string): AnalyzeJob | undefined;
79
97
  /** Return a snapshot of all tracked jobs for inspection. */
@@ -34,14 +34,24 @@ export class JobManager {
34
34
  constructor() {
35
35
  this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
36
36
  }
37
- /** Create a new job, or return existing active job for the same repo. */
37
+ /**
38
+ * Create a new job, or return the existing active job for the same repo AND
39
+ * the same branch.
40
+ *
41
+ * Branch is part of the identity deliberately. Deduping on repo alone would
42
+ * return the in-flight job for branch A to a caller that asked for branch B,
43
+ * and that caller would read the resulting 202/`complete` as "B is indexed"
44
+ * — the same silent wrong-branch outcome that made `branch` worth honoring in
45
+ * the first place. Falling through instead lets the single-slot guard below
46
+ * reject the request outright, which is a truthful answer.
47
+ */
38
48
  createJob(params) {
39
- // Dedup: return existing active job for the same repo (by URL or path)
49
+ // Dedup: return existing active job for the same repo (by URL or path) and branch
40
50
  for (const job of this.jobs.values()) {
41
51
  if (!this.isTerminal(job.status)) {
42
52
  const isSameRepo = (params.repoUrl && job.repoUrl === params.repoUrl) ||
43
53
  (params.repoPath && job.repoPath === params.repoPath);
44
- if (isSameRepo) {
54
+ if (isSameRepo && job.branch === params.branch) {
45
55
  return job;
46
56
  }
47
57
  }
@@ -57,6 +67,7 @@ export class JobManager {
57
67
  status: 'queued',
58
68
  repoUrl: params.repoUrl,
59
69
  repoPath: params.repoPath,
70
+ branch: params.branch,
60
71
  progress: { phase: 'queued', percent: 0, message: 'Waiting to start...' },
61
72
  startedAt: Date.now(),
62
73
  retryCount: 0,
@@ -31,6 +31,20 @@ export interface LaunchOptions {
31
31
  springActuatorPath?: string;
32
32
  asyncApiSpecPath?: string;
33
33
  registryName?: string;
34
+ /**
35
+ * Index-branch selector, forwarded to `AnalyzeOptions.branch`.
36
+ *
37
+ * Setting it does not by itself mean a `branches/<slug>/` sub-directory:
38
+ * `resolveBranchPlacement` (storage/branch-index.ts) keeps the run on the flat
39
+ * slot when that slot has no recorded owner, or when its owner already IS this
40
+ * label. Only a label that differs from the flat slot's owner gets its own
41
+ * sub-directory.
42
+ *
43
+ * The caller is responsible for having the branch checked out —
44
+ * `resolveWriteTarget` in core refuses a label that disagrees with the working
45
+ * tree, which is what keeps one branch's content out of another's slot (#2106).
46
+ */
47
+ branch?: string;
34
48
  }
35
49
  export declare function createLaunchAnalysisWorker(deps: LaunchDeps): (job: {
36
50
  id: string;