gitnexus 1.6.10-rc.225 → 1.6.10-rc.227
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/dist/cli/group.js +89 -15
- package/dist/cli/help-i18n.js +0 -2
- package/dist/cli/i18n/en.d.ts +2 -4
- package/dist/cli/i18n/en.js +2 -4
- package/dist/cli/i18n/resources.d.ts +2 -6
- package/dist/cli/i18n/zh-CN.d.ts +0 -2
- package/dist/cli/i18n/zh-CN.js +2 -4
- package/dist/config/ignore-service.js +21 -2
- package/dist/core/group/bridge-db.d.ts +31 -1
- package/dist/core/group/bridge-db.js +63 -7
- package/dist/core/group/completeness.d.ts +19 -1
- package/dist/core/group/completeness.js +24 -1
- package/dist/core/group/config-parser.js +0 -5
- package/dist/core/group/cross-impact.js +16 -7
- package/dist/core/group/cross-trace.js +1 -0
- package/dist/core/group/service.js +109 -7
- package/dist/core/group/storage.js +0 -5
- package/dist/core/group/sync.d.ts +9 -3
- package/dist/core/group/sync.js +37 -4
- package/dist/core/group/types.d.ts +31 -7
- package/dist/core/group/types.js +13 -1
- package/dist/mcp/resources.js +5 -1
- package/dist/mcp/tools.js +5 -6
- package/package.json +1 -1
- package/web/assets/{index-C-y1dO6S.js → index-2zdvEdzg.js} +1 -1
- package/web/index.html +1 -1
package/dist/cli/group.js
CHANGED
|
@@ -148,6 +148,16 @@ export function registerGroupCommands(program) {
|
|
|
148
148
|
if ((st.missingRepos || []).length > 0) {
|
|
149
149
|
console.log(`\n Last sync missing repos: ${st.missingRepos.join(', ')}`);
|
|
150
150
|
}
|
|
151
|
+
// Only the populated case prints. Absent means a registry that predates
|
|
152
|
+
// the field, and empty is the ordinary clean sync — neither is worth a
|
|
153
|
+
// line, whereas a narrowed registry changes how every later answer
|
|
154
|
+
// should be read.
|
|
155
|
+
const skippedStages = st.suppressedMatchStages ?? [];
|
|
156
|
+
if (skippedStages.length > 0) {
|
|
157
|
+
console.log(`\n Last sync skipped matching stages: ${skippedStages.join(', ')}` +
|
|
158
|
+
`\n Cross-links those stages would have found are absent by request.` +
|
|
159
|
+
`\n Re-run \`gitnexus group sync\` without --exact-only for the complete set.`);
|
|
160
|
+
}
|
|
151
161
|
}
|
|
152
162
|
finally {
|
|
153
163
|
await backend.dispose().catch(() => { });
|
|
@@ -156,10 +166,8 @@ export function registerGroupCommands(program) {
|
|
|
156
166
|
group
|
|
157
167
|
.command('sync <name>')
|
|
158
168
|
.description('Sync Contract Registry — extract contracts and build cross-links')
|
|
159
|
-
.option('--
|
|
160
|
-
.option('--
|
|
161
|
-
.option('--allow-stale', 'Skip stale index warnings')
|
|
162
|
-
.option('--verbose', 'Show each cross-link detail')
|
|
169
|
+
.option('--exact-only', 'Skip wildcard service matching; cross-link on exact contract-id match only (manifest links still apply)')
|
|
170
|
+
.option('--verbose', 'Show additional sync diagnostics')
|
|
163
171
|
.option('--json', 'JSON output')
|
|
164
172
|
.action(async (name, opts) => {
|
|
165
173
|
const { getGroupDir, getDefaultGitnexusDir } = await import('../core/group/storage.js');
|
|
@@ -173,9 +181,7 @@ export function registerGroupCommands(program) {
|
|
|
173
181
|
try {
|
|
174
182
|
result = await syncGroup(config, {
|
|
175
183
|
groupDir,
|
|
176
|
-
allowStale: Boolean(opts.allowStale),
|
|
177
184
|
verbose: Boolean(opts.verbose),
|
|
178
|
-
skipEmbeddings: Boolean(opts.skipEmbeddings),
|
|
179
185
|
exactOnly: Boolean(opts.exactOnly),
|
|
180
186
|
});
|
|
181
187
|
}
|
|
@@ -212,10 +218,41 @@ export function registerGroupCommands(program) {
|
|
|
212
218
|
console.log(`\n ⚠️ Not found in the registry: ${result.missingRepos.join(', ')}` +
|
|
213
219
|
`\n Index them with \`gitnexus analyze\`, or remove them from group.yaml.`);
|
|
214
220
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
221
|
+
// Every stage that produced a link, not just `exact`. This used to print
|
|
222
|
+
// `Matching cascade:` and then count `exact` alone, while the `Wrote
|
|
223
|
+
// contracts.json (…)` line below reports `result.crossLinks.length` —
|
|
224
|
+
// which also includes `manifest` and `wildcard` links. For any group with
|
|
225
|
+
// those, the two numbers disagreed with nothing on screen explaining why.
|
|
226
|
+
// Summing the stages here makes them reconcile by construction.
|
|
227
|
+
console.log(`\nMatching:`);
|
|
228
|
+
// Exhaustive by construction, same idiom as OUTCOME_LINE below: adding a
|
|
229
|
+
// MatchType fails the build here instead of silently going uncounted and
|
|
230
|
+
// reopening the very mismatch this replaced. Every stage prints even at
|
|
231
|
+
// zero — a stage that is absent reads as "did not apply", not "found none".
|
|
232
|
+
const STAGE_COUNTS = {
|
|
233
|
+
exact: 0,
|
|
234
|
+
manifest: 0,
|
|
235
|
+
wildcard: 0,
|
|
236
|
+
};
|
|
237
|
+
for (const link of result.crossLinks)
|
|
238
|
+
STAGE_COUNTS[link.matchType] += 1;
|
|
239
|
+
// A stage the sync was told to skip is reported as skipped, not as a
|
|
240
|
+
// zero count. The two are different facts — "ran, matched nothing" and
|
|
241
|
+
// "never ran" — and printing both as `0` is the same conflation this
|
|
242
|
+
// block replaced. Driven by what the sync did (`suppressedMatchStages`)
|
|
243
|
+
// rather than by what the caller asked for, so it stays correct on the
|
|
244
|
+
// outcomes where the run ended without writing a registry.
|
|
245
|
+
for (const stage of Object.keys(STAGE_COUNTS)) {
|
|
246
|
+
const count = STAGE_COUNTS[stage];
|
|
247
|
+
const label = `${stage}:`.padEnd(10);
|
|
248
|
+
if (result.suppressedMatchStages.includes(stage)) {
|
|
249
|
+
console.log(` ${label} skipped (--exact-only)`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const confidence = stage === 'exact' ? ' (confidence 1.0)' : '';
|
|
253
|
+
console.log(` ${label} ${count} cross-links${confidence}`);
|
|
254
|
+
}
|
|
255
|
+
console.log(` ${'unmatched:'.padEnd(10)} ${result.unmatched.length} contracts`);
|
|
219
256
|
// Driven by what actually happened to the file. This line used to be
|
|
220
257
|
// unconditional, so a run that deliberately preserved the previous
|
|
221
258
|
// registry still announced `Wrote contracts.json (0 contracts, 0
|
|
@@ -330,9 +367,28 @@ export function registerGroupCommands(program) {
|
|
|
330
367
|
// repos — reporting it as crossings understates a fan-out cap the
|
|
331
368
|
// same way #2787's totals did.
|
|
332
369
|
const dropped = raw?.truncatedRepos ?? [];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
370
|
+
const reason = raw?.truncationReason;
|
|
371
|
+
// Keyed on the REASON, not on which incidental fact happens to be
|
|
372
|
+
// non-empty. `truncatedRepos` is populated for a structural gap too
|
|
373
|
+
// — the bridge's incomplete repos are unioned into it even when ZERO
|
|
374
|
+
// crossings were attempted — so branching on its length first
|
|
375
|
+
// reported "fan-out stopped early" for a run where nothing stopped
|
|
376
|
+
// early, and omitted the only remedy that works. Same false-cause
|
|
377
|
+
// shape the contract listing was just re-gated for, one command over.
|
|
378
|
+
const floorReason = () => {
|
|
379
|
+
if (reason === 'suppressed-stage') {
|
|
380
|
+
return 'the last sync skipped a matching stage (--exact-only); re-run `gitnexus group sync` without it for the complete graph';
|
|
381
|
+
}
|
|
382
|
+
if (reason === 'incomplete-sync') {
|
|
383
|
+
return dropped.length > 0
|
|
384
|
+
? `the last sync could not account for ${dropped.join(', ')}; their contracts are absent from every query against this bridge — re-run \`gitnexus group sync\``
|
|
385
|
+
: 'the last sync could not say which repos it read — re-run `gitnexus group sync`';
|
|
386
|
+
}
|
|
387
|
+
return dropped.length > 0
|
|
388
|
+
? `fan-out stopped early; crossings to ${dropped.length} repo(s) not traversed: ${dropped.join(', ')}`
|
|
389
|
+
: 'the local impact walk did not complete (every bridge crossing was traversed)';
|
|
390
|
+
};
|
|
391
|
+
console.log(` risk is a LOWER BOUND — ${floorReason()}`);
|
|
336
392
|
}
|
|
337
393
|
}
|
|
338
394
|
}
|
|
@@ -402,7 +458,7 @@ export function registerGroupCommands(program) {
|
|
|
402
458
|
process.exitCode = 1;
|
|
403
459
|
return;
|
|
404
460
|
}
|
|
405
|
-
const { contracts, crossLinks, truncated, unreadableRepos, missingRepos } = raw;
|
|
461
|
+
const { contracts, crossLinks, truncated, unreadableRepos, missingRepos, suppressedMatchStages, truncationReason, } = raw;
|
|
406
462
|
if (opts.json) {
|
|
407
463
|
// The whole payload, not a re-serialized subset. Destructuring the two
|
|
408
464
|
// fields this command happens to print and rebuilding an object from
|
|
@@ -421,7 +477,25 @@ export function registerGroupCommands(program) {
|
|
|
421
477
|
for (const l of crossLinks) {
|
|
422
478
|
console.log(` ${l.from.repo} -> ${l.to.repo} [${l.matchType}, conf=${l.confidence}] ${l.contractId}`);
|
|
423
479
|
}
|
|
424
|
-
|
|
480
|
+
// Separate from `truncated` below, and deliberately so: that one means
|
|
481
|
+
// the sync could not read something and the remedy is to fix the repo.
|
|
482
|
+
// This one means the sync was ASKED to skip a stage, and the remedy is
|
|
483
|
+
// to re-run without the flag. A listing narrowed on purpose is still
|
|
484
|
+
// narrowed, and without this the human view showed nothing at all.
|
|
485
|
+
if (suppressedMatchStages && suppressedMatchStages.length > 0) {
|
|
486
|
+
console.log(`\n⚠️ This listing is a lower bound: the last sync skipped ${suppressedMatchStages.join(', ')} matching` +
|
|
487
|
+
`\n (--exact-only), so cross-links that stage would have found are absent.` +
|
|
488
|
+
`\n Re-run \`gitnexus group sync\` without --exact-only for the complete set.`);
|
|
489
|
+
}
|
|
490
|
+
// Gated on the REASON, not just the flag. A suppressed stage sets
|
|
491
|
+
// `truncated` with both repo lists empty, which sent this block down
|
|
492
|
+
// its else-branch and printed "the last sync did not record which
|
|
493
|
+
// repos it could read" — a false statement, with the wrong remedy,
|
|
494
|
+
// about a sync that recorded them fine. The suppressed-stage warning
|
|
495
|
+
// above already said the true thing. When a repo gap co-occurs the
|
|
496
|
+
// reason is 'incomplete-sync' (the repo side takes precedence in
|
|
497
|
+
// `crossRepoCompleteness`), so this block still runs for it.
|
|
498
|
+
if (truncated && truncationReason !== 'suppressed-stage') {
|
|
425
499
|
// Counts above are a floor, not a census. Name the repos when the
|
|
426
500
|
// registry recorded them, and say so plainly when it did not — a
|
|
427
501
|
// listing that cannot say what it is missing is still incomplete.
|
package/dist/cli/help-i18n.js
CHANGED
|
@@ -151,9 +151,7 @@ const OPTION_DESCRIPTION_KEYS = {
|
|
|
151
151
|
'embeddings install|--cuda': 'help.option.embeddings.install.cuda',
|
|
152
152
|
'embeddings install|--force': 'help.option.embeddings.install.force',
|
|
153
153
|
'group create|--force': 'help.option.group.create.force',
|
|
154
|
-
'group sync|--skip-embeddings': 'help.option.group.sync.skipEmbeddings',
|
|
155
154
|
'group sync|--exact-only': 'help.option.group.sync.exactOnly',
|
|
156
|
-
'group sync|--allow-stale': 'help.option.group.sync.allowStale',
|
|
157
155
|
'group sync|--verbose': 'help.option.group.sync.verbose',
|
|
158
156
|
'group sync|--json': 'help.option.json',
|
|
159
157
|
'group impact|--target <symbol>': 'help.option.group.impact.target',
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -233,10 +233,8 @@ export declare const en: {
|
|
|
233
233
|
readonly 'help.option.embeddings.install.cuda': "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)";
|
|
234
234
|
readonly 'help.option.embeddings.install.force': "Install into the runtime prefix even when the stack already resolves";
|
|
235
235
|
readonly 'help.option.group.create.force': "Overwrite existing group";
|
|
236
|
-
readonly 'help.option.group.sync.
|
|
237
|
-
readonly 'help.option.group.sync.
|
|
238
|
-
readonly 'help.option.group.sync.allowStale': "Skip stale index warnings";
|
|
239
|
-
readonly 'help.option.group.sync.verbose': "Show each cross-link detail";
|
|
236
|
+
readonly 'help.option.group.sync.exactOnly': "Skip wildcard service matching; cross-link on exact contract-id match only (manifest links still apply)";
|
|
237
|
+
readonly 'help.option.group.sync.verbose': "Show additional sync diagnostics";
|
|
240
238
|
readonly 'help.option.status.json': "Emit machine-readable index and analyzer provenance";
|
|
241
239
|
readonly 'help.option.json': "JSON output";
|
|
242
240
|
readonly 'help.option.group.impact.target': "Symbol or file name to analyze";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -235,10 +235,8 @@ export const en = {
|
|
|
235
235
|
'help.option.embeddings.install.cuda': "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)",
|
|
236
236
|
'help.option.embeddings.install.force': 'Install into the runtime prefix even when the stack already resolves',
|
|
237
237
|
'help.option.group.create.force': 'Overwrite existing group',
|
|
238
|
-
'help.option.group.sync.
|
|
239
|
-
'help.option.group.sync.
|
|
240
|
-
'help.option.group.sync.allowStale': 'Skip stale index warnings',
|
|
241
|
-
'help.option.group.sync.verbose': 'Show each cross-link detail',
|
|
238
|
+
'help.option.group.sync.exactOnly': 'Skip wildcard service matching; cross-link on exact contract-id match only (manifest links still apply)',
|
|
239
|
+
'help.option.group.sync.verbose': 'Show additional sync diagnostics',
|
|
242
240
|
'help.option.status.json': 'Emit machine-readable index and analyzer provenance',
|
|
243
241
|
'help.option.json': 'JSON output',
|
|
244
242
|
'help.option.group.impact.target': 'Symbol or file name to analyze',
|
|
@@ -234,10 +234,8 @@ export declare const cliResources: {
|
|
|
234
234
|
readonly 'help.option.embeddings.install.cuda': "Also download the CUDA GPU binaries (runs onnxruntime-node's NuGet postinstall; set GLOBAL_AGENT_HTTPS_PROXY behind a proxy)";
|
|
235
235
|
readonly 'help.option.embeddings.install.force': "Install into the runtime prefix even when the stack already resolves";
|
|
236
236
|
readonly 'help.option.group.create.force': "Overwrite existing group";
|
|
237
|
-
readonly 'help.option.group.sync.
|
|
238
|
-
readonly 'help.option.group.sync.
|
|
239
|
-
readonly 'help.option.group.sync.allowStale': "Skip stale index warnings";
|
|
240
|
-
readonly 'help.option.group.sync.verbose': "Show each cross-link detail";
|
|
237
|
+
readonly 'help.option.group.sync.exactOnly': "Skip wildcard service matching; cross-link on exact contract-id match only (manifest links still apply)";
|
|
238
|
+
readonly 'help.option.group.sync.verbose': "Show additional sync diagnostics";
|
|
241
239
|
readonly 'help.option.status.json': "Emit machine-readable index and analyzer provenance";
|
|
242
240
|
readonly 'help.option.json': "JSON output";
|
|
243
241
|
readonly 'help.option.group.impact.target': "Symbol or file name to analyze";
|
|
@@ -490,9 +488,7 @@ export declare const cliResources: {
|
|
|
490
488
|
'help.option.embeddings.install.cuda': string;
|
|
491
489
|
'help.option.embeddings.install.force': string;
|
|
492
490
|
'help.option.group.create.force': string;
|
|
493
|
-
'help.option.group.sync.skipEmbeddings': string;
|
|
494
491
|
'help.option.group.sync.exactOnly': string;
|
|
495
|
-
'help.option.group.sync.allowStale': string;
|
|
496
492
|
'help.option.group.sync.verbose': string;
|
|
497
493
|
'help.option.status.json': string;
|
|
498
494
|
'help.option.json': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -233,9 +233,7 @@ export declare const zhCN: {
|
|
|
233
233
|
'help.option.embeddings.install.cuda': string;
|
|
234
234
|
'help.option.embeddings.install.force': string;
|
|
235
235
|
'help.option.group.create.force': string;
|
|
236
|
-
'help.option.group.sync.skipEmbeddings': string;
|
|
237
236
|
'help.option.group.sync.exactOnly': string;
|
|
238
|
-
'help.option.group.sync.allowStale': string;
|
|
239
237
|
'help.option.group.sync.verbose': string;
|
|
240
238
|
'help.option.status.json': string;
|
|
241
239
|
'help.option.json': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -233,10 +233,8 @@ export const zhCN = {
|
|
|
233
233
|
'help.option.embeddings.install.cuda': '同时下载 CUDA GPU 二进制文件(运行 onnxruntime-node 的 NuGet postinstall;代理后请设置 GLOBAL_AGENT_HTTPS_PROXY)',
|
|
234
234
|
'help.option.embeddings.install.force': '即使嵌入组件已可解析,也强制安装到运行时目录',
|
|
235
235
|
'help.option.group.create.force': '覆盖现有仓库组',
|
|
236
|
-
'help.option.group.sync.
|
|
237
|
-
'help.option.group.sync.
|
|
238
|
-
'help.option.group.sync.allowStale': '跳过过期索引警告',
|
|
239
|
-
'help.option.group.sync.verbose': '显示每条跨仓库链接详情',
|
|
236
|
+
'help.option.group.sync.exactOnly': '跳过通配符服务匹配,仅按契约 ID 精确匹配建立跨仓链接(清单声明的链接仍然生效)',
|
|
237
|
+
'help.option.group.sync.verbose': '显示额外的同步诊断信息',
|
|
240
238
|
'help.option.status.json': '输出机器可读的索引和分析器来源信息',
|
|
241
239
|
'help.option.json': 'JSON 输出',
|
|
242
240
|
'help.option.group.impact.target': '要分析的符号或文件名',
|
|
@@ -53,13 +53,33 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|
|
53
53
|
'obj',
|
|
54
54
|
'target', // Java/Rust
|
|
55
55
|
'.next',
|
|
56
|
+
// `.next` is Next.js's build CACHE; `_next` is the EMITTED output, and the two
|
|
57
|
+
// are different directories. A Capacitor/Cordova shell copies the emitted
|
|
58
|
+
// bundle to `<platform>/app/src/main/assets/public/_next/static/…`, where none
|
|
59
|
+
// of the path segments hit this list — so a mobile-wrapped Next.js app had its
|
|
60
|
+
// shipped bundle indexed as source, and every Route node it produced pointed at
|
|
61
|
+
// a webpack chunk rather than code anyone wrote (#3007).
|
|
62
|
+
//
|
|
63
|
+
// The name is deliberately unanchored. No `<web-root>/_next` form matches a
|
|
64
|
+
// root-level `_next/static/…`, which is the shape the reported repo has, so
|
|
65
|
+
// anchoring it would miss the case it was added for. The accepted cost is a
|
|
66
|
+
// hand-written directory literally named `_next`; recover one with a bare
|
|
67
|
+
// `!_next/` line in `.gitnexusignore`.
|
|
68
|
+
'_next',
|
|
56
69
|
'.nuxt',
|
|
57
70
|
'.output',
|
|
58
71
|
'.vercel',
|
|
59
72
|
'.netlify',
|
|
60
73
|
'.serverless',
|
|
61
74
|
'_build',
|
|
62
|
-
'public/build'
|
|
75
|
+
// `'public/build'` used to sit here. This set is tested one path SEGMENT at a
|
|
76
|
+
// time, and `isHardcodedIgnoredDirectory(name)` takes a bare directory name,
|
|
77
|
+
// so a slash-containing member could never match either — it was inert. Its
|
|
78
|
+
// paths were never unignored though: bare `'build'` above already prunes
|
|
79
|
+
// `public/build/**`, so removing the entry changes no behavior (#3007).
|
|
80
|
+
// `test/unit/ignore-build-output.test.ts` keeps the next slash-bearing entry
|
|
81
|
+
// in this set — or in IGNORED_FILES, ROOT_ARTIFACT_DIRECTORIES or
|
|
82
|
+
// IGNORED_EXTENSIONS — from dying the same way.
|
|
63
83
|
'.parcel-cache',
|
|
64
84
|
'.turbo',
|
|
65
85
|
'.svelte-kit',
|
|
@@ -87,7 +107,6 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|
|
87
107
|
// remains covered by .gitignore/.gitnexusignore and the unambiguous names.
|
|
88
108
|
'monaco-workers', // Monaco editor web-worker bundles generated for browser runtime
|
|
89
109
|
'.terraform',
|
|
90
|
-
'.serverless',
|
|
91
110
|
// Documentation (optional - might want to keep)
|
|
92
111
|
// 'docs',
|
|
93
112
|
// 'documentation',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LbugValue } from '@ladybugdb/core';
|
|
2
|
-
import type { BridgeHandle, BridgeMeta, StoredContract, CrossLink, RepoSnapshot } from './types.js';
|
|
2
|
+
import type { BridgeHandle, BridgeMeta, StoredContract, CrossLink, RepoSnapshot, MatchType } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Serialize an operation on a cached handle's per-handle FIFO chain. Mirrors the
|
|
5
5
|
* promise-chain mechanic of `lbug/conn-lock.ts` (install a fresh unresolved
|
|
@@ -222,6 +222,29 @@ export declare function refreshPreservedBridgeMeta(groupDir: string, diagnostics
|
|
|
222
222
|
missingRepos: string[];
|
|
223
223
|
unreadableRepos: string[];
|
|
224
224
|
}): Promise<PreservedBridgeMetaOutcome>;
|
|
225
|
+
/**
|
|
226
|
+
* Withdraw the bridge's claim to be complete, without touching the database.
|
|
227
|
+
*
|
|
228
|
+
* The one path this exists for: `contracts.json` committed, then the bridge
|
|
229
|
+
* replacement failed. The old database is still physically usable and still
|
|
230
|
+
* answers queries, but it now describes an EARLIER sync than the canonical
|
|
231
|
+
* registry beside it — so `group_contracts` can report a narrowed or advanced
|
|
232
|
+
* contract set while `group_impact` traverses the old graph and calls its
|
|
233
|
+
* answer complete. Two public surfaces, contradictory epistemic claims, from
|
|
234
|
+
* one sync.
|
|
235
|
+
*
|
|
236
|
+
* Setting `provenanceUnknown` is the smallest thing that makes that safe:
|
|
237
|
+
* `bridgeMetaMatchesFile` gives it highest precedence and refuses to vouch for
|
|
238
|
+
* the pair, so every cross-repo answer downgrades to a floor until a sync
|
|
239
|
+
* succeeds. Deliberately NOT a re-stamp — the metadata still describes the
|
|
240
|
+
* database it was written for, and claiming otherwise is the mis-pairing the
|
|
241
|
+
* preserve path is careful to avoid. Deliberately not a delete either: the
|
|
242
|
+
* previous graph is better than nothing as long as nobody calls it complete.
|
|
243
|
+
*
|
|
244
|
+
* Best-effort by construction. It runs inside a failure handler, so a throw
|
|
245
|
+
* here would replace a reported bridge failure with an unrelated one.
|
|
246
|
+
*/
|
|
247
|
+
export declare function markBridgeProvenanceUnknown(groupDir: string): Promise<boolean>;
|
|
225
248
|
export interface WriteBridgeInput {
|
|
226
249
|
contracts: StoredContract[];
|
|
227
250
|
crossLinks: CrossLink[];
|
|
@@ -243,6 +266,13 @@ export interface WriteBridgeInput {
|
|
|
243
266
|
* contract those repos own.
|
|
244
267
|
*/
|
|
245
268
|
unreadableRepos?: string[];
|
|
269
|
+
/**
|
|
270
|
+
* Matching stages the sync was asked to skip. Recorded here for the same
|
|
271
|
+
* reason `unreadableRepos` is: a later cross-repo query reads this bridge
|
|
272
|
+
* with no access to the run that built it, and a graph narrowed by request
|
|
273
|
+
* looks exactly like a complete one.
|
|
274
|
+
*/
|
|
275
|
+
suppressedMatchStages?: MatchType[];
|
|
246
276
|
}
|
|
247
277
|
/**
|
|
248
278
|
* Non-fatal issues encountered during writeBridge. Callers can log these to
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import lbug from '@ladybugdb/core';
|
|
5
5
|
import { BRIDGE_SCHEMA_QUERIES, BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
|
|
6
|
-
import { recordedRepoList } from './completeness.js';
|
|
6
|
+
import { recordedMatchStages, recordedRepoList } from './completeness.js';
|
|
7
7
|
import { closeLbugConnection, openLbugConnection, } from '../lbug/lbug-config.js';
|
|
8
8
|
import { dedupeContracts, dedupeCrossLinks } from './normalization.js';
|
|
9
9
|
import { withGroupSyncLock } from './group-lock.js';
|
|
@@ -518,7 +518,15 @@ export async function closeBridgeDb(handle) {
|
|
|
518
518
|
/* writeBridgeMeta / readBridgeMeta */
|
|
519
519
|
/* ------------------------------------------------------------------ */
|
|
520
520
|
export async function writeBridgeMeta(groupDir, meta) {
|
|
521
|
-
|
|
521
|
+
// Strip the reader-only fields HERE rather than at each writer. `readBridgeMeta`
|
|
522
|
+
// sets both on what it returns, so any caller that reads-modifies-writes would
|
|
523
|
+
// round-trip them to disk — and `pairedWithDatabase` is the poisonous one:
|
|
524
|
+
// persisted, it tells every future reader the pair was verified when nothing
|
|
525
|
+
// verified it. That rule used to live in the body of the only such caller,
|
|
526
|
+
// which held exactly as long as there was one. There are now three writers and
|
|
527
|
+
// two of them read first. Enforced at the boundary, no writer can get it wrong.
|
|
528
|
+
const { repoListsUnreadable: _reader1, pairedWithDatabase: _reader2, ...persisted } = meta;
|
|
529
|
+
await writeFileAtomic(path.join(groupDir, 'meta.json'), JSON.stringify(persisted, null, 2));
|
|
522
530
|
}
|
|
523
531
|
/**
|
|
524
532
|
* Does `meta` still describe the `bridge.lbug` sitting next to it?
|
|
@@ -697,6 +705,12 @@ export async function readBridgeMeta(groupDir) {
|
|
|
697
705
|
meta.unreadableRepos = unreadableRepos;
|
|
698
706
|
else
|
|
699
707
|
delete meta.unreadableRepos;
|
|
708
|
+
// Same absent-vs-empty rule, through the one shared reader.
|
|
709
|
+
const suppressed = recordedMatchStages(raw.suppressedMatchStages);
|
|
710
|
+
if (suppressed)
|
|
711
|
+
meta.suppressedMatchStages = suppressed;
|
|
712
|
+
else
|
|
713
|
+
delete meta.suppressedMatchStages;
|
|
700
714
|
if (repoListsUnreadable)
|
|
701
715
|
meta.repoListsUnreadable = true;
|
|
702
716
|
return meta;
|
|
@@ -754,7 +768,14 @@ async function fileExists(filePath) {
|
|
|
754
768
|
* section ever appears, it needs the same treatment `writeBridge` got — a
|
|
755
769
|
* wrapper, not a lock moved down here.
|
|
756
770
|
*/
|
|
757
|
-
export async function refreshPreservedBridgeMeta(groupDir,
|
|
771
|
+
export async function refreshPreservedBridgeMeta(groupDir,
|
|
772
|
+
// Deliberately NOT `suppressedMatchStages`. This path preserves an EARLIER
|
|
773
|
+
// sync's database, so stamping it with this run's request would claim the
|
|
774
|
+
// untouched bridge was built with a flag it never saw. The registry's own
|
|
775
|
+
// preserve write (`{ ...prior, missingRepos, unreadableRepos }`) omits it for
|
|
776
|
+
// exactly this reason, and the two artifacts have to agree about which run
|
|
777
|
+
// they describe.
|
|
778
|
+
diagnostics) {
|
|
758
779
|
const dbPath = path.join(groupDir, 'bridge.lbug');
|
|
759
780
|
const [metaOnDisk, dbOnDisk] = await Promise.all([
|
|
760
781
|
fileExists(path.join(groupDir, 'meta.json')),
|
|
@@ -771,10 +792,8 @@ export async function refreshPreservedBridgeMeta(groupDir, diagnostics) {
|
|
|
771
792
|
const refreshed = { ...existing, ...diagnostics };
|
|
772
793
|
// NEVER PERSISTED (see `BridgeMeta`): both are things a READER computes ABOUT
|
|
773
794
|
// a file, and this is the first code in the repo that reads metadata and
|
|
774
|
-
// writes it back.
|
|
775
|
-
//
|
|
776
|
-
delete refreshed.repoListsUnreadable;
|
|
777
|
-
delete refreshed.pairedWithDatabase;
|
|
795
|
+
// writes it back. The strip itself now lives in `writeBridgeMeta`, so every
|
|
796
|
+
// writer inherits it rather than each remembering.
|
|
778
797
|
if (paired) {
|
|
779
798
|
const stat = await fsp.stat(dbPath).catch(() => null);
|
|
780
799
|
if (stat) {
|
|
@@ -791,6 +810,40 @@ export async function refreshPreservedBridgeMeta(groupDir, diagnostics) {
|
|
|
791
810
|
await writeBridgeMeta(groupDir, refreshed);
|
|
792
811
|
return 'provenance-unknown';
|
|
793
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Withdraw the bridge's claim to be complete, without touching the database.
|
|
815
|
+
*
|
|
816
|
+
* The one path this exists for: `contracts.json` committed, then the bridge
|
|
817
|
+
* replacement failed. The old database is still physically usable and still
|
|
818
|
+
* answers queries, but it now describes an EARLIER sync than the canonical
|
|
819
|
+
* registry beside it — so `group_contracts` can report a narrowed or advanced
|
|
820
|
+
* contract set while `group_impact` traverses the old graph and calls its
|
|
821
|
+
* answer complete. Two public surfaces, contradictory epistemic claims, from
|
|
822
|
+
* one sync.
|
|
823
|
+
*
|
|
824
|
+
* Setting `provenanceUnknown` is the smallest thing that makes that safe:
|
|
825
|
+
* `bridgeMetaMatchesFile` gives it highest precedence and refuses to vouch for
|
|
826
|
+
* the pair, so every cross-repo answer downgrades to a floor until a sync
|
|
827
|
+
* succeeds. Deliberately NOT a re-stamp — the metadata still describes the
|
|
828
|
+
* database it was written for, and claiming otherwise is the mis-pairing the
|
|
829
|
+
* preserve path is careful to avoid. Deliberately not a delete either: the
|
|
830
|
+
* previous graph is better than nothing as long as nobody calls it complete.
|
|
831
|
+
*
|
|
832
|
+
* Best-effort by construction. It runs inside a failure handler, so a throw
|
|
833
|
+
* here would replace a reported bridge failure with an unrelated one.
|
|
834
|
+
*/
|
|
835
|
+
export async function markBridgeProvenanceUnknown(groupDir) {
|
|
836
|
+
try {
|
|
837
|
+
const existing = await readBridgeMeta(groupDir);
|
|
838
|
+
if (existing.version === 0)
|
|
839
|
+
return false;
|
|
840
|
+
await writeBridgeMeta(groupDir, { ...existing, provenanceUnknown: true });
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
794
847
|
const MAX_SAMPLE_ERRORS = 10;
|
|
795
848
|
function errMessage(err) {
|
|
796
849
|
if (err instanceof Error)
|
|
@@ -1085,6 +1138,9 @@ export async function writeBridgeUnlocked(groupDir, input) {
|
|
|
1085
1138
|
// different claim from a bridge that never recorded the field. Omitted
|
|
1086
1139
|
// only when the caller passed nothing to record.
|
|
1087
1140
|
...(input.unreadableRepos ? { unreadableRepos: input.unreadableRepos } : {}),
|
|
1141
|
+
...(input.suppressedMatchStages
|
|
1142
|
+
? { suppressedMatchStages: input.suppressedMatchStages }
|
|
1143
|
+
: {}),
|
|
1088
1144
|
});
|
|
1089
1145
|
return report;
|
|
1090
1146
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Nothing here imports anything but types. Keep it that way: the moment this
|
|
14
14
|
* file gains a runtime import, every consumer pays for it again.
|
|
15
15
|
*/
|
|
16
|
-
import type { GroupImpactTruncationReason } from './types.js';
|
|
16
|
+
import type { GroupImpactTruncationReason, MatchType } from './types.js';
|
|
17
17
|
/**
|
|
18
18
|
* A union rather than `Pick<GroupImpactResult, ...>` so the two states are
|
|
19
19
|
* distinguishable by their `truncated` discriminant: a caller that folds these
|
|
@@ -58,6 +58,12 @@ export interface CrossRepoCompletenessInput {
|
|
|
58
58
|
*/
|
|
59
59
|
unreadableRepos?: readonly string[];
|
|
60
60
|
missingRepos?: readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* Matching stages the sync was asked to skip. Absent or empty means it
|
|
63
|
+
* suppressed none; a populated list makes the answer a floor for a reason
|
|
64
|
+
* that is neither a runtime limit nor an unreadable repo.
|
|
65
|
+
*/
|
|
66
|
+
suppressedMatchStages?: readonly string[];
|
|
61
67
|
/** Computed by the caller; see `bridgeProvenanceUnknown` for the bridge one. */
|
|
62
68
|
provenanceUnknown: boolean;
|
|
63
69
|
/**
|
|
@@ -80,6 +86,18 @@ export type CrossRepoCompleteness = TruncationFields & {
|
|
|
80
86
|
*/
|
|
81
87
|
incompleteRepos: string[];
|
|
82
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Read a persisted `suppressedMatchStages` list.
|
|
91
|
+
*
|
|
92
|
+
* Sibling of `recordedRepoList` and here for the same stated reason: it had
|
|
93
|
+
* lived in two files verbatim, so tightening one would silently leave the other.
|
|
94
|
+
* All-or-nothing like its sibling — a stale member (this repo has already
|
|
95
|
+
* retired `'bm25'` and `'embedding'`) makes the whole list unreadable rather
|
|
96
|
+
* than filtering down to `[]`, which on this field would mean "measured,
|
|
97
|
+
* nothing suppressed": a clean answer manufactured from a value we could not
|
|
98
|
+
* read.
|
|
99
|
+
*/
|
|
100
|
+
export declare function recordedMatchStages(value: unknown): MatchType[] | undefined;
|
|
83
101
|
/**
|
|
84
102
|
* The ONE computation of "is this cross-repo answer complete?" (KTD10).
|
|
85
103
|
*
|
|
@@ -16,6 +16,23 @@ reasonIfTruncated = 'partial') {
|
|
|
16
16
|
return { truncated: false };
|
|
17
17
|
return { truncated: true, truncationReason: reasonIfTruncated, riskEpistemic: 'lower-bound' };
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Read a persisted `suppressedMatchStages` list.
|
|
21
|
+
*
|
|
22
|
+
* Sibling of `recordedRepoList` and here for the same stated reason: it had
|
|
23
|
+
* lived in two files verbatim, so tightening one would silently leave the other.
|
|
24
|
+
* All-or-nothing like its sibling — a stale member (this repo has already
|
|
25
|
+
* retired `'bm25'` and `'embedding'`) makes the whole list unreadable rather
|
|
26
|
+
* than filtering down to `[]`, which on this field would mean "measured,
|
|
27
|
+
* nothing suppressed": a clean answer manufactured from a value we could not
|
|
28
|
+
* read.
|
|
29
|
+
*/
|
|
30
|
+
export function recordedMatchStages(value) {
|
|
31
|
+
if (!Array.isArray(value))
|
|
32
|
+
return undefined;
|
|
33
|
+
const known = ['exact', 'manifest', 'wildcard'];
|
|
34
|
+
return value.every((v) => known.includes(v)) ? value : undefined;
|
|
35
|
+
}
|
|
19
36
|
/**
|
|
20
37
|
* The ONE computation of "is this cross-repo answer complete?" (KTD10).
|
|
21
38
|
*
|
|
@@ -34,8 +51,14 @@ export function crossRepoCompleteness(input) {
|
|
|
34
51
|
const incompleteRepos = [
|
|
35
52
|
...new Set([...(input.unreadableRepos ?? []), ...(input.missingRepos ?? [])]),
|
|
36
53
|
].filter((repoPath) => input.inScope(repoPath));
|
|
54
|
+
// An unreadable or unaccounted repo outranks a suppressed stage: it is the
|
|
55
|
+
// more serious structural gap and its remedy (repair the repo, re-sync) has
|
|
56
|
+
// to be the one reported. A suppressed stage only decides the reason when
|
|
57
|
+
// the repo side is otherwise clean.
|
|
58
|
+
const suppressed = (input.suppressedMatchStages ?? []).length > 0;
|
|
59
|
+
const repoSideIncomplete = input.provenanceUnknown || incompleteRepos.length > 0;
|
|
37
60
|
return {
|
|
38
|
-
...truncationFields(
|
|
61
|
+
...truncationFields(repoSideIncomplete || suppressed, repoSideIncomplete ? 'incomplete-sync' : 'suppressed-stage'),
|
|
39
62
|
incompleteRepos,
|
|
40
63
|
};
|
|
41
64
|
}
|
|
@@ -25,15 +25,10 @@ const DEFAULT_DETECT = {
|
|
|
25
25
|
grpc: true,
|
|
26
26
|
thrift: true,
|
|
27
27
|
topics: true,
|
|
28
|
-
shared_libs: true,
|
|
29
|
-
embedding_fallback: true,
|
|
30
28
|
includes: false,
|
|
31
29
|
workspace_deps: false,
|
|
32
30
|
};
|
|
33
31
|
const DEFAULT_MATCHING = {
|
|
34
|
-
bm25_threshold: 0.7,
|
|
35
|
-
embedding_threshold: 0.65,
|
|
36
|
-
max_candidates_per_step: 3,
|
|
37
32
|
exclude_links_paths: [],
|
|
38
33
|
exclude_links_param_only_paths: false,
|
|
39
34
|
};
|
|
@@ -696,6 +696,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
696
696
|
const bridge = crossRepoCompleteness({
|
|
697
697
|
unreadableRepos: bridgePrep.meta.unreadableRepos,
|
|
698
698
|
missingRepos: bridgePrep.meta.missingRepos,
|
|
699
|
+
suppressedMatchStages: bridgePrep.meta.suppressedMatchStages,
|
|
699
700
|
provenanceUnknown,
|
|
700
701
|
inScope: (candidate) => repoInSubgroup(candidate, subgroup) || repoInSubgroup(candidate, repoPath, true),
|
|
701
702
|
});
|
|
@@ -716,13 +717,21 @@ export async function runGroupImpact(deps, params) {
|
|
|
716
717
|
// and under-reporting a blast radius is the unsafe direction (an agent told
|
|
717
718
|
// LOW proceeds; told CRITICAL it stops). Marking the floor keeps the
|
|
718
719
|
// warning intact while making the incompleteness legible.
|
|
719
|
-
// Runtime limits first — they are what the caller can retry.
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
|
|
720
|
+
// Runtime limits first — they are what the caller can retry. Past those, the
|
|
721
|
+
// BRIDGE's own reason wins: it already distinguished an unreadable repo
|
|
722
|
+
// ('incomplete-sync', remedy: re-sync) from a stage the sync was asked to
|
|
723
|
+
// skip ('suppressed-stage', remedy: re-sync WITHOUT the flag). Hardcoding
|
|
724
|
+
// the fallback here overrode that and told every caller to repair a repo
|
|
725
|
+
// that read fine — and made the second value unreachable from this surface
|
|
726
|
+
// while the tool description promised it. `cross-trace.ts` re-spreads the
|
|
727
|
+
// bridge's fields for the same reason.
|
|
728
|
+
...truncationFields(truncated, fanoutTimedOut
|
|
729
|
+
? 'timeout'
|
|
730
|
+
: runtimeTruncated
|
|
731
|
+
? 'partial'
|
|
732
|
+
: bridge.truncated
|
|
733
|
+
? bridge.truncationReason
|
|
734
|
+
: 'incomplete-sync'),
|
|
726
735
|
truncatedRepos: [...new Set([...truncatedRepos, ...bridge.incompleteRepos])],
|
|
727
736
|
summary: {
|
|
728
737
|
direct: localSum.direct ?? 0,
|
|
@@ -102,6 +102,7 @@ function bridgeCompletenessFor(meta, inScope) {
|
|
|
102
102
|
return crossRepoCompleteness({
|
|
103
103
|
unreadableRepos: meta.unreadableRepos,
|
|
104
104
|
missingRepos: meta.missingRepos,
|
|
105
|
+
suppressedMatchStages: meta.suppressedMatchStages,
|
|
105
106
|
provenanceUnknown: bridgeProvenanceUnknown(meta),
|
|
106
107
|
inScope,
|
|
107
108
|
});
|