From e944614a63efd0a3f89d0a942b193f47b55e7446 Mon Sep 17 00:00:00 2001 From: bilibili Date: Tue, 14 Jul 2026 00:29:51 +0800 Subject: [PATCH] fix: add three-tier fallback health state (healthy/partial/degraded) for Issue #682 Distinguish expected fallbacks (non-Rust-owned languages like YAML) from unexpected gaps (Rust-owned parse/extraction failures, missing files). State logic: - healthy: zero fallbacks, zero gaps - partial: only language-level-typescript-fallback (expected, non-Rust-owned) - degraded: any rust-owned-* gap or language-level-fallback-missing-file Changes: - RustHybridFallbackState type: add 'partial' - rustHybridFallbackStateFor(): three-tier logic - formatRustHybridFallbackHealthLines(): partial output with info line + lang breakdown - formatRustHybridFallbackDoctorHint(): partial returns hint without doctor command - clack.log: conditional info (partial) / warn (degraded) - statusFallbackDiagnostics: returns data for partial (empty doctorCommand) - classifyGraphHealth: partial treated as healthy graph health - diagnostics/index.ts: fallbackState override handles partial - 11 new TDD tests + 7 existing tests updated Closes #682 --- .../issue-680-fallback-messaging.test.ts | 4 +- .../issue-682-partial-fallback-state.test.ts | 167 ++++++++++++++++++ .../rust-hybrid-fallback-summary.test.ts | 7 +- .../rust-index-engine-cli-fallback.test.ts | 42 +++-- docs/plans/issue-680-fallback-messaging.json | 8 +- docs/plans/issue-680-fallback-messaging.md | 8 +- .../issue-682-partial-fallback-state.json | 109 ++++++++++++ .../plans/issue-682-partial-fallback-state.md | 29 +++ src/bin/zcodegraph.ts | 28 ++- src/diagnostics/fallback-summary.ts | 23 ++- src/diagnostics/index.ts | 6 +- src/indexing/rust-hybrid-contract.ts | 9 +- 12 files changed, 386 insertions(+), 54 deletions(-) create mode 100644 __tests__/issue-682-partial-fallback-state.test.ts create mode 100644 docs/plans/issue-682-partial-fallback-state.json create mode 100644 docs/plans/issue-682-partial-fallback-state.md diff --git a/__tests__/issue-680-fallback-messaging.test.ts b/__tests__/issue-680-fallback-messaging.test.ts index 048f21b..1756bed 100644 --- a/__tests__/issue-680-fallback-messaging.test.ts +++ b/__tests__/issue-680-fallback-messaging.test.ts @@ -26,8 +26,8 @@ describe('Issue #680: fallback messaging distinguishes implementation from sourc 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'); + // Partial state: info line uses "files indexed via TypeScript fallback (non-Rust-owned languages)" + expect(joined).toContain('30 files indexed via TypeScript fallback (non-Rust-owned languages)'); expect(joined).not.toContain('30 TypeScript fallback files'); // Language breakdown line diff --git a/__tests__/issue-682-partial-fallback-state.test.ts b/__tests__/issue-682-partial-fallback-state.test.ts new file mode 100644 index 0000000..337aa82 --- /dev/null +++ b/__tests__/issue-682-partial-fallback-state.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it } from 'vitest'; +import { + rustHybridFallbackStateFor, + planRustHybridAssignments, + mergeRustOwnedGapDiagnostics, + type RustHybridAssignmentPlan, +} from '../src/indexing/rust-hybrid-contract'; +import { + buildRustHybridFallbackSummary, + formatRustHybridFallbackHealthLines, + formatRustHybridFallbackDoctorHint, +} from '../src/diagnostics/fallback-summary'; +import { classifyGraphHealth } from '../src/diagnostics/graph-health'; + +describe('Issue #682: three-tier fallback health state (healthy/partial/degraded)', () => { + describe('rustHybridFallbackStateFor', () => { + it('returns partial when only language-level-typescript-fallback exists', () => { + const state = rustHybridFallbackStateFor(5, { + 'language-level-typescript-fallback': 5, + }); + expect(state).toBe('partial'); + }); + + it('returns degraded when rust-owned-parse-gap is present alongside expected fallback', () => { + const state = rustHybridFallbackStateFor(5, { + 'language-level-typescript-fallback': 5, + 'rust-owned-parse-gap': 1, + }); + expect(state).toBe('degraded'); + }); + + it('returns degraded when language-level-fallback-missing-file is present', () => { + const state = rustHybridFallbackStateFor(5, { + 'language-level-typescript-fallback': 5, + 'language-level-fallback-missing-file': 2, + }); + expect(state).toBe('degraded'); + }); + + it('returns healthy when no fallbacks exist', () => { + const state = rustHybridFallbackStateFor(0, {}); + expect(state).toBe('healthy'); + }); + }); + + describe('buildRustHybridFallbackSummary', () => { + it('returns partial state for non-Rust-owned language fallback without gaps', () => { + const summary = buildRustHybridFallbackSummary({ + success: true, + profile: { + typescriptFallbackAppend: { + fallbackFileCount: 58, + fallbackByLanguage: { yaml: 58 }, + }, + }, + errors: [], + }); + + expect(summary.fallbackState).toBe('partial'); + expect(summary.graphUsabilityMessage).not.toContain('need review'); + }); + + it('returns degraded state when rust-owned gaps are present', () => { + const summary = buildRustHybridFallbackSummary({ + success: true, + profile: { + typescriptFallbackAppend: { + fallbackFileCount: 1, + }, + }, + errors: [ + { severity: 'warning', code: 'rust-owned-parse-gap' }, + ], + }); + + expect(summary.fallbackState).toBe('degraded'); + }); + }); + + describe('formatRustHybridFallbackHealthLines', () => { + it('outputs partial format with info line and language breakdown', () => { + const summary = buildRustHybridFallbackSummary({ + success: true, + profile: { + typescriptFallbackAppend: { + fallbackFileCount: 58, + fallbackByLanguage: { yaml: 58 }, + }, + }, + errors: [], + }); + + const lines = formatRustHybridFallbackHealthLines(summary); + const joined = lines.join('\n'); + + expect(joined).toContain('Fallback health: partial'); + expect(joined).toContain('58 files indexed via TypeScript fallback (non-Rust-owned languages)'); + expect(joined).toContain('Fallback by source language: yaml (58)'); + expect(joined).not.toContain('need review'); + }); + + it('outputs partial format without language breakdown when absent', () => { + const summary = buildRustHybridFallbackSummary({ + success: true, + profile: { + typescriptFallbackAppend: { + fallbackFileCount: 5, + }, + }, + errors: [], + }); + + const lines = formatRustHybridFallbackHealthLines(summary); + const joined = lines.join('\n'); + + expect(joined).toContain('Fallback health: partial'); + expect(joined).toContain('5 files indexed via TypeScript fallback (non-Rust-owned languages)'); + expect(joined).not.toContain('Fallback by source language'); + expect(joined).not.toContain('need review'); + }); + }); + + describe('formatRustHybridFallbackDoctorHint', () => { + it('returns hint for partial state with info-level health line', () => { + const summary = buildRustHybridFallbackSummary({ + success: true, + profile: { + typescriptFallbackAppend: { + fallbackFileCount: 10, + fallbackByLanguage: { yaml: 10 }, + }, + }, + errors: [], + }); + + const hint = formatRustHybridFallbackDoctorHint(summary); + expect(hint.length).toBeGreaterThan(0); + expect(hint[0]).toBe('Indexed with rust-hybrid'); + expect(hint[1]).toContain('Fallback health: partial'); + }); + }); + + describe('classifyGraphHealth', () => { + it('treats partial fallback state as healthy graph health', () => { + const health = classifyGraphHealth({ + initialized: true, + databasePath: '/fake/path', + databasePresent: true, + hybridFallbackState: 'partial', + }); + + expect(health.state).toBe('healthy'); + expect(health.usable).toBe(true); + }); + + it('still treats degraded fallback state as degraded graph health', () => { + const health = classifyGraphHealth({ + initialized: true, + databasePath: '/fake/path', + databasePresent: true, + hybridFallbackState: 'degraded', + }); + + expect(health.state).toBe('degraded'); + }); + }); +}); diff --git a/__tests__/rust-hybrid-fallback-summary.test.ts b/__tests__/rust-hybrid-fallback-summary.test.ts index 9386c4f..b4ef75e 100644 --- a/__tests__/rust-hybrid-fallback-summary.test.ts +++ b/__tests__/rust-hybrid-fallback-summary.test.ts @@ -47,7 +47,7 @@ describe('rust-hybrid fallback summary contract', () => { expect(JSON.stringify(summary)).not.toContain('function '); }); - it('formats the existing first-screen degraded doctor hint from the summary', () => { + it('formats the first-screen partial doctor hint for non-Rust-owned language fallback', () => { const summary = buildRustHybridFallbackSummary({ success: true, profile: { typescriptFallbackAppend: { fallbackFileCount: 1 } }, @@ -56,9 +56,8 @@ 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 non-Rust-owned files via TypeScript fallback', - 'Run diagnostic bundle:\n zcodegraph doctor --engine rust-hybrid --bundle --last-run', + 'Fallback health: partial', + 'Index is complete; some files were indexed via TypeScript fallback for non-Rust-owned languages.\n1 files indexed via TypeScript fallback (non-Rust-owned languages)', ]); }); diff --git a/__tests__/rust-index-engine-cli-fallback.test.ts b/__tests__/rust-index-engine-cli-fallback.test.ts index 64c3433..617f79a 100644 --- a/__tests__/rust-index-engine-cli-fallback.test.ts +++ b/__tests__/rust-index-engine-cli-fallback.test.ts @@ -64,7 +64,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', () expect(plan.missingFallbackByLanguage).toEqual({}); expect(plan.missingFallbackFileCount).toBe(0); expect(plan.skippedGeneratedByLanguage.go).toBe(1); - expect(plan.fallbackState).toBe('degraded'); + expect(plan.fallbackState).toBe('partial'); expect(plan.fallbackReasonTaxonomy).toMatchObject({ 'language-level-typescript-fallback': 1 }); expect(plan.pendingFallbacks).toContain('rust-owned-parse-gap'); }); @@ -230,13 +230,13 @@ 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('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:'); - expect(result.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run'); + expect(result.stdout).toContain('Fallback health: partial'); + expect(result.stdout).toContain('files indexed via TypeScript fallback (non-Rust-owned languages)'); + expect(result.stdout).not.toContain('need review'); + expect(result.stdout).not.toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run'); }, 30_000); - it('surfaces degraded fallback quality details in status output', () => { + it('surfaces partial fallback quality details in status output', () => { fs.writeFileSync(path.join(tempDir, 'routing.yml'), 'app:\n path: /health\n'); const index = runZcodegraphCli(tempDir, ['index', '--quiet'], { @@ -247,16 +247,15 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', () 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('State: healthy'); 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('Fallback health: partial'); 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'); + expect(status.stdout).not.toContain('per-file-diagnostics.json'); + expect(status.stdout).not.toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run'); }, 30_000); - it('exposes degraded fallback quality details in status json', () => { + it('exposes partial fallback quality details in status json', () => { fs.writeFileSync(path.join(tempDir, 'routing.yml'), 'app:\n path: /health\n'); const index = runZcodegraphCli(tempDir, ['index', '--quiet'], { @@ -278,11 +277,11 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', () }; }; 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.', + state: 'partial', + doctorCommand: '', + artifactHint: '', }); - expect(status.fallbackDiagnostics.graphUsabilityMessage).toContain('The index is usable'); + expect(status.fallbackDiagnostics.graphUsabilityMessage).toContain('Index is complete'); expect(status.fallbackDiagnostics.topReasons).toEqual([ expect.objectContaining({ code: 'language-level-typescript-fallback', @@ -292,7 +291,7 @@ describe('zcodegraph rust-hybrid fallback degraded status and doctor output', () ]); }, 30_000); - it('prints the degraded fallback health explanation during rust-hybrid init', () => { + it('prints the partial fallback health explanation during rust-hybrid init', () => { const initDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zcodegraph-rust-hybrid-fallback-init-')); try { fs.writeFileSync(path.join(initDir, 'a.ts'), 'export const initValue = 1;\n'); @@ -305,11 +304,10 @@ 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('Initialized in'); expect(result.stdout).toContain('Indexed with rust-hybrid'); - 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('non-Rust-owned files via TypeScript fallback'); - expect(result.stdout).toContain('zcodegraph doctor --engine rust-hybrid --bundle --last-run'); + expect(result.stdout).toContain('Fallback health: partial'); + expect(result.stdout).toContain('files indexed via TypeScript fallback (non-Rust-owned languages)'); + expect(result.stdout).not.toContain('need review'); + expect(result.stdout).not.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 index 8988f08..31b2918 100644 --- a/docs/plans/issue-680-fallback-messaging.json +++ b/docs/plans/issue-680-fallback-messaging.json @@ -6,7 +6,7 @@ "1": { "id": "1", "label": "Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files", - "status": "in_progress", + "status": "completed", "mode": "explore", "parent": null, "children": [ @@ -99,17 +99,17 @@ "1-6": { "id": "1-6", "label": "端到端验证与 PR 提交", - "status": "in_progress", + "status": "completed", "mode": "explore", "parent": "1", "children": [], "decisions": [], - "notes": "" + "notes": "PR #684: https://github.com/jununfly/ZCodeGraph/pull/684 — 8/8 tests pass, tsc zero errors." } }, "metadata": { "created": "2026-07-13 23:34:32", - "updated": "2026-07-13 23:51:37", + "updated": "2026-07-13 23:53:07", "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 index ac59c32..09c6e35 100644 --- a/docs/plans/issue-680-fallback-messaging.md +++ b/docs/plans/issue-680-fallback-messaging.md @@ -1,15 +1,13 @@ ## ZJ Roadmap -> 数据文件: `issue-680-fallback-messaging.json` | 最后更新: 2026-07-13 23:51:37 +> 数据文件: `issue-680-fallback-messaging.json` | 最后更新: 2026-07-13 23:53:07 -[~][X+] 1. Issue #680: Clarify rust-hybrid fallback degraded summary for non-Rust-owned YAML files +[x][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 提交 +└── [x][X+] 1-6. 端到端验证与 PR 提交 diff --git a/docs/plans/issue-682-partial-fallback-state.json b/docs/plans/issue-682-partial-fallback-state.json new file mode 100644 index 0000000..9a74059 --- /dev/null +++ b/docs/plans/issue-682-partial-fallback-state.json @@ -0,0 +1,109 @@ +{ + "title": "Issue #682: Three-tier fallback health state (healthy/partial/degraded)", + "description": "Refine rustHybridFallbackStateFor() to distinguish expected fallbacks (non-Rust-owned languages) from unexpected gaps (rust-owned parse/extraction failures, missing files). Add 'partial' state for expected-only fallbacks.", + "version": 1, + "nodes": { + "1": { + "id": "1", + "label": "Issue #682: Three-tier fallback health state (healthy/partial/degraded)", + "status": "in_progress", + "mode": "explore", + "parent": null, + "children": [ + "1-1", + "1-2", + "1-3", + "1-4", + "1-5" + ], + "decisions": [ + { + "q": "PR #684 是否已解决", + "answer": "B. 需进一步改进 degraded 语义", + "note": "PR #684 修复了措辞但 degraded 语义仍需调整" + }, + { + "q": "状态模型", + "answer": "A. 三级 healthy/partial/degraded", + "note": "partial = 仅预期 fallback (non-Rust-owned 语言)" + }, + { + "q": "改动范围", + "answer": "A. 仅状态语义", + "note": "不扩展 Rust-owned 语言,单独 issue 跟踪" + }, + { + "q": "partial log 级别", + "answer": "A. info (degraded 保持 warn)", + "note": "log 级别与状态语义一致" + }, + { + "q": "partial 消息格式", + "answer": "A. 信息行 + 语言明细", + "note": "不含 need review 语句" + }, + { + "q": "missing-file 归类", + "answer": "A. 归入 degraded", + "note": "索引不完整" + } + ], + "notes": "" + }, + "1-1": { + "id": "1-1", + "label": "Add 'partial' to RustHybridFallbackState type and update rustHybridFallbackStateFor() three-tier logic", + "status": "completed", + "mode": "exploit", + "parent": "1", + "children": [], + "decisions": [], + "notes": "File: src/indexing/rust-hybrid-contract.ts. 1) RustHybridFallbackState type add 'partial'. 2) rustHybridFallbackStateFor(): three-tier logic. 3) fallback-summary.ts: PARTIAL_GRAPH_USABILITY_MESSAGE, formatRustHybridFallbackHealthLines partial branch, formatRustHybridFallbackDoctorHint partial branch. 4) graph-health.ts: no change needed (partial falls through to healthy). 11/11 tests pass." + }, + "1-2": { + "id": "1-2", + "label": "Update existing tests: assert 'partial' instead of 'degraded' for non-Rust-owned language fallback scenarios", + "status": "completed", + "mode": "exploit", + "parent": "1", + "children": [], + "decisions": [], + "notes": "Updated: rust-hybrid-fallback-summary.test.ts (1 test), issue-680-fallback-messaging.test.ts (1 test), rust-index-engine-cli-fallback.test.ts (5 tests). All non-Rust-owned language fallback assertions changed from degraded to partial." + }, + "1-3": { + "id": "1-3", + "label": "Update clack.log in zcodegraph.ts: conditional info/warn based on fallback state", + "status": "completed", + "mode": "exploit", + "parent": "1", + "children": [], + "decisions": [], + "notes": "zcodegraph.ts: fallbackSummaryFromHybridMetadata handles partial state. statusFallbackDiagnostics returns data for partial (empty doctorCommand). printRustHybridFallbackStatus shows for partial. clack.log conditional info/warn. printRustHybridDoctorHint conditional log level. diagnostics/index.ts: fallbackState override handles partial." + }, + "1-4": { + "id": "1-4", + "label": "Update formatRustHybridFallbackHealthLines() for partial state output (info line + language breakdown)", + "status": "completed", + "mode": "exploit", + "parent": "1", + "children": [], + "decisions": [], + "notes": "formatRustHybridFallbackHealthLines: partial branch with info line + language breakdown. formatRustHybridFallbackDoctorHint: partial returns hint without Run diagnostic bundle. PARTIAL_GRAPH_USABILITY_MESSAGE added." + }, + "1-5": { + "id": "1-5", + "label": "End-to-end verification and PR submission", + "status": "pending", + "mode": "explore", + "parent": "1", + "children": [], + "decisions": [], + "notes": "" + } + }, + "metadata": { + "created": "2026-07-14 00:12:37", + "updated": "2026-07-14 00:28:56", + "md_file": "D:/workspace/github/jununfly/ZCodeGraph/docs/plans/issue-682-partial-fallback-state.md" + } +} \ No newline at end of file diff --git a/docs/plans/issue-682-partial-fallback-state.md b/docs/plans/issue-682-partial-fallback-state.md new file mode 100644 index 0000000..2e4bcdd --- /dev/null +++ b/docs/plans/issue-682-partial-fallback-state.md @@ -0,0 +1,29 @@ + +## ZJ Roadmap + +> 数据文件: `issue-682-partial-fallback-state.json` | 最后更新: 2026-07-14 00:28:56 + +[~][X+] 1. Issue #682: Three-tier fallback health state (healthy/partial/degraded) +├── [x][Y+] 1-1. Add 'partial' to RustHybridFallbackState type and update rustHybridFallbackStateFor() three-tier logic +├── [x][Y+] 1-2. Update existing tests: assert 'partial' instead of 'degraded' for non-Rust-owned language fallback scenarios +├── [x][Y+] 1-3. Update clack.log in zcodegraph.ts: conditional info/warn based on fallback state +├── [x][Y+] 1-4. Update formatRustHybridFallbackHealthLines() for partial state output (info line + language breakdown) +└── [ ][X+] 1-5. End-to-end verification and PR submission + +### 当前施工:1. Issue #682: Three-tier fallback health state (healthy/partial/degraded) + +**决策:** +- Q: PR #684 是否已解决 → B. 需进一步改进 degraded 语义 (PR #684 修复了措辞但 degraded 语义仍需调整) +- Q: 状态模型 → A. 三级 healthy/partial/degraded (partial = 仅预期 fallback (non-Rust-owned 语言)) +- Q: 改动范围 → A. 仅状态语义 (不扩展 Rust-owned 语言,单独 issue 跟踪) +- Q: partial log 级别 → A. info (degraded 保持 warn) (log 级别与状态语义一致) +- Q: partial 消息格式 → A. 信息行 + 语言明细 (不含 need review 语句) +- Q: missing-file 归类 → A. 归入 degraded (索引不完整) + +**当前子树:** +├── [x][Y+] 1-1. Add 'partial' to RustHybridFallbackState type and update rustHybridFallbackStateFor() three-tier logic +├── [x][Y+] 1-2. Update existing tests: assert 'partial' instead of 'degraded' for non-Rust-owned language fallback scenarios +├── [x][Y+] 1-3. Update clack.log in zcodegraph.ts: conditional info/warn based on fallback state +├── [x][Y+] 1-4. Update formatRustHybridFallbackHealthLines() for partial state output (info line + language breakdown) +└── [ ][X+] 1-5. End-to-end verification and PR submission + diff --git a/src/bin/zcodegraph.ts b/src/bin/zcodegraph.ts index 36f6dae..2de2434 100644 --- a/src/bin/zcodegraph.ts +++ b/src/bin/zcodegraph.ts @@ -114,16 +114,21 @@ function fallbackSummaryFromHybridMetadata(hybrid: unknown): RustHybridFallbackS 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; + summary.fallbackState = (metadata.fallbackState === 'degraded' || metadata.fallbackState === 'partial') + ? metadata.fallbackState as RustHybridFallbackSummary['fallbackState'] + : summary.fallbackState; if (summary.fallbackState === 'degraded') { summary.graphUsabilityMessage = 'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.'; + } else if (summary.fallbackState === 'partial') { + summary.graphUsabilityMessage = 'Index is complete; some files were indexed via TypeScript fallback for non-Rust-owned languages.'; } return summary; } function statusFallbackDiagnostics(hybrid: unknown): RustHybridStatusFallbackDiagnostics { const summary = fallbackSummaryFromHybridMetadata(hybrid); - if (!summary || summary.fallbackState !== 'degraded') return null; + if (!summary || summary.fallbackState === 'healthy') return null; + const isDegraded = summary.fallbackState === 'degraded'; return { state: summary.fallbackState, graphUsabilityMessage: summary.graphUsabilityMessage, @@ -134,20 +139,22 @@ function statusFallbackDiagnostics(hybrid: unknown): RustHybridStatusFallbackDia ...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.', + doctorCommand: isDegraded ? 'zcodegraph doctor --engine rust-hybrid --bundle --last-run' : '', + artifactHint: isDegraded ? '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; + if (!summary || summary.fallbackState === 'healthy') 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'); + if (summary.fallbackState === 'degraded') { + 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(); } @@ -536,7 +543,9 @@ 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)} non-Rust-owned files via TypeScript fallback`); + const fallbackSummary = buildRustHybridFallbackSummary(result); + const logFn = fallbackSummary.fallbackState === 'degraded' ? clack.log.warn : clack.log.info; + logFn(`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`); @@ -856,7 +865,8 @@ function printRustHybridDoctorHint( const [indexedLine, healthLine, ...detailLines] = formatRustHybridFallbackDoctorHint(summary); if (!indexedLine || !healthLine) return; clack.log.info(indexedLine); - clack.log.warn(healthLine); + const healthLogFn = summary.fallbackState === 'degraded' ? clack.log.warn : clack.log.info; + healthLogFn(healthLine); for (const line of detailLines) { clack.log.info(line); } diff --git a/src/diagnostics/fallback-summary.ts b/src/diagnostics/fallback-summary.ts index f387a5f..43efb1b 100644 --- a/src/diagnostics/fallback-summary.ts +++ b/src/diagnostics/fallback-summary.ts @@ -35,6 +35,8 @@ const DEGRADED_GRAPH_USABILITY_MESSAGE = 'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.'; const HEALTHY_GRAPH_USABILITY_MESSAGE = 'The index is fully covered by rust-hybrid without fallback diagnostics.'; +const PARTIAL_GRAPH_USABILITY_MESSAGE = + 'Index is complete; some files were indexed via TypeScript fallback for non-Rust-owned languages.'; const FALLBACK_REASON_LABELS: Record = { 'language-level-typescript-fallback': 'non-Rust-owned files via TypeScript fallback', @@ -90,7 +92,9 @@ export function buildRustHybridFallbackSummary(input: RustHybridFallbackSummaryI topFallbackReasons: sortFallbackReasons(fallbackReasonTaxonomy), graphUsabilityMessage: fallbackState === 'degraded' ? DEGRADED_GRAPH_USABILITY_MESSAGE - : HEALTHY_GRAPH_USABILITY_MESSAGE, + : fallbackState === 'partial' + ? PARTIAL_GRAPH_USABILITY_MESSAGE + : HEALTHY_GRAPH_USABILITY_MESSAGE, }; } @@ -99,7 +103,18 @@ function formatReasonCount(reason: RustHybridFallbackReasonCount): string { } export function formatRustHybridFallbackHealthLines(summary: RustHybridFallbackSummary): string[] { - if (summary.fallbackState !== 'degraded') return ['Fallback health: healthy']; + if (summary.fallbackState === 'healthy') return ['Fallback health: healthy']; + if (summary.fallbackState === 'partial') { + 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: partial', + `${summary.graphUsabilityMessage}\n${summary.fallbackFileCount} files indexed via TypeScript fallback (non-Rust-owned languages)${langBlock}`, + ]; + } const reasons = summary.topFallbackReasons.slice(0, 3).map(formatReasonCount); const reasonBlock = reasons.length > 0 ? `Top fallback reasons:\n${reasons.map((reason) => ` ${reason}`).join('\n')}` @@ -119,10 +134,10 @@ export function formatRustHybridFallbackDoctorHint( summary: RustHybridFallbackSummary, doctorCommand = 'zcodegraph doctor --engine rust-hybrid --bundle --last-run', ): string[] { - if (summary.fallbackState !== 'degraded') return []; + if (summary.fallbackState === 'healthy') return []; return [ 'Indexed with rust-hybrid', ...formatRustHybridFallbackHealthLines(summary), - `Run diagnostic bundle:\n ${doctorCommand}`, + ...(summary.fallbackState === 'degraded' ? [`Run diagnostic bundle:\n ${doctorCommand}`] : []), ]; } diff --git a/src/diagnostics/index.ts b/src/diagnostics/index.ts index 6035362..66c72cf 100644 --- a/src/diagnostics/index.ts +++ b/src/diagnostics/index.ts @@ -591,13 +591,15 @@ export function buildDiagnosticBundleSummary(projectRoot: string, relativeBundle fallbackSummary.topFallbackReasons = Object.entries(fallbackReasonTaxonomy) .map(([code, count]) => ({ code, count })) .sort((left, right) => right.count - left.count || left.code.localeCompare(right.code)); - fallbackSummary.fallbackState = aggregateTaxonomy.fallbackState === 'degraded' - ? 'degraded' + fallbackSummary.fallbackState = (aggregateTaxonomy.fallbackState === 'degraded' || aggregateTaxonomy.fallbackState === 'partial') + ? aggregateTaxonomy.fallbackState as typeof fallbackSummary.fallbackState : fallbackSummary.fallbackState; const graphHealth = source === 'last-failure' ? 'failed' : fallbackSummary.fallbackState; lines.push(`Graph health: ${graphHealth}`); if (fallbackSummary.fallbackState === 'degraded') { fallbackSummary.graphUsabilityMessage = 'The index is usable; fallback-degraded files or diagnostics are the only parts that need review.'; + } else if (fallbackSummary.fallbackState === 'partial') { + fallbackSummary.graphUsabilityMessage = 'Index is complete; some files were indexed via TypeScript fallback for non-Rust-owned languages.'; } lines.push(...formatRustHybridFallbackHealthLines(fallbackSummary)); if (fallbackSummary.fallbackState === 'degraded') { diff --git a/src/indexing/rust-hybrid-contract.ts b/src/indexing/rust-hybrid-contract.ts index e74f3df..8dfbf91 100644 --- a/src/indexing/rust-hybrid-contract.ts +++ b/src/indexing/rust-hybrid-contract.ts @@ -7,7 +7,7 @@ import * as path from 'path'; export const RUST_HYBRID_PHASE = 'phase-6-rust-owned-per-file-gap-fallback'; export const RUST_HYBRID_RUST_OWNED_LANGUAGES = ['javascript', 'jsx', 'typescript', 'tsx', 'go', 'java', 'python', 'rust', 'c'] as const; -export type RustHybridFallbackState = 'healthy' | 'degraded' | 'pending'; +export type RustHybridFallbackState = 'healthy' | 'partial' | 'degraded' | 'pending'; export type RustOwnedGapCode = | 'rust-owned-parse-gap' | 'rust-owned-extraction-gap' @@ -60,7 +60,12 @@ export function rustHybridFallbackStateFor( fallbackFileCount: number, fallbackReasonTaxonomy: Record, ): RustHybridFallbackState { - return fallbackFileCount > 0 || Object.keys(fallbackReasonTaxonomy).length > 0 ? 'degraded' : 'healthy'; + const hasAnyReason = fallbackFileCount > 0 || Object.keys(fallbackReasonTaxonomy).length > 0; + if (!hasAnyReason) return 'healthy'; + const hasUnexpectedReasons = Object.keys(fallbackReasonTaxonomy).some( + (code) => code !== 'language-level-typescript-fallback', + ); + return hasUnexpectedReasons ? 'degraded' : 'partial'; } export function buildRustHybridMetadata(projectPath: string): RustHybridMetadata {