# p0151b: snake_case EvidenceMode JSON, ReadSet-anchored validator, block_condition relaxation

- [Architecture] `ISourceAnchorValidator` as a separate service rather than folding the rule into `IObservationNormalizer`. The normalizer's responsibility is "raw fields → typed observation"; gating that observation on runtime trace data is a different concern and would mix the two responsibilities. The validator takes the observation + an optional `IReadOnlyCollection<string>? readPaths` parameter — when null (no runtime layer wired, e.g. legacy test paths), the rule does not fire. When non-null, `evidence_mode=analyzed_from_source` observations whose `file` is not in the set are dropped with a Warning log.
- [Architecture] `SkillCallResult.ReadPaths` carries the trace's `ReadSet` snapshot out of the runtime layer so downstream parsers can apply the rule. The parser consumers (`SkillResponseParser.ParseAndDowngrade`, `FilterRoundCaller.InvokeBatchAsync`) pass it through to `ObservationParser`. Threading the value rather than reaching into the runtime keeps the parser side stateless and testable.
- [Architecture] JSON enum convention unified on `JsonNamingPolicy.SnakeCaseLower` for both `ObservationParser.JsonOptions` and `ConvergenceResultParser.JsonOptions`. The property name policy was already snake_case for the rest of the LLM-output contract; enum values were on CamelCase, which silently downgraded `analyzed_from_source` to `Potential` via the tolerant fallback. The flip eliminates the silent downgrade.
- [Architecture] `GateObservationParser.ParseEvidenceMode` was hand-rolled around `Enum.TryParse` (ignoreCase) — it accepted `analyzedFromSource` and `AnalyzedFromSource` but not `analyzed_from_source` because underscore stripping was missing. Added a `.Replace("_", "")` normalization so all three forms map to the same enum value. The Gate path does not get the ReadSet validator today; gate observations are decision-shaped and rarely cite source files.
- [Architecture] `ValidateBlockCondition` (in `NewFormatSkillValidator`) and the matching `CheckNewFormatRules` in `ValidateConceptsCommand` relaxed: `block_condition` is required for `role=judge` only when `output_schema` is `plan` or `gate`. Observation-emitting judges defer the gating decision to the consumer's policy; forcing `block_condition` on them was a residual rule from before the observation schema existed. The three source-reviewer skills (`auth-config-reviewer`, `controller-implementation-reviewer`, `upload-validator-reviewer`) now load.
- [Implementation] Validator drops the observation but never aborts the parse — a single bad observation should not throw away the rest of the skill's output. Same pattern as `ObservationParser.TryBuild` returning null for shape-invalid entries. The "skipped due to invalid JSON shape" warning was extended to include "or source-anchor mismatch" so operators see both reasons in the same line.
- [Implementation] `SourceAnchorValidator.Contains` is `OrdinalIgnoreCase`. Different-case spellings of the same path are folded together; downstream skills do not need to canonicalize before citing. Matches the case-insensitive convention used by `LoopTraceCollector.ReadSet`.
- [TradeOff] When `readPaths` is null (e.g. a unit test constructing `ObservationParser` directly with the test-helper factory), the validator is permissive. Production callers that go through `SkillCallRuntime` always populate `ReadPaths` (empty list if no reads happened) so the rule fires. The two-mode behavior could be controversial — a strict reading would always enforce — but the existing test fixtures bypass the runtime layer routinely (the parser is tested in isolation), and requiring an empty array everywhere would be ceremony.
- [TradeOff] `ObservationParser`'s public method signatures grew an optional `readPaths` parameter on `Parse`, `TryParseWithoutIds`, and `ParseWithoutIds`. The interface seam (`ISkillResponseParser.ParseAndDowngrade`) likewise gained an optional parameter. Backwards compatible at the source level — existing call sites that pass nothing default to the permissive mode. Callers that have the runtime result pass `result.ReadPaths`.
- [TradeOff] Two existing tests (`SkillMdParserNewFormatTests.ParseNewFormat_BlockConditionMissingOnJudge_Rejected` and `ValidateConceptsCommandNewFormatTests.Validate_NewFormatSkillJudgeWithoutBlockCondition_ExitOneAndPrintsRule`) asserted the old strict-for-all-judges contract. Both were rewritten as pairs: one accepting the observation-schema judge without `block_condition`, one rejecting a plan-schema judge without it. The intent of the original tests is preserved (rule is enforced where it matters) but scoped correctly.
