gitnexus 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/_shared/graph/types.d.ts +1 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +1 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +1 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +3 -1
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/mro-strategy.d.ts +19 -0
- package/dist/_shared/mro-strategy.d.ts.map +1 -0
- package/dist/_shared/mro-strategy.js +2 -0
- package/dist/_shared/mro-strategy.js.map +1 -0
- package/dist/cli/ai-context.d.ts +1 -0
- package/dist/cli/ai-context.js +28 -4
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +2 -1
- package/dist/cli/group.d.ts +2 -0
- package/dist/cli/group.js +233 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/serve.js +4 -1
- package/dist/cli/setup.js +34 -3
- package/dist/cli/wiki.js +15 -44
- package/dist/config/ignore-service.js +8 -3
- package/dist/core/augmentation/engine.js +1 -1
- package/dist/core/git-staleness.d.ts +13 -0
- package/dist/core/git-staleness.js +29 -0
- package/dist/core/group/bridge-db.d.ts +82 -0
- package/dist/core/group/bridge-db.js +460 -0
- package/dist/core/group/bridge-schema.d.ts +27 -0
- package/dist/core/group/bridge-schema.js +55 -0
- package/dist/core/group/config-parser.d.ts +3 -0
- package/dist/core/group/config-parser.js +83 -0
- package/dist/core/group/contract-extractor.d.ts +7 -0
- package/dist/core/group/contract-extractor.js +1 -0
- package/dist/core/group/extractors/grpc-extractor.d.ts +16 -0
- package/dist/core/group/extractors/grpc-extractor.js +264 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
- package/dist/core/group/extractors/http-route-extractor.js +428 -0
- package/dist/core/group/extractors/topic-extractor.d.ts +9 -0
- package/dist/core/group/extractors/topic-extractor.js +234 -0
- package/dist/core/group/matching.d.ts +13 -0
- package/dist/core/group/matching.js +198 -0
- package/dist/core/group/normalization.d.ts +3 -0
- package/dist/core/group/normalization.js +115 -0
- package/dist/core/group/service-boundary-detector.d.ts +8 -0
- package/dist/core/group/service-boundary-detector.js +155 -0
- package/dist/core/group/service.d.ts +46 -0
- package/dist/core/group/service.js +160 -0
- package/dist/core/group/storage.d.ts +9 -0
- package/dist/core/group/storage.js +91 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +148 -0
- package/dist/core/group/types.d.ts +130 -0
- package/dist/core/group/types.js +1 -0
- package/dist/core/ingestion/binding-accumulator.d.ts +207 -0
- package/dist/core/ingestion/binding-accumulator.js +332 -0
- package/dist/core/ingestion/call-processor.d.ts +155 -24
- package/dist/core/ingestion/call-processor.js +1129 -247
- package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
- package/dist/core/ingestion/class-extractors/generic.js +135 -0
- package/dist/core/ingestion/class-types.d.ts +34 -0
- package/dist/core/ingestion/class-types.js +1 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
- package/dist/core/ingestion/entry-point-scoring.js +1 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -1
- package/dist/core/ingestion/field-extractors/configs/helpers.js +13 -3
- package/dist/core/ingestion/field-types.d.ts +2 -2
- package/dist/core/ingestion/filesystem-walker.js +8 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +1 -0
- package/dist/core/ingestion/heritage-processor.d.ts +8 -15
- package/dist/core/ingestion/heritage-processor.js +15 -28
- package/dist/core/ingestion/import-processor.d.ts +1 -11
- package/dist/core/ingestion/import-processor.js +0 -12
- package/dist/core/ingestion/import-resolvers/utils.js +1 -0
- package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
- package/dist/core/ingestion/import-resolvers/vue.js +9 -0
- package/dist/core/ingestion/language-provider.d.ts +6 -3
- package/dist/core/ingestion/languages/c-cpp.js +168 -1
- package/dist/core/ingestion/languages/csharp.js +20 -0
- package/dist/core/ingestion/languages/dart.js +26 -4
- package/dist/core/ingestion/languages/go.js +22 -0
- package/dist/core/ingestion/languages/index.d.ts +1 -0
- package/dist/core/ingestion/languages/index.js +2 -0
- package/dist/core/ingestion/languages/java.js +17 -0
- package/dist/core/ingestion/languages/kotlin.js +24 -1
- package/dist/core/ingestion/languages/php.js +23 -11
- package/dist/core/ingestion/languages/python.js +9 -0
- package/dist/core/ingestion/languages/ruby.js +28 -0
- package/dist/core/ingestion/languages/rust.js +38 -0
- package/dist/core/ingestion/languages/swift.js +31 -0
- package/dist/core/ingestion/languages/typescript.d.ts +1 -0
- package/dist/core/ingestion/languages/typescript.js +54 -1
- package/dist/core/ingestion/languages/vue.d.ts +13 -0
- package/dist/core/ingestion/languages/vue.js +81 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
- package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
- package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
- package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
- package/dist/core/ingestion/method-extractors/configs/jvm.js +13 -4
- package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
- package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/ruby.js +285 -0
- package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
- package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
- package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +338 -0
- package/dist/core/ingestion/method-extractors/generic.js +38 -15
- package/dist/core/ingestion/method-types.d.ts +25 -0
- package/dist/core/ingestion/model/field-registry.d.ts +18 -0
- package/dist/core/ingestion/model/field-registry.js +22 -0
- package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
- package/dist/core/ingestion/model/heritage-map.js +159 -0
- package/dist/core/ingestion/model/index.d.ts +20 -0
- package/dist/core/ingestion/model/index.js +41 -0
- package/dist/core/ingestion/model/method-registry.d.ts +62 -0
- package/dist/core/ingestion/model/method-registry.js +130 -0
- package/dist/core/ingestion/model/registration-table.d.ts +139 -0
- package/dist/core/ingestion/model/registration-table.js +224 -0
- package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
- package/dist/core/ingestion/model/resolution-context.js +337 -0
- package/dist/core/ingestion/model/resolve.d.ts +56 -0
- package/dist/core/ingestion/model/resolve.js +242 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
- package/dist/core/ingestion/model/semantic-model.js +120 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
- package/dist/core/ingestion/model/symbol-table.js +206 -0
- package/dist/core/ingestion/model/type-registry.d.ts +39 -0
- package/dist/core/ingestion/model/type-registry.js +62 -0
- package/dist/core/ingestion/mro-processor.d.ts +4 -3
- package/dist/core/ingestion/mro-processor.js +310 -106
- package/dist/core/ingestion/parsing-processor.d.ts +5 -4
- package/dist/core/ingestion/parsing-processor.js +210 -85
- package/dist/core/ingestion/pipeline.d.ts +2 -0
- package/dist/core/ingestion/pipeline.js +192 -68
- package/dist/core/ingestion/tree-sitter-queries.d.ts +6 -6
- package/dist/core/ingestion/tree-sitter-queries.js +37 -0
- package/dist/core/ingestion/type-env.d.ts +15 -2
- package/dist/core/ingestion/type-env.js +163 -102
- package/dist/core/ingestion/type-extractors/csharp.js +17 -0
- package/dist/core/ingestion/type-extractors/jvm.js +11 -0
- package/dist/core/ingestion/type-extractors/php.js +0 -55
- package/dist/core/ingestion/type-extractors/ruby.js +0 -32
- package/dist/core/ingestion/type-extractors/swift.js +13 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
- package/dist/core/ingestion/type-extractors/typescript.js +66 -69
- package/dist/core/ingestion/utils/ast-helpers.d.ts +33 -43
- package/dist/core/ingestion/utils/ast-helpers.js +129 -565
- package/dist/core/ingestion/utils/method-props.d.ts +32 -0
- package/dist/core/ingestion/utils/method-props.js +147 -0
- package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
- package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
- package/dist/core/ingestion/workers/parse-worker.js +463 -198
- package/dist/core/lbug/lbug-adapter.d.ts +6 -0
- package/dist/core/lbug/lbug-adapter.js +68 -3
- package/dist/core/lbug/pool-adapter.d.ts +76 -0
- package/dist/core/lbug/pool-adapter.js +522 -0
- package/dist/core/run-analyze.d.ts +2 -0
- package/dist/core/run-analyze.js +1 -1
- package/dist/core/search/bm25-index.js +1 -1
- package/dist/core/tree-sitter/parser-loader.js +1 -0
- package/dist/core/wiki/graph-queries.js +1 -1
- package/dist/core/wiki/html-viewer.js +6 -4
- package/dist/core/wiki/llm-client.js +4 -6
- package/dist/mcp/core/embedder.js +6 -5
- package/dist/mcp/core/lbug-adapter.d.ts +3 -63
- package/dist/mcp/core/lbug-adapter.js +3 -484
- package/dist/mcp/local/local-backend.d.ts +31 -2
- package/dist/mcp/local/local-backend.js +255 -46
- package/dist/mcp/resources.js +5 -4
- package/dist/mcp/staleness.d.ts +3 -13
- package/dist/mcp/staleness.js +2 -31
- package/dist/mcp/tools.js +80 -4
- package/dist/server/analyze-job.d.ts +2 -0
- package/dist/server/analyze-job.js +4 -0
- package/dist/server/api.d.ts +20 -1
- package/dist/server/api.js +306 -71
- package/dist/server/git-clone.d.ts +2 -1
- package/dist/server/git-clone.js +98 -5
- package/dist/storage/git.d.ts +13 -0
- package/dist/storage/git.js +25 -0
- package/dist/storage/repo-manager.js +1 -1
- package/package.json +8 -2
- package/scripts/patch-tree-sitter-swift.cjs +78 -0
- package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
- package/dist/core/ingestion/named-binding-processor.js +0 -42
- package/dist/core/ingestion/resolution-context.d.ts +0 -58
- package/dist/core/ingestion/resolution-context.js +0 -135
- package/dist/core/ingestion/symbol-table.d.ts +0 -79
- package/dist/core/ingestion/symbol-table.js +0 -115
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import lbug from '@ladybugdb/core';
|
|
5
|
+
import { BRIDGE_SCHEMA_QUERIES, BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
|
|
6
|
+
import { dedupeContracts, dedupeCrossLinks } from './normalization.js';
|
|
7
|
+
export function contractNodeId(repo, contractId, role, filePath) {
|
|
8
|
+
return createHash('sha256').update(`${repo}\0${contractId}\0${role}\0${filePath}`).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
export function createContractLookupIndex() {
|
|
11
|
+
return {
|
|
12
|
+
byUid: new Map(),
|
|
13
|
+
byRef: new Map(),
|
|
14
|
+
byFile: new Map(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function uidKey(repo, role, symbolUid) {
|
|
18
|
+
return `${repo}\0${role}\0${symbolUid}`;
|
|
19
|
+
}
|
|
20
|
+
function refKey(repo, role, filePath, symbolName) {
|
|
21
|
+
return `${repo}\0${role}\0${filePath}\0${symbolName}`;
|
|
22
|
+
}
|
|
23
|
+
function fileKey(repo, role, filePath) {
|
|
24
|
+
return `${repo}\0${role}\0${filePath}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Add a successfully-inserted contract to the lookup index. Must be called
|
|
28
|
+
* AFTER the DB insert succeeds (not before) so failed inserts don't poison
|
|
29
|
+
* the index and cause cross-links to point at non-existent rows.
|
|
30
|
+
*/
|
|
31
|
+
export function indexContract(index, contract, nodeId) {
|
|
32
|
+
if (contract.symbolUid) {
|
|
33
|
+
index.byUid.set(uidKey(contract.repo, contract.role, contract.symbolUid), nodeId);
|
|
34
|
+
}
|
|
35
|
+
index.byRef.set(refKey(contract.repo, contract.role, contract.symbolRef.filePath, contract.symbolRef.name), nodeId);
|
|
36
|
+
const fk = fileKey(contract.repo, contract.role, contract.symbolRef.filePath);
|
|
37
|
+
const existing = index.byFile.get(fk);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.push(nodeId);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
index.byFile.set(fk, [nodeId]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a cross-link endpoint (consumer or provider reference) to an
|
|
47
|
+
* already-inserted contract node id. Returns `null` if no match — the
|
|
48
|
+
* caller is expected to count that as a dropped link in `WriteBridgeReport`.
|
|
49
|
+
*
|
|
50
|
+
* The resolution order matches the pre-cache DB-query behavior:
|
|
51
|
+
* 1. exact `symbolUid` match in the same `(repo, role)` scope
|
|
52
|
+
* 2. exact `(filePath, symbolName)` match
|
|
53
|
+
* 3. if exactly one contract lives in the file → that one (fallback for
|
|
54
|
+
* legacy graph-assisted extractors that couldn't resolve a symbol name)
|
|
55
|
+
*
|
|
56
|
+
* This is a pure function — no I/O, no DB — so it's trivial to unit-test
|
|
57
|
+
* in isolation (which was the reviewer's main clean-code concern on the
|
|
58
|
+
* original 35-line inner closure in `writeBridge`).
|
|
59
|
+
*/
|
|
60
|
+
export function findContractNode(index, repo, role, symbolUid, filePath, symbolName) {
|
|
61
|
+
if (symbolUid) {
|
|
62
|
+
const uidHit = index.byUid.get(uidKey(repo, role, symbolUid));
|
|
63
|
+
if (uidHit !== undefined)
|
|
64
|
+
return uidHit;
|
|
65
|
+
}
|
|
66
|
+
const refHit = index.byRef.get(refKey(repo, role, filePath, symbolName));
|
|
67
|
+
if (refHit !== undefined)
|
|
68
|
+
return refHit;
|
|
69
|
+
const fileCandidates = index.byFile.get(fileKey(repo, role, filePath));
|
|
70
|
+
if (fileCandidates && fileCandidates.length === 1)
|
|
71
|
+
return fileCandidates[0];
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
export async function openBridgeDb(dbPath) {
|
|
75
|
+
const parentDir = path.dirname(dbPath);
|
|
76
|
+
await fsp.mkdir(parentDir, { recursive: true });
|
|
77
|
+
const db = new lbug.Database(dbPath, 0, false, false); // writable
|
|
78
|
+
const conn = new lbug.Connection(db);
|
|
79
|
+
return { _db: db, _conn: conn, groupDir: parentDir };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* LadybugDB returns an error whose message contains this substring when a
|
|
83
|
+
* CREATE NODE TABLE or CREATE REL TABLE statement hits an already-existing
|
|
84
|
+
* table. LadybugDB DDL doesn't support IF NOT EXISTS, and its JS driver
|
|
85
|
+
* doesn't expose typed error codes, so we match on the message substring —
|
|
86
|
+
* the same pattern used by `core/lbug/lbug-adapter.ts`. If a future
|
|
87
|
+
* LadybugDB release changes the wording, update this constant.
|
|
88
|
+
*/
|
|
89
|
+
const LBUG_ALREADY_EXISTS_MSG = 'already exists';
|
|
90
|
+
export async function ensureBridgeSchema(handle) {
|
|
91
|
+
const conn = handle._conn;
|
|
92
|
+
for (const q of BRIDGE_SCHEMA_QUERIES) {
|
|
93
|
+
try {
|
|
94
|
+
await conn.query(q);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
if (!msg.includes(LBUG_ALREADY_EXISTS_MSG))
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function queryBridge(handle, cypher, params) {
|
|
104
|
+
const conn = handle._conn;
|
|
105
|
+
if (params && Object.keys(params).length > 0) {
|
|
106
|
+
const stmt = await conn.prepare(cypher);
|
|
107
|
+
if (!stmt.isSuccess()) {
|
|
108
|
+
const errMsg = await stmt.getErrorMessage();
|
|
109
|
+
throw new Error(`Bridge query prepare failed: ${errMsg}`);
|
|
110
|
+
}
|
|
111
|
+
const queryResult = await conn.execute(stmt, params);
|
|
112
|
+
const result = unwrapQueryResult(queryResult);
|
|
113
|
+
return (await result.getAll());
|
|
114
|
+
}
|
|
115
|
+
const queryResult = await conn.query(cypher);
|
|
116
|
+
const result = unwrapQueryResult(queryResult);
|
|
117
|
+
return (await result.getAll());
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* LadybugDB's `conn.query` / `conn.execute` can return either a single
|
|
121
|
+
* `QueryResult` (for a single statement) or an array of them (when a
|
|
122
|
+
* multi-statement script is dispatched). We always pass a single statement,
|
|
123
|
+
* so the array form is a wrapper we unwrap here — but an empty top-level
|
|
124
|
+
* array would cause `.getAll()` on `undefined` and crash with a confusing
|
|
125
|
+
* stack. Throwing an explicit error makes a driver-contract regression
|
|
126
|
+
* visible immediately instead of masking it.
|
|
127
|
+
*/
|
|
128
|
+
function unwrapQueryResult(queryResult) {
|
|
129
|
+
if (Array.isArray(queryResult)) {
|
|
130
|
+
if (queryResult.length === 0) {
|
|
131
|
+
throw new Error('Bridge query returned an empty QueryResult array');
|
|
132
|
+
}
|
|
133
|
+
return queryResult[0];
|
|
134
|
+
}
|
|
135
|
+
return queryResult;
|
|
136
|
+
}
|
|
137
|
+
export async function closeBridgeDb(handle) {
|
|
138
|
+
try {
|
|
139
|
+
await handle._conn.close();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* ignore */
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await handle._db.close();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* ignore */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/* ------------------------------------------------------------------ */
|
|
152
|
+
/* retryRename — handles transient EBUSY/EPERM/EACCES on Windows */
|
|
153
|
+
/* ------------------------------------------------------------------ */
|
|
154
|
+
const RETRY_CODES = new Set(['EBUSY', 'EPERM', 'EACCES']);
|
|
155
|
+
export async function retryRename(src, dst, attempts = 3) {
|
|
156
|
+
for (let i = 1; i <= attempts; i++) {
|
|
157
|
+
try {
|
|
158
|
+
await fsp.rename(src, dst);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const code = err.code;
|
|
163
|
+
if (!code || !RETRY_CODES.has(code) || i === attempts)
|
|
164
|
+
throw err;
|
|
165
|
+
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i - 1)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/* ------------------------------------------------------------------ */
|
|
170
|
+
/* writeBridgeMeta / readBridgeMeta */
|
|
171
|
+
/* ------------------------------------------------------------------ */
|
|
172
|
+
export async function writeBridgeMeta(groupDir, meta) {
|
|
173
|
+
const target = path.join(groupDir, 'meta.json');
|
|
174
|
+
const tmp = `${target}.tmp.${Date.now()}`;
|
|
175
|
+
await fsp.writeFile(tmp, JSON.stringify(meta, null, 2), 'utf-8');
|
|
176
|
+
// Use retryRename for consistency with writeBridge's atomic swap — on
|
|
177
|
+
// Windows a concurrent reader can cause EBUSY/EPERM even on a tiny
|
|
178
|
+
// meta.json, and we don't want meta write to be less robust than the
|
|
179
|
+
// bridge.lbug swap it accompanies.
|
|
180
|
+
await retryRename(tmp, target);
|
|
181
|
+
}
|
|
182
|
+
export async function readBridgeMeta(groupDir) {
|
|
183
|
+
try {
|
|
184
|
+
const content = await fsp.readFile(path.join(groupDir, 'meta.json'), 'utf-8');
|
|
185
|
+
return JSON.parse(content);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return { version: 0, generatedAt: '', missingRepos: [] };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const MAX_SAMPLE_ERRORS = 10;
|
|
192
|
+
function errMessage(err) {
|
|
193
|
+
if (err instanceof Error)
|
|
194
|
+
return err.message;
|
|
195
|
+
try {
|
|
196
|
+
return String(err);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return 'unknown error';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export async function writeBridge(groupDir, input) {
|
|
203
|
+
await fsp.mkdir(groupDir, { recursive: true });
|
|
204
|
+
const contracts = dedupeContracts(input.contracts);
|
|
205
|
+
const crossLinks = dedupeCrossLinks(input.crossLinks);
|
|
206
|
+
const finalPath = path.join(groupDir, 'bridge.lbug');
|
|
207
|
+
const tmpPath = path.join(groupDir, 'bridge.lbug.tmp');
|
|
208
|
+
const bakPath = path.join(groupDir, 'bridge.lbug.bak');
|
|
209
|
+
const report = {
|
|
210
|
+
contractsInserted: 0,
|
|
211
|
+
contractsFailed: 0,
|
|
212
|
+
snapshotsInserted: 0,
|
|
213
|
+
snapshotsFailed: 0,
|
|
214
|
+
linksInserted: 0,
|
|
215
|
+
linksFailed: 0,
|
|
216
|
+
linksDroppedMissingNode: 0,
|
|
217
|
+
sampleErrors: [],
|
|
218
|
+
};
|
|
219
|
+
const recordError = (kind, id, err) => {
|
|
220
|
+
if (report.sampleErrors.length < MAX_SAMPLE_ERRORS) {
|
|
221
|
+
report.sampleErrors.push({ kind, id, message: errMessage(err) });
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// Clean up any leftover tmp
|
|
225
|
+
try {
|
|
226
|
+
await fsp.rm(tmpPath, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
/* ignore */
|
|
230
|
+
}
|
|
231
|
+
// 1. Create temp DB, insert all data.
|
|
232
|
+
//
|
|
233
|
+
// Everything after `openBridgeDb` must run inside a try/finally so that
|
|
234
|
+
// if ANY step before the explicit `closeBridgeDb` throws — schema
|
|
235
|
+
// creation, a contract insert loop that rethrows, a snapshot write, the
|
|
236
|
+
// cross-link loop, or anything else — the handle is still released. A
|
|
237
|
+
// leaked handle holds the native LadybugDB file lock on tmpPath, which
|
|
238
|
+
// (a) leaks a FD and (b) prevents the next writeBridge call from
|
|
239
|
+
// reusing the same tmp slot.
|
|
240
|
+
const handle = await openBridgeDb(tmpPath);
|
|
241
|
+
let handleClosed = false;
|
|
242
|
+
try {
|
|
243
|
+
await ensureBridgeSchema(handle);
|
|
244
|
+
// Build the lookup index incrementally as contracts are inserted, so
|
|
245
|
+
// failed inserts are never in the index (and therefore never resolved
|
|
246
|
+
// by the cross-link loop below). This replaces a previous N+1 query
|
|
247
|
+
// pattern where each link made up to 6 DB round-trips to find its
|
|
248
|
+
// endpoints — see ContractLookupIndex.
|
|
249
|
+
const lookupIndex = createContractLookupIndex();
|
|
250
|
+
// Insert contracts — tolerate individual failures (e.g., a corrupt meta
|
|
251
|
+
// that can't be serialized). The whole sync must not fail because one
|
|
252
|
+
// contract is broken.
|
|
253
|
+
for (const c of contracts) {
|
|
254
|
+
const id = contractNodeId(c.repo, c.contractId, c.role, c.symbolRef.filePath);
|
|
255
|
+
try {
|
|
256
|
+
await queryBridge(handle, `CREATE (n:Contract {
|
|
257
|
+
id: $id,
|
|
258
|
+
contractId: $contractId,
|
|
259
|
+
type: $type,
|
|
260
|
+
role: $role,
|
|
261
|
+
repo: $repo,
|
|
262
|
+
service: $service,
|
|
263
|
+
symbolUid: $symbolUid,
|
|
264
|
+
filePath: $filePath,
|
|
265
|
+
symbolName: $symbolName,
|
|
266
|
+
confidence: $confidence,
|
|
267
|
+
meta: $meta
|
|
268
|
+
})`, {
|
|
269
|
+
id,
|
|
270
|
+
contractId: c.contractId,
|
|
271
|
+
type: c.type,
|
|
272
|
+
role: c.role,
|
|
273
|
+
repo: c.repo,
|
|
274
|
+
service: c.service ?? '',
|
|
275
|
+
symbolUid: c.symbolUid,
|
|
276
|
+
filePath: c.symbolRef.filePath,
|
|
277
|
+
symbolName: c.symbolName,
|
|
278
|
+
confidence: c.confidence,
|
|
279
|
+
meta: JSON.stringify(c.meta),
|
|
280
|
+
});
|
|
281
|
+
report.contractsInserted++;
|
|
282
|
+
// Only index on successful insert — the cross-link loop must never
|
|
283
|
+
// resolve to a row that isn't actually in the DB.
|
|
284
|
+
indexContract(lookupIndex, c, id);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
report.contractsFailed++;
|
|
288
|
+
recordError('contract', id, err);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Insert repo snapshots
|
|
292
|
+
for (const [repoId, snap] of Object.entries(input.repoSnapshots)) {
|
|
293
|
+
try {
|
|
294
|
+
await queryBridge(handle, `CREATE (s:RepoSnapshot {
|
|
295
|
+
id: $id,
|
|
296
|
+
indexedAt: $indexedAt,
|
|
297
|
+
lastCommit: $lastCommit
|
|
298
|
+
})`, {
|
|
299
|
+
id: repoId,
|
|
300
|
+
indexedAt: snap.indexedAt,
|
|
301
|
+
lastCommit: snap.lastCommit,
|
|
302
|
+
});
|
|
303
|
+
report.snapshotsInserted++;
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
report.snapshotsFailed++;
|
|
307
|
+
recordError('snapshot', repoId, err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Insert cross-links (tolerating missing nodes).
|
|
311
|
+
//
|
|
312
|
+
// `findContractNode` consults the in-memory lookup index built above,
|
|
313
|
+
// not the DB — that's an O(1) pure-function lookup per endpoint instead
|
|
314
|
+
// of the previous 2-3 DB queries. For M cross-links, the previous code
|
|
315
|
+
// issued up to 6M round-trips; this version issues zero.
|
|
316
|
+
//
|
|
317
|
+
// `link.contractId` may differ between the consumer and provider sides
|
|
318
|
+
// (e.g. wildcard consumer `grpc::Service/*` → method-level provider
|
|
319
|
+
// `grpc::Service/Method`) — that's why we resolve each endpoint
|
|
320
|
+
// independently via its own `(repo, role, symbolUid, filePath, symbolName)`
|
|
321
|
+
// tuple rather than matching on contractId.
|
|
322
|
+
for (const link of crossLinks) {
|
|
323
|
+
const linkId = `${link.from.repo}::${link.contractId}->${link.to.repo}::${link.contractId}`;
|
|
324
|
+
try {
|
|
325
|
+
const fromId = findContractNode(lookupIndex, link.from.repo, 'consumer', link.from.symbolUid, link.from.symbolRef.filePath, link.from.symbolRef.name);
|
|
326
|
+
const toId = findContractNode(lookupIndex, link.to.repo, 'provider', link.to.symbolUid, link.to.symbolRef.filePath, link.to.symbolRef.name);
|
|
327
|
+
if (!fromId || !toId) {
|
|
328
|
+
report.linksDroppedMissingNode++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
await queryBridge(handle, `
|
|
332
|
+
MATCH (a:Contract), (b:Contract)
|
|
333
|
+
WHERE a.id = $fromId AND b.id = $toId
|
|
334
|
+
CREATE (a)-[:ContractLink {
|
|
335
|
+
matchType: $matchType,
|
|
336
|
+
confidence: $confidence,
|
|
337
|
+
contractId: $contractId,
|
|
338
|
+
fromRepo: $fromRepo,
|
|
339
|
+
toRepo: $toRepo
|
|
340
|
+
}]->(b)
|
|
341
|
+
`, {
|
|
342
|
+
fromId,
|
|
343
|
+
toId,
|
|
344
|
+
matchType: link.matchType,
|
|
345
|
+
confidence: link.confidence,
|
|
346
|
+
contractId: link.contractId,
|
|
347
|
+
fromRepo: link.from.repo,
|
|
348
|
+
toRepo: link.to.repo,
|
|
349
|
+
});
|
|
350
|
+
report.linksInserted++;
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
report.linksFailed++;
|
|
354
|
+
recordError('link', linkId, err);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// 2. Close temp DB (happy path). The finally block also calls
|
|
358
|
+
// closeBridgeDb if we threw above; `handleClosed` prevents a
|
|
359
|
+
// double-close on the native handle.
|
|
360
|
+
await closeBridgeDb(handle);
|
|
361
|
+
handleClosed = true;
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (!handleClosed) {
|
|
365
|
+
await closeBridgeDb(handle).catch(() => {
|
|
366
|
+
/* ignore: cleanup path, best effort */
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// 3. Atomic swap: old→.bak, tmp→final, rm .bak
|
|
371
|
+
try {
|
|
372
|
+
await fsp.access(finalPath);
|
|
373
|
+
await retryRename(finalPath, bakPath);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
/* no existing db */
|
|
377
|
+
}
|
|
378
|
+
await retryRename(tmpPath, finalPath);
|
|
379
|
+
try {
|
|
380
|
+
await fsp.rm(bakPath, { recursive: true, force: true });
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
/* ignore */
|
|
384
|
+
}
|
|
385
|
+
// 4. Write meta.json
|
|
386
|
+
await writeBridgeMeta(groupDir, {
|
|
387
|
+
version: BRIDGE_SCHEMA_VERSION,
|
|
388
|
+
generatedAt: new Date().toISOString(),
|
|
389
|
+
missingRepos: input.missingRepos,
|
|
390
|
+
});
|
|
391
|
+
return report;
|
|
392
|
+
}
|
|
393
|
+
/* ------------------------------------------------------------------ */
|
|
394
|
+
/* openBridgeDbReadOnly */
|
|
395
|
+
/* ------------------------------------------------------------------ */
|
|
396
|
+
export async function openBridgeDbReadOnly(groupDir) {
|
|
397
|
+
const dbPath = path.join(groupDir, 'bridge.lbug');
|
|
398
|
+
try {
|
|
399
|
+
await fsp.access(dbPath);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// Check for .bak recovery. Use `retryRename` (not `fsp.rename`) for the
|
|
403
|
+
// exact same reason the rest of this file does: the scenario that
|
|
404
|
+
// triggers bak recovery is an interrupted writer, which on Windows may
|
|
405
|
+
// still be holding an open handle on `.bak` for a few milliseconds when
|
|
406
|
+
// a reader races in. EBUSY/EPERM retries recover that case silently.
|
|
407
|
+
const bakPath = path.join(groupDir, 'bridge.lbug.bak');
|
|
408
|
+
try {
|
|
409
|
+
await fsp.access(bakPath);
|
|
410
|
+
await retryRename(bakPath, dbPath);
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Version gate: check meta.json version compatibility
|
|
417
|
+
const meta = await readBridgeMeta(groupDir);
|
|
418
|
+
if (meta.version > 0 && meta.version !== BRIDGE_SCHEMA_VERSION) {
|
|
419
|
+
return null; // incompatible schema version — fallback to JSON or re-sync
|
|
420
|
+
}
|
|
421
|
+
// Open the native handle. If Connection construction throws AFTER
|
|
422
|
+
// Database was successfully allocated, we'd leak the native Database
|
|
423
|
+
// object. Wrap each step separately and tear down the partial handle.
|
|
424
|
+
let db;
|
|
425
|
+
let conn;
|
|
426
|
+
try {
|
|
427
|
+
db = new lbug.Database(dbPath, 0, false, true); // readOnly
|
|
428
|
+
conn = new lbug.Connection(db);
|
|
429
|
+
return { _db: db, _conn: conn, groupDir };
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
if (conn) {
|
|
433
|
+
try {
|
|
434
|
+
await conn.close();
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
/* ignore */
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (db) {
|
|
441
|
+
try {
|
|
442
|
+
await db.close();
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
/* ignore */
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/* ------------------------------------------------------------------ */
|
|
452
|
+
/* bridgeExists */
|
|
453
|
+
/* ------------------------------------------------------------------ */
|
|
454
|
+
export async function bridgeExists(groupDir) {
|
|
455
|
+
const handle = await openBridgeDbReadOnly(groupDir);
|
|
456
|
+
if (!handle)
|
|
457
|
+
return false;
|
|
458
|
+
await closeBridgeDb(handle);
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge LadybugDB schema for cross-repo Contract Registry.
|
|
3
|
+
* Separate from per-repo schema in lbug/schema.ts.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Version of the bridge.lbug schema below. `openBridgeDbReadOnly` compares
|
|
7
|
+
* this against `meta.json`'s version field and returns `null` on mismatch,
|
|
8
|
+
* which trips the caller into either the JSON fallback path or a fresh
|
|
9
|
+
* `group sync` that rebuilds `bridge.lbug` from scratch.
|
|
10
|
+
*
|
|
11
|
+
* Migration contract for contributors bumping this constant:
|
|
12
|
+
* 1. Bump the number (e.g. `1` → `2`).
|
|
13
|
+
* 2. Update the DDL below to match the new schema.
|
|
14
|
+
* 3. DO NOT attempt an online migration in this file — the version gate
|
|
15
|
+
* is intentionally a "discard and re-sync" strategy for V1. An old
|
|
16
|
+
* bridge.lbug whose version doesn't match is treated as opaque and
|
|
17
|
+
* rebuilt by the next `group sync`.
|
|
18
|
+
* 4. If online migration becomes necessary (e.g. when groups accumulate
|
|
19
|
+
* large amounts of embedding data), add a migration path as a
|
|
20
|
+
* separate `bridge-migrations.ts` module rather than bloating this
|
|
21
|
+
* file — keep schema and migration concerns separate.
|
|
22
|
+
*/
|
|
23
|
+
export declare const BRIDGE_SCHEMA_VERSION = 1;
|
|
24
|
+
export declare const CONTRACT_SCHEMA = "\nCREATE NODE TABLE Contract (\n id STRING,\n contractId STRING,\n type STRING,\n role STRING,\n repo STRING,\n service STRING DEFAULT '',\n symbolUid STRING DEFAULT '',\n filePath STRING DEFAULT '',\n symbolName STRING DEFAULT '',\n confidence DOUBLE DEFAULT 0.0,\n meta STRING DEFAULT '{}',\n PRIMARY KEY (id)\n)";
|
|
25
|
+
export declare const REPO_SNAPSHOT_SCHEMA = "\nCREATE NODE TABLE RepoSnapshot (\n id STRING,\n indexedAt STRING DEFAULT '',\n lastCommit STRING DEFAULT '',\n PRIMARY KEY (id)\n)";
|
|
26
|
+
export declare const CONTRACT_LINK_SCHEMA = "\nCREATE REL TABLE ContractLink (\n FROM Contract TO Contract,\n matchType STRING,\n confidence DOUBLE,\n contractId STRING,\n fromRepo STRING,\n toRepo STRING\n)";
|
|
27
|
+
export declare const BRIDGE_SCHEMA_QUERIES: string[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge LadybugDB schema for cross-repo Contract Registry.
|
|
3
|
+
* Separate from per-repo schema in lbug/schema.ts.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Version of the bridge.lbug schema below. `openBridgeDbReadOnly` compares
|
|
7
|
+
* this against `meta.json`'s version field and returns `null` on mismatch,
|
|
8
|
+
* which trips the caller into either the JSON fallback path or a fresh
|
|
9
|
+
* `group sync` that rebuilds `bridge.lbug` from scratch.
|
|
10
|
+
*
|
|
11
|
+
* Migration contract for contributors bumping this constant:
|
|
12
|
+
* 1. Bump the number (e.g. `1` → `2`).
|
|
13
|
+
* 2. Update the DDL below to match the new schema.
|
|
14
|
+
* 3. DO NOT attempt an online migration in this file — the version gate
|
|
15
|
+
* is intentionally a "discard and re-sync" strategy for V1. An old
|
|
16
|
+
* bridge.lbug whose version doesn't match is treated as opaque and
|
|
17
|
+
* rebuilt by the next `group sync`.
|
|
18
|
+
* 4. If online migration becomes necessary (e.g. when groups accumulate
|
|
19
|
+
* large amounts of embedding data), add a migration path as a
|
|
20
|
+
* separate `bridge-migrations.ts` module rather than bloating this
|
|
21
|
+
* file — keep schema and migration concerns separate.
|
|
22
|
+
*/
|
|
23
|
+
export const BRIDGE_SCHEMA_VERSION = 1;
|
|
24
|
+
export const CONTRACT_SCHEMA = `
|
|
25
|
+
CREATE NODE TABLE Contract (
|
|
26
|
+
id STRING,
|
|
27
|
+
contractId STRING,
|
|
28
|
+
type STRING,
|
|
29
|
+
role STRING,
|
|
30
|
+
repo STRING,
|
|
31
|
+
service STRING DEFAULT '',
|
|
32
|
+
symbolUid STRING DEFAULT '',
|
|
33
|
+
filePath STRING DEFAULT '',
|
|
34
|
+
symbolName STRING DEFAULT '',
|
|
35
|
+
confidence DOUBLE DEFAULT 0.0,
|
|
36
|
+
meta STRING DEFAULT '{}',
|
|
37
|
+
PRIMARY KEY (id)
|
|
38
|
+
)`;
|
|
39
|
+
export const REPO_SNAPSHOT_SCHEMA = `
|
|
40
|
+
CREATE NODE TABLE RepoSnapshot (
|
|
41
|
+
id STRING,
|
|
42
|
+
indexedAt STRING DEFAULT '',
|
|
43
|
+
lastCommit STRING DEFAULT '',
|
|
44
|
+
PRIMARY KEY (id)
|
|
45
|
+
)`;
|
|
46
|
+
export const CONTRACT_LINK_SCHEMA = `
|
|
47
|
+
CREATE REL TABLE ContractLink (
|
|
48
|
+
FROM Contract TO Contract,
|
|
49
|
+
matchType STRING,
|
|
50
|
+
confidence DOUBLE,
|
|
51
|
+
contractId STRING,
|
|
52
|
+
fromRepo STRING,
|
|
53
|
+
toRepo STRING
|
|
54
|
+
)`;
|
|
55
|
+
export const BRIDGE_SCHEMA_QUERIES = [CONTRACT_SCHEMA, REPO_SNAPSHOT_SCHEMA, CONTRACT_LINK_SCHEMA];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const _require = createRequire(import.meta.url);
|
|
3
|
+
const yaml = _require('js-yaml');
|
|
4
|
+
const VALID_CONTRACT_TYPES = ['http', 'grpc', 'topic', 'lib', 'custom'];
|
|
5
|
+
const VALID_ROLES = ['provider', 'consumer'];
|
|
6
|
+
const DEFAULT_DETECT = {
|
|
7
|
+
http: true,
|
|
8
|
+
grpc: true,
|
|
9
|
+
topics: true,
|
|
10
|
+
shared_libs: true,
|
|
11
|
+
embedding_fallback: true,
|
|
12
|
+
};
|
|
13
|
+
const DEFAULT_MATCHING = {
|
|
14
|
+
bm25_threshold: 0.7,
|
|
15
|
+
embedding_threshold: 0.65,
|
|
16
|
+
max_candidates_per_step: 3,
|
|
17
|
+
};
|
|
18
|
+
export function parseGroupConfig(yamlContent) {
|
|
19
|
+
const raw = yaml.load(yamlContent, { schema: yaml.JSON_SCHEMA });
|
|
20
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
21
|
+
throw new Error('Invalid YAML: expected an object');
|
|
22
|
+
}
|
|
23
|
+
if (raw.version === undefined)
|
|
24
|
+
throw new Error('version is required in group.yaml');
|
|
25
|
+
if (raw.version !== 1) {
|
|
26
|
+
throw new Error(`Unsupported group.yaml version: ${raw.version}. Expected 1.`);
|
|
27
|
+
}
|
|
28
|
+
if (!raw.name || typeof raw.name !== 'string')
|
|
29
|
+
throw new Error('name is required in group.yaml');
|
|
30
|
+
if (!raw.repos || typeof raw.repos !== 'object' || Array.isArray(raw.repos)) {
|
|
31
|
+
throw new Error('repos is required in group.yaml (must be a mapping)');
|
|
32
|
+
}
|
|
33
|
+
const repos = raw.repos;
|
|
34
|
+
const repoPaths = new Set(Object.keys(repos));
|
|
35
|
+
const rawLinks = raw.links || [];
|
|
36
|
+
const links = rawLinks.map((l, i) => {
|
|
37
|
+
const link = l;
|
|
38
|
+
if (!link.from || !repoPaths.has(link.from)) {
|
|
39
|
+
throw new Error(`links[${i}].from "${link.from}" does not match any repo path in group`);
|
|
40
|
+
}
|
|
41
|
+
if (!link.to || !repoPaths.has(link.to)) {
|
|
42
|
+
throw new Error(`links[${i}].to "${link.to}" does not match any repo path in group`);
|
|
43
|
+
}
|
|
44
|
+
if (!VALID_CONTRACT_TYPES.includes(link.type)) {
|
|
45
|
+
throw new Error(`links[${i}].type "${link.type}" is invalid. Expected: ${VALID_CONTRACT_TYPES.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
if (!VALID_ROLES.includes(link.role)) {
|
|
48
|
+
throw new Error(`links[${i}].role "${link.role}" is invalid. Expected: provider | consumer`);
|
|
49
|
+
}
|
|
50
|
+
if (link.contract === undefined ||
|
|
51
|
+
link.contract === null ||
|
|
52
|
+
String(link.contract).trim() === '') {
|
|
53
|
+
throw new Error(`links[${i}].contract is required`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
from: link.from,
|
|
57
|
+
to: link.to,
|
|
58
|
+
type: link.type,
|
|
59
|
+
contract: String(link.contract),
|
|
60
|
+
role: link.role,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
const detect = { ...DEFAULT_DETECT, ...(raw.detect || {}) };
|
|
64
|
+
const matching = { ...DEFAULT_MATCHING, ...(raw.matching || {}) };
|
|
65
|
+
const packages = raw.packages || {};
|
|
66
|
+
return {
|
|
67
|
+
version: 1,
|
|
68
|
+
name: raw.name,
|
|
69
|
+
description: raw.description || '',
|
|
70
|
+
repos,
|
|
71
|
+
links,
|
|
72
|
+
packages,
|
|
73
|
+
detect,
|
|
74
|
+
matching,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function loadGroupConfig(groupDir) {
|
|
78
|
+
const fsp = await import('node:fs/promises');
|
|
79
|
+
const path = await import('node:path');
|
|
80
|
+
const yamlPath = path.join(groupDir, 'group.yaml');
|
|
81
|
+
const content = await fsp.readFile(yamlPath, 'utf-8');
|
|
82
|
+
return parseGroupConfig(content);
|
|
83
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ContractType, ExtractedContract, RepoHandle } from './types.js';
|
|
2
|
+
export interface ContractExtractor {
|
|
3
|
+
type: ContractType;
|
|
4
|
+
canExtract(repo: RepoHandle): Promise<boolean>;
|
|
5
|
+
extract(dbExecutor: CypherExecutor | null, repoPath: string, repo: RepoHandle): Promise<ExtractedContract[]>;
|
|
6
|
+
}
|
|
7
|
+
export type CypherExecutor = (query: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js';
|
|
2
|
+
import type { ExtractedContract, RepoHandle } from '../types.js';
|
|
3
|
+
export declare class GrpcExtractor implements ContractExtractor {
|
|
4
|
+
type: "grpc";
|
|
5
|
+
canExtract(_repo: RepoHandle): Promise<boolean>;
|
|
6
|
+
extract(_dbExecutor: CypherExecutor | null, repoPath: string, _repo: RepoHandle): Promise<ExtractedContract[]>;
|
|
7
|
+
private parseProtoFile;
|
|
8
|
+
private scanGoProviders;
|
|
9
|
+
private scanGoConsumers;
|
|
10
|
+
private scanJavaProviders;
|
|
11
|
+
private scanJavaConsumers;
|
|
12
|
+
private scanPythonProviders;
|
|
13
|
+
private scanPythonConsumers;
|
|
14
|
+
private scanTsProviders;
|
|
15
|
+
private dedupe;
|
|
16
|
+
}
|