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.
- package/README.md +24 -21
- package/dist/_shared/graph/types.d.ts +6 -0
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/cli/ai-context.d.ts +4 -0
- package/dist/cli/ai-context.js +5 -2
- package/dist/cli/analyze-config.js +14 -0
- package/dist/cli/analyze-options.d.ts +5 -0
- package/dist/cli/analyze.js +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/watch.js +2 -0
- package/dist/core/group/cross-impact.d.ts +3 -0
- package/dist/core/group/cross-impact.js +26 -4
- package/dist/core/group/extractors/http-patterns/java.js +18 -4
- package/dist/core/group/extractors/http-patterns/kotlin.js +2 -0
- package/dist/core/group/normalization.d.ts +38 -1
- package/dist/core/group/normalization.js +59 -3
- package/dist/core/group/service.d.ts +11 -0
- package/dist/core/group/service.js +8 -0
- package/dist/core/group/sync.d.ts +21 -0
- package/dist/core/group/sync.js +15 -3
- package/dist/core/group/types.d.ts +13 -0
- package/dist/core/ingestion/frameworks/spring/actuator-runtime.d.ts +26 -0
- package/dist/core/ingestion/frameworks/spring/actuator-runtime.js +843 -0
- package/dist/core/ingestion/language-provider.d.ts +38 -1
- package/dist/core/ingestion/language-provider.js +22 -0
- package/dist/core/ingestion/languages/java/spring-actuator.d.ts +2 -0
- package/dist/core/ingestion/languages/java/spring-actuator.js +62 -0
- package/dist/core/ingestion/languages/java.js +8 -7
- package/dist/core/ingestion/languages/kotlin/spring-actuator.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/spring-actuator.js +172 -0
- package/dist/core/ingestion/languages/kotlin.js +3 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +6 -1
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +2 -1
- package/dist/core/ingestion/pipeline-phases/processes.js +39 -4
- package/dist/core/ingestion/pipeline-phases/scan.js +82 -1
- package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.js +8 -0
- package/dist/core/ingestion/pipeline.d.ts +8 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +16 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +10 -0
- package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +27 -1
- package/dist/core/ingestion/route-extractors/java-const-resolver.js +161 -13
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.d.ts +7 -8
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.js +209 -23
- package/dist/core/ingestion/workers/parse-worker.js +5 -1
- package/dist/core/lbug/csv-generator.js +4 -1
- package/dist/core/lbug/graph-emit-sink.d.ts +3 -2
- package/dist/core/lbug/graph-emit-sink.js +4 -2
- package/dist/core/lbug/lbug-adapter.js +1 -1
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +3 -0
- package/dist/core/run-analyze.d.ts +5 -0
- package/dist/core/run-analyze.js +56 -0
- package/dist/mcp/local/local-backend.js +46 -3
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.js +4 -4
- package/dist/server/analyze-launch.d.ts +1 -0
- package/dist/server/analyze-launch.js +1 -0
- package/dist/server/api.d.ts +4 -0
- package/dist/server/api.js +46 -4
- package/dist/storage/parse-cache.js +8 -1
- package/dist/storage/repo-meta.d.ts +12 -0
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
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,
|
package/dist/mcp/resources.js
CHANGED
|
@@ -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
|
|
@@ -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
|
});
|
package/dist/server/api.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/server/api.js
CHANGED
|
@@ -266,7 +266,18 @@ export const writeNdjsonRecord = async (res, record, signal) => {
|
|
|
266
266
|
throw err;
|
|
267
267
|
}
|
|
268
268
|
};
|
|
269
|
-
const
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
package/skills/gitnexus-cli.md
CHANGED
|
@@ -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
|