gitnexus 1.6.11-rc.21 → 1.6.11-rc.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +24 -21
  2. package/dist/_shared/graph/types.d.ts +6 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/cli/ai-context.d.ts +4 -0
  5. package/dist/cli/ai-context.js +5 -2
  6. package/dist/cli/analyze-config.js +14 -0
  7. package/dist/cli/analyze-options.d.ts +5 -0
  8. package/dist/cli/analyze.js +2 -0
  9. package/dist/cli/index.js +2 -0
  10. package/dist/cli/watch.js +2 -0
  11. package/dist/core/group/cross-impact.d.ts +3 -0
  12. package/dist/core/group/cross-impact.js +26 -4
  13. package/dist/core/group/extractors/http-patterns/java.js +18 -4
  14. package/dist/core/group/extractors/http-patterns/kotlin.js +2 -0
  15. package/dist/core/group/normalization.d.ts +38 -1
  16. package/dist/core/group/normalization.js +59 -3
  17. package/dist/core/group/service.d.ts +11 -0
  18. package/dist/core/group/service.js +8 -0
  19. package/dist/core/group/sync.d.ts +21 -0
  20. package/dist/core/group/sync.js +15 -3
  21. package/dist/core/group/types.d.ts +13 -0
  22. package/dist/core/ingestion/frameworks/spring/actuator-runtime.d.ts +26 -0
  23. package/dist/core/ingestion/frameworks/spring/actuator-runtime.js +843 -0
  24. package/dist/core/ingestion/language-provider.d.ts +38 -1
  25. package/dist/core/ingestion/language-provider.js +22 -0
  26. package/dist/core/ingestion/languages/java/spring-actuator.d.ts +2 -0
  27. package/dist/core/ingestion/languages/java/spring-actuator.js +62 -0
  28. package/dist/core/ingestion/languages/java.js +8 -7
  29. package/dist/core/ingestion/languages/kotlin/spring-actuator.d.ts +4 -0
  30. package/dist/core/ingestion/languages/kotlin/spring-actuator.js +172 -0
  31. package/dist/core/ingestion/languages/kotlin.js +3 -0
  32. package/dist/core/ingestion/pipeline-phases/parse-impl.js +6 -1
  33. package/dist/core/ingestion/pipeline-phases/processes.d.ts +2 -1
  34. package/dist/core/ingestion/pipeline-phases/processes.js +39 -4
  35. package/dist/core/ingestion/pipeline-phases/scan.js +82 -1
  36. package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.d.ts +2 -0
  37. package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.js +8 -0
  38. package/dist/core/ingestion/pipeline.d.ts +8 -0
  39. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +16 -0
  40. package/dist/core/ingestion/route-extractors/constant-resolver.js +10 -0
  41. package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +27 -1
  42. package/dist/core/ingestion/route-extractors/java-const-resolver.js +161 -13
  43. package/dist/core/ingestion/route-extractors/kotlin-const-resolver.d.ts +7 -8
  44. package/dist/core/ingestion/route-extractors/kotlin-const-resolver.js +209 -23
  45. package/dist/core/ingestion/workers/parse-worker.js +5 -1
  46. package/dist/core/lbug/csv-generator.js +4 -1
  47. package/dist/core/lbug/graph-emit-sink.d.ts +3 -2
  48. package/dist/core/lbug/graph-emit-sink.js +4 -2
  49. package/dist/core/lbug/lbug-adapter.js +1 -1
  50. package/dist/core/lbug/schema.d.ts +1 -1
  51. package/dist/core/lbug/schema.js +3 -0
  52. package/dist/core/run-analyze.d.ts +5 -0
  53. package/dist/core/run-analyze.js +56 -0
  54. package/dist/mcp/local/local-backend.js +46 -3
  55. package/dist/mcp/resources.js +1 -0
  56. package/dist/mcp/tools.js +4 -4
  57. package/dist/server/analyze-launch.d.ts +1 -0
  58. package/dist/server/analyze-launch.js +1 -0
  59. package/dist/server/api.d.ts +4 -0
  60. package/dist/server/api.js +46 -4
  61. package/dist/storage/parse-cache.js +8 -1
  62. package/dist/storage/repo-meta.d.ts +12 -0
  63. package/package.json +1 -1
  64. package/skills/gitnexus-cli.md +3 -0
@@ -694,6 +694,18 @@ export function buildDetectChangesDiffArgs(scope, baseRef) {
694
694
  * (#2054).
695
695
  */
696
696
  export const REPO_ID_HASH_LENGTH = 6;
697
+ const routeRuntimeEvidence = (route) => ({
698
+ confirmed: route.runtimeConfirmed,
699
+ ...(route.runtimeSource === null ? {} : { source: route.runtimeSource }),
700
+ ...(route.runtimeStatus === null ? {} : { status: route.runtimeStatus }),
701
+ });
702
+ const isMissingRouteRuntimePropertyError = (error) => {
703
+ const message = error instanceof Error ? error.message : String(error);
704
+ return (['runtimeConfirmed', 'runtimeSource', 'runtimeStatus'].some((property) => message.includes(property)) &&
705
+ (/cannot find property/i.test(message) ||
706
+ /property .* does not exist/i.test(message) ||
707
+ /property .* not found/i.test(message)));
708
+ };
697
709
  /**
698
710
  * Validate and normalise `list_repos` pagination arguments.
699
711
  *
@@ -6783,6 +6795,17 @@ export class LocalBackend {
6783
6795
  target: params.target,
6784
6796
  direction: params.direction,
6785
6797
  };
6798
+ // Forward the target-selector params like the trace branch above.
6799
+ // @group impact used to drop target_uid/file_path/kind here, so a name
6800
+ // shared by same-named Api/Impl/Controller layers resolved ambiguously
6801
+ // in the member repo and the documented "re-call with target_uid"
6802
+ // disambiguation loop (impact tool schema) never worked in group mode.
6803
+ if (typeof params.target_uid === 'string')
6804
+ impactArgs.target_uid = params.target_uid;
6805
+ if (typeof params.file_path === 'string')
6806
+ impactArgs.file_path = params.file_path;
6807
+ if (typeof params.kind === 'string')
6808
+ impactArgs.kind = params.kind;
6786
6809
  if (params.maxDepth !== undefined)
6787
6810
  impactArgs.maxDepth = params.maxDepth;
6788
6811
  if (params.crossDepth !== undefined)
@@ -6909,7 +6932,7 @@ export class LocalBackend {
6909
6932
  * Shared by routeMap and shapeCheck to avoid N+1 query patterns.
6910
6933
  */
6911
6934
  async fetchRoutesWithConsumers(repoId, routeFilter, params) {
6912
- const rows = await executeParameterized(repoId, `
6935
+ const routeQuery = (includeRuntimeEvidence) => `
6913
6936
  MATCH (n:Route)
6914
6937
  WHERE n.id STARTS WITH 'Route:' ${routeFilter}
6915
6938
  OPTIONAL MATCH (consumer)-[r:CodeRelation]->(n)
@@ -6917,8 +6940,19 @@ export class LocalBackend {
6917
6940
  RETURN n.id AS routeId, n.name AS routeName, n.filePath AS handlerFile,
6918
6941
  n.responseKeys AS responseKeys, n.errorKeys AS errorKeys, n.middleware AS middleware,
6919
6942
  consumer.name AS consumerName, consumer.filePath AS consumerFile,
6920
- r.reason AS fetchReason, n.method AS method
6921
- `, params);
6943
+ r.reason AS fetchReason, n.method AS method${includeRuntimeEvidence
6944
+ ? ', n.runtimeConfirmed AS runtimeConfirmed, n.runtimeSource AS runtimeSource, n.runtimeStatus AS runtimeStatus'
6945
+ : ''}
6946
+ `;
6947
+ let rows;
6948
+ try {
6949
+ rows = await executeParameterized(repoId, routeQuery(true), params);
6950
+ }
6951
+ catch (error) {
6952
+ if (!isMissingRouteRuntimePropertyError(error))
6953
+ throw error;
6954
+ rows = await executeParameterized(repoId, routeQuery(false), params);
6955
+ }
6922
6956
  // Strip wrapping quotes from DB array elements — CSV COPY stores ['key'] which
6923
6957
  // LadybugDB may return as "'key'" rather than "key"
6924
6958
  const stripQuotes = (keys) => keys ? keys.map((k) => k.replace(/^['"]|['"]$/g, '')) : null;
@@ -6938,6 +6972,9 @@ export class LocalBackend {
6938
6972
  // resource). Appended last in RETURN so positional fallbacks for the
6939
6973
  // consumer/reason columns above stay stable.
6940
6974
  const method = row.method ?? row[9] ?? null;
6975
+ const runtimeConfirmed = (row.runtimeConfirmed ?? row[10]) === true;
6976
+ const runtimeSource = row.runtimeSource ?? row[11] ?? null;
6977
+ const runtimeStatus = row.runtimeStatus ?? row[12] ?? null;
6941
6978
  if (!routeMap.has(id)) {
6942
6979
  routeMap.set(id, {
6943
6980
  id,
@@ -6947,6 +6984,9 @@ export class LocalBackend {
6947
6984
  responseKeys,
6948
6985
  errorKeys,
6949
6986
  middleware,
6987
+ runtimeConfirmed,
6988
+ runtimeSource,
6989
+ runtimeStatus,
6950
6990
  consumers: [],
6951
6991
  });
6952
6992
  }
@@ -7028,6 +7068,7 @@ export class LocalBackend {
7028
7068
  route: r.name,
7029
7069
  method: r.method,
7030
7070
  handler: r.filePath,
7071
+ runtimeEvidence: routeRuntimeEvidence(r),
7031
7072
  middleware: r.middleware || [],
7032
7073
  consumers: r.consumers,
7033
7074
  flows: flowMap.get(r.id) || [],
@@ -7083,6 +7124,7 @@ export class LocalBackend {
7083
7124
  route: r.name,
7084
7125
  method: r.method,
7085
7126
  handler: r.filePath,
7127
+ runtimeEvidence: routeRuntimeEvidence(r),
7086
7128
  ...(responseKeys.length > 0 ? { responseKeys } : {}),
7087
7129
  ...(errorKeys.length > 0 ? { errorKeys } : {}),
7088
7130
  consumers,
@@ -7246,6 +7288,7 @@ export class LocalBackend {
7246
7288
  route: r.name,
7247
7289
  method: r.method,
7248
7290
  handler: r.filePath,
7291
+ runtimeEvidence: routeRuntimeEvidence(r),
7249
7292
  responseShape: {
7250
7293
  success: responseKeys,
7251
7294
  error: errorKeys,
@@ -298,6 +298,7 @@ async function getContextResource(backend, repoName) {
298
298
  lines.push(` indexed_at: ${JSON.stringify(freshMeta?.indexedAt ?? null)}`);
299
299
  lines.push(` runner_identity: ${JSON.stringify(freshMeta?.runnerIdentity ?? null)}`);
300
300
  lines.push(` incomplete_reasons: ${JSON.stringify(incompleteReasons)}`);
301
+ lines.push(` spring_actuator: ${JSON.stringify(freshMeta?.springActuator ?? null)}`);
301
302
  const indexedRunnerSchema = freshMeta?.runnerIdentity
302
303
  ?.schemaVersion;
303
304
  lines.push(` runner_identity_schema_status: ${JSON.stringify(indexedRunnerSchema === ANALYZER_RUNNER_IDENTITY_SCHEMA_VERSION
package/dist/mcp/tools.js CHANGED
@@ -696,7 +696,7 @@ CONTRACT CAVEATS:
696
696
  WHEN TO USE: Understanding API consumption patterns, finding orphaned routes. For pre-change analysis, prefer \`api_impact\` which combines this data with mismatch detection and risk assessment.
697
697
  AFTER THIS: Use impact() on specific route handlers to see full blast radius.
698
698
 
699
- Returns: route nodes with their handlers, middleware wrapper chains (e.g., withAuth, withRateLimit), and consumers. Each route object includes its "method" (the HTTP verb, "*" for method-agnostic routes, or null for method-less routes).`,
699
+ Returns: route nodes with their handlers, middleware wrapper chains (e.g., withAuth, withRateLimit), and consumers. Each route object includes its "method" (the HTTP verb, "*" for method-agnostic routes, or null for method-less routes) and "runtimeEvidence". Runtime evidence is authoritative only when runtimeEvidence.confirmed is true; source records provenance, including conflicts.`,
700
700
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
701
701
  inputSchema: {
702
702
  type: 'object',
@@ -740,7 +740,7 @@ Returns: tool nodes with their handler files and descriptions.`,
740
740
  WHEN TO USE: Detecting mismatches between what an API route returns and what consumers expect. Finding shape drift. For pre-change analysis, prefer \`api_impact\` which combines this data with mismatch detection and risk assessment.
741
741
  REQUIRES: Route nodes with responseKeys (extracted from .json({...}) calls during indexing).
742
742
 
743
- Returns routes that have both detected response keys AND consumers. Shows top-level keys each endpoint returns (e.g., data, pagination, error) and what keys each consumer accesses. Reports MISMATCH status when a consumer accesses keys not present in the route's response shape. Each route object includes its "method" (the HTTP verb, "*" for method-agnostic routes, or null for method-less routes).`,
743
+ Returns routes that have both detected response keys AND consumers. Shows top-level keys each endpoint returns (e.g., data, pagination, error) and what keys each consumer accesses. Reports MISMATCH status when a consumer accesses keys not present in the route's response shape. Each route object includes its "method" (the HTTP verb, "*" for method-agnostic routes, or null for method-less routes) and "runtimeEvidence". Runtime evidence is authoritative only when runtimeEvidence.confirmed is true.`,
744
744
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
745
745
  inputSchema: {
746
746
  type: 'object',
@@ -765,7 +765,7 @@ WHEN TO USE: BEFORE modifying any API route handler. Shows what consumers depend
765
765
 
766
766
  Risk levels: LOW (0-3 consumers), MEDIUM (4-9 or any mismatches), HIGH (10+ consumers or mismatches with 4+ consumers). Mismatches with confidence "low" indicate the consumer file fetches multiple routes — property attribution is approximate.
767
767
 
768
- Response shape is keyed on how many routes match, not on the data: exactly one match returns a single route object; two or more return { routes: [...], total: N }. The same URL can expose multiple HTTP verbs (e.g. GET and POST /api/orders are distinct routes that share the URL), so a bare-URL lookup may return the wrapped form — every route object carries its own "method" so verbs are distinguishable. Pass "method" to narrow to one verb; the single-object shape is returned only when exactly one route remains after filtering — a substring route/file match spanning several URLs can still return the wrapped form. A URL/file that exists but has no route for the given verb returns an error. Each route's "method" is the literal "*" for method-agnostic routes (e.g. Django function views), which match any "method" selector, or null for method-less routes (filesystem, Laravel resource), which never match a selector. Combines route_map, shape_check, and impact data.`,
768
+ Response shape is keyed on how many routes match, not on the data: exactly one match returns a single route object; two or more return { routes: [...], total: N }. The same URL can expose multiple HTTP verbs (e.g. GET and POST /api/orders are distinct routes that share the URL), so a bare-URL lookup may return the wrapped form — every route object carries its own "method" so verbs are distinguishable. Pass "method" to narrow to one verb; the single-object shape is returned only when exactly one route remains after filtering — a substring route/file match spanning several URLs can still return the wrapped form. A URL/file that exists but has no route for the given verb returns an error. Each route's "method" is the literal "*" for method-agnostic routes (e.g. Django function views), which match any "method" selector, or null for method-less routes (filesystem, Laravel resource), which never match a selector. Every route also carries "runtimeEvidence"; treat it as authoritative only when runtimeEvidence.confirmed is true. Combines route_map, shape_check, and impact data.`,
769
769
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
770
770
  inputSchema: {
771
771
  type: 'object',
@@ -804,7 +804,7 @@ WHEN TO USE: Discover groups before group_sync. Optional "name" returns a single
804
804
 
805
805
  WHEN TO USE: After changing group.yaml or re-indexing member repos.
806
806
 
807
- READ THE RESULT: \`missingRepos\` are configured repos with no entry in the registry (index them, or drop them from group.yaml); \`unreadableRepos\` ARE registered but this sync could not extract from them — the index would not open (version skew, lock, corruption), or an extractor failed partway — so NONE of their contracts are in this sync and a following group_impact / group_contracts is a lower bound, not a verdict. \`registryOutcome\` says what happened to the file, and the three values a call here can return each need a different response: 'written' — this run's contracts replaced contracts.json; 'preserved' — nothing could be read, so contracts.json was rewritten keeping the previous sync's contracts and cross-links verbatim and refreshing only \`missingRepos\`/\`unreadableRepos\` to describe THIS run (the file changed, the contracts in it did not, and they are as old as the last sync that succeeded); 'superseded' — nothing could be read, and another sync replaced contracts.json while this one waited for the group lock; that file was left untouched and this run's lists were NOT recorded, because they describe an older group state than what is on disk (so the registry is fresher than this response's diagnostics, not staler); 'no-prior-registry' — nothing could be read AND there was no previous contracts.json to carry forward, so none was written and this group has no contract registry on disk. Only 'no-prior-registry' means there is nothing to read: after it, group_contracts / group_impact have no registry at all rather than a stale one, so fix the repos above and re-run before trusting either. \`suppressedMatchStages\` names matching stages this sync was ASKED to skip, with the same three states as the repo lists: ABSENT means a registry written before the field existed, \`[]\` means this sync suppressed nothing, and a populated list means the cross-link set is a lower bound BY REQUEST — a later group_impact / group_contracts on it reports truncationReason 'suppressed-stage'.\n\nPARAMETERS ARE VALIDATED: \`exactOnly\` must be a real boolean — the string "false" is rejected, not coerced to true. The retired \`skipEmbeddings\` and \`allowStale\` parameters are refused by name; drop them from the call.`,
807
+ READ THE RESULT: \`missingRepos\` are configured repos with no entry in the registry (index them, or drop them from group.yaml); \`unreadableRepos\` ARE registered but this sync could not extract from them — the index would not open (version skew, lock, corruption), or an extractor failed partway — so NONE of their contracts are in this sync and a following group_impact / group_contracts is a lower bound, not a verdict. \`degradedLinks\` is the count of persisted cross-links whose provider endpoint has no resolved graph symbol (\`degraded: true\`); re-analyze the provider so handlers resolve. \`failedRepos\` is \`{ repo, reason }[]\` for per-repo extraction throws — each also appears in \`unreadableRepos\`; \`repo\` is that group path (e.g. app/backend), not the registry display name. \`warnings\` are operator-facing run notes (e.g. bridge.lbug write failed after contracts.json was written); \`[]\` means none this run. \`registryOutcome\` says what happened to the file, and the three values a call here can return each need a different response: 'written' — this run's contracts replaced contracts.json; 'preserved' — nothing could be read, so contracts.json was rewritten keeping the previous sync's contracts and cross-links verbatim and refreshing only \`missingRepos\`/\`unreadableRepos\` to describe THIS run (the file changed, the contracts in it did not, and they are as old as the last sync that succeeded); 'superseded' — nothing could be read, and another sync replaced contracts.json while this one waited for the group lock; that file was left untouched and this run's lists were NOT recorded, because they describe an older group state than what is on disk (so the registry is fresher than this response's diagnostics, not staler); 'no-prior-registry' — nothing could be read AND there was no previous contracts.json to carry forward, so none was written and this group has no contract registry on disk. Only 'no-prior-registry' means there is nothing to read: after it, group_contracts / group_impact have no registry at all rather than a stale one, so fix the repos above and re-run before trusting either. \`suppressedMatchStages\` names matching stages this sync was ASKED to skip, with the same three states as the repo lists: ABSENT means a registry written before the field existed, \`[]\` means this sync suppressed nothing, and a populated list means the cross-link set is a lower bound BY REQUEST — a later group_impact / group_contracts on it reports truncationReason 'suppressed-stage'.\n\nPARAMETERS ARE VALIDATED: \`exactOnly\` must be a real boolean — the string "false" is rejected, not coerced to true. The retired \`skipEmbeddings\` and \`allowStale\` parameters are refused by name; drop them from the call.`,
808
808
  // Usually writes contracts.json, so conservatively non-idempotent even
809
809
  // though output is deterministic for identical input. When no configured
810
810
  // repo could be read it still rewrites the file, keeping the previous
@@ -28,6 +28,7 @@ export interface LaunchOptions {
28
28
  force?: boolean;
29
29
  embeddings?: boolean;
30
30
  dropEmbeddings?: boolean;
31
+ springActuatorPath?: string;
31
32
  registryName?: string;
32
33
  }
33
34
  export declare function createLaunchAnalysisWorker(deps: LaunchDeps): (job: {
@@ -280,6 +280,7 @@ export function createLaunchAnalysisWorker(deps) {
280
280
  force: !!opts.force,
281
281
  embeddings: !!opts.embeddings,
282
282
  dropEmbeddings: !!opts.dropEmbeddings,
283
+ ...(opts.springActuatorPath ? { springActuatorPath: opts.springActuatorPath } : {}),
283
284
  ...(opts.registryName ? { registryName: opts.registryName } : {}),
284
285
  },
285
286
  });
@@ -50,6 +50,10 @@ export declare const landingPageHtml: () => string;
50
50
  export declare const staticCacheControlSetHeaders: (res: express.Response, filePath: string) => void;
51
51
  export declare const registerWebUI: (app: express.Express, staticDir: string | null) => void;
52
52
  export declare const writeNdjsonRecord: (res: express.Response, record: GraphStreamRecord, signal?: AbortSignal) => Promise<void>;
53
+ export declare const buildGraph: (includeContent?: boolean) => Promise<{
54
+ nodes: GraphNode[];
55
+ relationships: GraphRelationship[];
56
+ }>;
53
57
  export declare const getNodeQuery: (table: string, includeContent: boolean) => string;
54
58
  export declare const streamGraphNdjson: (res: express.Response, includeContent?: boolean, signal?: AbortSignal) => Promise<void>;
55
59
  /**
@@ -266,7 +266,18 @@ export const writeNdjsonRecord = async (res, record, signal) => {
266
266
  throw err;
267
267
  }
268
268
  };
269
- const buildGraph = async (includeContent = false) => {
269
+ const ROUTE_NODE_CORE_PROJECTION = 'n.id AS id, n.name AS name, n.filePath AS filePath, ' +
270
+ 'n.responseKeys AS responseKeys, n.errorKeys AS errorKeys, n.middleware AS middleware';
271
+ const LEGACY_ROUTE_NODE_QUERY = `MATCH (n:\`Route\`) RETURN ${ROUTE_NODE_CORE_PROJECTION}`;
272
+ const isMissingRouteRuntimePropertyError = (err) => {
273
+ const message = err instanceof Error ? err.message : String(err);
274
+ const mentionsRuntimeProperty = ['runtimeConfirmed', 'runtimeSource', 'runtimeStatus'].some((property) => message.includes(property));
275
+ return (mentionsRuntimeProperty &&
276
+ (/cannot find property/i.test(message) ||
277
+ /property .* does not exist/i.test(message) ||
278
+ /property .* not found/i.test(message)));
279
+ };
280
+ export const buildGraph = async (includeContent = false) => {
270
281
  const nodes = [];
271
282
  for (const table of NODE_TABLES) {
272
283
  try {
@@ -276,6 +287,13 @@ const buildGraph = async (includeContent = false) => {
276
287
  }
277
288
  }
278
289
  catch (err) {
290
+ if (table === 'Route' && isMissingRouteRuntimePropertyError(err)) {
291
+ const rows = await executeQuery(LEGACY_ROUTE_NODE_QUERY);
292
+ for (const row of rows) {
293
+ nodes.push(mapGraphNodeRow(table, row, includeContent));
294
+ }
295
+ continue;
296
+ }
279
297
  if (!isIgnorableGraphQueryError(err)) {
280
298
  throw err;
281
299
  }
@@ -316,7 +334,7 @@ export const getNodeQuery = (table, includeContent) => {
316
334
  return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
317
335
  }
318
336
  if (table === 'Route') {
319
- return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.responseKeys AS responseKeys, n.errorKeys AS errorKeys, n.middleware AS middleware`;
337
+ return `MATCH (n:${tableLabel}) RETURN ${ROUTE_NODE_CORE_PROJECTION}, n.runtimeConfirmed AS runtimeConfirmed, n.runtimeSource AS runtimeSource, n.runtimeStatus AS runtimeStatus`;
320
338
  }
321
339
  if (table === 'Tool') {
322
340
  return `MATCH (n:${tableLabel}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.description AS description`;
@@ -344,6 +362,11 @@ const mapGraphNodeRow = (table, row, includeContent) => ({
344
362
  responseKeys: row.responseKeys,
345
363
  errorKeys: row.errorKeys,
346
364
  middleware: row.middleware,
365
+ // Normalize legacy Route projections to the modern contract. Source is
366
+ // provenance; only runtimeConfirmed === true is authoritative.
367
+ runtimeConfirmed: table === 'Route' ? (row.runtimeConfirmed ?? false) : undefined,
368
+ runtimeSource: table === 'Route' ? row.runtimeSource : undefined,
369
+ runtimeStatus: table === 'Route' ? row.runtimeStatus : undefined,
347
370
  heuristicLabel: row.heuristicLabel,
348
371
  cohesion: row.cohesion,
349
372
  symbolCount: row.symbolCount,
@@ -375,6 +398,15 @@ export const streamGraphNdjson = async (res, includeContent = false, signal) =>
375
398
  });
376
399
  }
377
400
  catch (err) {
401
+ if (table === 'Route' && isMissingRouteRuntimePropertyError(err)) {
402
+ await streamQuery(LEGACY_ROUTE_NODE_QUERY, async (row) => {
403
+ await writeNdjsonRecord(res, {
404
+ type: 'node',
405
+ data: mapGraphNodeRow(table, row, includeContent),
406
+ }, signal);
407
+ });
408
+ continue;
409
+ }
378
410
  if (!isIgnorableGraphQueryError(err)) {
379
411
  throw err;
380
412
  }
@@ -1225,7 +1257,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1225
1257
  // POST /api/analyze — start a new analysis job
1226
1258
  app.post('/api/analyze', createRouteLimiter({ limit: 10 }), requireTrustedOrigin, async (req, res) => {
1227
1259
  try {
1228
- const { url: repoUrl, path: repoLocalPath, force, embeddings, dropEmbeddings, token: repoToken, } = req.body;
1260
+ const { url: repoUrl, path: repoLocalPath, force, embeddings, dropEmbeddings, springActuatorPath, token: repoToken, } = req.body;
1229
1261
  // Input type validation
1230
1262
  if (repoUrl !== undefined && typeof repoUrl !== 'string') {
1231
1263
  res.status(400).json({ error: '"url" must be a string' });
@@ -1235,6 +1267,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1235
1267
  res.status(400).json({ error: '"path" must be a string' });
1236
1268
  return;
1237
1269
  }
1270
+ if (springActuatorPath !== undefined &&
1271
+ (typeof springActuatorPath !== 'string' || springActuatorPath.trim().length === 0)) {
1272
+ res.status(400).json({ error: '"springActuatorPath" must be a non-empty string' });
1273
+ return;
1274
+ }
1238
1275
  if (!repoUrl && !repoLocalPath) {
1239
1276
  res.status(400).json({ error: 'Provide "url" (git URL) or "path" (local path)' });
1240
1277
  return;
@@ -1303,7 +1340,12 @@ export const createServer = async (port, host = '127.0.0.1') => {
1303
1340
  if (!targetPath) {
1304
1341
  throw new Error('No target path resolved');
1305
1342
  }
1306
- launchAnalysisWorker(job, targetPath, { force, embeddings, dropEmbeddings });
1343
+ launchAnalysisWorker(job, targetPath, {
1344
+ force,
1345
+ embeddings,
1346
+ dropEmbeddings,
1347
+ springActuatorPath,
1348
+ });
1307
1349
  }
1308
1350
  catch (err) {
1309
1351
  if (targetPath)
@@ -680,7 +680,14 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
680
680
  // cache can replay stale names and declaration metadata.
681
681
  // 85 -> 86: Kotlin interface property accessors now record isAbstract on the
682
682
  // synthetic Method. A warm v85 cache replays them as concrete.
683
- const SCHEMA_BUMP = 86;
683
+ // 86 -> 87: ModuleConstants gained wildcardImports (static-import-asterisk
684
+ // materialization) — a warm v86 cache has no wildcard bindings, so folding
685
+ // would skip them and drop wildcard-imported route constants. origin/main at
686
+ // allocation is 86 (#2885).
687
+ // 87 -> 88: Java ModuleConstants now preserves unfoldable declaration names
688
+ // across worker/cache replay so wildcard expansion cannot resurrect an imported
689
+ // member hidden by a local field. A warm v87 cache lacks that shadow metadata.
690
+ const SCHEMA_BUMP = 88;
684
691
  const GITNEXUS_PKG_VERSION = (() => {
685
692
  try {
686
693
  // package.json sits at gitnexus/package.json — two levels up from
@@ -78,6 +78,18 @@ export interface RepoMeta {
78
78
  repoPath: string;
79
79
  lastCommit: string;
80
80
  indexedAt: string;
81
+ /**
82
+ * Runtime enrichment mode plus redacted scan exclusions. Payload data and
83
+ * absolute/external paths are deliberately excluded from metadata.
84
+ */
85
+ springActuator?: {
86
+ enabled: boolean;
87
+ /**
88
+ * Normalized repo-relative inputs that must remain excluded from all future
89
+ * source scans. The list is empty when every configured input was external.
90
+ */
91
+ repoRelativeInputs: string[];
92
+ };
81
93
  /**
82
94
  * Analyzer/runtime receipt for the successful run represented by this
83
95
  * metadata. Optional so indexes written by older GitNexus releases remain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.21",
3
+ "version": "1.6.11-rc.23",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -27,9 +27,12 @@ Run from the project root. This parses all source files, builds the knowledge gr
27
27
  | `--embeddings` | Enable embedding generation for semantic search (off by default) |
28
28
  | `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. |
29
29
  | `--pdg` | Build the program-dependence layers used by `explain` and `pdg_query` (taint, CDG, and REACHING_DEF). |
30
+ | `--spring-actuator <path>` | Import opt-in Spring Boot Actuator mappings, beans, conditions, configprops, and env snapshots. Forces a full rebuild; unsupported with `--watch`. |
30
31
 
31
32
  **When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook detects staleness after `git commit` and `git merge` and notifies the agent to run `analyze` — the hook does not run analyze itself, to avoid blocking the agent for up to 120s and risking KuzuDB corruption on timeout.
32
33
 
34
+ For Spring runtime enrichment, pass a JSON bundle, one endpoint JSON file, or a directory containing endpoint files. Route evidence is authoritative only when `runtimeConfirmed === true`; `runtimeSource` records provenance and may also accompany `handler-conflict`. Env/configprops values are never persisted.
35
+
33
36
  Use `node .gitnexus/run.cjs analyze --watch` for a long-lived local Git repository. It performs an initial analysis, queues scanner-admitted file changes, and retries intact failed batches with bounded backoff. Watch refreshes update only the graph: they skip AGENTS.md / CLAUDE.md injection and standard skill installation, so run a one-shot `analyze` when those generated files need updating. Watch rejects one-shot or context-output flags including `--force`, embedding flags, `--skills`, `--default-branch`, `--skip-agents-md`, `--skip-skills`, `--no-stats`, `--self-commit`, `--index-only`, and `--skip-git`. It never pulls remotes. Running MCP and `serve` processes periodically check for a published replacement and reopen it without a restart. MCP checks are throttled to once every five seconds, so a tool call before the next check can briefly use the previous index.
34
37
 
35
38
  ### status — Check index freshness