Files
AnotherReplayReader/WIP.md
T
2026-07-07 16:42:42 +02:00

27 KiB

AI Analysis WIP

User Need

The application is adding AI analysis for Red Alert 3 replay operation logs. The current flow sends player information and a compacted operation log to a chat-completion-compatible LLM, then displays the analysis in AIChatPanel.

The main goals are:

  • Improve the current system prompt.
  • Make the system prompt configurable by the user.
  • Reduce AI analysis errors, especially errors caused by general-world assumptions or overconfident UnitId guesses.
  • Add a validation path for LLM output so wrong claims can be detected and corrected without wasting all prior reasoning.

Current Code Areas

  • Utils/AIAnalyze.cs: builds the system prompt, user prompts, segment prompts, final summary prompts, and performs OpenAI-compatible chat completion calls.
  • AIChatPanel.xaml.cs: runs the analysis workflow, displays streaming chunks, retries failed segments, and now logs machine-readable claim validation results.
  • Utils/AiSettings.cs: stores AI provider/model settings and now prompt settings.
  • AIProviderSettingsControl.xaml(.cs): edits provider/model settings and now prompt settings.
  • EventDump.xaml.cs: generates the replay operation text and starts AI analysis.
  • Utils/AIAnalysisValidation.cs: new validation model and parser for machine-readable AI claims.
  • CONTEXT.md: glossary for the AI analysis domain.

Discussion Notes

Prompt Problems

The current prompt has a lot of useful game knowledge, but the LLM can still:

  • Use common sense that is wrong for the game or mod.
  • Assume infantry, helicopters, transports, amphibious movement, and water placement work like they do in other RTS games.
  • Treat one observed skill as conclusive evidence when multiple units share that skill.
  • Overstate UnitId guesses.

Examples discussed:

  • Only units explicitly marked amphibious can move on both land and water.
  • Only units explicitly marked as passenger transports can transport infantry.
  • Building water placement depends on game rules, not common assumptions.
  • SpecialPower_UnpackReplaceSelf does not uniquely identify an Allied MCV because Allied miners can also unpack into a command hub.
  • A UnitId claimed as an aircraft should be challenged if the same UnitId is observed using an unpack/deploy skill.
  • A UnitId claimed as a bomber should be challenged if it is operated before the player starts producing their first bomber.

Documented Example: MCV vs Miner Ambiguity (Allied)

Observed replay sequence:

  1. UnitId 246 (confirmed main base) → PackReplaceSelf
  2. Player selects UnitId 587
  3. UnitId 587 → UnpackReplaceSelf
  4. AI claims: 587 = AlliedMCV, evidenceLevel: confirmed

Why this cannot be definitively resolved:

  • After base 246 packs, the engine creates a new MCV (UnitId A). The miner (UnitId B) also exists on the map.
  • When the player selects 587, we cannot prove 587 = A vs 587 = B.
  • After UnpackReplaceSelf, 587 is replaced by yet another UnitId (C if MCV→base, D if miner→command hub).
  • Even if we later see C building things (开始建造建筑 [UnitId]C(建造者)), there is no replay-observable link connecting C back to 587.
  • Allied MCV in mobile form has no unique observable ability that would distinguish it from a miner.

Conclusion: There is no deterministic validation rule that can confirm an Allied MCV claim from replay operations alone. The upper bound for any such claim is possible, and an alternative (miner command hub) must always be listed.

Contrast with other factions:

  • Soviet/Japan/神州 MCVs may have different observable behaviors (e.g., unique deploy animations, different upgrade paths) — each faction needs independent analysis.

Validation rule (negative check only):

  • If a claim says confirmed or highly likely for AlliedMCV based only on PackReplaceSelf → UnpackReplaceSelf sequence, flag as overconfident (WeakEvidence). Downgrade recommendation: possible with miner command hub as alternative.

Prompt Decisions

The default prompt should explicitly require:

  • Evidence-first analysis.
  • No use of external common sense over replay facts and supplied game rules.
  • UnitId guesses with evidence levels.
  • Multiple candidates when a behavior has several possible sources.
  • Support evidence and possible counter-evidence for important claims.
  • Correction or abandonment of claims contradicted by replay facts.

Evidence levels currently used:

  • confirmed
  • highly likely
  • possible
  • uncertain
  • ruled out

The default provider temperature was lowered from 0.75 to 0.35 because this task is closer to audit/reconstruction than creative writing.

Prompt Configuration Decisions

The prompt is now configurable through AI settings.

The design has two prompt layers:

  • A built-in dynamic system prompt, still assembled from replay/mod/faction/map context.
  • User prompt settings:
    • optional full custom system prompt
    • additional rules appended to the final system prompt

This keeps the normal path safe while allowing advanced users to override the whole prompt.

Validation Philosophy

LLM natural-language analysis should not be treated as directly valid. The plan is to validate structured claims emitted by the LLM.

Important decision:

  • Do not immediately throw away a whole analysis when a problem is found.
  • Do not show the user two competing analyses or apology text such as "sorry, my previous answer was wrong."
  • Prefer a hidden revision pass: send the draft, validation issues, and relevant replay facts back to the AI, asking it to output a clean corrected version without mentioning the revision.
  • Limit retries/revisions. If the model still cannot resolve a claim, downgrade confidence or mark it uncertain instead of looping forever.

Severity model:

  • Info: useful diagnostic only.
  • WeakEvidence: claim may be plausible but lacks enough support.
  • Warning: malformed or questionable claim that should be logged or possibly revised.
  • Contradiction: claim conflicts with replay facts or game rules and should trigger revision.
  • Fatal: output cannot be used for the current phase, such as empty or unparseable required output.

JSON Format Decision

We discussed whether to require JSON or use a simpler line-based format.

Decision:

  • Use JSON for machine-readable claims.
  • Keep the schema small.
  • Make the parser tolerant.

Reasoning:

  • JSON can naturally represent evidence arrays, alternatives, and needed confirmations.
  • A custom line format would be easier for a trivial parser but would become fragile once nested data is needed.
  • The app can tolerate partial or missing JSON by logging validation issues instead of failing the whole analysis.

Knowledge Architecture Decisions (2026-07-07)

We conducted a /grilling session (via /domain-modeling skill) to address the growing split between prompt knowledge and validation knowledge.

Recognised problems:

  • Prompt knowledge lives in BuildDefaultSystemPrompt() as large hardcoded strings.
  • Validation knowledge lives in AIAnalysisValidation.cs as hardcoded string matching (claimLooksLikeBuilder checks "MCV", "基地车", "Nanocore" etc.).
  • The two are not synchronised — adding a unit type requires editing both places.
  • Users can only override the entire system prompt or append text.

Decisions reached (recorded in ADR 0002):

  1. KnowledgeSet as the single source of truth. Game knowledge is organised into named KnowledgeSets keyed by mod (e.g., "default", "corona"). Each set is self-contained and complete — no cross-set inheritance or conditional sharing. The mod name from the replay directly selects which set to load, replacing the current [MOD:] inline tag system.

  2. KnowledgeEntry is the unified format. Every entry has an id, tags[], and text (markdown). Same format for built-in and user-supplied entries — no separate internal/external format.

  3. Predefined finite tag taxonomy. Tags are the bridge between prompt knowledge and validation. Three categories: capability (builder, pack, unpack, amphibious, returnToProducer, ...), type (infantry, vehicle, aircraft, naval, structure, ...), combat role (antiInfantry, antiVehicle, antiAir, ...), plus specialPower:* references. No ad-hoc tags.

  4. Prompt rendering order: global entries → faction entries (per player) → map entries. User AdditionalRules appended at the end.

  5. Validation consumes tags instead of hardcoded strings. Validators query entries.WithTag("builder") instead of claim.IndexOf("MCV") >= 0.

  6. User extensibility via JSON. User knowledge file (AnotherReplayReader.user_knowledge.json) overlays built-in entries by matching id. No code changes needed to add map/faction/mod knowledge.

  7. Storage format: JSON container with markdown text in text fields. The existing AiPromptSettings text fields remain as a simpler escape hatch.

Refinement — mods are independent complete sets: Initially the ADR described mod knowledge sets as "overlaying or extending" the base set. After further discussion, this was corrected: each mod is a self-contained game version with its own complete knowledge set. There is no [MOD:]-style conditional sharing because:

  • Users editing a mod's JSON should see only that mod's entries, not conditional inclusion logic.
  • The replays already identify the mod; loading the right set is a simple name lookup.
  • Duplication between mod sets is acceptable for clarity — the deduplication cost of [MOD:] tags is not worth it in a structured data format.

Reversal from earlier statement: The user noted that mods are game versions and should not be a separate scope dimension. This was accepted: the mod selects which KnowledgeSet to load, and within a set only global, faction, and map scopes exist.

Reversal from earlier assumption: I (the agent) initially claimed that Z-coordinate rules were duplicated across faction sections. After re-reading the full prompt, the user was correct — Z rules are in the generalDescriptions (global) section only. No duplication.

Evidence Format Decision (2026-07-06)

We decided to move from free-form evidence text to a structured pipe-delimited format:

type|time|param1|param2|...

Supported types: build, place, produce, sell, select, move, power.

Reasoning:

  • Free-form text could not be programmatically validated without NLP.
  • Structured evidence can be parsed deterministically with a simple regex.
  • Enables deterministic validation rules like unpack-ambiguity checking.
  • The format is simple enough for AI models to follow reliably.

Current expected shape (all three claim types now have schema definitions in the prompt):

{
  "unitClaims": [
    {
      "unitId": 123,
      "player": "PlayerA",
      "claim": "AlliedMCV",
      "evidenceLevel": "possible",
      "evidence": ["8:30 使用 SpecialPower_UnpackReplaceSelf"],
      "alternatives": ["AlliedMiner 展开后的指挥中心"],
      "needsConfirmation": ["是否曾使用 SpecialPower_PackReplaceSelf", "后续是否作为建造者出现"]
    }
  ],
  "eventClaims": [
    {
      "claim": "PlayerA 主基地打包并开始迁移",
      "evidenceLevel": "confirmed",
      "evidence": ["1:24.00 SpecialPower_PackReplaceSelf", "后续移动和展开操作"]
    }
  ],
  "timelineClaims": [
    {
      "claim": "PlayerA 在开局 2 分钟内完成了基地迁移",
      "evidenceLevel": "confirmed",
      "evidence": ["1:24.00 打包", "1:41.00 展开"]
    }
  ]
}

The prompt asks the AI to output:

[机器可读声明]
```json
{ ... }

The parser first looks for the last fenced JSON block near `[机器可读声明]`, then falls back to the last `{...}`.

**Known format issue (fixed):** The original prompt only defined `unitClaims` entries; `eventClaims` and `timelineClaims` were shown as empty arrays. The AI therefore invented its own fields (e.g., `"event"` / `"time"` instead of `"claim"`), which the parser silently ignored. Fixed by:
1. Adding full schema definitions for all three claim types in the prompt.
2. Making the parser accept `"event"` as a fallback for `"claim"` in `eventClaims`.

**Claim count limits added:** Prompt instructs the AI to limit output (unitClaims ≤ 10, eventClaims ≤ 5, timelineClaims ≤ 3). The parser enforces these caps and emits Info-level issues if the AI exceeds them.

## Validation We Can Do

### Implemented Now

Format validation:

- Missing machine-readable claims.
- JSON parse failure.
- Root value is not an object.
- Claim count limits with truncation warnings.
- `eventClaims` accepts both `"claim"` and `"event"` as field names.
- Unknown `evidenceLevel` values logged as Info issue, fallback to `Uncertain`.

Self-consistency validation:

- Unit claim missing `unitId`.
- Unit claim missing `claim`.
- High-confidence UnitId guess without evidence.
- Low-confidence UnitId guess without alternatives or needed confirmation.

### Evidence Format (Structured)

The `evidence` field now uses a structured pipe-delimited format instead of free-form text:

build|time|assetName|builderUnitId place|time|assetName|builderUnitId|x,y,z produce|time|unitName|producerUnitId sell|time|unitId select|time|unitId move|time|x,y,z power|time|powerName|unitId


This format is parsed by `ParseStructuredEvidence()` into a `StructuredEvidence` record with typed `AIEvidenceType` enum. Parsing uses a single regex and is fully deterministic.

**Backward compatibility:** The parser silently returns `Unknown` type for strings that don't match the structured format. No validation rules currently fire on unknown-typed evidence, so it degrades gracefully but invisibly.

### Near-Term Validations

These need replay facts extracted from `CommandChunk` or an intermediate fact index:

- UnitId production timeline contradictions (e.g., "bomber" claimed before first bomber production — needs game knowledge of which unit names are bombers).
- Claims that use game knowledge not present in rules, such as transport/amphibious/building-placement abilities.
- **Overconfidence detection:** Claims with `confirmed`/`highly likely` that lack sufficient evidence given what is knowable from replay data alone (e.g., claiming AlliedMCV as `confirmed`).

### Implemented via Fact Index

The `ReplayFactIndex` now powers these checks:

- **Special power contradiction:** Evidence `power|...|SomePower|unitId` is cross-checked against the actual special powers observed for that UnitId. If the power was never used, a `Contradiction` issue is emitted.
- **UnitId existence:** Warns if a claim references a UnitId never seen in any replay command.
- **Builder consistency:** If a claim describes a unit as MCV/builder but the UnitId was never observed as a builder, emits `WeakEvidence`.

### Suggested Fact Index

Useful derived facts:

- `UnitId -> first observed time`
- `UnitId -> observed special powers`
- `UnitId -> observed as builder`
- `UnitId -> observed as production structure`
- `Player -> first production time by asset id`
- `Player -> selected UnitIds over time`
- `Player -> tech/protocol choices`
- `Player -> building placements by asset and position`

## Current Progress

Implemented:

- Added `CONTEXT.md` glossary.
- Added prompt settings:
  - `AiPromptSettings`
  - `UseCustomSystemPrompt`
  - `CustomSystemPrompt`
  - `AdditionalRules`
- Added prompt editing UI to `AIProviderSettingsControl`.
- Connected prompt settings from `EventDump` to `AIChatPanel` to `AIAnalyze`.
- Split default prompt construction from prompt composition.
- Strengthened default prompt with evidence-first and uncertainty rules.
- Added machine-readable JSON claim instructions to system and segment prompts.
- Added `Utils/AIAnalysisValidation.cs` with:
  - evidence level enum
  - machine-readable claim records
  - validation issue records
  - JSON extraction and parsing
  - initial self-consistency checks
- Added per-segment validation logging in `AIChatPanel`.
- Fixed inconsistent prompt ↔ parser schema for `eventClaims`/`timelineClaims`:
  - Added full schema definitions for all three claim types in the system prompt.
  - Parser now accepts `"event"` as fallback for `"claim"` in `eventClaims`.
  - Both prompt and parser enforce claim count limits (10 unit, 5 event, 3 timeline) with truncation warnings.
- Unknown `evidenceLevel` values now produce an Info-level validation issue (fallback to `Uncertain`).
- Created ADR 0001 documenting the hidden revision pass design decision.
- Recorded MCV vs Miner ambiguity as a documented validation scenario.
- Evidence format changed from free-form text to structured pipe-delimited format:
  - 7 evidence types: `build`, `place`, `produce`, `sell`, `select`, `move`, `power`.
  - Prompt updated to require structured format only.
  - Added `StructuredEvidence` record and `ParseStructuredEvidence()` parser.
  - Added `ParseAllEvidence()` to convert all evidence strings for a claim.
- Added first validation rule `ValidateUnpackAmbiguity()`:
  - Flags `confirmed`/`highly likely` claims that use `UnpackReplaceSelf` without matching `PackReplaceSelf`.
  - Emits `WeakEvidence`/`MissingAlternative` — the unpack could be MCV deploy or miner command hub deploy.
  - If `PackReplaceSelf` IS present in the same claim's evidence, the chain is consistent and no flag.
- Created `Utils/ReplayFactIndex.cs` — builds a fact index from raw `CommandChunk` data:
  - `UnitIdFirstObservedTime`: first time each UnitId appears in any command.
  - `UnitIdSpecialPowers`: set of special powers used by each UnitId.
  - `BuilderUnitIds`: UnitIds that appeared as builder in construction commands.
  - `ProducerUnitIds`: UnitIds that appeared as production structures.
  - `PlayerFirstProductionTime`: per player, first production time for each unit asset name.
  - `PlayerSelectedUnitIds`: which UnitIds each player has selected.
- Plumbed `ReplayFactIndex` through the analysis pipeline:
  - Built in `EventDump.ShowPlainText()` from `CommandChunk` + string hash table.
  - Passed to `AIChatPanel.StartAnalysisAsync()` as new parameter.
  - Forwarded to `AIAnalysisValidation.ValidateMachineReadableClaims()`.
- Added `ValidateTimelineConsistency()` — three checks using fact index:
  1. **UnitId existence check:** Warns if a claim references a UnitId never seen in the replay.
  2. **Special power verification:** Cross-references `power|...` evidence entries against actual special powers observed for that UnitId; emits `Contradiction` if the claim says a UnitId used a power it never used.
  3. **Builder consistency check:** If a claim describes a unit as MCV/builder/Nanocore but that UnitId was never observed as a builder, emits `WeakEvidence`.

Build status:

- `dotnet build AnotherReplayReader.csproj --no-restore` succeeds.
- Remaining warnings are existing nullable warnings in `AIAnalyze.cs` stream response handling and a `System.Text.Encoding.CodePages` support warning for `net461`.

## Current Progress (continued)

This session (2026-07-07 knowledge architecture grilling):

- Conducted `/grilling` session via `/domain-modeling` skill to analyse knowledge split between prompt and validation.
- Reached consensus on knowledge architecture (see "Knowledge Architecture Decisions" above, recorded in ADR 0002):
  - KnowledgeSet as single source of truth, keyed by mod.
  - KnowledgeEntry as unified format (id + tags + text), scope inherited from path.
  - Predefined finite tag taxonomy (capability, type, combat role, specialPower:*).
  - Prompt rendering order: global → factions → map.
  - Validation consumes tags instead of hardcoded string matching.
  - User extensibility via JSON overlay file.
  - Storage: JSON container with markdown text.
- Updated CONTEXT.md glossary with refined KnowledgeScope, plus new KnowledgeSet, KnowledgeEntry, and KnowledgeTag terms.
- Created ADR 0002 documenting the structured game knowledge decision.
- Updated WIP.md with discussion notes and migration plan.
- **Wrote `tools/expand_knowledge.py`** — Python script that extracts the 5 `@""` knowledge strings from `AIAnalyze.cs`, expands all `[MOD:]` / `[MOD:NO:]` tags (both line-level and inline), and outputs per-mod knowledge files.
- **Generated `knowledge_default.md`** (732 lines, 22427 chars) — base game knowledge with `[MOD:CORONA]` content stripped, `[MOD:NO:CORONA]` content retained.
- **Generated `knowledge_corona.md`** (743 lines, 23177 chars) — Corona mod knowledge with `[MOD:CORONA]` content retained, `[MOD:NO:CORONA]` content stripped.
- Verified all 27 `[MOD:]` tag locations across all content sections; confirmed correct expansion for line-level tags, inline tags, and double consecutive inline tags.
- **Created `Utils/AiKnowledge.cs`** with core data types:
  - `KnowledgeTag` — static class with predefined tag constants (capability, type, combat role, `SpecialPower()` helper).
  - `KnowledgeScope` / `KnowledgeScopeKind` — scope identification (global, faction, map).
  - `KnowledgeEntry` — record with `Id`, `Tags[]`, `Text`; query methods `HasTag()`, `HasAnyTag()`.
  - `KnowledgeSet` — collection with `ByScope()`, `ByTag()`, `ByAnyTag()` queries, `RenderAsPrompt()` rendering, and `ForMod()`/`ForReplay()` factory methods that load from `knowledge_{mod}.md` files.
- **Updated `AIAnalyze.GetSystemPrompt()`** — tries `KnowledgeSet.ForMod()` with file-based loading first, falls back to legacy `BuildDefaultSystemPrompt()` if file not found.
- **Updated `AnotherReplayReader.csproj`** — added `knowledge_*.md` as `<Content>` with `CopyToOutputDirectory=PreserveNewest`.
- Build verified: `dotnet build AnotherReplayReader.csproj --no-restore` succeeds (5 pre-existing nullable warnings).
- **Created `knowledge_units.json`** — structured JSON knowledge for 盟军 (20 units, 8 buildings), each with assetName, tags, specialPowers, producedBy, and text. First pilot faction.
- **Added `UnitKnowledge`/`BuildingKnowledge` records** + `StructuredKnowledge` class to `AiKnowledge.cs` — lazy-loaded singleton, queries by tag, special power, and asset name.
- **Updated `ValidateTimelineConsistency()`** — `claimLooksLikeBuilder` now queries `StructuredKnowledge.Instance.UnitsWithTag("builder")` first, falls back to heuristic string matching.
- Added `knowledge_units.json` to `.csproj` as `<Content>`.
- **Merged `UnitKnowledge`/`BuildingKnowledge` → `EntityKnowledge`** — unified record with nullable `Tier` and `IsBuilding`/`IsUnit` helpers via tag check.
- **Added `SpecialPowerInfo`** record (`Name` + `Description`) — special powers now carry descriptions for prompt rendering.
- **`knowledge_units.json` format 1.1** — `specialPowers` changed from string array to `[{name, description}]`; `text` de-duplicated (no longer repeats assetName, displayName, specialPowers, producedBy); `produces` field removed (production type expressed via tags); corrected tag semantics (removed `naval` from amphibious land units).
- **Structured field-based rendering** — units now render as multi-line entries with explicit `类型`/`技能`/`生产`/`描述` fields instead of dumping raw `text`. Buildings render as `displayName(assetName): text`. Added `TagDisplayName()` helper for Chinese tag labels.
- **Refined tag taxonomy** — split type tags from capability/role tags; fixed misapplied `naval` tag on AlliedMiner, AlliedMCV, 激流ACV (these are amphibious vehicles, not naval vessels).

## Resolved Open Questions

- "How much game-unit knowledge should live in code versus prompt text?" — **Resolved by ADR 0002.** Knowledge lives in KnowledgeSets (structured data), not in code strings or prompt text. Code renders it to prompt; validation queries it by tag.
- "Should the first verifier use hardcoded RA3/Corona knowledge, or should it load a small unit capability table from data files?" — **Resolved by ADR 0002.** The first verifier uses the same KnowledgeSet as the prompt builder, queried by tag.

## Remaining Open Questions

- Should AI natural-language output continue streaming live, or should content be buffered until validation and possible revision are complete?
- Should reasoning chunks remain visible during hidden revision, or should only final content be shown?
- How strict should missing machine-readable claims be?
  - Current behavior: warning log only.
  - Possible future behavior: one hidden repair request asking the model to append valid claims.
- Should validation issues be visible by default, or only in an advanced/debug foldout?

## Suggested Next Steps

1. ✅ Build a replay fact index from `CommandChunk` — done (`ReplayFactIndex`).
2. ✅ Add first deterministic validation rules:
   - ✅ ambiguous Allied unpack — done (`ValidateUnpackAmbiguity`).
   - ✅ pack/unpack consistency — covered by unpack rule.
   - ✅ UnitId used as builder — done (builder consistency check in `ValidateTimelineConsistency`).
   - ✅ special power contradictions — done (special power verification in `ValidateTimelineConsistency`).
   - ⬜ first production time vs first operation time — needs game knowledge of unit type names (e.g., "which names are bombers").
3. ✅ **Knowledge migration** — implement the KnowledgeSet/KnowledgeEntry model planned in ADR 0002:
   - ✅ Extract built-in knowledge from `BuildDefaultSystemPrompt()` strings into mod-specific text files (`knowledge_default.md`, `knowledge_corona.md`). `[MOD:]` tags expanded by `tools/expand_knowledge.py`.
   - ✅ Define C# records (`KnowledgeSet`, `KnowledgeEntry`, `KnowledgeScope`, `KnowledgeTag` constants) in `Utils/AiKnowledge.cs`.
   - ✅ Wire `KnowledgeSet.ForReplay()` / `ForMod()` into `AIAnalyze.GetSystemPrompt()` — loads `knowledge_{mod}.md` at runtime if available, falls back to legacy `BuildDefaultSystemPrompt()`.
   - ✅ Added `knowledge_*.md` as `<Content>` in `.csproj` with `CopyToOutputDirectory=PreserveNewest`.
   - ✅ **Step 3: Structured data participates in rendering.** `KnowledgeSet.ForMod()` now merges flat text (`knowledge_*.md`) with structured entries (`knowledge_units.json`). Unit/building sections are automatically stripped from flat text (via `StripUnitSections()`) and replaced by structured entries rendered by tier. `RenderAsPrompt()` outputs both clean global text and structured faction entries in correct order.
   - ✅ **Step 2: More validation rules migrated.** `ClaimLooksLikeBuilder()` now queries `StructuredKnowledge.Instance.UnitsWithTag("builder")` first; falls back to heuristic string matching if structured data is unavailable.
   - ✅ **Step 1: Pilot faction (盟军) in `knowledge_units.json`.** 20 units + 8 buildings with assetName, tags, specialPowers, producedBy. Includes `aliases` support for multi-source units (e.g., 激流ACV).
   - ✅ **EntityKnowledge unification.** `UnitKnowledge`/`BuildingKnowledge` merged into single `EntityKnowledge` record; `SpecialPowerInfo` added for name+description pairs.
   - ✅ **JSON format 1.1.** `specialPowers` → object array with `name`/`description`; `text` de-duplicated; `produces` removed; tag semantics corrected.
   - ✅ **Field-based structured rendering.** Units render with `类型`/`技能`/`生产`/`描述` fields; `TagDisplayName()` maps tags to Chinese labels.
   - ⬜ Add user knowledge JSON file loading in `AiSettings.Load()`.
4. Decide whether to buffer per-segment content before display.
5. Add one hidden revision pass for `Contradiction` issues.
6. Add validation summary UI, such as "验证器发现并修正 N 个问题".