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
71 changes: 71 additions & 0 deletions __tests__/issue-680-fallback-messaging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest';
import {
buildRustHybridFallbackSummary,
formatRustHybridFallbackHealthLines,
rustHybridFallbackReasonLabel,
} from '../src/diagnostics/fallback-summary';

describe('Issue #680: fallback messaging distinguishes implementation from source language', () => {
it('uses "non-Rust-owned files via TypeScript fallback" as the reason label', () => {
expect(rustHybridFallbackReasonLabel('language-level-typescript-fallback'))
.toBe('non-Rust-owned files via TypeScript fallback');
});

it('includes a "Fallback by source language" line when fallbackByLanguage is present', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 30,
fallbackByLanguage: { yaml: 30 },
},
},
errors: [],
});

const lines = formatRustHybridFallbackHealthLines(summary);
const joined = lines.join('\n');

// Reason line should use the new label
expect(joined).toContain('30 non-Rust-owned files via TypeScript fallback');
expect(joined).not.toContain('30 TypeScript fallback files');

// Language breakdown line
expect(joined).toContain('Fallback by source language: yaml (30)');
});

it('handles multiple source languages in the breakdown', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 30,
fallbackByLanguage: { yaml: 25, css: 3, html: 2 },
},
},
errors: [],
});

const lines = formatRustHybridFallbackHealthLines(summary);
const joined = lines.join('\n');

expect(joined).toContain('Fallback by source language: yaml (25), css (3), html (2)');
});

it('omits the language breakdown line when fallbackByLanguage is absent', () => {
const summary = buildRustHybridFallbackSummary({
success: true,
profile: {
typescriptFallbackAppend: {
fallbackFileCount: 5,
},
},
errors: [],
});

const lines = formatRustHybridFallbackHealthLines(summary);
const joined = lines.join('\n');

expect(joined).not.toContain('Fallback by source language');
});
});
2 changes: 1 addition & 1 deletion __tests__/rust-hybrid-fallback-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('rust-hybrid fallback summary contract', () => {
expect(formatRustHybridFallbackDoctorHint(summary)).toEqual([
'Indexed with rust-hybrid',
'Fallback health: degraded',
'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.\nTop fallback reasons:\n 1 TypeScript fallback files',
'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.\nTop fallback reasons:\n 1 non-Rust-owned files via TypeScript fallback',
'Run diagnostic bundle:\n zcodegraph doctor --engine rust-hybrid --bundle --last-run',
]);
});
Expand Down
10 changes: 5 additions & 5 deletions __tests__/rust-index-engine-cli-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('plans Rust-owned and TypeScript fallback files for rust-hybrid', () => {
it('plans Rust-owned and non-Rust-owned files via TypeScript fallback for rust-hybrid', () => {
fs.writeFileSync(path.join(tempDir, 'server.go'), 'package main\nfunc main() {}\n');
fs.writeFileSync(path.join(tempDir, 'service.py'), 'def service():\n return 1\n');
fs.writeFileSync(path.join(tempDir, 'worker.rs'), 'fn worker() -> i32 { 1 }\n');
Expand Down Expand Up @@ -229,7 +229,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
});

expect(result.status, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`).toBe(0);
expect(result.stdout).toContain('TypeScript fallback files');
expect(result.stdout).toContain('non-Rust-owned files via TypeScript fallback');
expect(result.stdout).toContain('Fallback health: degraded');
expect(result.stdout).toContain('The index is usable; fallback-degraded files or diagnostics are the only parts that need review.');
expect(result.stdout).toContain('Top fallback reasons:');
Expand All @@ -251,7 +251,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
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('non-Rust-owned files via TypeScript fallback');
expect(status.stdout).toContain('per-file-diagnostics.json');
expect(status.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
}, 30_000);
Expand Down Expand Up @@ -287,7 +287,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
expect.objectContaining({
code: 'language-level-typescript-fallback',
count: 1,
label: 'TypeScript fallback files',
label: 'non-Rust-owned files via TypeScript fallback',
}),
]);
}, 30_000);
Expand All @@ -308,7 +308,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', ()
expect(result.stdout).toContain('Fallback health: degraded');
expect(result.stdout).toContain('The index is usable; fallback-degraded files or diagnostics are the only parts that need review.');
expect(result.stdout).toContain('Top fallback reasons:');
expect(result.stdout).toContain('TypeScript fallback files');
expect(result.stdout).toContain('non-Rust-owned files via TypeScript fallback');
expect(result.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run');
} finally {
fs.rmSync(initDir, { recursive: true, force: true });
Expand Down
115 changes: 115 additions & 0 deletions docs/plans/issue-680-fallback-messaging.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"title": "Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files",
"description": "CLI says '30 TypeScript fallback files' but actual is 30 YAML files via TypeScript fallback path. Fix: distinguish fallback implementation from source language in all three print locations.",
"version": 1,
"nodes": {
"1": {
"id": "1",
"label": "Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files",
"status": "in_progress",
"mode": "explore",
"parent": null,
"children": [
"1-1",
"1-2",
"1-3",
"1-4",
"1-5",
"1-6"
],
"decisions": [
{
"q": "修复范围",
"answer": "B. 全面修复",
"note": "index + status + doctor 三个位置"
},
{
"q": "输出格式",
"answer": "B. 原因行+独立语言明细行",
"note": "可扩展"
},
{
"q": "数据传递",
"answer": "A. 扩展profile对象",
"note": "时序安全"
},
{
"q": "存储消息",
"answer": "A. 不改",
"note": "向后兼容"
},
{
"q": "warn一行",
"answer": "A. 改措辞不加明细",
"note": "简洁"
}
],
"notes": ""
},
"1-1": {
"id": "1-1",
"label": "修改 FALLBACK_REASON_LABELS 标签并添加语言明细行到 formatRustHybridFallbackHealthLines",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "文件: src/diagnostics/fallback-summary.ts。1) FALLBACK_REASON_LABELS label 改为 non-Rust-owned files via TypeScript fallback。2) RustHybridFallbackSummary 加 fallbackByLanguage?: Record<string, number>。3) formatRustHybridFallbackHealthLines 追加语言明细行。"
},
"1-2": {
"id": "1-2",
"label": "fallbackSummaryFromHybridMetadata 提取 fallbackByLanguage 字段",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "文件: src/bin/zcodegraph.ts。fallbackSummaryFromHybridMetadata 提取 fallbackByLanguage。"
},
"1-3": {
"id": "1-3",
"label": "typescriptFallbackAppend profile 对象添加 fallbackByLanguage 字段",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "文件: src/bin/zcodegraph.ts。RustIndexProfile.typescriptFallbackAppend 加 fallbackByLanguage。构建 profile 处从 plan 传入。"
},
"1-4": {
"id": "1-4",
"label": "clack.log.warn 一行式警告改措辞为 non-Rust-owned files via TypeScript fallback",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "文件: src/bin/zcodegraph.ts:536。clack.log.warn 改措辞。"
},
"1-5": {
"id": "1-5",
"label": "buildDiagnosticBundleSummary 传递 fallbackByLanguage 到 summary",
"status": "completed",
"mode": "exploit",
"parent": "1",
"children": [],
"decisions": [],
"notes": "文件: src/diagnostics/index.ts。buildDiagnosticBundleSummary 传递 fallbackByLanguage。"
},
"1-6": {
"id": "1-6",
"label": "端到端验证与 PR 提交",
"status": "in_progress",
"mode": "explore",
"parent": "1",
"children": [],
"decisions": [],
"notes": ""
}
},
"metadata": {
"created": "2026-07-13 23:34:32",
"updated": "2026-07-13 23:51:37",
"md_file": "D:/workspace/github/jununfly/ZCodeGraph/docs/plans/issue-680-fallback-messaging.md"
}
}
15 changes: 15 additions & 0 deletions docs/plans/issue-680-fallback-messaging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- ROADMAP_SECTION_START -->
## ZJ Roadmap

> 数据文件: `issue-680-fallback-messaging.json` | 最后更新: 2026-07-13 23:51:37

[~][X+] 1. Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files
├── [x][Y+] 1-1. 修改 FALLBACK_REASON_LABELS 标签并添加语言明细行到 formatRustHybridFallbackHealthLines
├── [x][Y+] 1-2. fallbackSummaryFromHybridMetadata 提取 fallbackByLanguage 字段
├── [x][Y+] 1-3. typescriptFallbackAppend profile 对象添加 fallbackByLanguage 字段
├── [x][Y+] 1-4. clack.log.warn 一行式警告改措辞为 non-Rust-owned files via TypeScript fallback
├── [x][Y+] 1-5. buildDiagnosticBundleSummary 传递 fallbackByLanguage 到 summary
└── [~][X+] 1-6. 端到端验证与 PR 提交

### 当前施工:1-6. 端到端验证与 PR 提交
<!-- ROADMAP_SECTION_END -->
8 changes: 7 additions & 1 deletion src/bin/zcodegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function fallbackSummaryFromHybridMetadata(hybrid: unknown): RustHybridFallbackS
const metadata = hybrid as {
fallbackState?: string;
fallbackFileCount?: number;
fallbackByLanguage?: Record<string, number>;
missingFallbackFileCount?: number;
missingFallbackByLanguage?: Record<string, number>;
fallbackReasonTaxonomy?: Record<string, number>;
Expand All @@ -103,6 +104,7 @@ function fallbackSummaryFromHybridMetadata(hybrid: unknown): RustHybridFallbackS
profile: {
typescriptFallbackAppend: {
fallbackFileCount,
fallbackByLanguage: metadata.fallbackByLanguage ?? {},
missingFallbackFileCount,
missingFallbackByLanguage: metadata.missingFallbackByLanguage ?? {},
},
Expand Down Expand Up @@ -468,6 +470,7 @@ type RustIndexProfile = {
typescriptFallbackAppend?: {
durationMs: number;
fallbackFileCount: number;
fallbackByLanguage?: Record<string, number>;
missingFallbackFileCount?: number;
missingFallbackByLanguage?: Record<string, number>;
errorTaxonomy: Record<string, number>;
Expand Down Expand Up @@ -533,7 +536,7 @@ function printIndexResult(clack: typeof import('@clack/prompts'), result: IndexR
clack.log.info(`${formatNumber(result.nodesCreated)} nodes, ${formatNumber(result.edgesCreated)} edges in ${formatDuration(result.durationMs)}`);
const fallbackAppend = (result.profile as RustIndexProfile | undefined)?.typescriptFallbackAppend;
if (fallbackAppend && fallbackAppend.fallbackFileCount > 0) {
clack.log.warn(`Rust-hybrid appended ${formatNumber(fallbackAppend.fallbackFileCount)} TypeScript fallback files`);
clack.log.warn(`Rust-hybrid appended ${formatNumber(fallbackAppend.fallbackFileCount)} non-Rust-owned files via TypeScript fallback`);
}
} else if (hasErrors) {
clack.log.error(`Indexing failed ${getGlyphs().dash} all ${formatNumber(result.filesErrored)} files had errors`);
Expand Down Expand Up @@ -718,6 +721,7 @@ async function runSelectedIndex(
typescriptFallbackAppend: {
durationMs: fallbackResult.durationMs,
fallbackFileCount: fallbackResult.fallbackFileCount,
fallbackByLanguage: runtimeHybridPlan.fallbackByLanguage,
missingFallbackFileCount: fallbackResult.missingFallbackFileCount,
missingFallbackByLanguage: fallbackResult.missingFallbackByLanguage,
errorTaxonomy: fallbackResult.errorTaxonomy,
Expand All @@ -741,6 +745,7 @@ async function runSelectedIndex(
typescriptFallbackAppend: {
durationMs: fallbackResult.durationMs,
fallbackFileCount: fallbackResult.fallbackFileCount,
fallbackByLanguage: runtimeHybridPlan.fallbackByLanguage,
missingFallbackFileCount: fallbackResult.missingFallbackFileCount,
missingFallbackByLanguage: fallbackResult.missingFallbackByLanguage,
errorTaxonomy: fallbackResult.errorTaxonomy,
Expand Down Expand Up @@ -779,6 +784,7 @@ async function runSelectedIndex(
typescriptFallbackAppend: {
durationMs: fallbackResult.durationMs,
fallbackFileCount: fallbackResult.fallbackFileCount,
fallbackByLanguage: runtimeHybridPlan?.fallbackByLanguage ?? {},
missingFallbackFileCount: fallbackResult.missingFallbackFileCount,
missingFallbackByLanguage: fallbackResult.missingFallbackByLanguage,
errorTaxonomy: fallbackResult.errorTaxonomy,
Expand Down
13 changes: 11 additions & 2 deletions src/diagnostics/fallback-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { rustHybridFallbackStateFor, RustHybridFallbackState } from '../indexing
export type RustHybridFallbackProfile = {
typescriptFallbackAppend?: {
fallbackFileCount?: number;
fallbackByLanguage?: Record<string, number>;
missingFallbackFileCount?: number;
missingFallbackByLanguage?: Record<string, number>;
};
Expand All @@ -22,6 +23,7 @@ export type RustHybridFallbackReasonCount = {
export type RustHybridFallbackSummary = {
fallbackState: RustHybridFallbackState;
fallbackFileCount: number;
fallbackByLanguage: Record<string, number>;
missingFallbackFileCount: number;
missingFallbackByLanguage: Record<string, number>;
fallbackReasonTaxonomy: Record<string, number>;
Expand All @@ -35,7 +37,7 @@ const HEALTHY_GRAPH_USABILITY_MESSAGE =
'The index is fully covered by rust-hybrid without fallback diagnostics.';

const FALLBACK_REASON_LABELS: Record<string, string> = {
'language-level-typescript-fallback': 'TypeScript fallback files',
'language-level-typescript-fallback': 'non-Rust-owned files via TypeScript fallback',
'language-level-fallback-missing-file': 'planned TypeScript fallback files missing from the checkout',
'rust-owned-parse-gap': 'Rust-owned files with parse diagnostics',
'rust-owned-extraction-gap': 'Rust-owned files with extraction diagnostics',
Expand All @@ -60,6 +62,7 @@ export function buildRustHybridFallbackSummary(input: RustHybridFallbackSummaryI
const profile = fallbackProfile(input);
const fallbackAppend = profile?.typescriptFallbackAppend;
const fallbackFileCount = fallbackAppend?.fallbackFileCount ?? 0;
const fallbackByLanguage = fallbackAppend?.fallbackByLanguage ?? {};
const missingFallbackFileCount = fallbackAppend?.missingFallbackFileCount ?? 0;
const missingFallbackByLanguage = fallbackAppend?.missingFallbackByLanguage ?? {};
const fallbackReasonTaxonomy: Record<string, number> = {};
Expand All @@ -80,6 +83,7 @@ export function buildRustHybridFallbackSummary(input: RustHybridFallbackSummaryI
return {
fallbackState,
fallbackFileCount,
fallbackByLanguage,
missingFallbackFileCount,
missingFallbackByLanguage,
fallbackReasonTaxonomy,
Expand All @@ -100,9 +104,14 @@ export function formatRustHybridFallbackHealthLines(summary: RustHybridFallbackS
const reasonBlock = reasons.length > 0
? `Top fallback reasons:\n${reasons.map((reason) => ` ${reason}`).join('\n')}`
: 'Top fallback reasons: unavailable';
const langEntries = Object.entries(summary.fallbackByLanguage)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
const langBlock = langEntries.length > 0
? `\nFallback by source language: ${langEntries.map(([lang, count]) => `${lang} (${count})`).join(', ')}`
: '';
return [
'Fallback health: degraded',
`${summary.graphUsabilityMessage}\n${reasonBlock}`,
`${summary.graphUsabilityMessage}\n${reasonBlock}${langBlock}`,
];
}

Expand Down
2 changes: 2 additions & 0 deletions src/diagnostics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@ export function buildDiagnosticBundleSummary(projectRoot: string, relativeBundle
fallbackState?: string | null;
fallbackFileCount?: number | null;
fallbackReasonTaxonomy?: Record<string, number>;
fallbackByLanguage?: Record<string, number>;
missingFallbackFileCount?: number | null;
missingFallbackByLanguage?: Record<string, number>;
} | null;
Expand Down Expand Up @@ -580,6 +581,7 @@ export function buildDiagnosticBundleSummary(projectRoot: string, relativeBundle
profile: {
typescriptFallbackAppend: {
fallbackFileCount: aggregateTaxonomy.fallbackFileCount ?? 0,
fallbackByLanguage: aggregateTaxonomy.fallbackByLanguage ?? {},
missingFallbackFileCount: aggregateTaxonomy.missingFallbackFileCount ?? 0,
missingFallbackByLanguage: aggregateTaxonomy.missingFallbackByLanguage ?? {},
},
Expand Down