gitnexus 1.6.11-rc.6 → 1.6.11-rc.8
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/_shared/impact-risk.d.ts +4 -2
- package/dist/_shared/impact-risk.d.ts.map +1 -1
- package/dist/_shared/impact-risk.js +35 -21
- package/dist/_shared/impact-risk.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/i18n/en.d.ts +8 -0
- package/dist/cli/i18n/en.js +8 -0
- package/dist/cli/i18n/resources.d.ts +16 -0
- package/dist/cli/i18n/zh-CN.d.ts +8 -0
- package/dist/cli/i18n/zh-CN.js +8 -0
- package/dist/cli/status.js +91 -6
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +23 -2
- package/dist/core/group/types.d.ts +12 -1
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/run-analyze.js +9 -1
- package/dist/mcp/local/local-backend.js +78 -46
- package/dist/mcp/tools.js +4 -2
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/repo-meta.d.ts +12 -0
- package/package.json +1 -1
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-BWAXPjC5.js} +106 -104
- package/web/assets/{index-2zdvEdzg.js → index-DDRie6_8.js} +3 -3
- package/web/index.html +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ImpactRisk = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | 'UNKNOWN';
|
|
2
2
|
export type ImpactRiskAxis = 'processes' | 'modules';
|
|
3
|
-
export type UnusedImpactRiskReason = 'file-nodes-have-no-process-or-community-membership' | 'enrichment-skipped' | 'enrichment-budget-exhausted' | 'enrichment-query-failed';
|
|
3
|
+
export type UnusedImpactRiskReason = 'file-nodes-have-no-process-or-community-membership' | 'enrichment-skipped' | 'enrichment-budget-exhausted' | 'enrichment-truncated' | 'enrichment-query-failed';
|
|
4
4
|
export interface UnusedImpactRiskAxis {
|
|
5
5
|
axis: ImpactRiskAxis;
|
|
6
6
|
reason: UnusedImpactRiskReason;
|
|
@@ -29,7 +29,9 @@ export declare function unusedAxesForImpactWalk(input: {
|
|
|
29
29
|
processQueryFailed: boolean;
|
|
30
30
|
moduleQueryFailed: boolean;
|
|
31
31
|
/** When 0, a zero chunk budget is not an unused-axis event — there was nothing to enrich. */
|
|
32
|
-
impactedCount
|
|
32
|
+
impactedCount: number;
|
|
33
|
+
/** True when process/module queries ran on a strict subset of impacted symbols. */
|
|
34
|
+
enrichmentTruncated?: boolean;
|
|
33
35
|
}): UnusedImpactRiskAxis[];
|
|
34
36
|
export declare function scoreImpactRisk(input: ImpactRiskInput): ImpactRiskResult;
|
|
35
37
|
//# sourceMappingURL=impact-risk.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impact-risk.d.ts","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;AAE5E,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,SAAS,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAC9B,oDAAoD,GACpD,oBAAoB,GACpB,6BAA6B,GAC7B,yBAAyB,CAAC;AAE9B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,GAAG,YAAY,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,cAAc,EAAE,UAAU,CAAC;IAC3B,SAAS,EAAE;QACT,qBAAqB,EAAE,OAAO,CAAC;QAC/B,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAC;KAC7C,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"impact-risk.d.ts","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;AAE5E,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,SAAS,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAC9B,oDAAoD,GACpD,oBAAoB,GACpB,6BAA6B,GAC7B,sBAAsB,GACtB,yBAAyB,CAAC;AAE9B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,GAAG,YAAY,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,cAAc,EAAE,UAAU,CAAC;IAC3B,SAAS,EAAE;QACT,qBAAqB,EAAE,OAAO,CAAC;QAC/B,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAC;KAC7C,CAAC;CACH;AAwDD,iGAAiG;AACjG,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,6FAA6F;IAC7F,aAAa,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,GAAG,oBAAoB,EAAE,CAqBzB;AAOD,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAuBxE"}
|
|
@@ -12,10 +12,23 @@ function score(input) {
|
|
|
12
12
|
return 'MEDIUM';
|
|
13
13
|
return 'LOW';
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
const UNMEASURED_REASONS = new Set([
|
|
16
|
+
'file-nodes-have-no-process-or-community-membership',
|
|
17
|
+
'enrichment-skipped',
|
|
18
|
+
'enrichment-budget-exhausted',
|
|
19
|
+
]);
|
|
20
|
+
function unusedPair(reason) {
|
|
21
|
+
return [
|
|
22
|
+
{ axis: 'processes', reason },
|
|
23
|
+
{ axis: 'modules', reason },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
function countsWithUnmeasuredAxesZeroed(input) {
|
|
16
27
|
let processCount = input.processCount;
|
|
17
28
|
let moduleCount = input.moduleCount;
|
|
18
29
|
for (const unused of input.unusedAxes ?? []) {
|
|
30
|
+
if (!UNMEASURED_REASONS.has(unused.reason))
|
|
31
|
+
continue;
|
|
19
32
|
if (unused.axis === 'processes')
|
|
20
33
|
processCount = 0;
|
|
21
34
|
if (unused.axis === 'modules')
|
|
@@ -32,30 +45,18 @@ function countsWithUnusedAxesZeroed(input) {
|
|
|
32
45
|
/** Map walk outcomes to unused process/module axes so comparability matches what was sampled. */
|
|
33
46
|
export function unusedAxesForImpactWalk(input) {
|
|
34
47
|
if (input.isFileTarget) {
|
|
35
|
-
return
|
|
36
|
-
{
|
|
37
|
-
axis: 'processes',
|
|
38
|
-
reason: 'file-nodes-have-no-process-or-community-membership',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
axis: 'modules',
|
|
42
|
-
reason: 'file-nodes-have-no-process-or-community-membership',
|
|
43
|
-
},
|
|
44
|
-
];
|
|
48
|
+
return unusedPair('file-nodes-have-no-process-or-community-membership');
|
|
45
49
|
}
|
|
46
50
|
if (input.skipEnrichment) {
|
|
47
|
-
return
|
|
48
|
-
{ axis: 'processes', reason: 'enrichment-skipped' },
|
|
49
|
-
{ axis: 'modules', reason: 'enrichment-skipped' },
|
|
50
|
-
];
|
|
51
|
+
return unusedPair('enrichment-skipped');
|
|
51
52
|
}
|
|
52
|
-
if (input.maxChunks === 0 &&
|
|
53
|
-
return
|
|
54
|
-
{ axis: 'processes', reason: 'enrichment-budget-exhausted' },
|
|
55
|
-
{ axis: 'modules', reason: 'enrichment-budget-exhausted' },
|
|
56
|
-
];
|
|
53
|
+
if (input.maxChunks === 0 && input.impactedCount > 0) {
|
|
54
|
+
return unusedPair('enrichment-budget-exhausted');
|
|
57
55
|
}
|
|
58
56
|
const unused = [];
|
|
57
|
+
if (input.enrichmentTruncated) {
|
|
58
|
+
unused.push(...unusedPair('enrichment-truncated'));
|
|
59
|
+
}
|
|
59
60
|
if (input.processQueryFailed) {
|
|
60
61
|
unused.push({ axis: 'processes', reason: 'enrichment-query-failed' });
|
|
61
62
|
}
|
|
@@ -64,10 +65,23 @@ export function unusedAxesForImpactWalk(input) {
|
|
|
64
65
|
}
|
|
65
66
|
return unused;
|
|
66
67
|
}
|
|
68
|
+
const INCOMPLETE_SAMPLE_REASONS = new Set([
|
|
69
|
+
'enrichment-query-failed',
|
|
70
|
+
'enrichment-truncated',
|
|
71
|
+
]);
|
|
67
72
|
export function scoreImpactRisk(input) {
|
|
68
73
|
const unusedAxes = input.unusedAxes ?? [];
|
|
74
|
+
const observedRisk = score(countsWithUnmeasuredAxesZeroed(input));
|
|
75
|
+
const incompleteSample = unusedAxes.some((unused) => INCOMPLETE_SAMPLE_REASONS.has(unused.reason));
|
|
76
|
+
// Failed queries and truncated samples make observed process/module counts
|
|
77
|
+
// lower bounds. Preserve any HIGH/CRITICAL warning already proved by those
|
|
78
|
+
// counts, but never emit a confident LOW/MEDIUM edit gate from an incomplete
|
|
79
|
+
// enrichment pass.
|
|
80
|
+
const risk = incompleteSample && (observedRisk === 'LOW' || observedRisk === 'MEDIUM')
|
|
81
|
+
? 'UNKNOWN'
|
|
82
|
+
: observedRisk;
|
|
69
83
|
return {
|
|
70
|
-
risk
|
|
84
|
+
risk,
|
|
71
85
|
riskSharedAxes: score({ ...input, processCount: 0, moduleCount: 0 }),
|
|
72
86
|
riskScale: {
|
|
73
87
|
comparableAcrossKinds: unusedAxes.length === 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impact-risk.js","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"impact-risk.js","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -139,14 +139,14 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
|
|
|
139
139
|
|
|
140
140
|
## Always Do
|
|
141
141
|
|
|
142
|
-
- **MUST run impact
|
|
142
|
+
- **MUST run impact before editing.** Use \`impact({target: "symbolName", direction: "upstream"})\` or \`${runner} impact "symbolName" --direction upstream --repo .\`; report callers, processes, and risk. Never substitute grep for graph analysis.${hasPdg
|
|
143
143
|
? ` For unified PDG impact, add \`mode: "pdg"\` with optional \`line: <N>\` — it returns statement-level \`affectedStatements\` over CDG + REACHING_DEF and inter-procedural symbols in \`interproceduralByDepth\`/\`byDepth\`; no-layer/degraded PDG results are UNKNOWN-risk notes (\`--pdg\` layer). CLI equivalent: \`${runner} impact "symbolName" --direction upstream --mode pdg --line <N> --repo .\`.`
|
|
144
144
|
: ''}
|
|
145
145
|
- **MUST analyze graph changes before committing.** Use \`detect_changes({scope: "all"})\` (MCP) or \`${runner} detect-changes --scope all --repo .\` (CLI fallback). \`partial: true\` or \`truncated: true\` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\` or \`${runner} detect-changes --scope compare --base-ref ${JSON.stringify(markdownSafeBranch(defaultBranch))} --repo .\`.
|
|
146
|
-
-
|
|
146
|
+
- MUST warn on HIGH/CRITICAL \`risk\` pre-edit; never use \`riskSharedAxes\` to waive a HIGH/CRITICAL \`risk\` warning. Compare File/symbol: MCP File omits axes; Graph-RAG expands File.
|
|
147
147
|
- **MUST treat \`risk: UNKNOWN\` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). \`impact\` pairs \`UNKNOWN\` with a \`riskNote\` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero.
|
|
148
|
-
-
|
|
149
|
-
-
|
|
148
|
+
- Explore with \`query({search_query: "concept"})\` for process-grouped flows.
|
|
149
|
+
- Use \`context({name: "symbolName"})\` for callers, callees, and flows.
|
|
150
150
|
- For security review, \`explain({target: "fileOrSymbol"})\` lists taint findings (source→sink flows; needs \`analyze --pdg\`).${hasPdg
|
|
151
151
|
? `\n- For control/data dependence, \`pdg_query({mode: "controls", target: "fileOrSymbol"})\` answers "under what condition does X run?" (CDG, incl. guard clauses) and \`pdg_query({mode: "flows", target, variable})\` traces "where does variable Y flow?" (REACHING_DEF). \`--pdg\` layer.`
|
|
152
152
|
: ''}
|
package/dist/cli/eval-server.js
CHANGED
|
@@ -240,6 +240,17 @@ function formatTruncationSuffix(result) {
|
|
|
240
240
|
: '';
|
|
241
241
|
return label ? ` (by ${label})` : '';
|
|
242
242
|
}
|
|
243
|
+
function pushCallgraphRiskLines(lines, result) {
|
|
244
|
+
if (result.risk) {
|
|
245
|
+
lines.push(`Risk: ${result.risk}`);
|
|
246
|
+
}
|
|
247
|
+
if (result.riskNote) {
|
|
248
|
+
lines.push(String(result.riskNote));
|
|
249
|
+
}
|
|
250
|
+
if (result.riskScale?.comparableAcrossKinds === false && result.riskSharedAxes) {
|
|
251
|
+
lines.push(`Shared-axes risk: ${result.riskSharedAxes} (process/module axes are unavailable — compare File vs symbol only; do not use this to waive a HIGH/CRITICAL risk warning)`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
243
254
|
export function formatImpactResult(result) {
|
|
244
255
|
if (result.error) {
|
|
245
256
|
const suggestion = result.suggestion ? `\nSuggestion: ${result.suggestion}` : '';
|
|
@@ -467,15 +478,20 @@ export function formatImpactResult(result) {
|
|
|
467
478
|
// #1858 — "isolated" is a confident claim. If an interface / indirection
|
|
468
479
|
// boundary is on the path, the true count is a lower bound, not zero;
|
|
469
480
|
// callers binding via DI / dynamic dispatch were not traced. Say so instead.
|
|
481
|
+
const lines = [];
|
|
470
482
|
if (result.epistemic === 'lower-bound') {
|
|
471
|
-
|
|
472
|
-
`${target?.name || '?'}: no direct ${direction} dependencies traced, but this is a LOWER BOUND — unresolved indirection on the path (actual impact may be higher):`,
|
|
473
|
-
];
|
|
483
|
+
lines.push(`${target?.name || '?'}: no direct ${direction} dependencies traced, but this is a LOWER BOUND — unresolved indirection on the path (actual impact may be higher):`);
|
|
474
484
|
for (const b of result.boundaries || [])
|
|
475
485
|
lines.push(` • ${b}`);
|
|
476
|
-
return lines.join('\n');
|
|
477
486
|
}
|
|
478
|
-
|
|
487
|
+
else if (direction === 'upstream') {
|
|
488
|
+
lines.push(`${target?.name || '?'}: No ${direction} callers resolved. This is not evidence the symbol is unused or isolated.`);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
lines.push(`${target?.name || '?'}: No ${direction} dependencies found.`);
|
|
492
|
+
}
|
|
493
|
+
pushCallgraphRiskLines(lines, result);
|
|
494
|
+
return lines.join('\n');
|
|
479
495
|
}
|
|
480
496
|
const lines = [];
|
|
481
497
|
const dirLabel = direction === 'upstream' ? 'depends on this (will break if changed)' : 'this depends on';
|
|
@@ -490,6 +506,7 @@ export function formatImpactResult(result) {
|
|
|
490
506
|
for (const b of result.boundaries || [])
|
|
491
507
|
lines.push(` • ${b}`);
|
|
492
508
|
}
|
|
509
|
+
pushCallgraphRiskLines(lines, result);
|
|
493
510
|
lines.push('');
|
|
494
511
|
const depthLabels = {
|
|
495
512
|
1: 'WILL BREAK (direct)',
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ export declare const en: {
|
|
|
32
32
|
readonly 'status.detached': "(detached HEAD)";
|
|
33
33
|
readonly 'status.workspaceIndexLabel': "Workspace index: last analyzed on '{{primary}}' (re-run gitnexus analyze to follow the current branch)";
|
|
34
34
|
readonly 'status.status': "Status";
|
|
35
|
+
readonly 'status.indexContentCurrent': "Index content: matches all {{count}} covered file(s)";
|
|
36
|
+
readonly 'status.indexContentDrifted': "Index content: {{changed}} changed, {{added}} added, {{deleted}} deleted";
|
|
37
|
+
readonly 'status.indexContentMore': " ...and {{count}} more {{label}}";
|
|
38
|
+
readonly 'status.indexContentUnmeasurable': "Index content: not comparable ({{reason}}); fell back to the working-tree check";
|
|
39
|
+
readonly 'status.indexContentScanFailed': "Index content: coverage scan failed; treating the index as stale";
|
|
40
|
+
readonly 'status.driftChanged': "changed";
|
|
41
|
+
readonly 'status.driftAdded': "added";
|
|
42
|
+
readonly 'status.driftDeleted': "deleted";
|
|
35
43
|
readonly 'status.upToDate': "✅ up-to-date";
|
|
36
44
|
readonly 'status.stale': "⚠️ stale (re-run gitnexus analyze)";
|
|
37
45
|
readonly 'clean.deleteAll': "This will delete GitNexus indexes for {{count}} repo(s):";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -32,6 +32,14 @@ export const en = {
|
|
|
32
32
|
'status.detached': '(detached HEAD)',
|
|
33
33
|
'status.workspaceIndexLabel': "Workspace index: last analyzed on '{{primary}}' (re-run gitnexus analyze to follow the current branch)",
|
|
34
34
|
'status.status': 'Status',
|
|
35
|
+
'status.indexContentCurrent': 'Index content: matches all {{count}} covered file(s)',
|
|
36
|
+
'status.indexContentDrifted': 'Index content: {{changed}} changed, {{added}} added, {{deleted}} deleted',
|
|
37
|
+
'status.indexContentMore': ' ...and {{count}} more {{label}}',
|
|
38
|
+
'status.indexContentUnmeasurable': 'Index content: not comparable ({{reason}}); fell back to the working-tree check',
|
|
39
|
+
'status.indexContentScanFailed': 'Index content: coverage scan failed; treating the index as stale',
|
|
40
|
+
'status.driftChanged': 'changed',
|
|
41
|
+
'status.driftAdded': 'added',
|
|
42
|
+
'status.driftDeleted': 'deleted',
|
|
35
43
|
'status.upToDate': '✅ up-to-date',
|
|
36
44
|
'status.stale': '⚠️ stale (re-run gitnexus analyze)',
|
|
37
45
|
'clean.deleteAll': 'This will delete GitNexus indexes for {{count}} repo(s):',
|
|
@@ -33,6 +33,14 @@ export declare const cliResources: {
|
|
|
33
33
|
readonly 'status.detached': "(detached HEAD)";
|
|
34
34
|
readonly 'status.workspaceIndexLabel': "Workspace index: last analyzed on '{{primary}}' (re-run gitnexus analyze to follow the current branch)";
|
|
35
35
|
readonly 'status.status': "Status";
|
|
36
|
+
readonly 'status.indexContentCurrent': "Index content: matches all {{count}} covered file(s)";
|
|
37
|
+
readonly 'status.indexContentDrifted': "Index content: {{changed}} changed, {{added}} added, {{deleted}} deleted";
|
|
38
|
+
readonly 'status.indexContentMore': " ...and {{count}} more {{label}}";
|
|
39
|
+
readonly 'status.indexContentUnmeasurable': "Index content: not comparable ({{reason}}); fell back to the working-tree check";
|
|
40
|
+
readonly 'status.indexContentScanFailed': "Index content: coverage scan failed; treating the index as stale";
|
|
41
|
+
readonly 'status.driftChanged': "changed";
|
|
42
|
+
readonly 'status.driftAdded': "added";
|
|
43
|
+
readonly 'status.driftDeleted': "deleted";
|
|
36
44
|
readonly 'status.upToDate': "✅ up-to-date";
|
|
37
45
|
readonly 'status.stale': "⚠️ stale (re-run gitnexus analyze)";
|
|
38
46
|
readonly 'clean.deleteAll': "This will delete GitNexus indexes for {{count}} repo(s):";
|
|
@@ -289,6 +297,14 @@ export declare const cliResources: {
|
|
|
289
297
|
'status.detached': string;
|
|
290
298
|
'status.workspaceIndexLabel': string;
|
|
291
299
|
'status.status': string;
|
|
300
|
+
'status.indexContentCurrent': string;
|
|
301
|
+
'status.indexContentDrifted': string;
|
|
302
|
+
'status.indexContentMore': string;
|
|
303
|
+
'status.indexContentUnmeasurable': string;
|
|
304
|
+
'status.indexContentScanFailed': string;
|
|
305
|
+
'status.driftChanged': string;
|
|
306
|
+
'status.driftAdded': string;
|
|
307
|
+
'status.driftDeleted': string;
|
|
292
308
|
'status.upToDate': string;
|
|
293
309
|
'status.stale': string;
|
|
294
310
|
'clean.deleteAll': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -32,6 +32,14 @@ export declare const zhCN: {
|
|
|
32
32
|
'status.detached': string;
|
|
33
33
|
'status.workspaceIndexLabel': string;
|
|
34
34
|
'status.status': string;
|
|
35
|
+
'status.indexContentCurrent': string;
|
|
36
|
+
'status.indexContentDrifted': string;
|
|
37
|
+
'status.indexContentMore': string;
|
|
38
|
+
'status.indexContentUnmeasurable': string;
|
|
39
|
+
'status.indexContentScanFailed': string;
|
|
40
|
+
'status.driftChanged': string;
|
|
41
|
+
'status.driftAdded': string;
|
|
42
|
+
'status.driftDeleted': string;
|
|
35
43
|
'status.upToDate': string;
|
|
36
44
|
'status.stale': string;
|
|
37
45
|
'clean.deleteAll': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -32,6 +32,14 @@ export const zhCN = {
|
|
|
32
32
|
'status.detached': '(分离 HEAD)',
|
|
33
33
|
'status.workspaceIndexLabel': "工作区索引:最近在 '{{primary}}' 分支上分析(重新运行 gitnexus analyze 以跟随当前分支)",
|
|
34
34
|
'status.status': '状态',
|
|
35
|
+
'status.indexContentCurrent': '索引内容:与覆盖的全部 {{count}} 个文件一致',
|
|
36
|
+
'status.indexContentDrifted': '索引内容:{{changed}} 个已修改,{{added}} 个新增,{{deleted}} 个已删除',
|
|
37
|
+
'status.indexContentMore': ' ……另有 {{count}} 个 {{label}}',
|
|
38
|
+
'status.indexContentUnmeasurable': '索引内容:无法比对({{reason}}),已回退到工作区检查',
|
|
39
|
+
'status.indexContentScanFailed': '索引内容:覆盖扫描失败,按过期处理',
|
|
40
|
+
'status.driftChanged': '已修改',
|
|
41
|
+
'status.driftAdded': '新增',
|
|
42
|
+
'status.driftDeleted': '已删除',
|
|
35
43
|
'status.upToDate': '✅ 已是最新',
|
|
36
44
|
'status.stale': '⚠️ 已过期(重新运行 gitnexus analyze)',
|
|
37
45
|
'clean.deleteAll': '将删除 {{count}} 个仓库的 GitNexus 索引:',
|
package/dist/cli/status.js
CHANGED
|
@@ -8,7 +8,64 @@ import { findRepo, getStoragePaths, loadMeta, hasKuzuIndex } from '../storage/re
|
|
|
8
8
|
import { getCurrentCommit, getCurrentBranch, isGitRepo, getGitRoot, isWorkingTreeDirty, } from '../storage/git.js';
|
|
9
9
|
import { analyzerRunnerIdentitiesEqual, resolveAnalyzerRunnerIdentity, } from '../core/analyzer-identity.js';
|
|
10
10
|
import { getIndexIncompleteReasons } from '../core/index-freshness.js';
|
|
11
|
+
import { detectIndexContentDrift } from '../core/index-content-drift.js';
|
|
11
12
|
import { t } from './i18n/index.js';
|
|
13
|
+
/** How many drifted paths the report names before summarizing the rest. */
|
|
14
|
+
const DRIFT_SAMPLE_LIMIT = 10;
|
|
15
|
+
/**
|
|
16
|
+
* Machine-readable form of the per-file comparison. `'not-checked'` is its own
|
|
17
|
+
* value rather than a silent omission: it says the index was already stale on
|
|
18
|
+
* metadata alone, so the scan was skipped, which is not the same claim as a
|
|
19
|
+
* scan that ran and found nothing.
|
|
20
|
+
*/
|
|
21
|
+
const describeContentDrift = (drift) => {
|
|
22
|
+
if (!drift)
|
|
23
|
+
return { status: 'not-checked' };
|
|
24
|
+
if (drift.kind === 'current') {
|
|
25
|
+
return { status: 'current', coveredFiles: drift.coveredFileCount };
|
|
26
|
+
}
|
|
27
|
+
if (drift.kind === 'unmeasurable') {
|
|
28
|
+
return { status: 'unmeasurable', reason: drift.reason };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
status: 'drifted',
|
|
32
|
+
counts: {
|
|
33
|
+
changed: drift.changed.length,
|
|
34
|
+
added: drift.added.length,
|
|
35
|
+
deleted: drift.deleted.length,
|
|
36
|
+
},
|
|
37
|
+
changed: drift.changed.slice(0, DRIFT_SAMPLE_LIMIT),
|
|
38
|
+
added: drift.added.slice(0, DRIFT_SAMPLE_LIMIT),
|
|
39
|
+
deleted: drift.deleted.slice(0, DRIFT_SAMPLE_LIMIT),
|
|
40
|
+
truncated: {
|
|
41
|
+
changed: drift.changed.length > DRIFT_SAMPLE_LIMIT,
|
|
42
|
+
added: drift.added.length > DRIFT_SAMPLE_LIMIT,
|
|
43
|
+
deleted: drift.deleted.length > DRIFT_SAMPLE_LIMIT,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/** Escape control characters in repo-relative paths before printing. */
|
|
48
|
+
const formatDriftPath = (rel) => /[\u0000-\u001f\u007f]/.test(rel) ? JSON.stringify(rel) : rel;
|
|
49
|
+
const printDriftDetail = (drift) => {
|
|
50
|
+
console.log(t('status.indexContentDrifted', {
|
|
51
|
+
changed: drift.changed.length,
|
|
52
|
+
added: drift.added.length,
|
|
53
|
+
deleted: drift.deleted.length,
|
|
54
|
+
}));
|
|
55
|
+
const labelled = [
|
|
56
|
+
[t('status.driftChanged'), drift.changed],
|
|
57
|
+
[t('status.driftAdded'), drift.added],
|
|
58
|
+
[t('status.driftDeleted'), drift.deleted],
|
|
59
|
+
];
|
|
60
|
+
for (const [label, paths] of labelled) {
|
|
61
|
+
for (const p of paths.slice(0, DRIFT_SAMPLE_LIMIT)) {
|
|
62
|
+
console.log(` ${label}: ${formatDriftPath(p)}`);
|
|
63
|
+
}
|
|
64
|
+
const remaining = paths.length - DRIFT_SAMPLE_LIMIT;
|
|
65
|
+
if (remaining > 0)
|
|
66
|
+
console.log(t('status.indexContentMore', { count: remaining, label }));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
12
69
|
export const statusCommand = async (options = {}) => {
|
|
13
70
|
const cwd = process.cwd();
|
|
14
71
|
if (!isGitRepo(cwd)) {
|
|
@@ -63,13 +120,26 @@ export const statusCommand = async (options = {}) => {
|
|
|
63
120
|
const currentRunnerIdentity = resolveAnalyzerRunnerIdentity(import.meta.url);
|
|
64
121
|
const runnerIdentityIsCurrent = analyzerRunnerIdentitiesEqual(activeMeta.runnerIdentity, currentRunnerIdentity);
|
|
65
122
|
const incompleteReasons = getIndexIncompleteReasons(activeMeta);
|
|
66
|
-
|
|
67
|
-
// so a repo with uncommitted source changes is stale even at the same commit.
|
|
68
|
-
// Skip the check for non-git folders (currentCommit === '') to match analyze.
|
|
69
|
-
const isUpToDate = currentCommit === activeMeta.lastCommit &&
|
|
123
|
+
const metadataIsCurrent = currentCommit === activeMeta.lastCommit &&
|
|
70
124
|
runnerIdentityIsCurrent &&
|
|
71
|
-
incompleteReasons.length === 0
|
|
72
|
-
|
|
125
|
+
incompleteReasons.length === 0;
|
|
126
|
+
// A matching HEAD is not enough: `analyze` re-indexes changed content at the
|
|
127
|
+
// same commit, so the files the index covers must still be compared against
|
|
128
|
+
// disk. Only worth the scan once the cheap metadata checks agree, and skipped
|
|
129
|
+
// for non-git folders (currentCommit === '') to match analyze.
|
|
130
|
+
const contentDrift = metadataIsCurrent && currentCommit !== ''
|
|
131
|
+
? await detectIndexContentDrift(repo.repoPath, activeMeta.fileHashes, activeMeta.indexCoverage)
|
|
132
|
+
: undefined;
|
|
133
|
+
// The repo-wide dirty flag survives only as the fallback for metadata written
|
|
134
|
+
// before `fileHashes` existed. Where the per-file comparison can run it
|
|
135
|
+
// decides, so a file the index does not cover no longer pins a byte-current
|
|
136
|
+
// index to a "stale" verdict that `analyze` is powerless to clear (#3077).
|
|
137
|
+
const contentIsCurrent = contentDrift === undefined ||
|
|
138
|
+
contentDrift.kind === 'current' ||
|
|
139
|
+
(contentDrift.kind === 'unmeasurable' &&
|
|
140
|
+
contentDrift.reason === 'no-file-hashes' &&
|
|
141
|
+
!isWorkingTreeDirty(repo.repoPath));
|
|
142
|
+
const isUpToDate = metadataIsCurrent && contentIsCurrent;
|
|
73
143
|
if (options.json) {
|
|
74
144
|
console.log(JSON.stringify({
|
|
75
145
|
schemaVersion: 1,
|
|
@@ -87,6 +157,7 @@ export const statusCommand = async (options = {}) => {
|
|
|
87
157
|
commit: currentCommit,
|
|
88
158
|
runnerIdentity: currentRunnerIdentity,
|
|
89
159
|
},
|
|
160
|
+
contentDrift: describeContentDrift(contentDrift),
|
|
90
161
|
status: isUpToDate ? 'up-to-date' : 'stale',
|
|
91
162
|
}));
|
|
92
163
|
return;
|
|
@@ -107,5 +178,19 @@ export const statusCommand = async (options = {}) => {
|
|
|
107
178
|
console.log(`Index incomplete reasons: ${JSON.stringify(incompleteReasons)}`);
|
|
108
179
|
}
|
|
109
180
|
console.log(`${t('status.currentRunnerIdentity')}: ${JSON.stringify(currentRunnerIdentity)}`);
|
|
181
|
+
if (contentDrift?.kind === 'current') {
|
|
182
|
+
console.log(t('status.indexContentCurrent', { count: contentDrift.coveredFileCount }));
|
|
183
|
+
}
|
|
184
|
+
else if (contentDrift?.kind === 'drifted') {
|
|
185
|
+
printDriftDetail(contentDrift);
|
|
186
|
+
}
|
|
187
|
+
else if (contentDrift?.kind === 'unmeasurable') {
|
|
188
|
+
if (contentDrift.reason === 'scan-failed') {
|
|
189
|
+
console.log(t('status.indexContentScanFailed'));
|
|
190
|
+
}
|
|
191
|
+
else if (!isUpToDate) {
|
|
192
|
+
console.log(t('status.indexContentUnmeasurable', { reason: contentDrift.reason }));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
110
195
|
console.log(`${t('status.status')}: ${isUpToDate ? t('status.upToDate') : t('status.stale')}`);
|
|
111
196
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Cross-repo impact (Phase 1 local walk + Phase 2 bridge fan-out).
|
|
3
3
|
* All bridge Cypher for this feature lives in this module.
|
|
4
4
|
*/
|
|
5
|
+
import type { ImpactRisk } from '../../_shared/index.js';
|
|
5
6
|
import type { BridgeHandle, BridgeMeta, CrossRepoImpact, GroupImpactResult } from './types.js';
|
|
6
7
|
import type { GroupToolPort } from './service.js';
|
|
7
8
|
export { truncationFields, crossRepoCompleteness, type TruncationFields, type CrossRepoCompleteness, type CrossRepoCompletenessInput, } from './completeness.js';
|
|
@@ -109,7 +110,7 @@ export declare function collectImpactSymbolUids(local: unknown, servicePrefix: s
|
|
|
109
110
|
uids: string[];
|
|
110
111
|
targetFilePath?: string;
|
|
111
112
|
};
|
|
112
|
-
export declare function mergeRisk(localRisk:
|
|
113
|
+
export declare function mergeRisk(localRisk: ImpactRisk, cross: CrossRepoImpact[]): ImpactRisk;
|
|
113
114
|
/**
|
|
114
115
|
* Is this bridge's metadata unable to say where its contents came from?
|
|
115
116
|
*
|
|
@@ -279,6 +279,15 @@ function extractProcessNames(impact) {
|
|
|
279
279
|
// permanently that a PDG `risk:'UNKNOWN'` never coalesces to a confident `LOW`.
|
|
280
280
|
// No behavior change — `'UNKNOWN'` was already handled correctly at the
|
|
281
281
|
// `(localRisk === 'LOW' || localRisk === 'UNKNOWN')` branch below.
|
|
282
|
+
function asImpactRisk(value, fallback = 'LOW') {
|
|
283
|
+
return value === 'LOW' ||
|
|
284
|
+
value === 'MEDIUM' ||
|
|
285
|
+
value === 'HIGH' ||
|
|
286
|
+
value === 'CRITICAL' ||
|
|
287
|
+
value === 'UNKNOWN'
|
|
288
|
+
? value
|
|
289
|
+
: fallback;
|
|
290
|
+
}
|
|
282
291
|
export function mergeRisk(localRisk, cross) {
|
|
283
292
|
const traversed = cross.filter((c) => c.fanout_status !== 'not_attempted');
|
|
284
293
|
const highConf = traversed.some((c) => c.contract.confidence >= 0.85);
|
|
@@ -292,6 +301,15 @@ export function mergeRisk(localRisk, cross) {
|
|
|
292
301
|
return 'MEDIUM';
|
|
293
302
|
return localRisk;
|
|
294
303
|
}
|
|
304
|
+
function liftLocalRiskMeta(local, cross) {
|
|
305
|
+
const { riskSharedAxes, riskScale } = local;
|
|
306
|
+
return {
|
|
307
|
+
...(riskSharedAxes !== undefined
|
|
308
|
+
? { riskSharedAxes: mergeRisk(asImpactRisk(riskSharedAxes), cross) }
|
|
309
|
+
: {}),
|
|
310
|
+
...(riskScale !== undefined ? { riskScale } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
295
313
|
/**
|
|
296
314
|
* Is this bridge's metadata unable to say where its contents came from?
|
|
297
315
|
*
|
|
@@ -461,6 +479,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
461
479
|
cross_repo_hits: 0,
|
|
462
480
|
},
|
|
463
481
|
risk: 'UNKNOWN',
|
|
482
|
+
...liftLocalRiskMeta(local, []),
|
|
464
483
|
timeoutMs,
|
|
465
484
|
crossDepthWarning,
|
|
466
485
|
};
|
|
@@ -513,7 +532,8 @@ export async function runGroupImpact(deps, params) {
|
|
|
513
532
|
modules_affected: s.modules_affected ?? 0,
|
|
514
533
|
cross_repo_hits: 0,
|
|
515
534
|
},
|
|
516
|
-
risk:
|
|
535
|
+
risk: asImpactRisk(local.risk),
|
|
536
|
+
...liftLocalRiskMeta(local, []),
|
|
517
537
|
timeoutMs,
|
|
518
538
|
crossDepthWarning,
|
|
519
539
|
};
|
|
@@ -666,7 +686,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
666
686
|
await closeBridgeDb(handle);
|
|
667
687
|
}
|
|
668
688
|
const localSum = local?.summary || {};
|
|
669
|
-
const localRisk =
|
|
689
|
+
const localRisk = asImpactRisk(local.risk);
|
|
670
690
|
const localPartial = Boolean(local.partial);
|
|
671
691
|
// The bridge's own incompleteness, in the shared vocabulary, read through
|
|
672
692
|
// what this query DECLARED. The fan-out above already drops every neighbour
|
|
@@ -740,6 +760,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
740
760
|
cross_repo_hits: cross.length,
|
|
741
761
|
},
|
|
742
762
|
risk: mergeRisk(localRisk, cross),
|
|
763
|
+
...liftLocalRiskMeta(local, cross),
|
|
743
764
|
timeoutMs,
|
|
744
765
|
crossDepthWarning,
|
|
745
766
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImpactRisk, ImpactRiskResult } from '../../_shared/index.js';
|
|
1
2
|
export type ContractType = 'http' | 'graphql' | 'grpc' | 'thrift' | 'topic' | 'lib' | 'custom' | 'include';
|
|
2
3
|
export type ManifestContractType = Exclude<ContractType, 'graphql'>;
|
|
3
4
|
export type MatchType = 'exact' | 'manifest' | 'wildcard';
|
|
@@ -166,7 +167,17 @@ export interface GroupImpactResult {
|
|
|
166
167
|
modules_affected: number;
|
|
167
168
|
cross_repo_hits: number;
|
|
168
169
|
};
|
|
169
|
-
risk:
|
|
170
|
+
risk: ImpactRisk;
|
|
171
|
+
/**
|
|
172
|
+
* Two-axis (direct + total) risk from the local leg, then `mergeRisk` with
|
|
173
|
+
* crossings — compare File vs symbol here, not via top-level `risk`.
|
|
174
|
+
*/
|
|
175
|
+
riskSharedAxes?: ImpactRisk;
|
|
176
|
+
/**
|
|
177
|
+
* Local-leg scale metadata (File / skipped enrichment). Crossings do not
|
|
178
|
+
* invent process/module membership for File nodes.
|
|
179
|
+
*/
|
|
180
|
+
riskScale?: ImpactRiskResult['riskScale'];
|
|
170
181
|
/**
|
|
171
182
|
* `'lower-bound'` when the fan-out was cut short, so `risk` is a FLOOR, not a
|
|
172
183
|
* verdict. Same vocabulary as single-repo `impact`'s `epistemic` field.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the index still reflect the files it actually covers?
|
|
3
|
+
*
|
|
4
|
+
* `status` used to answer this with a repo-wide `git status --porcelain`
|
|
5
|
+
* boolean, which says something different: whether the working tree differs
|
|
6
|
+
* from HEAD. Those two questions diverge in both directions. A scratch file,
|
|
7
|
+
* a build artifact, or a tracked file under a tool directory the indexer
|
|
8
|
+
* never reads makes the tree dirty while every indexed file is byte-current —
|
|
9
|
+
* and because `analyze` cannot commit or delete that file, the resulting
|
|
10
|
+
* "stale (re-run gitnexus analyze)" verdict was unclearable (#3077). It also
|
|
11
|
+
* misses the reverse case: reverting a file that was indexed while dirty
|
|
12
|
+
* leaves a clean tree over an index holding the pre-revert content.
|
|
13
|
+
*
|
|
14
|
+
* `meta.fileHashes` already records the exact set of files the last run
|
|
15
|
+
* covered, so the question can be answered directly. This module recomputes
|
|
16
|
+
* the coverage set with the same `walkRepositoryPaths` scan (ignore rules and
|
|
17
|
+
* dotfile handling stay shared) and the large-file cap recorded in
|
|
18
|
+
* `meta.indexCoverage`, hashes only the paths that can actually have changed
|
|
19
|
+
* since that run, and diffs against what was recorded.
|
|
20
|
+
*/
|
|
21
|
+
import type { RepoMeta } from '../storage/repo-meta.js';
|
|
22
|
+
/** Why the recorded coverage set could not be compared against disk at all. */
|
|
23
|
+
export type IndexContentUnmeasurableReason =
|
|
24
|
+
/** Metadata predates per-file hashes, or the run recorded none (non-git). */
|
|
25
|
+
'no-file-hashes'
|
|
26
|
+
/** The repository scan or hashing pass threw. */
|
|
27
|
+
| 'scan-failed';
|
|
28
|
+
/**
|
|
29
|
+
* A three-way verdict. `'unmeasurable'` is kept apart from `'current'` on
|
|
30
|
+
* purpose: it means the comparison never ran, which is not evidence the index
|
|
31
|
+
* is fresh. Legacy metadata without hashes still falls back to the working-tree
|
|
32
|
+
* check; a failed scan must not.
|
|
33
|
+
*/
|
|
34
|
+
export type IndexContentDrift = {
|
|
35
|
+
kind: 'current';
|
|
36
|
+
coveredFileCount: number;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'drifted';
|
|
39
|
+
changed: string[];
|
|
40
|
+
added: string[];
|
|
41
|
+
deleted: string[];
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'unmeasurable';
|
|
44
|
+
reason: IndexContentUnmeasurableReason;
|
|
45
|
+
};
|
|
46
|
+
export type IndexCoveragePolicy = NonNullable<RepoMeta['indexCoverage']>;
|
|
47
|
+
/**
|
|
48
|
+
* Compare the files recorded in `fileHashes` against the current working tree.
|
|
49
|
+
*
|
|
50
|
+
* `added` covers files the index would pick up but has never seen, so a new
|
|
51
|
+
* source file still reports stale — the index is genuinely incomplete then,
|
|
52
|
+
* and comparing only the recorded entries would wave that through.
|
|
53
|
+
*/
|
|
54
|
+
export declare const detectIndexContentDrift: (repoPath: string, fileHashes: Readonly<Record<string, string>> | undefined, coverage?: IndexCoveragePolicy) => Promise<IndexContentDrift>;
|