gitnexus 1.6.2-rc.2 → 1.6.2-rc.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.
|
@@ -68,14 +68,29 @@ export function manifestSymbolUid(repo, contractId) {
|
|
|
68
68
|
}
|
|
69
69
|
export class ManifestExtractor {
|
|
70
70
|
async extractFromManifest(links, dbExecutors) {
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
71
|
+
const resolveCache = new Map();
|
|
72
|
+
const resolveOnce = (repo, link) => {
|
|
73
|
+
const key = `${repo}\u0000${link.type}\u0000${link.contract}`;
|
|
74
|
+
let pending = resolveCache.get(key);
|
|
75
|
+
if (!pending) {
|
|
76
|
+
pending = this.resolveSymbol(repo, link, dbExecutors);
|
|
77
|
+
resolveCache.set(key, pending);
|
|
78
|
+
}
|
|
79
|
+
return pending;
|
|
80
|
+
};
|
|
81
|
+
const perLink = await Promise.all(links.map(async (link) => {
|
|
74
82
|
const contractId = this.buildContractId(link.type, link.contract);
|
|
75
83
|
const providerRepo = link.role === 'provider' ? link.from : link.to;
|
|
76
84
|
const consumerRepo = link.role === 'provider' ? link.to : link.from;
|
|
77
|
-
const providerSymbol = await
|
|
78
|
-
|
|
85
|
+
const [providerSymbol, consumerSymbol] = await Promise.all([
|
|
86
|
+
resolveOnce(providerRepo, link),
|
|
87
|
+
resolveOnce(consumerRepo, link),
|
|
88
|
+
]);
|
|
89
|
+
return { link, contractId, providerRepo, consumerRepo, providerSymbol, consumerSymbol };
|
|
90
|
+
}));
|
|
91
|
+
const contracts = [];
|
|
92
|
+
const crossLinks = [];
|
|
93
|
+
for (const { link, contractId, providerRepo, consumerRepo, providerSymbol, consumerSymbol, } of perLink) {
|
|
79
94
|
const providerRef = providerSymbol || { filePath: '', name: link.contract };
|
|
80
95
|
const consumerRef = consumerSymbol || { filePath: '', name: link.contract };
|
|
81
96
|
// When the resolver finds a real graph symbol we keep its uid, otherwise
|
package/dist/core/group/sync.js
CHANGED
|
@@ -6,6 +6,7 @@ import { readRegistry } from '../../storage/repo-manager.js';
|
|
|
6
6
|
import { HttpRouteExtractor } from './extractors/http-route-extractor.js';
|
|
7
7
|
import { GrpcExtractor } from './extractors/grpc-extractor.js';
|
|
8
8
|
import { TopicExtractor } from './extractors/topic-extractor.js';
|
|
9
|
+
import { ManifestExtractor } from './extractors/manifest-extractor.js';
|
|
9
10
|
import { runExactMatch } from './matching.js';
|
|
10
11
|
import { detectServiceBoundaries, assignService } from './service-boundary-detector.js';
|
|
11
12
|
import { writeContractRegistry } from './storage.js';
|
|
@@ -34,10 +35,28 @@ function defaultResolveHandle(allEntries) {
|
|
|
34
35
|
};
|
|
35
36
|
};
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Dedupe cross-links that point from the same consumer endpoint to the same
|
|
40
|
+
* provider endpoint for the same contract. Preserves first-seen order so the
|
|
41
|
+
* caller controls precedence (e.g., pass manifest links first).
|
|
42
|
+
*/
|
|
43
|
+
function dedupeCrossLinks(links) {
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const link of links) {
|
|
47
|
+
const key = `${link.from.repo}::${link.from.symbolUid}|${link.to.repo}::${link.to.symbolUid}|${link.type}|${link.contractId}`;
|
|
48
|
+
if (seen.has(key))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(key);
|
|
51
|
+
out.push(link);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
37
55
|
export async function syncGroup(config, opts) {
|
|
38
56
|
const missingRepos = [];
|
|
39
57
|
const repoSnapshots = {};
|
|
40
58
|
let autoContracts = [];
|
|
59
|
+
let manifestCrossLinks = [];
|
|
41
60
|
let dbExecutors;
|
|
42
61
|
const eo = opts?.extractorOverride;
|
|
43
62
|
if (eo && eo.length === 0) {
|
|
@@ -124,8 +143,37 @@ export async function syncGroup(config, opts) {
|
|
|
124
143
|
}
|
|
125
144
|
}
|
|
126
145
|
}
|
|
146
|
+
// Process manifest links declared in group.yaml.
|
|
147
|
+
// ManifestExtractor is fully implemented but was never wired into this
|
|
148
|
+
// pipeline — config.links were parsed and validated but silently dropped.
|
|
149
|
+
// Placed after the DB try/finally: resolveSymbol falls back to synthetic
|
|
150
|
+
// UIDs when dbExecutors is undefined or a pool is closed, so cross-links
|
|
151
|
+
// are always generated regardless of whether real DB executors are available.
|
|
152
|
+
if (config.links.length > 0) {
|
|
153
|
+
// Warn about dangling links that reference repos not declared in config.repos.
|
|
154
|
+
// They still generate cross-links via synthetic UIDs (determinism is preserved),
|
|
155
|
+
// but the operator probably meant something that now silently does nothing useful.
|
|
156
|
+
const knownRepos = new Set(Object.keys(config.repos));
|
|
157
|
+
for (const link of config.links) {
|
|
158
|
+
const dangling = [link.from, link.to].filter((r) => !knownRepos.has(r));
|
|
159
|
+
if (dangling.length > 0) {
|
|
160
|
+
console.warn(`[group/sync] manifest link ${link.type}:${link.contract} references repos not in config.repos: ${dangling.join(', ')} — cross-links will use synthetic UIDs`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const manifestEx = new ManifestExtractor();
|
|
164
|
+
const manifestResult = await manifestEx.extractFromManifest(config.links, dbExecutors);
|
|
165
|
+
autoContracts.push(...manifestResult.contracts);
|
|
166
|
+
manifestCrossLinks = manifestResult.crossLinks;
|
|
167
|
+
if (opts?.verbose) {
|
|
168
|
+
console.log(` manifest: ${manifestCrossLinks.length} cross-links from ${config.links.length} declared links`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
127
171
|
const { matched, unmatched } = runExactMatch(autoContracts);
|
|
128
|
-
|
|
172
|
+
// Dedupe cross-links. Manifest contracts participate in runExactMatch, so a
|
|
173
|
+
// manifest-declared link can also emit a matchType:'exact' CrossLink with the
|
|
174
|
+
// same endpoints. Prefer the manifest version — it reflects operator intent
|
|
175
|
+
// and carries matchType:'manifest' which downstream consumers may rely on.
|
|
176
|
+
const crossLinks = dedupeCrossLinks([...manifestCrossLinks, ...matched]);
|
|
129
177
|
const allContracts = autoContracts;
|
|
130
178
|
const registry = {
|
|
131
179
|
version: 1,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import { createReadStream, createWriteStream } from 'fs';
|
|
3
3
|
import { createInterface } from 'readline';
|
|
4
|
+
import { once } from 'events';
|
|
5
|
+
import { finished } from 'stream/promises';
|
|
4
6
|
import path from 'path';
|
|
5
7
|
import lbug from '@ladybugdb/core';
|
|
6
8
|
import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, } from './schema.js';
|
|
@@ -25,100 +27,82 @@ export const splitRelCsvByLabelPair = async (csvPath, csvDir, validTables, getNo
|
|
|
25
27
|
const pairWriteStreams = new Map();
|
|
26
28
|
let skippedRels = 0;
|
|
27
29
|
let totalValidRels = 0;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
catch { }
|
|
48
|
-
try {
|
|
49
|
-
inputStream.destroy();
|
|
50
|
-
}
|
|
51
|
-
catch { }
|
|
52
|
-
for (const ws of pairWriteStreams.values()) {
|
|
53
|
-
try {
|
|
54
|
-
ws.destroy();
|
|
55
|
-
}
|
|
56
|
-
catch { }
|
|
57
|
-
}
|
|
58
|
-
reject(err);
|
|
59
|
-
};
|
|
30
|
+
const inputStream = createReadStream(csvPath, 'utf-8');
|
|
31
|
+
const rl = createInterface({ input: inputStream, crlfDelay: Infinity });
|
|
32
|
+
// If any pair WriteStream errors (disk full, EMFILE, etc.) or the input
|
|
33
|
+
// stream fails, we need to abort the pending `once(ws, 'drain')` await.
|
|
34
|
+
// An AbortController gives us one signal to cancel all pending waits
|
|
35
|
+
// without a custom state machine.
|
|
36
|
+
const abortOnError = new AbortController();
|
|
37
|
+
let streamError = null;
|
|
38
|
+
const markStreamError = (err) => {
|
|
39
|
+
streamError ??= err;
|
|
40
|
+
abortOnError.abort(err);
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
// `for await (const line of rl)` replaces the old manual
|
|
44
|
+
// on('line')/pause()/resume()/waitingForDrain state machine: readline's
|
|
45
|
+
// async iterator naturally serializes line delivery with our awaits, so
|
|
46
|
+
// at most one ws can be in backpressure at a time and we just await its
|
|
47
|
+
// 'drain' event.
|
|
60
48
|
let isFirst = true;
|
|
61
|
-
|
|
49
|
+
for await (const line of rl) {
|
|
50
|
+
if (streamError)
|
|
51
|
+
throw streamError;
|
|
62
52
|
if (isFirst) {
|
|
63
53
|
relHeader = line;
|
|
64
54
|
isFirst = false;
|
|
65
|
-
|
|
55
|
+
continue;
|
|
66
56
|
}
|
|
67
57
|
if (!line.trim())
|
|
68
|
-
|
|
58
|
+
continue;
|
|
69
59
|
const match = line.match(/"([^"]*)","([^"]*)"/);
|
|
70
60
|
if (!match) {
|
|
71
61
|
skippedRels++;
|
|
72
|
-
|
|
62
|
+
continue;
|
|
73
63
|
}
|
|
74
64
|
const fromLabel = getNodeLabel(match[1]);
|
|
75
65
|
const toLabel = getNodeLabel(match[2]);
|
|
76
66
|
if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
|
|
77
67
|
skippedRels++;
|
|
78
|
-
|
|
68
|
+
continue;
|
|
79
69
|
}
|
|
80
70
|
const pairKey = `${fromLabel}|${toLabel}`;
|
|
81
71
|
let ws = pairWriteStreams.get(pairKey);
|
|
82
72
|
if (!ws) {
|
|
83
73
|
const pairCsvPath = path.join(csvDir, `rel_${fromLabel}_${toLabel}.csv`);
|
|
84
74
|
ws = wsFactory(pairCsvPath);
|
|
85
|
-
|
|
86
|
-
// tear down everything and reject the Promise. Without this handler,
|
|
87
|
-
// a stream error while rl is paused waiting for drain would cause
|
|
88
|
-
// the drain callback to never fire and the Promise to hang forever.
|
|
89
|
-
ws.on('error', cleanup);
|
|
90
|
-
ws.write(relHeader + '\n');
|
|
75
|
+
ws.on('error', markStreamError);
|
|
91
76
|
pairWriteStreams.set(pairKey, ws);
|
|
92
77
|
relsByPairMeta.set(pairKey, { csvPath: pairCsvPath, rows: 0 });
|
|
78
|
+
if (!ws.write(relHeader + '\n')) {
|
|
79
|
+
await once(ws, 'drain', { signal: abortOnError.signal });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!ws.write(line + '\n')) {
|
|
83
|
+
await once(ws, 'drain', { signal: abortOnError.signal });
|
|
93
84
|
}
|
|
94
|
-
const ok = ws.write(line + '\n');
|
|
95
85
|
relsByPairMeta.get(pairKey).rows++;
|
|
96
86
|
totalValidRels++;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
settled = true;
|
|
117
|
-
resolve();
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
rl.on('error', cleanup);
|
|
121
|
-
});
|
|
87
|
+
}
|
|
88
|
+
if (streamError)
|
|
89
|
+
throw streamError;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
// Tear down everything so no fd is left dangling. If the abort was caused
|
|
93
|
+
// by a stream error, rethrow that error (more actionable than AbortError).
|
|
94
|
+
for (const ws of pairWriteStreams.values())
|
|
95
|
+
ws.destroy();
|
|
96
|
+
inputStream.destroy();
|
|
97
|
+
throw streamError ?? err;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
// Readline 'close' fires before the underlying fs.ReadStream releases its
|
|
101
|
+
// fd — on Windows that race caused ENOTEMPTY on the parent dir.
|
|
102
|
+
// stream/promises.finished is the stdlib "wait until this stream is fully
|
|
103
|
+
// closed" primitive and handles both success and error paths.
|
|
104
|
+
await finished(inputStream).catch(() => { });
|
|
105
|
+
}
|
|
122
106
|
return { relHeader, relsByPairMeta, pairWriteStreams, skippedRels, totalValidRels };
|
|
123
107
|
};
|
|
124
108
|
let db = null;
|
|
@@ -332,15 +316,13 @@ export const loadGraphToLbug = async (graph, repoPath, storagePath, onProgress)
|
|
|
332
316
|
}
|
|
333
317
|
// Bulk COPY relationships — split by FROM→TO label pair (LadybugDB requires it)
|
|
334
318
|
const { relHeader, relsByPairMeta, pairWriteStreams, skippedRels, totalValidRels } = await splitRelCsvByLabelPair(csvResult.relCsvPath, csvDir, validTables, getNodeLabel);
|
|
335
|
-
// Close all per-pair write streams before COPY
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
ws.end(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
});
|
|
343
|
-
})));
|
|
319
|
+
// Close all per-pair write streams before COPY. `stream/promises.finished`
|
|
320
|
+
// resolves on the stream's 'finish' event and rejects on 'error' — replaces
|
|
321
|
+
// a hand-rolled promisification with the stdlib primitive.
|
|
322
|
+
await Promise.all(Array.from(pairWriteStreams.values()).map(async (ws) => {
|
|
323
|
+
ws.end();
|
|
324
|
+
await finished(ws);
|
|
325
|
+
}));
|
|
344
326
|
const insertedRels = totalValidRels;
|
|
345
327
|
const warnings = [];
|
|
346
328
|
if (insertedRels > 0) {
|
package/package.json
CHANGED