Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- `zcodegraph status` and `zcodegraph doctor` now share graph health language, so users and scripts can distinguish healthy, degraded, stale, failed, unavailable, and corrupted indexes with exact next-step commands.
- `rust-hybrid` indexing now understands Python calls, imported names, and module-level values, so Python search, callers/callees, and file dependents work through the Rust indexer.
- `zcodegraph status` now shows decision-ready `rust-hybrid` fallback diagnostics, including top reason groups, graph usability, the exact doctor command, and the privacy-preserving bundle artifact to share with maintainers. (#668, #669, #670)

### Fixes

- `zcodegraph doctor --engine rust-hybrid --bundle` can now create a local diagnostic bundle for a corrupted graph database without opening the broken database or collecting source paths. (#671)
- `zcodegraph doctor --engine rust-hybrid --bundle --last-run` now points degraded fallback reports to the per-file diagnostic artifact and next maintainer handoff step instead of leaving users at the bundle path alone. (#669, #670)

## [0.10.2] - 2026-06-30

Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,8 @@ shared health states are:

- `healthy` — the graph is current and fully usable.
- `degraded` — the graph is usable, but fallback diagnostics need review.
`zcodegraph status` shows the top fallback reason groups and the exact
`doctor` command to create the per-file diagnostic artifact.
- `stale` — the graph is usable but out of date; run `zcodegraph sync` or
`zcodegraph index --force`.
- `failed` — the latest build failed; run
Expand All @@ -646,13 +648,28 @@ shared health states are:

**Indexing is slow** — Check that `node_modules` and other large directories are excluded. Use `--quiet` to reduce output overhead.

**Indexing completed with fallback or failed** — Run a local diagnostic bundle and attach it to your issue. Bundles do not include source code by default:
**Indexing completed with fallback or failed** — Start with `zcodegraph status`
to separate freshness from quality: `stale` means run `zcodegraph sync` or
`zcodegraph index --force`, while `degraded` means the graph is current enough
to use but some files or Rust-owned diagnostics need review. `init` creates the
project index, `index` rebuilds it, `sync` catches up source edits, `status`
answers whether the current graph can be trusted, and `doctor` packages the
evidence for maintainers.

Run a local diagnostic bundle and attach it to your issue. Bundles do not
include source code by default:

```bash
zcodegraph doctor --engine rust-hybrid --bundle --last-run
zcodegraph doctor --engine rust-hybrid --bundle --last-failure
```

For degraded fallback, `status` and `doctor` summarize the top reason groups in
human terms. The bundle's `per-file-diagnostics.json` records path hashes,
extensions, languages, reason categories, and sanitized messages so maintainers
can classify affected files without receiving plaintext source paths or source
slices.

If `doctor` or one of those flags is missing, capture command-resolution
evidence before filing the issue:

Expand Down
2 changes: 2 additions & 0 deletions __tests__/rust-hybrid-doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ describe('rust-hybrid doctor diagnostic bundles', () => {
expect(doctor.stdout).toContain('The index is usable; fallback-degraded files or diagnostics are the only parts that need review.');
expect(doctor.stdout).toContain('Top fallback reasons:');
expect(doctor.stdout).toContain('1 Rust-owned files with extraction diagnostics');
expect(doctor.stdout).toContain('Diagnostic artifact: per-file-diagnostics.json uses path hashes and reason categories without source slices.');
expect(doctor.stdout).toContain('Next step: share this bundle path with the maintainer or attach the bundle contents requested by them.');
const bundleDir = path.resolve(tempDir, latestBundlePath(doctor.stdout));

const status = readJson(path.join(bundleDir, 'status.json'));
Expand Down
56 changes: 56 additions & 0 deletions __tests__/rust-index-engine-cli-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,62 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
expect(result.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
}, 30_000);

it('surfaces degraded fallback quality details in status output', () => {
fs.writeFileSync(path.join(tempDir, 'routing.yml'), 'app:\n path: /health\n');

const index = runZcodegraphCli(tempDir, ['index', '--quiet'], {
ZCODEGRAPH_RUST_CORE_BINARY: RUST_CORE_BIN,
});
expect(index.status, `stdout:\n${index.stdout}\nstderr:\n${index.stderr}`).toBe(0);

const status = runZcodegraphCli(tempDir, ['status']);
expect(status.status, `stdout:\n${status.stdout}\nstderr:\n${status.stderr}`).toBe(0);
expect(status.stdout).toContain('Graph Health:');
expect(status.stdout).toContain('State: degraded');
expect(status.stdout).toContain('Rust-hybrid Fallback:');
expect(status.stdout).toContain('Fallback health: degraded');
expect(status.stdout).toContain('Top fallback reasons:');
expect(status.stdout).toContain('TypeScript fallback files');
expect(status.stdout).toContain('per-file-diagnostics.json');
expect(status.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
}, 30_000);

it('exposes degraded fallback quality details in status json', () => {
fs.writeFileSync(path.join(tempDir, 'routing.yml'), 'app:\n path: /health\n');

const index = runZcodegraphCli(tempDir, ['index', '--quiet'], {
ZCODEGRAPH_RUST_CORE_BINARY: RUST_CORE_BIN,
});
expect(index.status, `stdout:\n${index.stdout}\nstderr:\n${index.stderr}`).toBe(0);

const statusResult = runZcodegraphCli(tempDir, ['status', '--json']);
expect(statusResult.status, `stdout:\n${statusResult.stdout}\nstderr:\n${statusResult.stderr}`).toBe(0);
const statusLine = statusResult.stdout.trim().split('\n').filter(Boolean).pop();
expect(statusLine).toBeDefined();
const status = JSON.parse(statusLine!) as {
fallbackDiagnostics: {
state: string;
graphUsabilityMessage: string;
doctorCommand: string;
artifactHint: string;
topReasons: Array<{ code: string; count: number; label: string }>;
};
};
expect(status.fallbackDiagnostics).toMatchObject({
state: 'degraded',
doctorCommand: 'zcodegraph doctor --engine rust-hybrid --bundle --last-run',
artifactHint: 'per-file-diagnostics.json uses path hashes and reason categories without source slices.',
});
expect(status.fallbackDiagnostics.graphUsabilityMessage).toContain('The index is usable');
expect(status.fallbackDiagnostics.topReasons).toEqual([
expect.objectContaining({
code: 'language-level-typescript-fallback',
count: 1,
label: 'TypeScript fallback files',
}),
]);
}, 30_000);

it('prints the degraded fallback health explanation during rust-hybrid init', () => {
const initDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zcodegraph-rust-hybrid-fallback-init-'));
try {
Expand Down
84 changes: 84 additions & 0 deletions docs/plans/2026-07-02-fallback-diagnostics-ux-roadmap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"title": "fallback diagnostics UX closeout",
"description": "Resolve issues #668, #669, and #670 by making degraded rust-hybrid fallback diagnostics actionable across index/init, status, doctor, docs, and issue closeout.",
"version": 1,
"nodes": {
"1": {
"id": "1",
"label": "fallback diagnostics UX closeout",
"status": "completed",
"mode": "explore",
"parent": null,
"children": [
"1-1",
"1-2",
"1-3",
"1-4",
"1-5"
],
"decisions": [
{
"q": "这轮是否包含 Vue/TypeScript 索引覆盖提升?",
"answer": "不把 Vue/TypeScript 解析能力提升作为本轮 P0;本轮关闭诊断透明度和工作流可解释性。",
"note": "#670 的 Vue fallback reduction 可用文档和可测量后续项承接;否则本轮会从 UX 透明度扩张到语言覆盖迁移。"
}
],
"notes": ""
},
"1-1": {
"id": "1-1",
"label": "界定 668 669 670 的剩余交付边界",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "Remaining scope narrowed to degraded fallback diagnostic transparency. Vue/TypeScript parser coverage is out of this loop and should follow from measurable fallback reduction work."
},
"1-2": {
"id": "1-2",
"label": "让 status 显示 last-run fallback 质量摘要",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "Status now prints a Rust-hybrid Fallback section with fallback health, top translated reasons, per-file-diagnostics artifact hint, and exact doctor command; status --json exposes fallbackDiagnostics."
},
"1-3": {
"id": "1-3",
"label": "让 doctor 输出原因翻译与 artifact 指向",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "Doctor degraded summaries now name per-file-diagnostics.json as the privacy-preserving artifact and state the maintainer handoff next step."
},
"1-4": {
"id": "1-4",
"label": "补充 global workflow 和 degraded guidance 文档",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "README troubleshooting now clarifies init/index/sync/status/doctor workflow, degraded vs stale, and per-file diagnostic bundle privacy semantics."
},
"1-5": {
"id": "1-5",
"label": "验证并关闭 issues 668 669 670",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "Verification passed: fallback CLI tests, doctor bundle tests, status-json tests, npm run build, and git diff --check. Issues #668/#669/#670 should be closed after PR merge because their visible behavior depends on main."
}
},
"metadata": {
"created": "2026-07-02 22:17:45",
"updated": "2026-07-02 22:27:56",
"md_file": "docs/plans/2026-07-02-fallback-diagnostics-ux-roadmap.md"
}
}
12 changes: 12 additions & 0 deletions docs/plans/2026-07-02-fallback-diagnostics-ux-roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!-- ROADMAP_SECTION_START -->
## ZJ Roadmap

> 数据文件: `2026-07-02-fallback-diagnostics-ux-roadmap.json` | 最后更新: 2026-07-02 22:27:56

[x][X+] 1. fallback diagnostics UX closeout
├── [x][Y+] 1-1. 界定 668 669 670 的剩余交付边界
├── [x][Y+] 1-2. 让 status 显示 last-run fallback 质量摘要
├── [x][Y+] 1-3. 让 doctor 输出原因翻译与 artifact 指向
├── [x][Y+] 1-4. 补充 global workflow 和 degraded guidance 文档
└── [x][Y+] 1-5. 验证并关闭 issues 668 669 670
<!-- ROADMAP_SECTION_END -->
86 changes: 85 additions & 1 deletion src/bin/zcodegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@ import {
RustOwnedPerFileGapDiagnostic,
} from '../indexing/rust-hybrid-contract';
import { buildDiagnosticBundleSummary, createDiagnosticBundle, diagnosticRecordPath, writeDiagnosticRunRecord } from '../diagnostics';
import { buildRustHybridFallbackSummary, formatRustHybridFallbackDoctorHint } from '../diagnostics/fallback-summary';
import {
buildRustHybridFallbackSummary,
formatRustHybridFallbackDoctorHint,
formatRustHybridFallbackHealthLines,
rustHybridFallbackReasonLabel,
RustHybridFallbackReasonCount,
RustHybridFallbackSummary,
} from '../diagnostics/fallback-summary';
import { classifyGraphHealth, formatGraphHealthLines, GraphHealth } from '../diagnostics/graph-health';

import { buildNode25BlockBanner, buildNodeTooOldBanner, MIN_NODE_MAJOR } from './node-version-check';
Expand All @@ -67,6 +74,81 @@ function resolveSqliteWriteMode(raw: string | undefined): 'disk' | 'final-flush'
throw new Error(`Unsupported SQLite write mode "${raw}". Supported modes: disk, final-flush, memory-final-flush`);
}

type RustHybridStatusFallbackDiagnostics = {
state: string;
graphUsabilityMessage: string;
fallbackFileCount: number;
missingFallbackFileCount: number;
reasonTaxonomy: Record<string, number>;
topReasons: Array<RustHybridFallbackReasonCount & { label: string }>;
doctorCommand: string;
artifactHint: string;
} | null;

function fallbackSummaryFromHybridMetadata(hybrid: unknown): RustHybridFallbackSummary | null {
if (!hybrid || typeof hybrid !== 'object') return null;
const metadata = hybrid as {
fallbackState?: string;
fallbackFileCount?: number;
missingFallbackFileCount?: number;
missingFallbackByLanguage?: Record<string, number>;
fallbackReasonTaxonomy?: Record<string, number>;
};
const fallbackReasonTaxonomy = metadata.fallbackReasonTaxonomy ?? {};
const fallbackFileCount = metadata.fallbackFileCount ?? 0;
const missingFallbackFileCount = metadata.missingFallbackFileCount ?? 0;
const summary = buildRustHybridFallbackSummary({
success: true,
errors: [],
profile: {
typescriptFallbackAppend: {
fallbackFileCount,
missingFallbackFileCount,
missingFallbackByLanguage: metadata.missingFallbackByLanguage ?? {},
},
},
});
summary.fallbackReasonTaxonomy = fallbackReasonTaxonomy;
summary.topFallbackReasons = Object.entries(fallbackReasonTaxonomy)
.map(([code, count]) => ({ code, count }))
.sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
summary.fallbackState = metadata.fallbackState === 'degraded' ? 'degraded' : summary.fallbackState;
if (summary.fallbackState === 'degraded') {
summary.graphUsabilityMessage = 'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.';
}
return summary;
}

function statusFallbackDiagnostics(hybrid: unknown): RustHybridStatusFallbackDiagnostics {
const summary = fallbackSummaryFromHybridMetadata(hybrid);
if (!summary || summary.fallbackState !== 'degraded') return null;
return {
state: summary.fallbackState,
graphUsabilityMessage: summary.graphUsabilityMessage,
fallbackFileCount: summary.fallbackFileCount,
missingFallbackFileCount: summary.missingFallbackFileCount,
reasonTaxonomy: summary.fallbackReasonTaxonomy,
topReasons: summary.topFallbackReasons.map((reason) => ({
...reason,
label: rustHybridFallbackReasonLabel(reason.code),
})),
doctorCommand: 'zcodegraph doctor --engine rust-hybrid --bundle --last-run',
artifactHint: 'per-file-diagnostics.json uses path hashes and reason categories without source slices.',
};
}

function printRustHybridFallbackStatus(hybrid: unknown): void {
const summary = fallbackSummaryFromHybridMetadata(hybrid);
if (!summary || summary.fallbackState !== 'degraded') return;
console.log('Rust-hybrid Fallback:');
for (const line of formatRustHybridFallbackHealthLines(summary)) {
console.log(` ${line.replace(/\n/g, '\n ')}`);
}
console.log(' Diagnostic artifact: per-file-diagnostics.json uses path hashes and reason categories without source slices.');
console.log(' Next step: zcodegraph doctor --engine rust-hybrid --bundle --last-run');
console.log();
}

type ProfileCheckpointState = 'started' | 'completed';

type ProfileCheckpoint = {
Expand Down Expand Up @@ -1249,6 +1331,7 @@ program
currentExtractionVersion: EXTRACTION_VERSION,
reindexRecommended,
},
fallbackDiagnostics: statusFallbackDiagnostics(buildInfo.hybrid),
rust: getRustReadinessDiagnostics(projectPath, buildInfo),
}));
cg.destroy();
Expand All @@ -1265,6 +1348,7 @@ program
console.log();

printGraphHealthSummary(health);
printRustHybridFallbackStatus(buildInfo.hybrid);

// Index stats
console.log(chalk.bold('Index Statistics:'));
Expand Down
6 changes: 5 additions & 1 deletion src/diagnostics/fallback-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const FALLBACK_REASON_LABELS: Record<string, string> = {
'rust-owned-gap-with-partial-write-blocked': 'Rust-owned files with partial Rust writes not fallback-appended',
};

export function rustHybridFallbackReasonLabel(code: string): string {
return FALLBACK_REASON_LABELS[code] ?? code;
}

function fallbackProfile(input: RustHybridFallbackSummaryInput): RustHybridFallbackProfile | undefined {
return input.profile as RustHybridFallbackProfile | undefined;
}
Expand Down Expand Up @@ -87,7 +91,7 @@ export function buildRustHybridFallbackSummary(input: RustHybridFallbackSummaryI
}

function formatReasonCount(reason: RustHybridFallbackReasonCount): string {
return `${reason.count} ${FALLBACK_REASON_LABELS[reason.code] ?? reason.code}`;
return `${reason.count} ${rustHybridFallbackReasonLabel(reason.code)}`;
}

export function formatRustHybridFallbackHealthLines(summary: RustHybridFallbackSummary): string[] {
Expand Down
4 changes: 4 additions & 0 deletions src/diagnostics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,10 @@ export function buildDiagnosticBundleSummary(projectRoot: string, relativeBundle
fallbackSummary.graphUsabilityMessage = 'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.';
}
lines.push(...formatRustHybridFallbackHealthLines(fallbackSummary));
if (fallbackSummary.fallbackState === 'degraded') {
lines.push('Diagnostic artifact: per-file-diagnostics.json uses path hashes and reason categories without source slices.');
lines.push('Next step: share this bundle path with the maintainer or attach the bundle contents requested by them.');
}
return {
engine,
source,
Expand Down