gitnexus 1.6.13-rc.73 → 1.6.13-rc.74
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/mcp/local/aop-metadata.d.ts +1 -0
- package/dist/mcp/local/aop-metadata.js +16 -1
- package/dist/mcp/local/local-backend.d.ts +2 -1
- package/dist/mcp/local/local-backend.js +190 -27
- package/dist/mcp/local/pdg-impact.js +25 -1
- package/dist/mcp/local/query-result-integrity.d.ts +14 -0
- package/dist/mcp/local/query-result-integrity.js +44 -0
- package/package.json +1 -1
|
@@ -72,6 +72,7 @@ export interface SpringAopMetadata {
|
|
|
72
72
|
* shared decoder. Other DECLARES edges (for example Spring Bean factories)
|
|
73
73
|
* and malformed/forward-version evidence are ignored. Query failures are
|
|
74
74
|
* fail-soft because older or partially upgraded indexes must remain readable.
|
|
75
|
+
* Corrupt identities propagate to the context/impact integrity error boundary.
|
|
75
76
|
*/
|
|
76
77
|
export declare function querySpringAopMetadata(lbugPath: string, symbolId: string, symbolType: string): Promise<SpringAopMetadata | undefined>;
|
|
77
78
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { executeParameterized } from '../../core/lbug/pool-adapter.js';
|
|
2
|
+
import { assertSymbolIdentity, assertIdentityFields, rethrowSymbolIdentityError, } from './query-result-integrity.js';
|
|
2
3
|
import { decodeSpringAopReason, } from '../../core/ingestion/frameworks/spring/aop.js';
|
|
3
4
|
const SUPPORTED_SYMBOL_TYPES = new Set(['Class', 'Interface', 'Method', 'CodeElement']);
|
|
4
5
|
const QUERY_RESULT_LIMIT = 1_000;
|
|
@@ -60,6 +61,7 @@ const DETERMINISTIC_RELATIONSHIP_ORDER = 'ORDER BY sourceId, targetId, reason, s
|
|
|
60
61
|
* shared decoder. Other DECLARES edges (for example Spring Bean factories)
|
|
61
62
|
* and malformed/forward-version evidence are ignored. Query failures are
|
|
62
63
|
* fail-soft because older or partially upgraded indexes must remain readable.
|
|
64
|
+
* Corrupt identities propagate to the context/impact integrity error boundary.
|
|
63
65
|
*/
|
|
64
66
|
export async function querySpringAopMetadata(lbugPath, symbolId, symbolType) {
|
|
65
67
|
if (!SUPPORTED_SYMBOL_TYPES.has(symbolType))
|
|
@@ -91,6 +93,18 @@ export async function querySpringAopMetadata(lbugPath, symbolId, symbolType) {
|
|
|
91
93
|
${DETERMINISTIC_RELATIONSHIP_ORDER}
|
|
92
94
|
LIMIT 1001`, { symbolId }),
|
|
93
95
|
]);
|
|
96
|
+
for (const rows of [
|
|
97
|
+
outgoingAdviceRows,
|
|
98
|
+
incomingAdviceRows,
|
|
99
|
+
outgoingPointcutRows,
|
|
100
|
+
incomingPointcutRows,
|
|
101
|
+
]) {
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
assertSymbolIdentity(readRowValue(row, 'sourceId', 0));
|
|
104
|
+
assertSymbolIdentity(readRowValue(row, 'targetId', 3));
|
|
105
|
+
assertIdentityFields(readRowValue(row, 'sourceName', 1), readRowValue(row, 'sourceFilePath', 2), readRowValue(row, 'targetName', 4), readRowValue(row, 'targetFilePath', 5));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
94
108
|
const behaviors = [];
|
|
95
109
|
const advices = [];
|
|
96
110
|
const resolvedPointcuts = [];
|
|
@@ -221,7 +235,8 @@ export async function querySpringAopMetadata(lbugPath, symbolId, symbolType) {
|
|
|
221
235
|
unresolvedPointcuts: dedupedPointcuts,
|
|
222
236
|
};
|
|
223
237
|
}
|
|
224
|
-
catch {
|
|
238
|
+
catch (error) {
|
|
239
|
+
rethrowSymbolIdentityError(error);
|
|
225
240
|
return undefined;
|
|
226
241
|
}
|
|
227
242
|
}
|
|
@@ -660,9 +660,10 @@ export declare class LocalBackend {
|
|
|
660
660
|
* "unknown kind" and, worse, makes the `kind` disambiguation hint unable to
|
|
661
661
|
* filter it out (#2687).
|
|
662
662
|
*
|
|
663
|
-
*
|
|
663
|
+
* Ordinary query failures are swallowed: label enrichment is an optimisation for
|
|
664
664
|
* downstream scoring and #480 Class/Interface BFS seeding; if it fails
|
|
665
665
|
* the symbol still resolves, just without the kind-priority bonus.
|
|
666
|
+
* Corrupt identities propagate to the context/impact error envelope.
|
|
666
667
|
*/
|
|
667
668
|
private enrichCandidateLabels;
|
|
668
669
|
/**
|
|
@@ -13,6 +13,7 @@ import { scoreImpactRisk, unusedAxesForImpactWalk, getLanguageFromFilename, } fr
|
|
|
13
13
|
import { initLbug, executeQuery, executeParameterized, ensureVectorExtension, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
|
|
14
14
|
import { queryClassBeanMetadata } from './bean-metadata.js';
|
|
15
15
|
import { querySpringAopMetadata } from './aop-metadata.js';
|
|
16
|
+
import { SYMBOL_IDENTITY_RECOVERY_SUGGESTION, SymbolIdentityError, assertSymbolIdentity, queryRowValue, assertIdentityFields, assertQueryIdentity, rethrowSymbolIdentityError, } from './query-result-integrity.js';
|
|
16
17
|
import { queryConvexDispatchMetadata } from './convex-metadata.js';
|
|
17
18
|
import { isValidQueryParams } from '../../core/lbug/query-params.js';
|
|
18
19
|
import { toDisplayLine } from './line-display.js';
|
|
@@ -190,20 +191,69 @@ function resolveAliasString(canonical, legacy) {
|
|
|
190
191
|
function nonBlankUid(value) {
|
|
191
192
|
return typeof value === 'string' ? value.trim() || undefined : undefined;
|
|
192
193
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
function assertSymbolRowIdentity(row) {
|
|
195
|
+
assertQueryIdentity(row, 'id', 0, [
|
|
196
|
+
['name', 1],
|
|
197
|
+
['type', 2],
|
|
198
|
+
['filePath', 3],
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
function assertContextRefs(rows) {
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
assertQueryIdentity(row, 'uid', 1, [
|
|
204
|
+
['name', 2],
|
|
205
|
+
['filePath', 3],
|
|
206
|
+
['kind', 4],
|
|
207
|
+
]);
|
|
208
|
+
assertSymbolIdentity(queryRowValue(row, 'relType', 0));
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
const RESPONSE_IDENTITY_FIELDS = new Set([
|
|
212
|
+
'id',
|
|
213
|
+
'uid',
|
|
214
|
+
'name',
|
|
215
|
+
'filePath',
|
|
216
|
+
'label',
|
|
217
|
+
'kind',
|
|
218
|
+
'type',
|
|
219
|
+
'relationType',
|
|
220
|
+
'processType',
|
|
221
|
+
'url',
|
|
222
|
+
'method',
|
|
223
|
+
'sourceId',
|
|
224
|
+
'targetId',
|
|
225
|
+
'symbolId',
|
|
226
|
+
'symbolName',
|
|
227
|
+
'symbolFilePath',
|
|
228
|
+
'adviceId',
|
|
229
|
+
'adviceName',
|
|
230
|
+
'adviceFilePath',
|
|
231
|
+
'advisedId',
|
|
232
|
+
'advisedName',
|
|
233
|
+
'advisedFilePath',
|
|
234
|
+
'evidenceId',
|
|
235
|
+
]);
|
|
236
|
+
/** Cover nested additive identity fields while leaving source/metadata text alone. */
|
|
237
|
+
function assertResponseIdentities(value) {
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
for (const item of value)
|
|
240
|
+
assertResponseIdentities(item);
|
|
241
|
+
}
|
|
242
|
+
else if (value !== null && typeof value === 'object') {
|
|
243
|
+
for (const [key, field] of Object.entries(value)) {
|
|
244
|
+
if (key === 'seedBlocks' || key === 'reachableBlocks' || key === 'intraReachableBlocks') {
|
|
245
|
+
if (!Array.isArray(field))
|
|
246
|
+
throw new SymbolIdentityError();
|
|
247
|
+
for (const id of field)
|
|
248
|
+
assertSymbolIdentity(id);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (RESPONSE_IDENTITY_FIELDS.has(key))
|
|
252
|
+
assertIdentityFields(field);
|
|
253
|
+
if (key !== 'content' && key !== 'methodMetadata' && key !== 'bean') {
|
|
254
|
+
assertResponseIdentities(field);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
207
257
|
}
|
|
208
258
|
}
|
|
209
259
|
const TOOL_STRING_ALIASES = {
|
|
@@ -3467,9 +3517,10 @@ export class LocalBackend {
|
|
|
3467
3517
|
* "unknown kind" and, worse, makes the `kind` disambiguation hint unable to
|
|
3468
3518
|
* filter it out (#2687).
|
|
3469
3519
|
*
|
|
3470
|
-
*
|
|
3520
|
+
* Ordinary query failures are swallowed: label enrichment is an optimisation for
|
|
3471
3521
|
* downstream scoring and #480 Class/Interface BFS seeding; if it fails
|
|
3472
3522
|
* the symbol still resolves, just without the kind-priority bonus.
|
|
3523
|
+
* Corrupt identities propagate to the context/impact error envelope.
|
|
3473
3524
|
*/
|
|
3474
3525
|
async enrichCandidateLabels(repo, candidates) {
|
|
3475
3526
|
const ids = candidates.filter((c) => c.type === '' && c.id).map((c) => c.id);
|
|
@@ -3497,6 +3548,7 @@ export class LocalBackend {
|
|
|
3497
3548
|
`, { ids });
|
|
3498
3549
|
const labelById = new Map();
|
|
3499
3550
|
for (const r of rows) {
|
|
3551
|
+
assertQueryIdentity(r, 'id', 0, [['label', 1]]);
|
|
3500
3552
|
const id = (r.id ?? r[0]);
|
|
3501
3553
|
const label = (r.label ?? r[1]);
|
|
3502
3554
|
if (id && label && !labelById.has(id))
|
|
@@ -3507,7 +3559,8 @@ export class LocalBackend {
|
|
|
3507
3559
|
c.type = labelById.get(c.id);
|
|
3508
3560
|
}
|
|
3509
3561
|
}
|
|
3510
|
-
catch {
|
|
3562
|
+
catch (error) {
|
|
3563
|
+
rethrowSymbolIdentityError(error);
|
|
3511
3564
|
/* best-effort — downstream resolvers still work without the label */
|
|
3512
3565
|
}
|
|
3513
3566
|
}
|
|
@@ -3578,6 +3631,7 @@ export class LocalBackend {
|
|
|
3578
3631
|
const rows = await executeParameterized(repo.lbugPath, `MATCH (n {id: $uid}) RETURN ${selectClause} LIMIT 1`, { uid });
|
|
3579
3632
|
if (rows.length === 0)
|
|
3580
3633
|
return { kind: 'not_found' };
|
|
3634
|
+
assertSymbolRowIdentity(rows[0]);
|
|
3581
3635
|
const r = rows[0];
|
|
3582
3636
|
const symbol = {
|
|
3583
3637
|
id: (r.id ?? r[0]),
|
|
@@ -3700,6 +3754,10 @@ export class LocalBackend {
|
|
|
3700
3754
|
}
|
|
3701
3755
|
if (rows.length === 0)
|
|
3702
3756
|
return { kind: 'not_found' };
|
|
3757
|
+
// Reject every raw candidate before narrowing/scoring can hide a corrupt row.
|
|
3758
|
+
for (const row of rows) {
|
|
3759
|
+
assertSymbolRowIdentity(row);
|
|
3760
|
+
}
|
|
3703
3761
|
// Normalise row shape across object / tuple returns from LadybugDB.
|
|
3704
3762
|
let normalized = rows.map((r) => ({
|
|
3705
3763
|
id: (r.id ?? r[0]),
|
|
@@ -3710,10 +3768,6 @@ export class LocalBackend {
|
|
|
3710
3768
|
endLine: (r.endLine ?? r[5]),
|
|
3711
3769
|
...(include_content ? { content: (r.content ?? r[6]) } : {}),
|
|
3712
3770
|
}));
|
|
3713
|
-
// Reject the whole result before narrowing or scoring: dropping a corrupt
|
|
3714
|
-
// candidate could make an unrelated surviving symbol look unambiguous.
|
|
3715
|
-
for (const candidate of normalized)
|
|
3716
|
-
assertSymbolIdentity(candidate.id);
|
|
3717
3771
|
// An exact File path wins over anchored suffix candidates. Without this,
|
|
3718
3772
|
// `lib/a.ts` and `src/lib/a.ts` both score as File candidates and turn an
|
|
3719
3773
|
// otherwise unambiguous exact target into `ambiguous` (#3084 review P2).
|
|
@@ -3839,7 +3893,10 @@ export class LocalBackend {
|
|
|
3839
3893
|
*/
|
|
3840
3894
|
async context(repo, params) {
|
|
3841
3895
|
try {
|
|
3842
|
-
|
|
3896
|
+
const result = await this._contextImpl(repo, params);
|
|
3897
|
+
if (!result.error)
|
|
3898
|
+
assertResponseIdentities(result);
|
|
3899
|
+
return result;
|
|
3843
3900
|
}
|
|
3844
3901
|
catch (err) {
|
|
3845
3902
|
const msg = (err instanceof Error ? err.message : String(err)) || 'Context query failed';
|
|
@@ -3930,6 +3987,8 @@ export class LocalBackend {
|
|
|
3930
3987
|
LIMIT 30
|
|
3931
3988
|
`, { symId }),
|
|
3932
3989
|
]);
|
|
3990
|
+
assertContextRefs(incomingRows);
|
|
3991
|
+
assertContextRefs(incomingAdvisedRows);
|
|
3933
3992
|
incomingRows.push(...incomingAdvisedRows);
|
|
3934
3993
|
let typedPropertyRows = [];
|
|
3935
3994
|
// Fix #480: Class/Interface nodes have no direct CALLS/IMPORTS edges —
|
|
@@ -4013,6 +4072,16 @@ export class LocalBackend {
|
|
|
4013
4072
|
genericArg: `<${sym.name}>`,
|
|
4014
4073
|
}),
|
|
4015
4074
|
]);
|
|
4075
|
+
assertContextRefs(ctorIncoming);
|
|
4076
|
+
assertContextRefs(fileIncoming);
|
|
4077
|
+
assertContextRefs(typedPropertyIncoming);
|
|
4078
|
+
for (const row of typedProperties) {
|
|
4079
|
+
assertQueryIdentity(row, 'uid', 0, [
|
|
4080
|
+
['name', 1],
|
|
4081
|
+
['filePath', 2],
|
|
4082
|
+
['kind', 3],
|
|
4083
|
+
]);
|
|
4084
|
+
}
|
|
4016
4085
|
typedPropertyRows = typedProperties;
|
|
4017
4086
|
// Deduplicate by (relType, uid) — a caller can have multiple relation
|
|
4018
4087
|
// types to the same target (e.g. both IMPORTS and CALLS), and each
|
|
@@ -4027,6 +4096,7 @@ export class LocalBackend {
|
|
|
4027
4096
|
}
|
|
4028
4097
|
}
|
|
4029
4098
|
catch (e) {
|
|
4099
|
+
rethrowSymbolIdentityError(e);
|
|
4030
4100
|
logQueryError('context:class-incoming-expansion', e);
|
|
4031
4101
|
}
|
|
4032
4102
|
}
|
|
@@ -4048,6 +4118,8 @@ export class LocalBackend {
|
|
|
4048
4118
|
LIMIT 30
|
|
4049
4119
|
`, { symId }),
|
|
4050
4120
|
]);
|
|
4121
|
+
assertContextRefs(outgoingRows);
|
|
4122
|
+
assertContextRefs(outgoingAdvisedRows);
|
|
4051
4123
|
outgoingRows.push(...outgoingAdvisedRows);
|
|
4052
4124
|
// Process participation.
|
|
4053
4125
|
//
|
|
@@ -4066,8 +4138,15 @@ export class LocalBackend {
|
|
|
4066
4138
|
RETURN p.id AS pid, p.heuristicLabel AS label, MIN(r.step) AS step, p.stepCount AS stepCount, p.entryPointId AS entryPointId
|
|
4067
4139
|
ORDER BY pid
|
|
4068
4140
|
`, { symId });
|
|
4141
|
+
for (const row of processRows) {
|
|
4142
|
+
assertQueryIdentity(row, 'pid', 0, [
|
|
4143
|
+
['label', 1],
|
|
4144
|
+
['entryPointId', 4],
|
|
4145
|
+
]);
|
|
4146
|
+
}
|
|
4069
4147
|
}
|
|
4070
4148
|
catch (e) {
|
|
4149
|
+
rethrowSymbolIdentityError(e);
|
|
4071
4150
|
logQueryError('context:process-participation', e);
|
|
4072
4151
|
}
|
|
4073
4152
|
// Route lookup — HTTP endpoints this symbol handles, plus entry-point
|
|
@@ -4099,6 +4178,7 @@ export class LocalBackend {
|
|
|
4099
4178
|
// (GET/POST pair). URL-only dedup would drop the second endpoint.
|
|
4100
4179
|
const seenRoutes = new Set();
|
|
4101
4180
|
for (const r of routeRows) {
|
|
4181
|
+
assertIdentityFields(queryRowValue(r, 'url', 0), queryRowValue(r, 'method', 1));
|
|
4102
4182
|
const url = r.url ?? r[0];
|
|
4103
4183
|
const method = r.method ?? r[1];
|
|
4104
4184
|
const dedupKey = routeEnrichmentKey(method ? String(method) : undefined, url);
|
|
@@ -4109,7 +4189,8 @@ export class LocalBackend {
|
|
|
4109
4189
|
}
|
|
4110
4190
|
}
|
|
4111
4191
|
catch (e) {
|
|
4112
|
-
|
|
4192
|
+
rethrowSymbolIdentityError(e);
|
|
4193
|
+
// Ordinary query failures leave this best-effort enrichment unavailable.
|
|
4113
4194
|
logQueryError('context:route-lookup', e);
|
|
4114
4195
|
}
|
|
4115
4196
|
// Helper to categorize refs
|
|
@@ -4151,6 +4232,7 @@ export class LocalBackend {
|
|
|
4151
4232
|
const epistemicPromise = this.computeEpistemicBoundary(repo, symId, epistemicSymType, (sym.name || sym[1]));
|
|
4152
4233
|
const beanMetadataPromise = queryClassBeanMetadata(repo.lbugPath, symId, epistemicSymType);
|
|
4153
4234
|
const aopMetadataPromise = querySpringAopMetadata(repo.lbugPath, symId, epistemicSymType);
|
|
4235
|
+
void aopMetadataPromise.catch(() => undefined);
|
|
4154
4236
|
// R3-1. A `Property` whose name the analyzer declined to link — because
|
|
4155
4237
|
// every definition of it lives in another language — otherwise returns an
|
|
4156
4238
|
// incoming list byte-identical to a genuinely unread field. Those demand
|
|
@@ -4237,6 +4319,7 @@ export class LocalBackend {
|
|
|
4237
4319
|
chain = await this._computeContextChain(repo, symId, requestedDepth);
|
|
4238
4320
|
}
|
|
4239
4321
|
catch (e) {
|
|
4322
|
+
rethrowSymbolIdentityError(e);
|
|
4240
4323
|
logQueryError('context:chain-bfs', e);
|
|
4241
4324
|
}
|
|
4242
4325
|
}
|
|
@@ -4276,8 +4359,8 @@ export class LocalBackend {
|
|
|
4276
4359
|
processes: processRows.map((r) => ({
|
|
4277
4360
|
id: r.pid || r[0],
|
|
4278
4361
|
name: r.label || r[1],
|
|
4279
|
-
step_index: r.step
|
|
4280
|
-
step_count: r.stepCount
|
|
4362
|
+
step_index: r.step ?? r[2],
|
|
4363
|
+
step_count: r.stepCount ?? r[3],
|
|
4281
4364
|
})),
|
|
4282
4365
|
};
|
|
4283
4366
|
}
|
|
@@ -4342,6 +4425,13 @@ export class LocalBackend {
|
|
|
4342
4425
|
});
|
|
4343
4426
|
if (rows.length === 0)
|
|
4344
4427
|
return { nextFrontier: [] };
|
|
4428
|
+
for (const row of rows) {
|
|
4429
|
+
assertQueryIdentity(row, 'uid', 0, [
|
|
4430
|
+
['name', 1],
|
|
4431
|
+
['filePath', 2],
|
|
4432
|
+
['kind', 3],
|
|
4433
|
+
]);
|
|
4434
|
+
}
|
|
4345
4435
|
// MATCH is one row per CALLS edge. Cypher `WITH DISTINCT` applies
|
|
4346
4436
|
// LIMIT 50 to unique neighbors; this second pass still collapses
|
|
4347
4437
|
// twins if a driver/engine ever returns duplicate rows.
|
|
@@ -4364,6 +4454,7 @@ export class LocalBackend {
|
|
|
4364
4454
|
return { nodes, nextFrontier: fresh.map((r) => r.uid) };
|
|
4365
4455
|
}
|
|
4366
4456
|
catch (e) {
|
|
4457
|
+
rethrowSymbolIdentityError(e);
|
|
4367
4458
|
logQueryError(logLabel, e);
|
|
4368
4459
|
return { nextFrontier: [] };
|
|
4369
4460
|
}
|
|
@@ -5778,7 +5869,10 @@ export class LocalBackend {
|
|
|
5778
5869
|
}
|
|
5779
5870
|
async impact(repo, params) {
|
|
5780
5871
|
try {
|
|
5781
|
-
|
|
5872
|
+
const result = await this._impactImpl(repo, params);
|
|
5873
|
+
if (!result.error)
|
|
5874
|
+
assertResponseIdentities(result);
|
|
5875
|
+
return result;
|
|
5782
5876
|
}
|
|
5783
5877
|
catch (err) {
|
|
5784
5878
|
// Return structured error instead of crashing (#321)
|
|
@@ -6058,6 +6152,7 @@ export class LocalBackend {
|
|
|
6058
6152
|
catch (e) {
|
|
6059
6153
|
probeFailed = true;
|
|
6060
6154
|
candidateProbeFailed = true;
|
|
6155
|
+
rethrowSymbolIdentityError(e);
|
|
6061
6156
|
logQueryError('impact:ambiguous-candidate', e);
|
|
6062
6157
|
}
|
|
6063
6158
|
return {
|
|
@@ -6298,6 +6393,7 @@ export class LocalBackend {
|
|
|
6298
6393
|
return composeUnifiedPdgImpactResult(pdgResult, interproceduralResult);
|
|
6299
6394
|
}
|
|
6300
6395
|
catch (e) {
|
|
6396
|
+
rethrowSymbolIdentityError(e);
|
|
6301
6397
|
logQueryError('impact:pdg-interprocedural-reach', e);
|
|
6302
6398
|
return composeUnifiedPdgImpactResult(pdgResult, null, e);
|
|
6303
6399
|
}
|
|
@@ -6330,6 +6426,7 @@ export class LocalBackend {
|
|
|
6330
6426
|
try {
|
|
6331
6427
|
const rows = await executeParameterized(repo.lbugPath, `MATCH (b:BasicBlock) WHERE b.id IN $ids RETURN b.callees AS callees`, { ids: blockIds });
|
|
6332
6428
|
for (const r of rows) {
|
|
6429
|
+
assertIdentityFields(r.callees ?? r[0]);
|
|
6333
6430
|
const raw = String(r.callees ?? r[0] ?? '');
|
|
6334
6431
|
for (const n of raw.split(' '))
|
|
6335
6432
|
if (n)
|
|
@@ -6337,6 +6434,7 @@ export class LocalBackend {
|
|
|
6337
6434
|
}
|
|
6338
6435
|
}
|
|
6339
6436
|
catch (e) {
|
|
6437
|
+
rethrowSymbolIdentityError(e);
|
|
6340
6438
|
logQueryError('impact:pdg-slice-callees', e);
|
|
6341
6439
|
}
|
|
6342
6440
|
return names;
|
|
@@ -6370,6 +6468,7 @@ export class LocalBackend {
|
|
|
6370
6468
|
}
|
|
6371
6469
|
}
|
|
6372
6470
|
catch (e) {
|
|
6471
|
+
rethrowSymbolIdentityError(e);
|
|
6373
6472
|
logQueryError('impact:pdg-slice-callee-ids', e);
|
|
6374
6473
|
}
|
|
6375
6474
|
return ids;
|
|
@@ -6488,7 +6587,10 @@ export class LocalBackend {
|
|
|
6488
6587
|
WHERE x.id = $symId AND r.type IN $heritage
|
|
6489
6588
|
RETURN DISTINCT iface.id AS id, iface.name AS name, labels(iface)[0] AS label
|
|
6490
6589
|
ORDER BY id
|
|
6491
|
-
LIMIT 25`, { symId, heritage: HERITAGE_TYPES }).catch(() =>
|
|
6590
|
+
LIMIT 25`, { symId, heritage: HERITAGE_TYPES }).catch((error) => {
|
|
6591
|
+
rethrowSymbolIdentityError(error);
|
|
6592
|
+
return [];
|
|
6593
|
+
});
|
|
6492
6594
|
const undecidedSummary = meta?.undecidedInterfaceSatisfaction;
|
|
6493
6595
|
const undecidedDrops = undecidedSummary === undefined
|
|
6494
6596
|
? { notes: [], undecided: 0 }
|
|
@@ -6526,6 +6628,10 @@ export class LocalBackend {
|
|
|
6526
6628
|
}
|
|
6527
6629
|
const ifaceRows = await interfaceRowsPromise;
|
|
6528
6630
|
for (const r of ifaceRows) {
|
|
6631
|
+
assertQueryIdentity(r, 'id', 0, [
|
|
6632
|
+
['name', 1],
|
|
6633
|
+
['label', 2],
|
|
6634
|
+
]);
|
|
6529
6635
|
const id = (r.id ?? r[0]);
|
|
6530
6636
|
if (id && !boundary.has(id)) {
|
|
6531
6637
|
boundary.set(id, {
|
|
@@ -6546,7 +6652,10 @@ export class LocalBackend {
|
|
|
6546
6652
|
await Promise.all(ifaceIds.map(async (ifaceId) => {
|
|
6547
6653
|
const rows = await executeParameterized(repo.lbugPath, `MATCH (other)-[r:CodeRelation]->(iface)
|
|
6548
6654
|
WHERE iface.id = $ifaceId AND r.type IN $types
|
|
6549
|
-
RETURN COUNT(DISTINCT other.id) AS cnt`, { ifaceId, types }).catch(() =>
|
|
6655
|
+
RETURN COUNT(DISTINCT other.id) AS cnt`, { ifaceId, types }).catch((error) => {
|
|
6656
|
+
rethrowSymbolIdentityError(error);
|
|
6657
|
+
return [];
|
|
6658
|
+
});
|
|
6550
6659
|
const cnt = rows.length > 0 ? Number(rows[0].cnt ?? rows[0][0] ?? 0) : 0;
|
|
6551
6660
|
m.set(ifaceId, cnt);
|
|
6552
6661
|
}));
|
|
@@ -6600,7 +6709,8 @@ export class LocalBackend {
|
|
|
6600
6709
|
},
|
|
6601
6710
|
};
|
|
6602
6711
|
}
|
|
6603
|
-
catch {
|
|
6712
|
+
catch (error) {
|
|
6713
|
+
rethrowSymbolIdentityError(error);
|
|
6604
6714
|
// Never let the heritage probe's failure suppress a drop we already know
|
|
6605
6715
|
// about — the whole point is that silence must not read as certainty.
|
|
6606
6716
|
return epistemicFrom(droppedBoundaries);
|
|
@@ -6648,6 +6758,7 @@ export class LocalBackend {
|
|
|
6648
6758
|
if (!symId) {
|
|
6649
6759
|
throw new Error(`Impact target '${sym.name || sym[1] || '?'}' resolved without a node id; refusing to report a blast radius`);
|
|
6650
6760
|
}
|
|
6761
|
+
assertSymbolRowIdentity(sym);
|
|
6651
6762
|
// #1858 — kick off the epistemic boundary probe concurrently with the BFS.
|
|
6652
6763
|
// It depends only on symId/symType/symName (all known now) and touches no
|
|
6653
6764
|
// shared state, so its extra round-trip overlaps the traversal instead of
|
|
@@ -6670,6 +6781,7 @@ export class LocalBackend {
|
|
|
6670
6781
|
const aopMetadataPromise = opts.skipEpistemic || summaryOnly
|
|
6671
6782
|
? Promise.resolve(undefined)
|
|
6672
6783
|
: querySpringAopMetadata(repo.lbugPath, symId, symType);
|
|
6784
|
+
void aopMetadataPromise.catch(() => undefined);
|
|
6673
6785
|
const impacted = [];
|
|
6674
6786
|
const visited = new Set([symId]);
|
|
6675
6787
|
const pdgBridgeEvidenceById = new Map();
|
|
@@ -6704,6 +6816,7 @@ export class LocalBackend {
|
|
|
6704
6816
|
`, { symId }),
|
|
6705
6817
|
]);
|
|
6706
6818
|
for (const r of ctorRows) {
|
|
6819
|
+
assertSymbolRowIdentity(r);
|
|
6707
6820
|
const rid = r.id || r[0];
|
|
6708
6821
|
if (rid && !visited.has(rid)) {
|
|
6709
6822
|
visited.add(rid);
|
|
@@ -6711,6 +6824,7 @@ export class LocalBackend {
|
|
|
6711
6824
|
}
|
|
6712
6825
|
}
|
|
6713
6826
|
for (const r of fileRows) {
|
|
6827
|
+
assertSymbolRowIdentity(r);
|
|
6714
6828
|
const rid = r.id || r[0];
|
|
6715
6829
|
if (rid && !visited.has(rid)) {
|
|
6716
6830
|
visited.add(rid);
|
|
@@ -6729,6 +6843,7 @@ export class LocalBackend {
|
|
|
6729
6843
|
genericArg: `<${sym.name}>`,
|
|
6730
6844
|
});
|
|
6731
6845
|
for (const r of typedPropertyRows) {
|
|
6846
|
+
assertSymbolRowIdentity(r);
|
|
6732
6847
|
const rid = r.id || r[0];
|
|
6733
6848
|
if (rid && !visited.has(rid)) {
|
|
6734
6849
|
visited.add(rid);
|
|
@@ -6737,6 +6852,7 @@ export class LocalBackend {
|
|
|
6737
6852
|
}
|
|
6738
6853
|
}
|
|
6739
6854
|
catch (e) {
|
|
6855
|
+
rethrowSymbolIdentityError(e);
|
|
6740
6856
|
logQueryError('impact:class-node-expansion', e);
|
|
6741
6857
|
traversalComplete = false;
|
|
6742
6858
|
}
|
|
@@ -6767,6 +6883,9 @@ export class LocalBackend {
|
|
|
6767
6883
|
ORDER BY id
|
|
6768
6884
|
LIMIT ${OBJECT_CALLABLE_MEMBER_CAP + 1}
|
|
6769
6885
|
`, { symId });
|
|
6886
|
+
for (const row of memberRows) {
|
|
6887
|
+
assertSymbolRowIdentity(row);
|
|
6888
|
+
}
|
|
6770
6889
|
memberRows.sort((a, b) => compareCodeUnits(String(a.id ?? a[0]), String(b.id ?? b[0])));
|
|
6771
6890
|
if (memberRows.length > OBJECT_CALLABLE_MEMBER_CAP)
|
|
6772
6891
|
traversalComplete = false;
|
|
@@ -6788,6 +6907,7 @@ export class LocalBackend {
|
|
|
6788
6907
|
}
|
|
6789
6908
|
}
|
|
6790
6909
|
catch (e) {
|
|
6910
|
+
rethrowSymbolIdentityError(e);
|
|
6791
6911
|
logQueryError('impact:object-callable-expansion', e);
|
|
6792
6912
|
traversalComplete = false;
|
|
6793
6913
|
}
|
|
@@ -6840,6 +6960,17 @@ export class LocalBackend {
|
|
|
6840
6960
|
relTypes: relationTypes,
|
|
6841
6961
|
...(safeMinConfidence > 0 ? { minConfidence: safeMinConfidence } : {}),
|
|
6842
6962
|
});
|
|
6963
|
+
// Validate before filtering/deduplication; a discarded corrupt edge is
|
|
6964
|
+
// still evidence that this result's counts cannot be trusted.
|
|
6965
|
+
for (const row of related) {
|
|
6966
|
+
assertQueryIdentity(row, 'id', 1, [
|
|
6967
|
+
['sourceId', 0],
|
|
6968
|
+
['name', 2],
|
|
6969
|
+
['type', 3],
|
|
6970
|
+
['filePath', 4],
|
|
6971
|
+
]);
|
|
6972
|
+
assertSymbolIdentity(queryRowValue(row, 'relType', 5));
|
|
6973
|
+
}
|
|
6843
6974
|
const edges = related.map((rel) => ({
|
|
6844
6975
|
id: rel.id || rel[1],
|
|
6845
6976
|
name: rel.name || rel[2],
|
|
@@ -6931,6 +7062,7 @@ export class LocalBackend {
|
|
|
6931
7062
|
}
|
|
6932
7063
|
}
|
|
6933
7064
|
catch (e) {
|
|
7065
|
+
rethrowSymbolIdentityError(e);
|
|
6934
7066
|
logQueryError('impact:depth-traversal', e);
|
|
6935
7067
|
// Break out of depth loop on query failure but return partial results
|
|
6936
7068
|
// collected so far, rather than silently swallowing the error (#321)
|
|
@@ -7031,12 +7163,21 @@ export class LocalBackend {
|
|
|
7031
7163
|
ep.name AS epName, labels(ep)[0] AS epType, ep.filePath AS epFilePath
|
|
7032
7164
|
ORDER BY pId
|
|
7033
7165
|
`, { ids }).catch((err) => {
|
|
7166
|
+
rethrowSymbolIdentityError(err);
|
|
7034
7167
|
processQueryFailed = true;
|
|
7035
7168
|
enrichmentDegraded = true;
|
|
7036
7169
|
logQueryError('impact:process-chunk', err);
|
|
7037
7170
|
return [];
|
|
7038
7171
|
});
|
|
7039
7172
|
for (const row of rows) {
|
|
7173
|
+
assertQueryIdentity(row, 'pId', 0, [
|
|
7174
|
+
['name', 1],
|
|
7175
|
+
['processType', 2],
|
|
7176
|
+
['entryPointId', 3],
|
|
7177
|
+
['epName', 7],
|
|
7178
|
+
['epType', 8],
|
|
7179
|
+
['epFilePath', 9],
|
|
7180
|
+
]);
|
|
7040
7181
|
const pId = row.pId ?? row[0];
|
|
7041
7182
|
const epId = row.entryPointId ?? row[3] ?? row.pId ?? row[0];
|
|
7042
7183
|
// Track mapping from process -> entryPoint so we can backfill missing minStep
|
|
@@ -7083,6 +7224,7 @@ export class LocalBackend {
|
|
|
7083
7224
|
}
|
|
7084
7225
|
}
|
|
7085
7226
|
catch (e) {
|
|
7227
|
+
rethrowSymbolIdentityError(e);
|
|
7086
7228
|
processQueryFailed = true;
|
|
7087
7229
|
enrichmentDegraded = true;
|
|
7088
7230
|
logQueryError('impact:process-chunk', e);
|
|
@@ -7100,11 +7242,13 @@ export class LocalBackend {
|
|
|
7100
7242
|
WHERE p.id IN $pIds AND s.id IN $ids
|
|
7101
7243
|
RETURN p.id AS pid, MIN(r.step) AS minStep
|
|
7102
7244
|
`, { pIds, ids: allImpactedIds }).catch((err) => {
|
|
7245
|
+
rethrowSymbolIdentityError(err);
|
|
7103
7246
|
enrichmentDegraded = true;
|
|
7104
7247
|
logQueryError('impact:process-chunk-backfill', err);
|
|
7105
7248
|
return [];
|
|
7106
7249
|
});
|
|
7107
7250
|
for (const mr of missingRows) {
|
|
7251
|
+
assertQueryIdentity(mr, 'pid', 0, []);
|
|
7108
7252
|
const pid = mr.pid ?? mr[0];
|
|
7109
7253
|
const minStep = mr.minStep ?? mr[1];
|
|
7110
7254
|
const epId = processToEntryPoint.get(String(pid));
|
|
@@ -7119,6 +7263,7 @@ export class LocalBackend {
|
|
|
7119
7263
|
}
|
|
7120
7264
|
}
|
|
7121
7265
|
catch (e) {
|
|
7266
|
+
rethrowSymbolIdentityError(e);
|
|
7122
7267
|
enrichmentDegraded = true;
|
|
7123
7268
|
logQueryError('impact:process-chunk-backfill', e);
|
|
7124
7269
|
}
|
|
@@ -7168,12 +7313,14 @@ export class LocalBackend {
|
|
|
7168
7313
|
ORDER BY hits DESC, name
|
|
7169
7314
|
LIMIT 20
|
|
7170
7315
|
`, { ids: idsChunk }).catch((err) => {
|
|
7316
|
+
rethrowSymbolIdentityError(err);
|
|
7171
7317
|
moduleQueryFailed = true;
|
|
7172
7318
|
enrichmentDegraded = true;
|
|
7173
7319
|
logQueryError('impact:module-chunk', err);
|
|
7174
7320
|
return [];
|
|
7175
7321
|
});
|
|
7176
7322
|
for (const r of rows) {
|
|
7323
|
+
assertIdentityFields(queryRowValue(r, 'name', 0));
|
|
7177
7324
|
const name = r.name ?? r[0] ?? null;
|
|
7178
7325
|
const hits = (r.hits ?? r[1]) || 0;
|
|
7179
7326
|
if (!name)
|
|
@@ -7182,6 +7329,7 @@ export class LocalBackend {
|
|
|
7182
7329
|
}
|
|
7183
7330
|
}
|
|
7184
7331
|
catch (e) {
|
|
7332
|
+
rethrowSymbolIdentityError(e);
|
|
7185
7333
|
moduleQueryFailed = true;
|
|
7186
7334
|
enrichmentDegraded = true;
|
|
7187
7335
|
logQueryError('impact:module-chunk', e);
|
|
@@ -7205,18 +7353,21 @@ export class LocalBackend {
|
|
|
7205
7353
|
WHERE s.id IN $ids
|
|
7206
7354
|
RETURN DISTINCT c.heuristicLabel AS name
|
|
7207
7355
|
`, { ids: idsChunk }).catch((err) => {
|
|
7356
|
+
rethrowSymbolIdentityError(err);
|
|
7208
7357
|
enrichmentDegraded = true;
|
|
7209
7358
|
moduleClassificationFailed = true;
|
|
7210
7359
|
logQueryError('impact:direct-module-chunk', err);
|
|
7211
7360
|
return [];
|
|
7212
7361
|
});
|
|
7213
7362
|
for (const r of rows) {
|
|
7363
|
+
assertIdentityFields(queryRowValue(r, 'name', 0));
|
|
7214
7364
|
const name = r.name ?? r[0] ?? null;
|
|
7215
7365
|
if (name)
|
|
7216
7366
|
directModuleSet.add(name);
|
|
7217
7367
|
}
|
|
7218
7368
|
}
|
|
7219
7369
|
catch (e) {
|
|
7370
|
+
rethrowSymbolIdentityError(e);
|
|
7220
7371
|
enrichmentDegraded = true;
|
|
7221
7372
|
moduleClassificationFailed = true;
|
|
7222
7373
|
logQueryError('impact:direct-module-chunk', e);
|
|
@@ -7269,11 +7420,14 @@ export class LocalBackend {
|
|
|
7269
7420
|
RETURN h.id AS hid, route.name AS url, route.method AS method
|
|
7270
7421
|
ORDER BY url, method
|
|
7271
7422
|
`, { ids: chunkIds }).catch((err) => {
|
|
7423
|
+
rethrowSymbolIdentityError(err);
|
|
7272
7424
|
enrichmentDegraded = true;
|
|
7273
7425
|
logQueryError('impact:route-chunk', err);
|
|
7274
7426
|
return [];
|
|
7275
7427
|
});
|
|
7276
7428
|
for (const row of rows) {
|
|
7429
|
+
assertSymbolIdentity(queryRowValue(row, 'hid', 0));
|
|
7430
|
+
assertIdentityFields(queryRowValue(row, 'url', 1), queryRowValue(row, 'method', 2));
|
|
7277
7431
|
const hid = String(row.hid ?? row[0] ?? '');
|
|
7278
7432
|
const url = row.url ?? row[1];
|
|
7279
7433
|
if (!hid || typeof url !== 'string')
|
|
@@ -7421,8 +7575,16 @@ export class LocalBackend {
|
|
|
7421
7575
|
WHERE s.id IN $ids
|
|
7422
7576
|
RETURN s.id AS sid, p.id AS pid, p.heuristicLabel AS pName,
|
|
7423
7577
|
p.processType AS pType, MIN(r.step) AS step
|
|
7424
|
-
`, { ids: chunkIds }).catch(() =>
|
|
7578
|
+
`, { ids: chunkIds }).catch((err) => {
|
|
7579
|
+
rethrowSymbolIdentityError(err);
|
|
7580
|
+
return [];
|
|
7581
|
+
});
|
|
7425
7582
|
for (const row of rows) {
|
|
7583
|
+
assertQueryIdentity(row, 'sid', 0, []);
|
|
7584
|
+
assertQueryIdentity(row, 'pid', 1, [
|
|
7585
|
+
['pName', 2],
|
|
7586
|
+
['pType', 3],
|
|
7587
|
+
]);
|
|
7426
7588
|
const sid = row.sid ?? row[0];
|
|
7427
7589
|
if (!sid)
|
|
7428
7590
|
continue;
|
|
@@ -7440,6 +7602,7 @@ export class LocalBackend {
|
|
|
7440
7602
|
}
|
|
7441
7603
|
}
|
|
7442
7604
|
catch (e) {
|
|
7605
|
+
rethrowSymbolIdentityError(e);
|
|
7443
7606
|
logQueryError('impact:per-symbol-process-chunk', e);
|
|
7444
7607
|
}
|
|
7445
7608
|
}
|
|
@@ -22,6 +22,7 @@ import { toDisplayLine } from './line-display.js';
|
|
|
22
22
|
import { toOneBasedLine } from '../../core/ingestion/utils/line-base.js';
|
|
23
23
|
import { decodeCallSummary } from '../../core/ingestion/taint/call-summary-codec.js';
|
|
24
24
|
import { decodeReachingDefReason } from '../../core/ingestion/cfg/reaching-def-reason-codec.js';
|
|
25
|
+
import { assertSymbolIdentity, assertIdentityFields, assertQueryIdentity, } from './query-result-integrity.js';
|
|
25
26
|
/**
|
|
26
27
|
* Parse the `<fnLine>` segment out of a `BasicBlock` id (1-based function start
|
|
27
28
|
* line). The id template is
|
|
@@ -81,16 +82,21 @@ const INTERPROC_NODE_BUDGET = 5000;
|
|
|
81
82
|
* `classifyPdgBridgeEvidence`); this is the same fact, read at the descent side.
|
|
82
83
|
*/
|
|
83
84
|
function parseCalleeIdsCell(raw) {
|
|
85
|
+
assertIdentityFields(raw);
|
|
84
86
|
const ids = [];
|
|
85
87
|
let truncated = false;
|
|
88
|
+
if (!String(raw ?? '').trim())
|
|
89
|
+
return { ids, truncated };
|
|
86
90
|
// Split on the SHARED CALLEE_ID_SEP (tab) — ids embed file paths / multi-word
|
|
87
91
|
// C++ type tokens that can contain a space, so a space split would fragment
|
|
88
92
|
// them. Producer (calleeIdsOfBlock) joins with the same constant.
|
|
89
93
|
for (const id of String(raw ?? '').split(CALLEE_ID_SEP)) {
|
|
90
94
|
if (id === CALLEES_TRUNCATED_SENTINEL)
|
|
91
95
|
truncated = true;
|
|
92
|
-
else
|
|
96
|
+
else {
|
|
97
|
+
assertSymbolIdentity(id);
|
|
93
98
|
ids.push(id);
|
|
99
|
+
}
|
|
94
100
|
}
|
|
95
101
|
return { ids, truncated };
|
|
96
102
|
}
|
|
@@ -179,6 +185,7 @@ async function selfReachingDefEdgesByBlock(lbugPath, blockIds, exec) {
|
|
|
179
185
|
WHERE r.type = 'REACHING_DEF' AND a.id IN $ids
|
|
180
186
|
RETURN a.id AS id, r.reason AS reason`, { ids: blockIds });
|
|
181
187
|
for (const r of rows) {
|
|
188
|
+
assertQueryIdentity(r, 'id', 0);
|
|
182
189
|
const id = String(r['id'] ?? '');
|
|
183
190
|
if (!id)
|
|
184
191
|
continue;
|
|
@@ -251,6 +258,7 @@ chainWalkBlocks = new Map()) {
|
|
|
251
258
|
// Narrow the awaited rows ONCE at the boundary to a typed record shape; read
|
|
252
259
|
// the aliased cells via bracket access with String()/Number() coercion.
|
|
253
260
|
for (const r of rows) {
|
|
261
|
+
assertQueryIdentity(r, 'id', 0);
|
|
254
262
|
const id = String(r['id'] ?? '');
|
|
255
263
|
const line = Number(r['line'] ?? 0);
|
|
256
264
|
if (!id || !Number.isFinite(line) || line <= 0)
|
|
@@ -416,6 +424,10 @@ async function projectBlocksToSymbols(deps) {
|
|
|
416
424
|
// non-aliased row shape) — no per-field `as any`, matching the typed-row
|
|
417
425
|
// pattern used elsewhere in this file (e.g. lines ~264, ~1309, ~1386).
|
|
418
426
|
for (const r of rows) {
|
|
427
|
+
assertQueryIdentity(r, 'id', 0, [
|
|
428
|
+
['name', 1],
|
|
429
|
+
['label', 2],
|
|
430
|
+
]);
|
|
419
431
|
resolved.push({
|
|
420
432
|
id: String(r['id'] ?? r['0'] ?? ''),
|
|
421
433
|
name: String(r['name'] ?? r['1'] ?? ''),
|
|
@@ -1078,6 +1090,8 @@ async function bfsReachableBlocks(input) {
|
|
|
1078
1090
|
// Narrow the awaited rows ONCE at the boundary (executeParameterized returns
|
|
1079
1091
|
// any[]) to a typed record shape, then read the aliased `id` via bracket
|
|
1080
1092
|
// access — no `as any` sprayed per field.
|
|
1093
|
+
for (const row of rawRows)
|
|
1094
|
+
assertQueryIdentity(row, 'id', 0);
|
|
1081
1095
|
const rows = rawRows.slice(0, stepLimit);
|
|
1082
1096
|
depthReached = depth + 1;
|
|
1083
1097
|
if (rawRows.length > stepLimit)
|
|
@@ -1134,6 +1148,10 @@ async function calleeIdsByBlock(lbugPath, blockIds, exec) {
|
|
|
1134
1148
|
// Narrow the awaited rows ONCE at the boundary to a typed record shape; read
|
|
1135
1149
|
// the aliased cells via bracket access — no per-field `as any`.
|
|
1136
1150
|
for (const r of rows) {
|
|
1151
|
+
assertQueryIdentity(r, 'id', 0, [
|
|
1152
|
+
['calleeIds', 1],
|
|
1153
|
+
['callees', 2],
|
|
1154
|
+
]);
|
|
1137
1155
|
const blockId = String(r['id'] ?? '');
|
|
1138
1156
|
if (!blockId)
|
|
1139
1157
|
continue;
|
|
@@ -1184,6 +1202,7 @@ async function calleesWithReturnFlow(lbugPath, calleeIds, exec) {
|
|
|
1184
1202
|
WHERE r.type = 'CALL_SUMMARY' AND c.id IN $ids
|
|
1185
1203
|
RETURN c.id AS id, r.reason AS reason`, { ids: calleeIds });
|
|
1186
1204
|
for (const r of rows) {
|
|
1205
|
+
assertQueryIdentity(r, 'id', 0);
|
|
1187
1206
|
const id = String(r['id'] ?? '');
|
|
1188
1207
|
if (!id)
|
|
1189
1208
|
continue;
|
|
@@ -1232,6 +1251,7 @@ async function resolveCalleeSpans(lbugPath, calleeIds, exec) {
|
|
|
1232
1251
|
// the aliased columns via bracket access with Number()/String() coercion —
|
|
1233
1252
|
// no per-field `as any` (the same boundary-narrowing the typed helpers use).
|
|
1234
1253
|
for (const r of rows) {
|
|
1254
|
+
assertQueryIdentity(r, 'id', 0, [['filePath', 1]]);
|
|
1235
1255
|
const id = String(r['id'] ?? '');
|
|
1236
1256
|
const filePath = String(r['filePath'] ?? '');
|
|
1237
1257
|
const startLine = Number(r['startLine']);
|
|
@@ -1408,6 +1428,8 @@ async function interproceduralDescent(input) {
|
|
|
1408
1428
|
const spanSeeds = await Promise.all(spans.map(async (span) => {
|
|
1409
1429
|
const { anchorClause, queryParams } = blockAnchorForResolvedSymbol(span);
|
|
1410
1430
|
const rawSeedRows = await exec(lbugPath, seedBlockQuery(anchorClause, probeLimit), queryParams);
|
|
1431
|
+
for (const row of rawSeedRows)
|
|
1432
|
+
assertQueryIdentity(row, 'id', 0);
|
|
1411
1433
|
const exceeded = rawSeedRows.length > stepLimit;
|
|
1412
1434
|
const seeds = rawSeedRows
|
|
1413
1435
|
.slice(0, stepLimit)
|
|
@@ -1559,6 +1581,8 @@ export async function runImpactPDG(deps) {
|
|
|
1559
1581
|
: blockAnchorForResolvedSymbol(sym);
|
|
1560
1582
|
const probeLimit = stepLimit + 1;
|
|
1561
1583
|
const rawSeedRows = await exec(repo.lbugPath, seedBlockQuery(anchorClause, probeLimit), queryParams);
|
|
1584
|
+
for (const row of rawSeedRows)
|
|
1585
|
+
assertQueryIdentity(row, 'id', 0);
|
|
1562
1586
|
const seedRows = rawSeedRows.slice(0, stepLimit);
|
|
1563
1587
|
let seedBlocks = seedRows
|
|
1564
1588
|
.map((r) => String(r['id'] ?? ''))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared integrity boundary for identities returned by impact/context queries. */
|
|
2
|
+
export declare const SYMBOL_IDENTITY_RECOVERY_SUGGESTION = "Run gitnexus analyze --force from the affected repository root to rebuild the index.";
|
|
3
|
+
export declare class SymbolIdentityError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/** Validate database identities before using them as graph traversal anchors. */
|
|
7
|
+
export declare function assertSymbolIdentity(id: unknown, expectedUid?: string): asserts id is string;
|
|
8
|
+
/** Read either native row shape without turning an absent row into a TypeError. */
|
|
9
|
+
export declare function queryRowValue(row: unknown, key: string, index: number): unknown;
|
|
10
|
+
/** Optional labels/paths may be empty or NULL; NUL is never a usable identity. */
|
|
11
|
+
export declare function assertIdentityFields(...values: unknown[]): void;
|
|
12
|
+
export declare function assertQueryIdentity(row: unknown, idKey: string, idIndex: number, fields?: ReadonlyArray<readonly [string, number]>): void;
|
|
13
|
+
/** Ordinary query failures may degrade; corrupt identities must reach the outer error envelope. */
|
|
14
|
+
export declare function rethrowSymbolIdentityError(error: unknown): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Shared integrity boundary for identities returned by impact/context queries. */
|
|
2
|
+
export const SYMBOL_IDENTITY_RECOVERY_SUGGESTION = 'Run gitnexus analyze --force from the affected repository root to rebuild the index.';
|
|
3
|
+
export class SymbolIdentityError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super('The index returned an invalid symbol identity. ' + SYMBOL_IDENTITY_RECOVERY_SUGGESTION);
|
|
6
|
+
this.name = 'SymbolIdentityError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Validate database identities before using them as graph traversal anchors. */
|
|
10
|
+
export function assertSymbolIdentity(id, expectedUid) {
|
|
11
|
+
if (typeof id !== 'string' ||
|
|
12
|
+
!id.trim() ||
|
|
13
|
+
id.includes('\0') ||
|
|
14
|
+
(expectedUid !== undefined && id !== expectedUid)) {
|
|
15
|
+
throw new SymbolIdentityError();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Read either native row shape without turning an absent row into a TypeError. */
|
|
19
|
+
export function queryRowValue(row, key, index) {
|
|
20
|
+
if (typeof row !== 'object' || row === null)
|
|
21
|
+
return undefined;
|
|
22
|
+
const value = row;
|
|
23
|
+
return value[key] ?? value[index];
|
|
24
|
+
}
|
|
25
|
+
/** Optional labels/paths may be empty or NULL; NUL is never a usable identity. */
|
|
26
|
+
export function assertIdentityFields(...values) {
|
|
27
|
+
for (const value of values) {
|
|
28
|
+
if (value !== null &&
|
|
29
|
+
value !== undefined &&
|
|
30
|
+
(typeof value !== 'string' || value.includes('\0'))) {
|
|
31
|
+
throw new SymbolIdentityError();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function assertQueryIdentity(row, idKey, idIndex, fields = []) {
|
|
36
|
+
assertSymbolIdentity(queryRowValue(row, idKey, idIndex));
|
|
37
|
+
for (const [key, index] of fields)
|
|
38
|
+
assertIdentityFields(queryRowValue(row, key, index));
|
|
39
|
+
}
|
|
40
|
+
/** Ordinary query failures may degrade; corrupt identities must reach the outer error envelope. */
|
|
41
|
+
export function rethrowSymbolIdentityError(error) {
|
|
42
|
+
if (error instanceof SymbolIdentityError)
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
package/package.json
CHANGED