From 394f691e3917d165fc3b6a9dd69be8fa9aa03cd5 Mon Sep 17 00:00:00 2001 From: bilibili Date: Mon, 13 Jul 2026 23:52:07 +0800 Subject: [PATCH] fix: clarify rust-hybrid fallback messaging for non-Rust-owned files (Issue #680) CLI said '30 TypeScript fallback files' but actual is 30 YAML files indexed via the TypeScript fallback path. The label conflated the fallback implementation (TypeScript) with the source file language. Fix: distinguish 'non-Rust-owned files via TypeScript fallback' (reason) from source language breakdown (yaml: 30, css: 3, ...) in all three CLI print locations. Changes: - FALLBACK_REASON_LABELS: 'TypeScript fallback files' -> 'non-Rust-owned files via TypeScript fallback' - formatRustHybridFallbackHealthLines: append 'Fallback by source language' line - RustHybridFallbackSummary + Profile: add fallbackByLanguage field - fallbackSummaryFromHybridMetadata: extract fallbackByLanguage from DB metadata - typescriptFallbackAppend profile: pass fallbackByLanguage from plan (3 construction sites) - clack.log.warn: 'TypeScript fallback files' -> 'non-Rust-owned files via TypeScript fallback' - buildDiagnosticBundleSummary: pass fallbackByLanguage from aggregateTaxonomy - Update existing test assertions to new label TDD: 4 new tests (issue-680-fallback-messaging.test.ts), 8/8 pass. TypeScript: zero errors. Storage message (fallbackMessage) unchanged per Q4 decision. --- .../issue-680-fallback-messaging.test.ts | 71 +++++++++++ .../rust-hybrid-fallback-summary.test.ts | 2 +- .../rust-index-engine-cli-fallback.test.ts | 10 +- docs/plans/issue-680-fallback-messaging.json | 115 ++++++++++++++++++ docs/plans/issue-680-fallback-messaging.md | 15 +++ src/bin/zcodegraph.ts | 8 +- src/diagnostics/fallback-summary.ts | 13 +- src/diagnostics/index.ts | 2 + 8 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 __tests__/issue-680-fallback-messaging.test.ts create mode 100644 docs/plans/issue-680-fallback-messaging.json create mode 100644 docs/plans/issue-680-fallback-messaging.md 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 ?? {}, },