#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlindSpot {
pub area: &'static str,
pub description: &'static str,
pub description_ja: &'static str,
}
#[derive(Debug, Clone, Default)]
pub struct ManifestContext {
pub git_used: bool,
pub tests_excluded: bool,
pub parse_failures: usize,
}
#[derive(Debug, Clone, Default)]
pub struct AnalysisManifest {
pub blind_spots: Vec<BlindSpot>,
pub notes: Vec<String>,
pub notes_ja: Vec<String>,
}
impl AnalysisManifest {
pub fn localized_notes(&self, japanese: bool) -> &[String] {
if japanese {
&self.notes_ja
} else {
&self.notes
}
}
}
const STRUCTURAL_BLIND_SPOTS: &[BlindSpot] = &[
BlindSpot {
area: "dynamic-connascence",
description: "Dynamic connascence (Execution, Timing, Values, Identity) is a runtime \
property; static AST analysis cannot detect it. Order/timing/shared-state \
coupling will not appear here.",
description_ja: "動的コナーセンス (Execution, Timing, Values, Identity) は実行時の性質です。\
静的AST解析では検出できないため、順序・タイミング・共有状態による結合はここには現れません。",
},
BlindSpot {
area: "implicit-functional-coupling",
description: "Duplicated business logic and connascence of meaning/algorithm with no \
explicit call or import are only partially covered: strong temporal \
co-change can reveal hidden coupling between files, but duplicated logic \
that does not co-change remains invisible.",
description_ja: "明示的な呼び出しやimportを伴わないビジネスロジックの重複、意味やアルゴリズムのコナーセンスは一部しか扱えません。\
強い共変更はファイル間の隠れた結合を示せますが、共変更しない重複ロジックは見えません。",
},
BlindSpot {
area: "distance-axes",
description: "Distance is measured by code structure only. Organizational distance \
(Conway's Law / team boundaries) and runtime distance (sync vs async) are \
not modeled, so the balance verdict reflects only structural distance.",
description_ja: "距離はコード構造だけで測定します。組織上の距離 (コンウェイの法則やチーム境界) と実行時の距離 (同期/非同期) はモデル化しないため、\
バランス判定は構造上の距離だけを反映します。",
},
BlindSpot {
area: "macro-and-cfg",
description: "Coupling introduced by macro expansion, or behind inactive `cfg(...)`, is \
invisible to syn-based parsing. Generated code is not analyzed unless it \
exists as source.",
description_ja: "マクロ展開で生じる結合や、無効な `cfg(...)` の背後にある結合は、synベースの解析では見えません。\
生成コードはソースとして存在しない限り解析されません。",
},
];
pub fn build_manifest(ctx: &ManifestContext) -> AnalysisManifest {
let mut notes = Vec::new();
let mut notes_ja = Vec::new();
if !ctx.git_used {
notes.push(
"Git history was not analyzed (--no-git or no repository): volatility and temporal \
(co-change) coupling were skipped, so scores reflect structure only."
.to_string(),
);
notes_ja.push(
"Git履歴は解析されませんでした (--no-git またはリポジトリ外): 変更頻度と時間的な共変更結合はスキップされ、スコアは構造のみを反映します。"
.to_string(),
);
}
if ctx.tests_excluded {
notes.push(
"Test code was excluded: couplings that originate in tests are not counted."
.to_string(),
);
notes_ja.push(
"テストコードは除外されました: テスト由来の結合はカウントされません。".to_string(),
);
}
if ctx.parse_failures > 0 {
notes.push(format!(
"{} source file(s) failed to parse and were skipped; coupling inside them is unknown.",
ctx.parse_failures
));
notes_ja.push(format!(
"{} 件のソースファイルを解析できずスキップしました。その内部の結合は不明です。",
ctx.parse_failures
));
}
AnalysisManifest {
blind_spots: STRUCTURAL_BLIND_SPOTS.to_vec(),
notes,
notes_ja,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn structural_blind_spots_are_always_present() {
let manifest = build_manifest(&ManifestContext {
git_used: true,
tests_excluded: false,
parse_failures: 0,
});
assert_eq!(manifest.blind_spots.len(), STRUCTURAL_BLIND_SPOTS.len());
assert!(
manifest
.blind_spots
.iter()
.any(|b| b.area == "dynamic-connascence")
);
assert!(
manifest
.blind_spots
.iter()
.any(|b| b.area == "implicit-functional-coupling")
);
}
#[test]
fn full_context_has_no_degradation_notes() {
let manifest = build_manifest(&ManifestContext {
git_used: true,
tests_excluded: false,
parse_failures: 0,
});
assert!(manifest.notes.is_empty());
}
#[test]
fn no_git_adds_a_note() {
let manifest = build_manifest(&ManifestContext {
git_used: false,
..Default::default()
});
assert!(manifest.notes.iter().any(|n| n.contains("Git history")));
}
#[test]
fn excluded_tests_adds_a_note() {
let manifest = build_manifest(&ManifestContext {
git_used: true,
tests_excluded: true,
parse_failures: 0,
});
assert!(manifest.notes.iter().any(|n| n.contains("Test code")));
}
#[test]
fn parse_failures_are_reported_with_count() {
let manifest = build_manifest(&ManifestContext {
git_used: true,
tests_excluded: false,
parse_failures: 3,
});
assert!(manifest.notes.iter().any(|n| n.contains("3 source file")));
}
#[test]
fn all_degradations_accumulate() {
let manifest = build_manifest(&ManifestContext {
git_used: false,
tests_excluded: true,
parse_failures: 2,
});
assert_eq!(manifest.notes.len(), 3);
}
}