diff --git a/__tests__/issue-680-fallback-messaging.test.ts b/__tests__/issue-680-fallback-messaging.test.ts new file mode 100644 index 00000000..048f21b8 --- /dev/null +++ b/__tests__/issue-680-fallback-messaging.test.ts @@ -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'); + }); +}); diff --git a/__tests__/rust-hybrid-fallback-summary.test.ts b/__tests__/rust-hybrid-fallback-summary.test.ts index 2ffa5c93..9386c4f5 100644 --- a/__tests__/rust-hybrid-fallback-summary.test.ts +++ b/__tests__/rust-hybrid-fallback-summary.test.ts @@ -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', ]); }); diff --git a/__tests__/rust-index-engine-cli-fallback.test.ts b/__tests__/rust-index-engine-cli-fallback.test.ts index e07cc335..64c34338 100644 --- a/__tests__/rust-index-engine-cli-fallback.test.ts +++ b/__tests__/rust-index-engine-cli-fallback.test.ts @@ -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'); @@ -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:'); @@ -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); @@ -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); @@ -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 }); diff --git a/docs/plans/issue-680-fallback-messaging.json b/docs/plans/issue-680-fallback-messaging.json new file mode 100644 index 00000000..8988f087 --- /dev/null +++ b/docs/plans/issue-680-fallback-messaging.json @@ -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。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" + } +} \ No newline at end of file diff --git a/docs/plans/issue-680-fallback-messaging.md b/docs/plans/issue-680-fallback-messaging.md new file mode 100644 index 00000000..ac59c325 --- /dev/null +++ b/docs/plans/issue-680-fallback-messaging.md @@ -0,0 +1,15 @@ + +## 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 提交 + diff --git a/src/bin/zcodegraph.ts b/src/bin/zcodegraph.ts index 37902b9c..36f6dae0 100644 --- a/src/bin/zcodegraph.ts +++ b/src/bin/zcodegraph.ts @@ -90,6 +90,7 @@ function fallbackSummaryFromHybridMetadata(hybrid: unknown): RustHybridFallbackS const metadata = hybrid as { fallbackState?: string; fallbackFileCount?: number; + fallbackByLanguage?: Record; missingFallbackFileCount?: number; missingFallbackByLanguage?: Record; fallbackReasonTaxonomy?: Record; @@ -103,6 +104,7 @@ function fallbackSummaryFromHybridMetadata(hybrid: unknown): RustHybridFallbackS profile: { typescriptFallbackAppend: { fallbackFileCount, + fallbackByLanguage: metadata.fallbackByLanguage ?? {}, missingFallbackFileCount, missingFallbackByLanguage: metadata.missingFallbackByLanguage ?? {}, }, @@ -468,6 +470,7 @@ type RustIndexProfile = { typescriptFallbackAppend?: { durationMs: number; fallbackFileCount: number; + fallbackByLanguage?: Record; missingFallbackFileCount?: number; missingFallbackByLanguage?: Record; errorTaxonomy: Record; @@ -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`); @@ -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, @@ -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, @@ -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, diff --git a/src/diagnostics/fallback-summary.ts b/src/diagnostics/fallback-summary.ts index 32f9a444..f387a5f3 100644 --- a/src/diagnostics/fallback-summary.ts +++ b/src/diagnostics/fallback-summary.ts @@ -3,6 +3,7 @@ import { rustHybridFallbackStateFor, RustHybridFallbackState } from '../indexing export type RustHybridFallbackProfile = { typescriptFallbackAppend?: { fallbackFileCount?: number; + fallbackByLanguage?: Record; missingFallbackFileCount?: number; missingFallbackByLanguage?: Record; }; @@ -22,6 +23,7 @@ export type RustHybridFallbackReasonCount = { export type RustHybridFallbackSummary = { fallbackState: RustHybridFallbackState; fallbackFileCount: number; + fallbackByLanguage: Record; missingFallbackFileCount: number; missingFallbackByLanguage: Record; fallbackReasonTaxonomy: Record; @@ -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 = { - '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', @@ -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 = {}; @@ -80,6 +83,7 @@ export function buildRustHybridFallbackSummary(input: RustHybridFallbackSummaryI return { fallbackState, fallbackFileCount, + fallbackByLanguage, missingFallbackFileCount, missingFallbackByLanguage, fallbackReasonTaxonomy, @@ -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}`, ]; } diff --git a/src/diagnostics/index.ts b/src/diagnostics/index.ts index 46b33784..6035362b 100644 --- a/src/diagnostics/index.ts +++ b/src/diagnostics/index.ts @@ -541,6 +541,7 @@ export function buildDiagnosticBundleSummary(projectRoot: string, relativeBundle fallbackState?: string | null; fallbackFileCount?: number | null; fallbackReasonTaxonomy?: Record; + fallbackByLanguage?: Record; missingFallbackFileCount?: number | null; missingFallbackByLanguage?: Record; } | null; @@ -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 ?? {}, },