watashi-db 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CLAUDE.md +36 -0
  2. package/LICENSE +1 -1
  3. package/README.md +33 -2
  4. package/cowork-plugin/skills/groom/SKILL.md +51 -15
  5. package/cowork-plugin/skills/recall/SKILL.md +5 -6
  6. package/cowork-plugin/skills/reflect/SKILL.md +4 -4
  7. package/cowork-plugin/skills/remember/SKILL.md +3 -3
  8. package/cowork-plugin/skills/session-start/SKILL.md +3 -3
  9. package/dist/config/schema.js +1 -1
  10. package/dist/constants.d.ts +5 -1
  11. package/dist/constants.js +19 -3
  12. package/dist/constants.js.map +1 -1
  13. package/dist/database/archive.js +6 -6
  14. package/dist/database/queries-core.d.ts +75 -1
  15. package/dist/database/queries-core.js +283 -12
  16. package/dist/database/queries-core.js.map +1 -1
  17. package/dist/database/queries.d.ts +71 -1
  18. package/dist/database/queries.js +29 -0
  19. package/dist/database/queries.js.map +1 -1
  20. package/dist/database/schema.d.ts +1 -0
  21. package/dist/database/schema.js +1915 -214
  22. package/dist/database/schema.js.map +1 -1
  23. package/dist/embedding/embed-on-write.d.ts +7 -1
  24. package/dist/embedding/embed-on-write.js +8 -3
  25. package/dist/embedding/embed-on-write.js.map +1 -1
  26. package/dist/hook.js +9 -6
  27. package/dist/hook.js.map +1 -1
  28. package/dist/index.js +3 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/resources/config-guide-content.d.ts +1 -1
  31. package/dist/resources/config-guide-content.js +2 -2
  32. package/dist/server-instructions.js +16 -17
  33. package/dist/server-instructions.js.map +1 -1
  34. package/dist/server.d.ts +4 -1
  35. package/dist/server.js +42 -18
  36. package/dist/server.js.map +1 -1
  37. package/dist/setup.js +5 -6
  38. package/dist/setup.js.map +1 -1
  39. package/dist/store/federation.d.ts +12 -1
  40. package/dist/store/federation.js +38 -0
  41. package/dist/store/federation.js.map +1 -1
  42. package/dist/store/sync-manager.d.ts +1 -1
  43. package/dist/store/sync-manager.js +9 -9
  44. package/dist/tools/claim-tools.d.ts +1 -1
  45. package/dist/tools/claim-tools.js +7 -7
  46. package/dist/tools/claim-tools.js.map +1 -1
  47. package/dist/tools/decision-tools.d.ts +1 -1
  48. package/dist/tools/decision-tools.js +2 -2
  49. package/dist/tools/decision-tools.js.map +1 -1
  50. package/dist/tools/episode-tools.d.ts +1 -1
  51. package/dist/tools/episode-tools.js +2 -2
  52. package/dist/tools/episode-tools.js.map +1 -1
  53. package/dist/tools/file-tools.d.ts +3 -0
  54. package/dist/tools/file-tools.js +347 -0
  55. package/dist/tools/file-tools.js.map +1 -0
  56. package/dist/tools/get-tools.d.ts +1 -1
  57. package/dist/tools/get-tools.js +39 -5
  58. package/dist/tools/get-tools.js.map +1 -1
  59. package/dist/tools/knowledge-tools.d.ts +1 -1
  60. package/dist/tools/knowledge-tools.js +2 -2
  61. package/dist/tools/knowledge-tools.js.map +1 -1
  62. package/dist/tools/maintenance-tools.d.ts +1 -1
  63. package/dist/tools/maintenance-tools.js +38 -6
  64. package/dist/tools/maintenance-tools.js.map +1 -1
  65. package/dist/tools/memo-tools.d.ts +7 -11
  66. package/dist/tools/memo-tools.js +499 -307
  67. package/dist/tools/memo-tools.js.map +1 -1
  68. package/dist/tools/query-tools.d.ts +1 -1
  69. package/dist/tools/query-tools.js +28 -5
  70. package/dist/tools/query-tools.js.map +1 -1
  71. package/dist/types.d.ts +370 -48
  72. package/dist/types.js +124 -16
  73. package/dist/types.js.map +1 -1
  74. package/misc/20260316_110841_groom-recipe.md +483 -0
  75. package/misc/20260316_xaml-testing-library-recipe.md +817 -0
  76. package/package.json +4 -2
  77. package/scripts/update-license-version.sh +7 -0
@@ -0,0 +1,817 @@
1
+ # XAML Testing Library — Phase B: Binding 静的検証 実装レシピ
2
+
3
+ ## Context
4
+
5
+ WPF アプリは「画面を開いて初めて Binding ミスに気づく」状態が多い。XAML の `{Binding Path}` が ViewModel のプロパティに正しく対応しているかを、**画面表示なし・コンパイル時に静的チェック**するツールを作る。
6
+
7
+ **技術スタック**: C# / .NET 8+ / Roslyn (Microsoft.CodeAnalysis) / System.Xaml
8
+
9
+ **ゴール**: CI で回せる XAML Binding 検証ツールの PoC
10
+
11
+ ---
12
+
13
+ ## プロジェクト構成
14
+
15
+ ```
16
+ XamlTestLib/
17
+ ├── src/
18
+ │ ├── XamlTestLib/ # メインライブラリ
19
+ │ │ ├── XamlTestLib.csproj
20
+ │ │ ├── Parsing/
21
+ │ │ │ ├── XamlBindingExtractor.cs # XAML から Binding 式を抽出
22
+ │ │ │ ├── BindingInfo.cs # Binding のメタデータ
23
+ │ │ │ └── DataContextResolver.cs # View → ViewModel の対応解決
24
+ │ │ ├── Analysis/
25
+ │ │ │ ├── RoslynProjectLoader.cs # .csproj を Roslyn で読み込み
26
+ │ │ │ ├── ViewModelAnalyzer.cs # ViewModel のプロパティ・型を解析
27
+ │ │ │ └── BindingValidator.cs # Binding パスの検証ロジック
28
+ │ │ └── Reporting/
29
+ │ │ ├── ValidationResult.cs # 検証結果モデル
30
+ │ │ └── ConsoleReporter.cs # コンソール出力
31
+ │ ├── XamlTestLib.Cli/ # CLI ツール(dotnet tool)
32
+ │ │ ├── XamlTestLib.Cli.csproj
33
+ │ │ └── Program.cs
34
+ │ └── XamlTestLib.MsBuild/ # MSBuild タスク(将来)
35
+ │ └── XamlTestLib.MsBuild.csproj
36
+ ├── tests/
37
+ │ ├── XamlTestLib.Tests/ # ユニットテスト
38
+ │ │ ├── XamlTestLib.Tests.csproj
39
+ │ │ ├── Parsing/
40
+ │ │ │ └── XamlBindingExtractorTests.cs
41
+ │ │ ├── Analysis/
42
+ │ │ │ ├── ViewModelAnalyzerTests.cs
43
+ │ │ │ └── BindingValidatorTests.cs
44
+ │ │ └── TestData/ # テスト用 XAML / ViewModel
45
+ │ │ ├── SimpleView.xaml
46
+ │ │ ├── SimpleViewModel.cs
47
+ │ │ ├── NestedBindingView.xaml
48
+ │ │ ├── ConverterView.xaml
49
+ │ │ └── BrokenBindingView.xaml
50
+ │ └── SampleWpfApp/ # Dog-fooding 対象
51
+ │ ├── SampleWpfApp.csproj
52
+ │ ├── Views/
53
+ │ │ └── MainWindow.xaml
54
+ │ └── ViewModels/
55
+ │ └── MainWindowViewModel.cs
56
+ ├── XamlTestLib.sln
57
+ ├── .gitignore
58
+ ├── CLAUDE.md
59
+ └── README.md
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Step 1: ソリューション初期構成
65
+
66
+ ```bash
67
+ mkdir XamlTestLib && cd XamlTestLib
68
+ dotnet new sln
69
+
70
+ # メインライブラリ
71
+ dotnet new classlib -n XamlTestLib -o src/XamlTestLib -f net8.0
72
+ dotnet sln add src/XamlTestLib
73
+
74
+ # CLI ツール
75
+ dotnet new console -n XamlTestLib.Cli -o src/XamlTestLib.Cli -f net8.0
76
+ dotnet sln add src/XamlTestLib.Cli
77
+
78
+ # テストプロジェクト
79
+ dotnet new xunit -n XamlTestLib.Tests -o tests/XamlTestLib.Tests -f net8.0
80
+ dotnet sln add tests/XamlTestLib.Tests
81
+
82
+ # サンプル WPF アプリ(dog-fooding 用)
83
+ dotnet new wpf -n SampleWpfApp -o tests/SampleWpfApp -f net8.0-windows
84
+ dotnet sln add tests/SampleWpfApp
85
+
86
+ # プロジェクト参照
87
+ dotnet add src/XamlTestLib.Cli reference src/XamlTestLib
88
+ dotnet add tests/XamlTestLib.Tests reference src/XamlTestLib
89
+ ```
90
+
91
+ ### NuGet パッケージ(XamlTestLib.csproj)
92
+
93
+ ```xml
94
+ <ItemGroup>
95
+ <!-- Roslyn: C# ソースの意味解析 -->
96
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
97
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2" />
98
+ <PackageReference Include="Microsoft.Build.Locator" Version="1.7.8" />
99
+ <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.9.2" />
100
+ </ItemGroup>
101
+ ```
102
+
103
+ > MSBuildLocator は Roslyn が .csproj をロードするために必要。
104
+ > バージョンは最新を確認すること。
105
+
106
+ ---
107
+
108
+ ## Step 2: XAML パーサー — Binding 式の抽出
109
+
110
+ ### `BindingInfo.cs`
111
+
112
+ ```csharp
113
+ namespace XamlTestLib.Parsing;
114
+
115
+ /// <summary>Binding 式のメタデータ</summary>
116
+ public record BindingInfo(
117
+ string Path, // Binding Path(例: "UserName", "Items.Count")
118
+ string? Converter, // Converter 名(例: "BoolToVisibility")
119
+ string? ConverterParameter,
120
+ string? StringFormat,
121
+ string? ElementName, // ElementName binding の場合
122
+ string? RelativeSource, // RelativeSource の場合
123
+ string SourceXamlFile, // 元 XAML ファイルパス
124
+ int LineNumber, // XAML 内の行番号
125
+ string ParentElementName, // 親要素名(例: "TextBox")
126
+ string TargetProperty // 対象プロパティ(例: "Text")
127
+ );
128
+ ```
129
+
130
+ ### `XamlBindingExtractor.cs` — コア実装
131
+
132
+ XAML を XML として読み、`{Binding ...}` を正規表現 + 簡易パーサーで抽出する。
133
+ `System.Xaml.XamlReader` ではなく XML パーサーを使う理由:
134
+ - XamlReader は型解決が必要(アセンブリ参照がないと失敗する)
135
+ - 静的検証の目的では XML レベルで十分
136
+ - エラー耐性が高い(壊れた XAML でも部分的に処理可能)
137
+
138
+ ```csharp
139
+ namespace XamlTestLib.Parsing;
140
+
141
+ using System.Text.RegularExpressions;
142
+ using System.Xml.Linq;
143
+
144
+ public class XamlBindingExtractor
145
+ {
146
+ // {Binding Path=XXX, Converter={StaticResource YYY}} のパターン
147
+ // {Binding XXX} の省略形も対応
148
+ private static readonly Regex BindingRegex = new(
149
+ @"\{Binding\s+(?:Path=)?([^,\}]+)(?:,\s*([^}]*))?\}",
150
+ RegexOptions.Compiled);
151
+
152
+ public IReadOnlyList<BindingInfo> Extract(string xamlFilePath)
153
+ {
154
+ var results = new List<BindingInfo>();
155
+ var doc = XDocument.Load(xamlFilePath, LoadOptions.SetLineInfo);
156
+
157
+ foreach (var element in doc.Descendants())
158
+ {
159
+ foreach (var attr in element.Attributes())
160
+ {
161
+ var matches = BindingRegex.Matches(attr.Value);
162
+ foreach (Match match in matches)
163
+ {
164
+ var path = match.Groups[1].Value.Trim();
165
+ var options = match.Groups[2].Success ? match.Groups[2].Value : null;
166
+
167
+ var info = new BindingInfo(
168
+ Path: path,
169
+ Converter: ExtractOption(options, "Converter"),
170
+ ConverterParameter: ExtractOption(options, "ConverterParameter"),
171
+ StringFormat: ExtractOption(options, "StringFormat"),
172
+ ElementName: ExtractOption(options, "ElementName"),
173
+ RelativeSource: ExtractOption(options, "RelativeSource"),
174
+ SourceXamlFile: xamlFilePath,
175
+ LineNumber: ((IXmlLineInfo)element).LineNumber,
176
+ ParentElementName: element.Name.LocalName,
177
+ TargetProperty: attr.Name.LocalName
178
+ );
179
+ results.Add(info);
180
+ }
181
+ }
182
+ }
183
+
184
+ return results;
185
+ }
186
+
187
+ private static string? ExtractOption(string? options, string key)
188
+ {
189
+ if (options == null) return null;
190
+ var regex = new Regex($@"{key}=([^,\}}]+)");
191
+ var match = regex.Match(options);
192
+ return match.Success ? match.Groups[1].Value.Trim() : null;
193
+ }
194
+ }
195
+ ```
196
+
197
+ ### 考慮事項
198
+ - `{Binding}` (パスなし = DataContext 自体) → Path="" で記録
199
+ - `{x:Bind}` (UWP/WinUI) → 将来対応、今は `{Binding}` のみ
200
+ - `MultiBinding` → 子の `<Binding>` 要素として出現するので要素走査で対応(Step 2 では後回し)
201
+ - ネストした `{StaticResource}` 内の Binding → 正規表現を改良する必要あり
202
+
203
+ ---
204
+
205
+ ## Step 3: DataContext 解決 — View と ViewModel の対応付け
206
+
207
+ ### 対応付けの3戦略(優先順)
208
+
209
+ 1. **d:DataContext 属性**(デザイン時 DataContext)
210
+ ```xml
211
+ <Window d:DataContext="{d:DesignInstance vm:MainWindowViewModel}">
212
+ ```
213
+ → 最も信頼性が高い。これがあれば確定。
214
+
215
+ 2. **code-behind の DataContext 代入**
216
+ ```csharp
217
+ DataContext = new MainWindowViewModel();
218
+ ```
219
+ → Roslyn で code-behind を解析して型を特定。
220
+
221
+ 3. **命名規約**
222
+ - `MainWindow.xaml` → `MainWindowViewModel`
223
+ - `UserListView.xaml` → `UserListViewModel`
224
+ → フォールバック。設定でカスタマイズ可能にする。
225
+
226
+ ### `DataContextResolver.cs`
227
+
228
+ ```csharp
229
+ namespace XamlTestLib.Parsing;
230
+
231
+ using System.Xml.Linq;
232
+
233
+ public class DataContextResolver
234
+ {
235
+ private static readonly XNamespace DesignNs =
236
+ "http://schemas.microsoft.com/expression/blend/2008";
237
+
238
+ /// <summary>XAML から DataContext 型名を解決する</summary>
239
+ public string? ResolveFromXaml(string xamlFilePath)
240
+ {
241
+ var doc = XDocument.Load(xamlFilePath);
242
+ var root = doc.Root;
243
+ if (root == null) return null;
244
+
245
+ // 戦略1: d:DataContext
246
+ var designContext = root.Attribute(DesignNs + "DataContext");
247
+ if (designContext != null)
248
+ {
249
+ return ExtractDesignInstanceType(designContext.Value, root);
250
+ }
251
+
252
+ // 戦略3: 命名規約(フォールバック)
253
+ var fileName = Path.GetFileNameWithoutExtension(xamlFilePath);
254
+ return fileName + "ViewModel";
255
+ }
256
+
257
+ private string? ExtractDesignInstanceType(string value, XElement root)
258
+ {
259
+ // {d:DesignInstance vm:MainWindowViewModel} のパターン
260
+ var match = Regex.Match(value, @"DesignInstance\s+(?:Type=)?(\w+:)?(\w+)");
261
+ if (!match.Success) return null;
262
+
263
+ var prefix = match.Groups[1].Value.TrimEnd(':');
264
+ var typeName = match.Groups[2].Value;
265
+
266
+ if (!string.IsNullOrEmpty(prefix))
267
+ {
268
+ // xmlns:vm="clr-namespace:SampleApp.ViewModels" からフル名を解決
269
+ var ns = root.Attributes()
270
+ .FirstOrDefault(a => a.Name.LocalName == prefix)?
271
+ .Value;
272
+ if (ns != null)
273
+ {
274
+ var clrNs = ExtractClrNamespace(ns);
275
+ if (clrNs != null) return $"{clrNs}.{typeName}";
276
+ }
277
+ }
278
+
279
+ return typeName;
280
+ }
281
+
282
+ private static string? ExtractClrNamespace(string xmlns)
283
+ {
284
+ // "clr-namespace:SampleApp.ViewModels;assembly=SampleApp"
285
+ var match = Regex.Match(xmlns, @"clr-namespace:([^;]+)");
286
+ return match.Success ? match.Groups[1].Value : null;
287
+ }
288
+ }
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Step 4: Roslyn で ViewModel を解析
294
+
295
+ ### `RoslynProjectLoader.cs`
296
+
297
+ ```csharp
298
+ namespace XamlTestLib.Analysis;
299
+
300
+ using Microsoft.Build.Locator;
301
+ using Microsoft.CodeAnalysis;
302
+ using Microsoft.CodeAnalysis.MSBuild;
303
+
304
+ public class RoslynProjectLoader : IDisposable
305
+ {
306
+ private MSBuildWorkspace? _workspace;
307
+
308
+ static RoslynProjectLoader()
309
+ {
310
+ // MSBuild の場所を自動検出(初回のみ)
311
+ if (!MSBuildLocator.IsRegistered)
312
+ MSBuildLocator.RegisterDefaults();
313
+ }
314
+
315
+ public async Task<Compilation?> LoadProjectAsync(string csprojPath)
316
+ {
317
+ _workspace = MSBuildWorkspace.Create();
318
+ var project = await _workspace.OpenProjectAsync(csprojPath);
319
+ return await project.GetCompilationAsync();
320
+ }
321
+
322
+ public void Dispose() => _workspace?.Dispose();
323
+ }
324
+ ```
325
+
326
+ ### `ViewModelAnalyzer.cs`
327
+
328
+ ```csharp
329
+ namespace XamlTestLib.Analysis;
330
+
331
+ using Microsoft.CodeAnalysis;
332
+
333
+ public class ViewModelAnalyzer
334
+ {
335
+ private readonly Compilation _compilation;
336
+
337
+ public ViewModelAnalyzer(Compilation compilation)
338
+ {
339
+ _compilation = compilation;
340
+ }
341
+
342
+ /// <summary>型名から INamedTypeSymbol を取得</summary>
343
+ public INamedTypeSymbol? FindType(string fullTypeName)
344
+ {
345
+ return _compilation.GetTypeByMetadataName(fullTypeName);
346
+ }
347
+
348
+ /// <summary>型名の部分一致検索(namespace 不明の場合)</summary>
349
+ public INamedTypeSymbol? FindTypeByName(string typeName)
350
+ {
351
+ return _compilation.GetSymbolsWithName(typeName, SymbolFilter.Type)
352
+ .OfType<INamedTypeSymbol>()
353
+ .FirstOrDefault();
354
+ }
355
+
356
+ /// <summary>Binding パスが ViewModel 上で有効かチェック</summary>
357
+ public PropertyCheckResult CheckBindingPath(INamedTypeSymbol viewModelType, string bindingPath)
358
+ {
359
+ // "Items.Count" → ["Items", "Count"]
360
+ var segments = bindingPath.Split('.');
361
+ var currentType = viewModelType;
362
+
363
+ foreach (var segment in segments)
364
+ {
365
+ // インデクサ [0] や添付プロパティ (Grid.Row) は今はスキップ
366
+ if (segment.Contains('[') || segment.Contains('('))
367
+ {
368
+ return new PropertyCheckResult(true, null, "Skipped: indexer or attached property");
369
+ }
370
+
371
+ var member = FindProperty(currentType, segment);
372
+ if (member == null)
373
+ {
374
+ return new PropertyCheckResult(
375
+ false,
376
+ currentType,
377
+ $"Property '{segment}' not found on type '{currentType.Name}'"
378
+ );
379
+ }
380
+
381
+ // 次のセグメントのために型を進める
382
+ currentType = GetPropertyType(member);
383
+ if (currentType == null)
384
+ {
385
+ return new PropertyCheckResult(true, null, "Skipped: unresolvable type");
386
+ }
387
+ }
388
+
389
+ return new PropertyCheckResult(true, currentType, null);
390
+ }
391
+
392
+ private ISymbol? FindProperty(INamedTypeSymbol type, string name)
393
+ {
394
+ // 本人 + 基底クラスを走査
395
+ var current = type;
396
+ while (current != null)
397
+ {
398
+ var member = current.GetMembers(name)
399
+ .FirstOrDefault(m => m.Kind == SymbolKind.Property
400
+ || m.Kind == SymbolKind.Field);
401
+ if (member != null) return member;
402
+ current = current.BaseType;
403
+ }
404
+ return null;
405
+ }
406
+
407
+ private INamedTypeSymbol? GetPropertyType(ISymbol member)
408
+ {
409
+ var type = member switch
410
+ {
411
+ IPropertySymbol p => p.Type,
412
+ IFieldSymbol f => f.Type,
413
+ _ => null
414
+ };
415
+ return type as INamedTypeSymbol;
416
+ }
417
+ }
418
+
419
+ public record PropertyCheckResult(
420
+ bool IsValid,
421
+ INamedTypeSymbol? ResolvedType,
422
+ string? Message
423
+ );
424
+ ```
425
+
426
+ ---
427
+
428
+ ## Step 5: Binding 検証の統合
429
+
430
+ ### `BindingValidator.cs`
431
+
432
+ ```csharp
433
+ namespace XamlTestLib.Analysis;
434
+
435
+ using XamlTestLib.Parsing;
436
+ using XamlTestLib.Reporting;
437
+
438
+ public class BindingValidator
439
+ {
440
+ private readonly ViewModelAnalyzer _analyzer;
441
+ private readonly DataContextResolver _contextResolver;
442
+
443
+ public BindingValidator(ViewModelAnalyzer analyzer)
444
+ {
445
+ _analyzer = analyzer;
446
+ _contextResolver = new DataContextResolver();
447
+ }
448
+
449
+ public ValidationReport Validate(IEnumerable<string> xamlFiles)
450
+ {
451
+ var extractor = new XamlBindingExtractor();
452
+ var results = new List<ValidationResult>();
453
+
454
+ foreach (var xamlFile in xamlFiles)
455
+ {
456
+ // 1. Binding 式を抽出
457
+ var bindings = extractor.Extract(xamlFile);
458
+
459
+ // 2. DataContext(ViewModel 型)を解決
460
+ var viewModelName = _contextResolver.ResolveFromXaml(xamlFile);
461
+ if (viewModelName == null)
462
+ {
463
+ results.Add(new ValidationResult(
464
+ xamlFile, 0, Severity.Warning,
465
+ "DataContext を解決できませんでした。d:DataContext または命名規約を確認してください。"
466
+ ));
467
+ continue;
468
+ }
469
+
470
+ // 3. ViewModel 型を Roslyn で取得
471
+ var vmType = _analyzer.FindTypeByName(viewModelName)
472
+ ?? _analyzer.FindType(viewModelName);
473
+ if (vmType == null)
474
+ {
475
+ results.Add(new ValidationResult(
476
+ xamlFile, 0, Severity.Warning,
477
+ $"ViewModel '{viewModelName}' がプロジェクト内に見つかりません。"
478
+ ));
479
+ continue;
480
+ }
481
+
482
+ // 4. 各 Binding パスを検証
483
+ foreach (var binding in bindings)
484
+ {
485
+ // ElementName / RelativeSource binding はスキップ
486
+ if (binding.ElementName != null || binding.RelativeSource != null)
487
+ continue;
488
+
489
+ // 空パス(DataContext 自体への Binding)はスキップ
490
+ if (string.IsNullOrWhiteSpace(binding.Path))
491
+ continue;
492
+
493
+ var check = _analyzer.CheckBindingPath(vmType, binding.Path);
494
+ if (!check.IsValid)
495
+ {
496
+ results.Add(new ValidationResult(
497
+ xamlFile,
498
+ binding.LineNumber,
499
+ Severity.Error,
500
+ $"{binding.ParentElementName}.{binding.TargetProperty}: "
501
+ + $"Binding Path=\"{binding.Path}\" — {check.Message}"
502
+ ));
503
+ }
504
+ }
505
+ }
506
+
507
+ return new ValidationReport(results);
508
+ }
509
+ }
510
+ ```
511
+
512
+ ### `ValidationResult.cs` / `ValidationReport.cs`
513
+
514
+ ```csharp
515
+ namespace XamlTestLib.Reporting;
516
+
517
+ public enum Severity { Info, Warning, Error }
518
+
519
+ public record ValidationResult(
520
+ string FilePath,
521
+ int LineNumber,
522
+ Severity Severity,
523
+ string Message
524
+ );
525
+
526
+ public class ValidationReport
527
+ {
528
+ public IReadOnlyList<ValidationResult> Results { get; }
529
+ public int ErrorCount => Results.Count(r => r.Severity == Severity.Error);
530
+ public int WarningCount => Results.Count(r => r.Severity == Severity.Warning);
531
+ public bool HasErrors => ErrorCount > 0;
532
+
533
+ public ValidationReport(IReadOnlyList<ValidationResult> results)
534
+ {
535
+ Results = results;
536
+ }
537
+ }
538
+ ```
539
+
540
+ ---
541
+
542
+ ## Step 6: CLI ツール
543
+
544
+ ### `Program.cs`
545
+
546
+ ```csharp
547
+ using XamlTestLib.Analysis;
548
+
549
+ if (args.Length == 0)
550
+ {
551
+ Console.Error.WriteLine("Usage: xaml-test <path-to.csproj>");
552
+ return 1;
553
+ }
554
+
555
+ var csprojPath = Path.GetFullPath(args[0]);
556
+ if (!File.Exists(csprojPath))
557
+ {
558
+ Console.Error.WriteLine($"File not found: {csprojPath}");
559
+ return 1;
560
+ }
561
+
562
+ Console.WriteLine($"Loading project: {csprojPath}");
563
+
564
+ using var loader = new RoslynProjectLoader();
565
+ var compilation = await loader.LoadProjectAsync(csprojPath);
566
+ if (compilation == null)
567
+ {
568
+ Console.Error.WriteLine("Failed to load project.");
569
+ return 1;
570
+ }
571
+
572
+ // XAML ファイルを収集
573
+ var projectDir = Path.GetDirectoryName(csprojPath)!;
574
+ var xamlFiles = Directory.GetFiles(projectDir, "*.xaml", SearchOption.AllDirectories)
575
+ .Where(f => !f.Contains("obj") && !f.Contains("bin"))
576
+ .ToList();
577
+
578
+ Console.WriteLine($"Found {xamlFiles.Count} XAML files.");
579
+
580
+ var analyzer = new ViewModelAnalyzer(compilation);
581
+ var validator = new BindingValidator(analyzer);
582
+ var report = validator.Validate(xamlFiles);
583
+
584
+ // 結果出力
585
+ foreach (var result in report.Results)
586
+ {
587
+ var icon = result.Severity switch
588
+ {
589
+ Severity.Error => "ERROR",
590
+ Severity.Warning => "WARN ",
591
+ _ => "INFO "
592
+ };
593
+ Console.WriteLine($" [{icon}] {Path.GetFileName(result.FilePath)}:{result.LineNumber}");
594
+ Console.WriteLine($" {result.Message}");
595
+ }
596
+
597
+ Console.WriteLine();
598
+ Console.WriteLine($"Errors: {report.ErrorCount}, Warnings: {report.WarningCount}");
599
+
600
+ return report.HasErrors ? 1 : 0;
601
+ ```
602
+
603
+ ---
604
+
605
+ ## Step 7: テストデータとユニットテスト
606
+
607
+ ### `TestData/SimpleViewModel.cs`(テスト用ソース文字列)
608
+
609
+ ```csharp
610
+ public class SimpleViewModel
611
+ {
612
+ public string UserName { get; set; }
613
+ public int Age { get; set; }
614
+ public Address Address { get; set; }
615
+ }
616
+
617
+ public class Address
618
+ {
619
+ public string City { get; set; }
620
+ public string ZipCode { get; set; }
621
+ }
622
+ ```
623
+
624
+ ### `XamlBindingExtractorTests.cs`
625
+
626
+ ```csharp
627
+ public class XamlBindingExtractorTests
628
+ {
629
+ [Fact]
630
+ public void Extract_SimpleBinding_ReturnsPath()
631
+ {
632
+ var xaml = CreateTempXaml("""
633
+ <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
634
+ <TextBox Text="{Binding UserName}" />
635
+ </Window>
636
+ """);
637
+
638
+ var extractor = new XamlBindingExtractor();
639
+ var bindings = extractor.Extract(xaml);
640
+
641
+ Assert.Single(bindings);
642
+ Assert.Equal("UserName", bindings[0].Path);
643
+ Assert.Equal("TextBox", bindings[0].ParentElementName);
644
+ Assert.Equal("Text", bindings[0].TargetProperty);
645
+ }
646
+
647
+ [Fact]
648
+ public void Extract_NestedPath_ReturnsDottedPath()
649
+ {
650
+ var xaml = CreateTempXaml("""
651
+ <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
652
+ <TextBlock Text="{Binding Address.City}" />
653
+ </Window>
654
+ """);
655
+
656
+ var extractor = new XamlBindingExtractor();
657
+ var bindings = extractor.Extract(xaml);
658
+
659
+ Assert.Single(bindings);
660
+ Assert.Equal("Address.City", bindings[0].Path);
661
+ }
662
+
663
+ [Fact]
664
+ public void Extract_BrokenBinding_StillParsesValid()
665
+ {
666
+ var xaml = CreateTempXaml("""
667
+ <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
668
+ <TextBox Text="{Binding UserName}" />
669
+ <TextBox Text="{Binding NonExistent}" />
670
+ </Window>
671
+ """);
672
+
673
+ var extractor = new XamlBindingExtractor();
674
+ var bindings = extractor.Extract(xaml);
675
+
676
+ Assert.Equal(2, bindings.Count);
677
+ }
678
+
679
+ private static string CreateTempXaml(string content)
680
+ {
681
+ var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xaml");
682
+ File.WriteAllText(path, content);
683
+ return path;
684
+ }
685
+ }
686
+ ```
687
+
688
+ ### `BindingValidatorTests.cs`(Roslyn インメモリ Compilation)
689
+
690
+ ```csharp
691
+ using Microsoft.CodeAnalysis;
692
+ using Microsoft.CodeAnalysis.CSharp;
693
+
694
+ public class BindingValidatorTests
695
+ {
696
+ [Fact]
697
+ public void Validate_ValidBinding_NoErrors()
698
+ {
699
+ // Roslyn でインメモリ Compilation を作成(.csproj 不要)
700
+ var compilation = CreateCompilation("""
701
+ public class MainWindowViewModel
702
+ {
703
+ public string UserName { get; set; }
704
+ }
705
+ """);
706
+
707
+ var analyzer = new ViewModelAnalyzer(compilation);
708
+ var vmType = analyzer.FindTypeByName("MainWindowViewModel");
709
+
710
+ var result = analyzer.CheckBindingPath(vmType!, "UserName");
711
+ Assert.True(result.IsValid);
712
+ }
713
+
714
+ [Fact]
715
+ public void Validate_InvalidBinding_ReturnsError()
716
+ {
717
+ var compilation = CreateCompilation("""
718
+ public class MainWindowViewModel
719
+ {
720
+ public string UserName { get; set; }
721
+ }
722
+ """);
723
+
724
+ var analyzer = new ViewModelAnalyzer(compilation);
725
+ var vmType = analyzer.FindTypeByName("MainWindowViewModel");
726
+
727
+ var result = analyzer.CheckBindingPath(vmType!, "NonExistent");
728
+ Assert.False(result.IsValid);
729
+ Assert.Contains("not found", result.Message);
730
+ }
731
+
732
+ [Fact]
733
+ public void Validate_NestedPath_ResolvesCorrectly()
734
+ {
735
+ var compilation = CreateCompilation("""
736
+ public class MainWindowViewModel
737
+ {
738
+ public Address Address { get; set; }
739
+ }
740
+ public class Address
741
+ {
742
+ public string City { get; set; }
743
+ }
744
+ """);
745
+
746
+ var analyzer = new ViewModelAnalyzer(compilation);
747
+ var vmType = analyzer.FindTypeByName("MainWindowViewModel");
748
+
749
+ Assert.True(analyzer.CheckBindingPath(vmType!, "Address.City").IsValid);
750
+ Assert.False(analyzer.CheckBindingPath(vmType!, "Address.Country").IsValid);
751
+ }
752
+
753
+ private static Compilation CreateCompilation(string source)
754
+ {
755
+ var tree = CSharpSyntaxTree.ParseText(source);
756
+ return CSharpCompilation.Create("TestAssembly")
757
+ .AddReferences(MetadataReference.CreateFromFile(
758
+ typeof(object).Assembly.Location))
759
+ .AddSyntaxTrees(tree);
760
+ }
761
+ }
762
+ ```
763
+
764
+ ---
765
+
766
+ ## 実装順序(PoC 最短ルート)
767
+
768
+ 1. **ソリューション初期構成** — `dotnet new` でプロジェクト群を作成
769
+ 2. **BindingInfo + XamlBindingExtractor** — XAML → Binding 式抽出(XML ベース)
770
+ 3. **XamlBindingExtractorTests** — テストデータで動作確認
771
+ 4. **ViewModelAnalyzer** — Roslyn インメモリ Compilation でプロパティ検証
772
+ 5. **BindingValidatorTests** — インメモリ Compilation でユニットテスト通過
773
+ 6. **DataContextResolver** — d:DataContext + 命名規約で View → ViewModel 対応
774
+ 7. **BindingValidator** — 統合ロジック
775
+ 8. **CLI Program.cs** — 実プロジェクト(SampleWpfApp)で E2E 動作確認
776
+
777
+ **PoC ゴール**: SampleWpfApp に意図的に壊れた Binding を入れて、CLI が検出できること。
778
+
779
+ ---
780
+
781
+ ## 将来の拡張(Phase B+)
782
+
783
+ | 機能 | 概要 | 難易度 |
784
+ |------|------|--------|
785
+ | **DataTemplate 内の Binding** | ItemsControl 系の DataTemplate の DataType から型解決 | 中 |
786
+ | **Converter 存在チェック** | StaticResource で参照される Converter がリソースに存在するか | 中 |
787
+ | **Command 存在チェック** | `Command="{Binding SaveCommand}"` → ICommand プロパティの存在確認 | 低 |
788
+ | **MSBuild タスク** | ビルド時に自動実行、Warning/Error として VS に表示 | 中 |
789
+ | **Roslyn Analyzer** | IDE 内リアルタイム警告(赤波線) | 高 |
790
+ | **x:Bind 対応** | WinUI / UWP 向け | 中 |
791
+ | **CI 用 JSON 出力** | GitHub Actions 等で Annotation として表示 | 低 |
792
+
793
+ ---
794
+
795
+ ## CLAUDE.md(初期内容案)
796
+
797
+ ```markdown
798
+ # XamlTestLib
799
+
800
+ WPF XAML Binding の静的検証ツール。画面表示なしで Binding パスの正当性を検証する。
801
+
802
+ ## ビルド・テスト
803
+
804
+ dotnet build
805
+ dotnet test
806
+
807
+ ## アーキテクチャ
808
+
809
+ 1. Parsing: XAML を XML として読み、{Binding} 式を抽出
810
+ 2. Analysis: Roslyn で ViewModel の型情報を取得し、Binding パスを検証
811
+ 3. Reporting: 結果をコンソール / JSON で出力
812
+
813
+ ## 命名規約
814
+
815
+ - View: `*Window.xaml`, `*View.xaml`, `*Page.xaml`
816
+ - ViewModel: `*ViewModel` (View 名 + "ViewModel")
817
+ ```