gitnexus 1.6.4-rc.99 → 1.6.4

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 (143) hide show
  1. package/dist/_shared/index.d.ts +8 -0
  2. package/dist/_shared/index.d.ts.map +1 -1
  3. package/dist/_shared/index.js +9 -0
  4. package/dist/_shared/index.js.map +1 -1
  5. package/dist/_shared/integrations/circuit-breaker.d.ts +183 -0
  6. package/dist/_shared/integrations/circuit-breaker.d.ts.map +1 -0
  7. package/dist/_shared/integrations/circuit-breaker.js +236 -0
  8. package/dist/_shared/integrations/circuit-breaker.js.map +1 -0
  9. package/dist/_shared/integrations/resilient-fetch.d.ts +99 -0
  10. package/dist/_shared/integrations/resilient-fetch.d.ts.map +1 -0
  11. package/dist/_shared/integrations/resilient-fetch.js +204 -0
  12. package/dist/_shared/integrations/resilient-fetch.js.map +1 -0
  13. package/dist/_shared/integrations/retry.d.ts +60 -0
  14. package/dist/_shared/integrations/retry.d.ts.map +1 -0
  15. package/dist/_shared/integrations/retry.js +67 -0
  16. package/dist/_shared/integrations/retry.js.map +1 -0
  17. package/dist/_shared/integrations/understand-quickly.d.ts +79 -0
  18. package/dist/_shared/integrations/understand-quickly.d.ts.map +1 -0
  19. package/dist/_shared/integrations/understand-quickly.js +139 -0
  20. package/dist/_shared/integrations/understand-quickly.js.map +1 -0
  21. package/dist/_shared/test-helpers.d.ts +13 -0
  22. package/dist/_shared/test-helpers.d.ts.map +1 -0
  23. package/dist/_shared/test-helpers.js +13 -0
  24. package/dist/_shared/test-helpers.js.map +1 -0
  25. package/dist/cli/index.js +9 -0
  26. package/dist/cli/publish.d.ts +29 -0
  27. package/dist/cli/publish.js +174 -0
  28. package/dist/config/ignore-service.js +2 -0
  29. package/dist/core/embeddings/hf-env.d.ts +9 -37
  30. package/dist/core/embeddings/hf-env.js +65 -83
  31. package/dist/core/embeddings/http-client.d.ts +6 -0
  32. package/dist/core/embeddings/http-client.js +22 -18
  33. package/dist/core/group/bridge-db.js +7 -6
  34. package/dist/core/group/config-parser.js +19 -1
  35. package/dist/core/group/extractors/include-extractor.d.ts +39 -0
  36. package/dist/core/group/extractors/include-extractor.js +565 -0
  37. package/dist/core/group/extractors/manifest-extractor.js +8 -0
  38. package/dist/core/group/extractors/thrift-extractor.js +8 -0
  39. package/dist/core/group/matching.js +2 -0
  40. package/dist/core/group/storage.js +8 -1
  41. package/dist/core/group/sync.js +33 -0
  42. package/dist/core/group/types.d.ts +2 -1
  43. package/dist/core/ingestion/call-processor.js +6 -4
  44. package/dist/core/ingestion/cpp-ue-preprocessor.d.ts +12 -0
  45. package/dist/core/ingestion/cpp-ue-preprocessor.js +260 -0
  46. package/dist/core/ingestion/heritage-processor.js +9 -4
  47. package/dist/core/ingestion/import-processor.js +3 -2
  48. package/dist/core/ingestion/language-provider.d.ts +32 -0
  49. package/dist/core/ingestion/languages/c-cpp.js +2 -0
  50. package/dist/core/ingestion/parsing-processor.js +4 -0
  51. package/dist/core/ingestion/workers/parse-worker.js +4 -0
  52. package/dist/core/wiki/llm-client.d.ts +16 -0
  53. package/dist/core/wiki/llm-client.js +101 -63
  54. package/dist/server/api.js +7 -2
  55. package/package.json +2 -2
  56. package/vendor/node_modules/node-addon-api/node_addon_api.target.mk +16 -14
  57. package/vendor/node_modules/node-addon-api/node_addon_api_except.target.mk +16 -14
  58. package/vendor/node_modules/node-addon-api/node_addon_api_except_all.target.mk +16 -14
  59. package/vendor/node_modules/node-addon-api/node_addon_api_maybe.target.mk +16 -14
  60. package/web/assets/{agent-Dl2d-mDi.js → agent-BpHQleHE.js} +17 -17
  61. package/web/assets/architecture-YZFGNWBL-S5CXDPWN-BUzkWgDl.js +1 -0
  62. package/web/assets/{architectureDiagram-EMZXCZ2Q-uiiIna83.js → architectureDiagram-EMZXCZ2Q-CXbzok1G.js} +3 -3
  63. package/web/assets/{blockDiagram-IGV67L2C-uBKdDzXA.js → blockDiagram-IGV67L2C-DA1QJ4ZB.js} +4 -4
  64. package/web/assets/{c4Diagram-DFAF54RM-DTxaX43T.js → c4Diagram-DFAF54RM-DeJV1rCn.js} +2 -2
  65. package/web/assets/{chunk-3GS5O3IE-BxKKu7d7.js → chunk-3GS5O3IE-B8hpnia8.js} +2 -2
  66. package/web/assets/{chunk-3YCYZ6SJ-BJZnGFjg.js → chunk-3YCYZ6SJ-B-BVu21C.js} +1 -1
  67. package/web/assets/{chunk-6NTNNK5N-CIL_eO2d.js → chunk-6NTNNK5N-DW-jYSDr.js} +1 -1
  68. package/web/assets/{chunk-A34GCYZU-Dv4Vkem9.js → chunk-A34GCYZU-BbZl-IKy.js} +1 -1
  69. package/web/assets/{chunk-DJ7UZH7F-Cskl-nb3.js → chunk-DJ7UZH7F-B5tMfF87.js} +1 -1
  70. package/web/assets/{chunk-DKKBVRCY-D3S2BD56.js → chunk-DKKBVRCY-Djm-6K_e.js} +2 -2
  71. package/web/assets/{chunk-DU5LTGQ6-B8Zh51LG.js → chunk-DU5LTGQ6-QPoynYcp.js} +1 -1
  72. package/web/assets/{chunk-FXACKDTF-Cb4xHVUz.js → chunk-FXACKDTF-jieSmbe2.js} +3 -3
  73. package/web/assets/chunk-H3VCZNTA-8EMHZ3SV.js +1 -0
  74. package/web/assets/chunk-HN6EAY2L-CgE_agsD.js +1 -0
  75. package/web/assets/{chunk-O5ABG6QK-V3sk7Bps.js → chunk-O5ABG6QK-DEZV1O19.js} +1 -1
  76. package/web/assets/{chunk-PK6DOVAG-FvG7aKfZ.js → chunk-PK6DOVAG-BXv0JnsJ.js} +3 -3
  77. package/web/assets/{chunk-RNJOYNJ4-BPANsy8V.js → chunk-RNJOYNJ4-BiBkF9dN.js} +1 -1
  78. package/web/assets/chunk-RWUO3TPN-DTWzAhQ1.js +1 -0
  79. package/web/assets/chunk-TBF5ZNIQ-BSl6s9lT.js +1 -0
  80. package/web/assets/{chunk-TYMNRAUI-BnMe6HfP.js → chunk-TYMNRAUI-D1NDLIMG.js} +1 -1
  81. package/web/assets/{chunk-W7ZLLLMY-BqZFKM5j.js → chunk-W7ZLLLMY-Bu-cdLzs.js} +1 -1
  82. package/web/assets/{chunk-WSB5WSVC-EKS7-Nnc.js → chunk-WSB5WSVC-Dor71SOz.js} +1 -1
  83. package/web/assets/{chunk-XGPFEOL4-BBab-enK.js → chunk-XGPFEOL4-CUonLRle.js} +1 -1
  84. package/web/assets/classDiagram-PPOCWD7C-Cdd6UdVv.js +1 -0
  85. package/web/assets/classDiagram-v2-23LJLIIU-DNhxY2Ib.js +1 -0
  86. package/web/assets/{cose-bilkent-PNC4W37J-DjEQkTZ4.js → cose-bilkent-PNC4W37J-DdZq7Qz_.js} +1 -1
  87. package/web/assets/dagre-E77IOHMT-Cfj_6e17.js +4 -0
  88. package/web/assets/{diagram-H7BISOXX-CMuISoJL.js → diagram-H7BISOXX-BNvpsrBv.js} +2 -2
  89. package/web/assets/{diagram-JC5VWROH-DHq_eoeM.js → diagram-JC5VWROH-Cn0rtDbJ.js} +1 -1
  90. package/web/assets/{diagram-LXUTUG65-C0NczIgW.js → diagram-LXUTUG65-CHfoVKNm.js} +2 -2
  91. package/web/assets/{diagram-WEHSV5V5-WsBnpSuc.js → diagram-WEHSV5V5-BLxHksnu.js} +1 -1
  92. package/web/assets/{erDiagram-GCSMX5X6-DxHFHFR6.js → erDiagram-GCSMX5X6-DfdN6iqF.js} +2 -2
  93. package/web/assets/flowDiagram-OTCZ4VVT-aAZw4c_M.js +162 -0
  94. package/web/assets/ganttDiagram-MUNLMDZQ-BEfC_7Or.js +292 -0
  95. package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-B6ZeRLP0.js +1 -0
  96. package/web/assets/gitGraphDiagram-3HKGZ4G3-CG0kC8Q5.js +106 -0
  97. package/web/assets/{index-DDcYO1IJ.js → index-3NUCUW2s.js} +119 -119
  98. package/web/assets/info-OMHHGYJF-BF2H5H6G-BIOsZy0W.js +1 -0
  99. package/web/assets/infoDiagram-MN7RKWGX-DUoMH-pq.js +2 -0
  100. package/web/assets/{ishikawaDiagram-YMYX4NHK-C-JI2Ih7.js → ishikawaDiagram-YMYX4NHK-BF8AtDaj.js} +2 -2
  101. package/web/assets/{journeyDiagram-SO5T7YLQ-IPX4h3iD.js → journeyDiagram-SO5T7YLQ-BW8oKa3N.js} +3 -3
  102. package/web/assets/{kanban-definition-LJHFXRCJ-B23LtshQ.js → kanban-definition-LJHFXRCJ-DzbOsR-2.js} +6 -6
  103. package/web/assets/{mindmap-definition-2EUWGEK5-DJKUsrCo.js → mindmap-definition-2EUWGEK5-BIXGQALq.js} +25 -25
  104. package/web/assets/packet-4T2RLAQJ-EV4IVRXR-Bs7ysNMU.js +1 -0
  105. package/web/assets/pie-ZZUOXDRM-N23DN5KN-Csi8nnbH.js +1 -0
  106. package/web/assets/{pieDiagram-3IATQBI2-Bj26KTKk.js → pieDiagram-3IATQBI2-QY-u7P6a.js} +3 -3
  107. package/web/assets/{quadrantDiagram-E256RVCF-VAk5oIRr.js → quadrantDiagram-E256RVCF-Di4aNiU8.js} +3 -3
  108. package/web/assets/radar-PYXPWWZC-P6TP7ZYP-DJlyXh2-.js +1 -0
  109. package/web/assets/{requirementDiagram-M5DCFWZL-CmlTJ_vQ.js → requirementDiagram-M5DCFWZL-CzUUrhQD.js} +4 -4
  110. package/web/assets/{sankeyDiagram-L3NBLAOT-C-f_DdcX.js → sankeyDiagram-L3NBLAOT-D2fbMiIU.js} +3 -3
  111. package/web/assets/{sequenceDiagram-ZOUHS735-DmtNf1NF.js → sequenceDiagram-ZOUHS735-BNdkqguh.js} +6 -6
  112. package/web/assets/stateDiagram-MLPALWAM-51_Q2w2L.js +1 -0
  113. package/web/assets/stateDiagram-v2-B5LQ5ZB2-8nHs2aNE.js +1 -0
  114. package/web/assets/{timeline-definition-5SPVSISX-BkpqZxt2.js → timeline-definition-5SPVSISX-DkMiGj4Z.js} +9 -9
  115. package/web/assets/treeView-SZITEDCU-5DXDK3XO-Cy6P0_CZ.js +1 -0
  116. package/web/assets/treemap-W4RFUUIX-WYLRDWKO-C_pp1R7A.js +1 -0
  117. package/web/assets/{vennDiagram-IE5QUKF5-iAJ08JBe.js → vennDiagram-IE5QUKF5-BCxMmf9q.js} +3 -3
  118. package/web/assets/wardley-RL74JXVD-BCRCBASE-CMStJhpn.js +1 -0
  119. package/web/assets/{wardleyDiagram-XU3VSMPF-BDiKTkL4.js → wardleyDiagram-XU3VSMPF-ijlLF77s.js} +4 -4
  120. package/web/assets/{xychartDiagram-ZHJ5623Y-CknyZyQK.js → xychartDiagram-ZHJ5623Y-DZ7fP1_3.js} +3 -3
  121. package/web/index.html +1 -1
  122. package/web/assets/architecture-YZFGNWBL-S5CXDPWN-BYXnTu07.js +0 -1
  123. package/web/assets/chunk-H3VCZNTA-BOGhqvxW.js +0 -1
  124. package/web/assets/chunk-HN6EAY2L-CitKFy1e.js +0 -1
  125. package/web/assets/chunk-RWUO3TPN-BHusg2jI.js +0 -1
  126. package/web/assets/chunk-TBF5ZNIQ-CXamrpH3.js +0 -1
  127. package/web/assets/classDiagram-PPOCWD7C-B-LkKU0P.js +0 -1
  128. package/web/assets/classDiagram-v2-23LJLIIU-_NGI4VEh.js +0 -1
  129. package/web/assets/dagre-E77IOHMT-D6Xacg1x.js +0 -4
  130. package/web/assets/flowDiagram-OTCZ4VVT-_G1VxyQS.js +0 -162
  131. package/web/assets/ganttDiagram-MUNLMDZQ-BIHObj0t.js +0 -292
  132. package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-BXJXsixL.js +0 -1
  133. package/web/assets/gitGraphDiagram-3HKGZ4G3-COZUkHTD.js +0 -106
  134. package/web/assets/info-OMHHGYJF-BF2H5H6G-C820LK0J.js +0 -1
  135. package/web/assets/infoDiagram-MN7RKWGX-BeOjfPc_.js +0 -2
  136. package/web/assets/packet-4T2RLAQJ-EV4IVRXR-BvWtM5jq.js +0 -1
  137. package/web/assets/pie-ZZUOXDRM-N23DN5KN-BnDgj0TQ.js +0 -1
  138. package/web/assets/radar-PYXPWWZC-P6TP7ZYP-2IuDlZ6r.js +0 -1
  139. package/web/assets/stateDiagram-MLPALWAM-Lq7UVrkj.js +0 -1
  140. package/web/assets/stateDiagram-v2-B5LQ5ZB2-DCiXBPeS.js +0 -1
  141. package/web/assets/treeView-SZITEDCU-5DXDK3XO-Ipq1EIaF.js +0 -1
  142. package/web/assets/treemap-W4RFUUIX-WYLRDWKO-DESFxovu.js +0 -1
  143. package/web/assets/wardley-RL74JXVD-BCRCBASE-BVkhckBY.js +0 -1
@@ -0,0 +1,39 @@
1
+ import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js';
2
+ import type { ExtractedContract, RepoHandle } from '../types.js';
3
+ export declare class IncludeExtractor implements ContractExtractor {
4
+ type: "include";
5
+ /**
6
+ * Always returns `true`. NOT called by `sync.ts`, which gates extraction via
7
+ * `config.detect.includes` instead (see `sync.ts:174`). Kept solely to satisfy
8
+ * the `ContractExtractor` interface so the type stays uniform across extractors.
9
+ */
10
+ canExtract(_repo: RepoHandle): Promise<boolean>;
11
+ extract(dbExecutor: CypherExecutor | null, repoPath: string, _repo: RepoHandle): Promise<ExtractedContract[]>;
12
+ /**
13
+ * Discover repo-relative file paths using exactly the same rules the
14
+ * ingestion pipeline uses (`walkRepositoryPaths` in
15
+ * `gitnexus/src/core/ingestion/filesystem-walker.ts`):
16
+ * - `createIgnoreFilter` honors `.gitignore`, `.gitnexusignore`, the
17
+ * hardcoded ignore list, and `.gitnexusignore` last-match-wins
18
+ * negation.
19
+ * - `getMaxFileSizeBytes()` drops files larger than the cap so we
20
+ * never emit `File:<rel>` UIDs for files ingestion would skip.
21
+ *
22
+ * Uses sequential stat — there is no `READ_CONCURRENCY` batching here
23
+ * because group sync runs at startup-time, not the ingestion hot path,
24
+ * and parallelism gains are not worth the import-graph weight.
25
+ *
26
+ * MAINTENANCE: if `walkRepositoryPaths` changes its glob options, ignore
27
+ * filter shape, or size-cap logic, mirror those changes here. The two
28
+ * implementations exist because the consumers need different return
29
+ * shapes (string[] vs ScannedFile[]) and different concurrency, but
30
+ * they MUST agree on which files are reachable — that is what makes
31
+ * `File:<rel>` UIDs in cross-links correspond to graph File nodes.
32
+ */
33
+ private discoverIndexableFiles;
34
+ private extractProviders;
35
+ private extractProvidersGraph;
36
+ private extractProvidersFallback;
37
+ private extractConsumers;
38
+ private dedupe;
39
+ }
@@ -0,0 +1,565 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs/promises';
3
+ import { glob } from 'glob';
4
+ import Parser from 'tree-sitter';
5
+ import C from 'tree-sitter-c';
6
+ import Cpp from 'tree-sitter-cpp';
7
+ import { readSafe } from './fs-utils.js';
8
+ import { buildSuffixIndex } from '../../ingestion/import-resolvers/utils.js';
9
+ import { createIgnoreFilter } from '../../../config/ignore-service.js';
10
+ import { getMaxFileSizeBytes } from '../../ingestion/utils/max-file-size.js';
11
+ import { logger } from '../../logger.js';
12
+ /**
13
+ * Cross-repo C/C++ `#include` dependency extractor.
14
+ *
15
+ * **Provider side:** registers every `.h/.hpp/.hxx/.hh` file in the repo
16
+ * as a provider contract with `include::<relative-path>`.
17
+ *
18
+ * **Consumer side:** parses all C/C++ source/header files for `#include "…"`
19
+ * directives, attempts suffix-based resolution against the repo's own file
20
+ * list (reusing the same algorithm as the single-repo ingestion pipeline),
21
+ * and emits unresolved include paths as consumer contracts.
22
+ *
23
+ * Matching: a consumer's `include::map/base/dice_map_view.h` in repo A
24
+ * matches a provider's `include::map/base/dice_map_view.h` in repo B via
25
+ * exact contract-id equality in `runExactMatch`.
26
+ */
27
+ // ---------- constants ----------
28
+ const HEADER_EXTENSIONS = new Set(['.h', '.hpp', '.hxx', '.hh']);
29
+ // Source = headers (provider-eligible) ∪ implementation files (.c/.cpp/.cc/.cxx).
30
+ // Spread keeps the subset relationship explicit so a future contributor adding
31
+ // a new header extension to HEADER_EXTENSIONS does not have to remember to
32
+ // also add it here.
33
+ const SOURCE_EXTENSIONS = new Set([...HEADER_EXTENSIONS, '.c', '.cpp', '.cc', '.cxx']);
34
+ const INCLUDE_QUERY_SRC = '(preproc_include path: (_) @import.source) @import';
35
+ /**
36
+ * Well-known C/C++ standard library headers that can appear in `#include "…"`
37
+ * form (some projects use quotes for system headers).
38
+ */
39
+ const SYSTEM_HEADERS = new Set([
40
+ // C standard
41
+ 'assert.h',
42
+ 'complex.h',
43
+ 'ctype.h',
44
+ 'errno.h',
45
+ 'fenv.h',
46
+ 'float.h',
47
+ 'inttypes.h',
48
+ 'iso646.h',
49
+ 'limits.h',
50
+ 'locale.h',
51
+ 'math.h',
52
+ 'setjmp.h',
53
+ 'signal.h',
54
+ 'stdalign.h',
55
+ 'stdarg.h',
56
+ 'stdatomic.h',
57
+ 'stdbool.h',
58
+ 'stddef.h',
59
+ 'stdint.h',
60
+ 'stdio.h',
61
+ 'stdlib.h',
62
+ 'stdnoreturn.h',
63
+ 'string.h',
64
+ 'tgmath.h',
65
+ 'threads.h',
66
+ 'time.h',
67
+ 'uchar.h',
68
+ 'wchar.h',
69
+ 'wctype.h',
70
+ // C++ standard (extensionless)
71
+ 'algorithm',
72
+ 'any',
73
+ 'array',
74
+ 'atomic',
75
+ 'barrier',
76
+ 'bit',
77
+ 'bitset',
78
+ 'cassert',
79
+ 'cctype',
80
+ 'cerrno',
81
+ 'cfenv',
82
+ 'cfloat',
83
+ 'charconv',
84
+ 'chrono',
85
+ 'cinttypes',
86
+ 'climits',
87
+ 'clocale',
88
+ 'cmath',
89
+ 'codecvt',
90
+ 'compare',
91
+ 'complex',
92
+ 'concepts',
93
+ 'condition_variable',
94
+ 'coroutine',
95
+ 'csetjmp',
96
+ 'csignal',
97
+ 'cstdarg',
98
+ 'cstddef',
99
+ 'cstdint',
100
+ 'cstdio',
101
+ 'cstdlib',
102
+ 'cstring',
103
+ 'ctime',
104
+ 'cuchar',
105
+ 'cwchar',
106
+ 'cwctype',
107
+ 'deque',
108
+ 'exception',
109
+ 'execution',
110
+ 'expected',
111
+ 'filesystem',
112
+ 'format',
113
+ 'forward_list',
114
+ 'fstream',
115
+ 'functional',
116
+ 'future',
117
+ 'generator',
118
+ 'initializer_list',
119
+ 'iomanip',
120
+ 'ios',
121
+ 'iosfwd',
122
+ 'iostream',
123
+ 'istream',
124
+ 'iterator',
125
+ 'latch',
126
+ 'limits',
127
+ 'list',
128
+ 'locale',
129
+ 'map',
130
+ 'mdspan',
131
+ 'memory',
132
+ 'memory_resource',
133
+ 'mutex',
134
+ 'new',
135
+ 'numbers',
136
+ 'numeric',
137
+ 'optional',
138
+ 'ostream',
139
+ 'print',
140
+ 'queue',
141
+ 'random',
142
+ 'ranges',
143
+ 'ratio',
144
+ 'regex',
145
+ 'scoped_allocator',
146
+ 'semaphore',
147
+ 'set',
148
+ 'shared_mutex',
149
+ 'source_location',
150
+ 'span',
151
+ 'spanstream',
152
+ 'sstream',
153
+ 'stack',
154
+ 'stacktrace',
155
+ 'stdexcept',
156
+ 'stdfloat',
157
+ 'stop_token',
158
+ 'streambuf',
159
+ 'string',
160
+ 'string_view',
161
+ 'strstream',
162
+ 'syncstream',
163
+ 'system_error',
164
+ 'thread',
165
+ 'tuple',
166
+ 'type_traits',
167
+ 'typeindex',
168
+ 'typeinfo',
169
+ 'unordered_map',
170
+ 'unordered_set',
171
+ 'utility',
172
+ 'valarray',
173
+ 'variant',
174
+ 'vector',
175
+ 'version',
176
+ ]);
177
+ /** Path prefixes that indicate system/kernel headers. */
178
+ const SYSTEM_PATH_PREFIXES = [
179
+ 'sys/',
180
+ 'net/',
181
+ 'netinet/',
182
+ 'arpa/',
183
+ 'linux/',
184
+ 'asm/',
185
+ 'bits/',
186
+ 'gnu/',
187
+ 'mach/',
188
+ 'machine/',
189
+ 'xlocale/',
190
+ ];
191
+ /** Regex fallback for files that exceed tree-sitter's 32 KB parse limit. */
192
+ const INCLUDE_REGEX = /^[ \t]*#\s*include\s*"([^"]+)"/gm;
193
+ // ---------- helpers ----------
194
+ /**
195
+ * Normalize an include path to a canonical lowercase forward-slash form.
196
+ *
197
+ * IMPORTANT — case-folding caveat (PR #1156 review finding #3):
198
+ * Header paths are lowercased so consumer `#include "Foo/Bar.h"` and
199
+ * provider file `Foo/Bar.h` normalize to the same contract-id. This is
200
+ * the right trade-off on case-insensitive filesystems (macOS, Windows)
201
+ * but on case-sensitive Linux filesystems two distinct headers `Foo.h`
202
+ * and `foo.h` in the same repo will collide onto the same provider
203
+ * contract-id; only one survives `dedupe()`. The gain (reliable
204
+ * cross-platform matching) outweighs the cost (extremely rare header
205
+ * casing collisions inside a single repo).
206
+ */
207
+ function normalizeIncludePath(raw) {
208
+ return raw.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/').toLowerCase();
209
+ }
210
+ /**
211
+ * Strip C/C++ block comments from a source blob. Used only by the
212
+ * regex-fallback path to avoid emitting consumer contracts for
213
+ * commented-out #include directives. Line comments (`// …`) cannot hide
214
+ * #include directives because the regex anchors on start-of-line.
215
+ * See PR #1156 review finding #5.
216
+ */
217
+ function stripBlockComments(src) {
218
+ return src.replace(/\/\*[\s\S]*?\*\//g, '');
219
+ }
220
+ function isAngleBracketInclude(rawNodeText) {
221
+ const trimmed = rawNodeText.trim();
222
+ return trimmed.startsWith('<') && trimmed.endsWith('>');
223
+ }
224
+ function isSystemHeader(cleanedPath) {
225
+ // Check well-known standard headers
226
+ if (SYSTEM_HEADERS.has(cleanedPath))
227
+ return true;
228
+ // Check system path prefixes
229
+ const lower = cleanedPath.toLowerCase();
230
+ return SYSTEM_PATH_PREFIXES.some((prefix) => lower.startsWith(prefix));
231
+ }
232
+ function isHeaderFile(filePath) {
233
+ return HEADER_EXTENSIONS.has(path.extname(filePath).toLowerCase());
234
+ }
235
+ function getLanguageForFile(filePath) {
236
+ const ext = path.extname(filePath).toLowerCase();
237
+ switch (ext) {
238
+ case '.c':
239
+ case '.h':
240
+ return C;
241
+ case '.cpp':
242
+ case '.cc':
243
+ case '.cxx':
244
+ case '.hpp':
245
+ case '.hxx':
246
+ case '.hh':
247
+ return Cpp;
248
+ default:
249
+ return null;
250
+ }
251
+ }
252
+ /**
253
+ * Check whether an include path resolves to a file inside the local repo.
254
+ *
255
+ * Uses *exact full-path* matching on the suffix index — we never accept a
256
+ * truncated suffix match. For `#include "foo/bar.h"` this checks:
257
+ * (a) a file whose path ends with the full `foo/bar.h`
258
+ * (b) if the include omitted the extension, a file whose path ends with
259
+ * the include + one of the C/C++ header extensions
260
+ *
261
+ * Returns `true` when a local file matches — caller should suppress the
262
+ * cross-repo consumer contract.
263
+ *
264
+ * See PR #1156 review finding #4 (suffixResolve ambiguity).
265
+ */
266
+ function isLocalInclude(cleaned, suffixIndex) {
267
+ const candidates = [cleaned];
268
+ if (!/\.[a-zA-Z0-9]+$/.test(cleaned)) {
269
+ for (const ext of ['.h', '.hpp', '.hxx', '.hh'])
270
+ candidates.push(cleaned + ext);
271
+ }
272
+ for (const c of candidates) {
273
+ if (suffixIndex.get(c) || suffixIndex.getInsensitive(c))
274
+ return true;
275
+ }
276
+ return false;
277
+ }
278
+ // ---------- main class ----------
279
+ export class IncludeExtractor {
280
+ type = 'include';
281
+ /**
282
+ * Always returns `true`. NOT called by `sync.ts`, which gates extraction via
283
+ * `config.detect.includes` instead (see `sync.ts:174`). Kept solely to satisfy
284
+ * the `ContractExtractor` interface so the type stays uniform across extractors.
285
+ */
286
+ async canExtract(_repo) {
287
+ return true;
288
+ }
289
+ async extract(dbExecutor, repoPath, _repo) {
290
+ // 1. Build the local file list using the same discovery as ingestion
291
+ // (createIgnoreFilter + getMaxFileSizeBytes). This guarantees the
292
+ // universe of provider/consumer paths matches the universe of File
293
+ // nodes in the LadybugDB graph — so no cross-link points at a UID
294
+ // that group impact cannot fan out to.
295
+ // (PR #1156 Codex follow-up: discovery aligned with ingestion.)
296
+ const allFiles = await this.discoverIndexableFiles(repoPath);
297
+ const normalizedFiles = allFiles.map((f) => f.replace(/\\/g, '/'));
298
+ const suffixIndex = buildSuffixIndex(normalizedFiles, allFiles);
299
+ // 2. Provider: register all header files
300
+ const providers = await this.extractProviders(dbExecutor, repoPath, allFiles);
301
+ // 3. Consumer: filter the shared discovery list for source extensions
302
+ // and parse #include directives in those files.
303
+ const sourceFiles = allFiles.filter((f) => SOURCE_EXTENSIONS.has(path.extname(f).toLowerCase()));
304
+ const consumers = await this.extractConsumers(repoPath, sourceFiles, suffixIndex);
305
+ return this.dedupe([...providers, ...consumers]);
306
+ }
307
+ /**
308
+ * Discover repo-relative file paths using exactly the same rules the
309
+ * ingestion pipeline uses (`walkRepositoryPaths` in
310
+ * `gitnexus/src/core/ingestion/filesystem-walker.ts`):
311
+ * - `createIgnoreFilter` honors `.gitignore`, `.gitnexusignore`, the
312
+ * hardcoded ignore list, and `.gitnexusignore` last-match-wins
313
+ * negation.
314
+ * - `getMaxFileSizeBytes()` drops files larger than the cap so we
315
+ * never emit `File:<rel>` UIDs for files ingestion would skip.
316
+ *
317
+ * Uses sequential stat — there is no `READ_CONCURRENCY` batching here
318
+ * because group sync runs at startup-time, not the ingestion hot path,
319
+ * and parallelism gains are not worth the import-graph weight.
320
+ *
321
+ * MAINTENANCE: if `walkRepositoryPaths` changes its glob options, ignore
322
+ * filter shape, or size-cap logic, mirror those changes here. The two
323
+ * implementations exist because the consumers need different return
324
+ * shapes (string[] vs ScannedFile[]) and different concurrency, but
325
+ * they MUST agree on which files are reachable — that is what makes
326
+ * `File:<rel>` UIDs in cross-links correspond to graph File nodes.
327
+ */
328
+ async discoverIndexableFiles(repoPath) {
329
+ const ignoreFilter = await createIgnoreFilter(repoPath);
330
+ const maxFileSizeBytes = getMaxFileSizeBytes();
331
+ const candidates = await glob('**/*', {
332
+ cwd: repoPath,
333
+ nodir: true,
334
+ dot: false,
335
+ ignore: ignoreFilter,
336
+ });
337
+ const survivors = [];
338
+ for (const rel of candidates) {
339
+ try {
340
+ const stat = await fs.stat(path.join(repoPath, rel));
341
+ if (stat.size > maxFileSizeBytes)
342
+ continue;
343
+ survivors.push(rel);
344
+ }
345
+ catch (err) {
346
+ // ENOENT is the documented benign race (glob enumerated a file
347
+ // that was deleted before we stat'd it — same race
348
+ // walkRepositoryPaths absorbs via Promise.allSettled). Anything
349
+ // else (EACCES, EMFILE, EIO) deserves a warning so an operator
350
+ // can spot a permission/resource problem instead of silently
351
+ // shipping fewer contracts than expected.
352
+ const code = err?.code;
353
+ if (code !== 'ENOENT') {
354
+ logger.warn({ err: err.message, file: rel, repoPath }, '⚠️ IncludeExtractor: stat failed during discovery; skipping file');
355
+ }
356
+ }
357
+ }
358
+ return survivors;
359
+ }
360
+ // ---------- provider extraction ----------
361
+ async extractProviders(dbExecutor, repoPath, allFiles) {
362
+ // Strategy A: graph-assisted
363
+ if (dbExecutor) {
364
+ const graphProviders = await this.extractProvidersGraph(dbExecutor, repoPath);
365
+ if (graphProviders.length > 0)
366
+ return graphProviders;
367
+ }
368
+ // Strategy B: filesystem fallback
369
+ return this.extractProvidersFallback(repoPath, allFiles);
370
+ }
371
+ async extractProvidersGraph(db, repoPath) {
372
+ try {
373
+ const rows = await db(`MATCH (f:File)
374
+ WHERE f.filePath =~ '.*\\\\.(h|hpp|hxx|hh)$'
375
+ RETURN f.filePath AS filePath, f.id AS fileId`);
376
+ // gitnexus analyze stores absolute paths in the File.filePath column.
377
+ // Provider contract IDs MUST be repo-relative — otherwise the consumer
378
+ // emits `include::map/base/view.h` and the provider emits
379
+ // `include::/abs/path/to/repo/map/base/view.h`, which never match
380
+ // through runExactMatch and the cross-link silently disappears.
381
+ // (PR #1156 follow-up review: graph provider absolute-path bug.)
382
+ const normalizedRepoPath = path.resolve(repoPath);
383
+ const out = [];
384
+ for (const r of rows) {
385
+ if (typeof r.filePath !== 'string' || !r.filePath)
386
+ continue;
387
+ const absolute = r.filePath;
388
+ const rel = path.relative(normalizedRepoPath, absolute);
389
+ // Skip rows that resolve outside the repo (e.g., system headers
390
+ // somehow indexed, or stale absolute paths from a different machine).
391
+ // path.relative returns a `..`-prefixed path or an absolute path
392
+ // when the target is outside the base — both are wrong for our IDs.
393
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
394
+ continue;
395
+ const normalizedRel = rel.replace(/\\/g, '/');
396
+ out.push({
397
+ contractId: `include::${normalizeIncludePath(normalizedRel)}`,
398
+ type: 'include',
399
+ role: 'provider',
400
+ symbolUid: String(r.fileId ?? ''),
401
+ symbolRef: { filePath: normalizedRel, name: path.basename(normalizedRel) },
402
+ symbolName: path.basename(normalizedRel),
403
+ confidence: 1.0,
404
+ meta: { source: 'graph' },
405
+ });
406
+ }
407
+ return out;
408
+ }
409
+ catch {
410
+ return [];
411
+ }
412
+ }
413
+ extractProvidersFallback(_repoPath, allFiles) {
414
+ return allFiles
415
+ .filter((f) => isHeaderFile(f))
416
+ .map((f) => {
417
+ const filePath = f.replace(/\\/g, '/');
418
+ return {
419
+ contractId: `include::${normalizeIncludePath(filePath)}`,
420
+ type: 'include',
421
+ role: 'provider',
422
+ symbolUid: `File:${filePath}`,
423
+ symbolRef: { filePath, name: path.basename(filePath) },
424
+ symbolName: path.basename(filePath),
425
+ confidence: 0.95,
426
+ meta: { source: 'filesystem' },
427
+ };
428
+ });
429
+ }
430
+ // ---------- consumer extraction ----------
431
+ async extractConsumers(repoPath, sourceFiles, suffixIndex) {
432
+ const parser = new Parser();
433
+ const out = [];
434
+ // Compile the include query once per grammar to avoid re-compilation per file
435
+ const queryCache = new Map();
436
+ for (const rel of sourceFiles) {
437
+ const lang = getLanguageForFile(rel);
438
+ if (!lang)
439
+ continue;
440
+ const content = readSafe(repoPath, rel);
441
+ if (!content)
442
+ continue;
443
+ let query = queryCache.get(lang);
444
+ if (!query) {
445
+ try {
446
+ query = new Parser.Query(lang, INCLUDE_QUERY_SRC);
447
+ queryCache.set(lang, query);
448
+ }
449
+ catch {
450
+ continue;
451
+ }
452
+ }
453
+ // Collect raw include paths: tree-sitter first, regex fallback for large files.
454
+ // `extractionSource` is stamped on each emitted consumer contract so
455
+ // regex-fallback contracts stay auditable post-hoc (PR #1156 review finding #6).
456
+ let rawIncludes;
457
+ let extractionSource;
458
+ try {
459
+ parser.setLanguage(lang);
460
+ const tree = parser.parse(content);
461
+ let matches;
462
+ try {
463
+ matches = query.matches(tree.rootNode);
464
+ }
465
+ catch {
466
+ matches = [];
467
+ }
468
+ rawIncludes = [];
469
+ extractionSource = 'tree_sitter';
470
+ for (const match of matches) {
471
+ const sourceNode = match.captures.find((c) => c.name === 'import.source');
472
+ if (!sourceNode)
473
+ continue;
474
+ const rawText = sourceNode.node.text;
475
+ if (isAngleBracketInclude(rawText))
476
+ continue;
477
+ const cleaned = rawText.replace(/['"<>]/g, '');
478
+ if (cleaned && cleaned.length <= 2048)
479
+ rawIncludes.push(cleaned);
480
+ }
481
+ }
482
+ catch {
483
+ // tree-sitter failed (e.g. file > 32 KB) — fall back to regex.
484
+ // Strip block comments first so we don't emit a consumer contract
485
+ // for a commented-out #include (PR #1156 review finding #5).
486
+ rawIncludes = [];
487
+ extractionSource = 'regex_fallback';
488
+ const scanTarget = stripBlockComments(content);
489
+ INCLUDE_REGEX.lastIndex = 0;
490
+ let m;
491
+ while ((m = INCLUDE_REGEX.exec(scanTarget)) !== null) {
492
+ if (m[1] && m[1].length <= 2048)
493
+ rawIncludes.push(m[1]);
494
+ }
495
+ }
496
+ for (const cleaned of rawIncludes) {
497
+ // Filter: skip known system headers and system path prefixes
498
+ if (isSystemHeader(cleaned))
499
+ continue;
500
+ // Skip relative-up includes: `#include "../include/foo.h"` is
501
+ // almost always an intra-repo reference. The suffix index is built
502
+ // from repo-relative paths, so isLocalInclude can never match
503
+ // `../foo.h`, and emitting it as a consumer contract just pollutes
504
+ // the registry with an entry no provider can ever satisfy.
505
+ // (PR #1156 follow-up review: `../` relative includes produce
506
+ // spurious consumer contracts.)
507
+ if (cleaned.startsWith('../') || cleaned.startsWith('..\\'))
508
+ continue;
509
+ // Skip macro-style includes: `#include PLATFORM_HEADER` parses as an
510
+ // identifier under tree-sitter's `(_) @import.source` wildcard. The
511
+ // identifier text passes the strip/clean step unchanged, so without
512
+ // this guard we would emit `include::platform_header` as a consumer
513
+ // contract — and no provider in any repo will ever expose a contract
514
+ // for a macro identifier (no file is named `PLATFORM_HEADER`). The
515
+ // contract would sit permanently orphaned in the registry. Real
516
+ // header references always contain a path separator (`/`, `\`) or an
517
+ // extension dot (`foo.h`), so an absent both is a reliable signal we
518
+ // are looking at a macro identifier. (PR #1156 follow-up review:
519
+ // macro includes emit orphaned consumer contracts.)
520
+ if (!/[./\\]/.test(cleaned))
521
+ continue;
522
+ // Local resolution (PR #1156 review finding #4): only accept an
523
+ // exact-suffix match on the *full* include path. The generic
524
+ // suffixResolve() iterates all truncated suffixes, which would
525
+ // silently suppress a cross-repo `#include "map/base/view.h"`
526
+ // when the local repo has any `internal/view.h` — a realistic
527
+ // false-negative in large C++ codebases. Here we only resolve
528
+ // locally if a file path ends with the complete include string
529
+ // (optionally re-appending one of the C/C++ header extensions
530
+ // when the include already omits it).
531
+ if (isLocalInclude(cleaned, suffixIndex))
532
+ continue;
533
+ // Unresolved: emit as consumer contract
534
+ const normalizedRel = rel.replace(/\\/g, '/');
535
+ out.push({
536
+ contractId: `include::${normalizeIncludePath(cleaned)}`,
537
+ type: 'include',
538
+ role: 'consumer',
539
+ symbolUid: `File:${normalizedRel}`,
540
+ symbolRef: { filePath: normalizedRel, name: cleaned },
541
+ symbolName: cleaned,
542
+ confidence: 0.85,
543
+ meta: {
544
+ source: extractionSource,
545
+ includePath: cleaned,
546
+ },
547
+ });
548
+ }
549
+ }
550
+ return out;
551
+ }
552
+ // ---------- deduplication ----------
553
+ dedupe(items) {
554
+ const seen = new Set();
555
+ const out = [];
556
+ for (const c of items) {
557
+ const k = `${c.contractId}|${c.role}|${c.symbolRef.filePath}`;
558
+ if (seen.has(k))
559
+ continue;
560
+ seen.add(k);
561
+ out.push(c);
562
+ }
563
+ return out;
564
+ }
565
+ }
@@ -221,6 +221,12 @@ export class ManifestExtractor {
221
221
  rows = await executor(`MATCH (n:Package|Module) WHERE n.name = $contract
222
222
  RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
223
223
  ORDER BY n.filePath ASC
224
+ LIMIT 1`, { contract: link.contract });
225
+ }
226
+ else if (link.type === 'include') {
227
+ rows = await executor(`MATCH (f:File) WHERE f.filePath = $contract
228
+ RETURN f.id AS uid, f.name AS name, f.filePath AS filePath
229
+ ORDER BY f.filePath ASC
224
230
  LIMIT 1`, { contract: link.contract });
225
231
  }
226
232
  else if (link.type === 'custom') {
@@ -303,6 +309,8 @@ export class ManifestExtractor {
303
309
  return `lib::${contract}`;
304
310
  case 'custom':
305
311
  return `custom::${contract}`;
312
+ case 'include':
313
+ return `include::${contract}`;
306
314
  default: {
307
315
  const _exhaustive = type;
308
316
  throw new Error(`Unhandled ContractType: ${String(_exhaustive)}`);
@@ -155,6 +155,10 @@ export async function buildThriftContext(repoPath) {
155
155
  cwd: repoPath,
156
156
  absolute: false,
157
157
  nodir: true,
158
+ // TODO(#1156-followup): replace this hand-rolled list with createIgnoreFilter
159
+ // (the canonical ingestion ignore filter, like include-extractor.ts now uses).
160
+ // New entries to DEFAULT_IGNORE_LIST in src/config/ignore-service.ts (e.g.
161
+ // third_party, 3rdparty added in commit a9936a9b) silently do not apply here.
158
162
  ignore: ['**/node_modules/**', '**/.git/**', '**/vendor/**', '**/dist/**', '**/build/**'],
159
163
  });
160
164
  const namespacesByThrift = new Map();
@@ -207,6 +211,10 @@ export class ThriftExtractor {
207
211
  cwd: repoPath,
208
212
  absolute: false,
209
213
  nodir: true,
214
+ // TODO(#1156-followup): replace this hand-rolled list with createIgnoreFilter
215
+ // (the canonical ingestion ignore filter, like include-extractor.ts now uses).
216
+ // New entries to DEFAULT_IGNORE_LIST in src/config/ignore-service.ts (e.g.
217
+ // third_party, 3rdparty added in commit a9936a9b) silently do not apply here.
210
218
  ignore: ['**/node_modules/**', '**/.git/**', '**/vendor/**', '**/dist/**', '**/build/**'],
211
219
  });
212
220
  const parser = new Parser();
@@ -93,6 +93,8 @@ export function normalizeContractId(id) {
93
93
  return `topic::${rest.trim().toLowerCase()}`;
94
94
  case 'lib':
95
95
  return `lib::${rest.toLowerCase()}`;
96
+ case 'include':
97
+ return `include::${rest.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/').toLowerCase()}`;
96
98
  default:
97
99
  return id;
98
100
  }
@@ -3,6 +3,7 @@ import * as fsp from 'node:fs/promises';
3
3
  import * as path from 'node:path';
4
4
  import * as os from 'node:os';
5
5
  import { randomBytes } from 'node:crypto';
6
+ import { retryRename } from './bridge-db.js';
6
7
  /**
7
8
  * Build an unpredictable suffix for atomic-write tmp files. Replaces the
8
9
  * previous `Date.now()` pattern which CodeQL flagged as
@@ -45,7 +46,13 @@ export async function writeContractRegistry(groupDir, registry) {
45
46
  finally {
46
47
  await handle.close();
47
48
  }
48
- await fsp.rename(tmpPath, targetPath);
49
+ // retryRename absorbs the documented Windows EPERM/EBUSY/EACCES race that
50
+ // fires when AV scanners or another concurrent rename briefly hold the
51
+ // destination handle between rename calls. Same helper bridge-db.ts uses
52
+ // (lines 304, 583, 587, 595, 605, 677) for the bridge.lbug atomic swap —
53
+ // single source of truth for the Windows-rename pattern across the group
54
+ // package.
55
+ await retryRename(tmpPath, targetPath);
49
56
  }
50
57
  export async function readContractRegistry(groupDir) {
51
58
  const filePath = path.join(groupDir, CONTRACTS_FILE);