[content] HydrationStage integration for content resolution
## Problem to Solve
The query pipeline (Security → Compilation → ClickHouse → Extraction → Authorization → Redaction → Hydration → Output) currently has no hook for resolving externally-sourced data. Content resolution needs to happen after ClickHouse hydration (we need `project_id`, `branch`, `file_path`, `traversal_path` from the hydrated rows) but before the output stage.
## Proposed Solution
Add a **content resolution step** within or immediately after `HydrationStage`:
1. After ClickHouse hydration completes, check if any node in the result set has a `content` virtual property requested
2. Extract `(node_id, project_id, branch, file_path, start_byte?, end_byte?, traversal_path)` from hydrated rows for nodes needing content
3. Derive `organization_id` from `SecurityContext.org_id` and `root_namespace_id` from `traversal_path` second segment
4. Build `ContentRequest` batch and call `ContentResolver::resolve_batch()`
5. Merge returned content bytes back into the row map as the `content` property
**Integration point**: Extend `HydrationStage::execute()` to call content resolution after `parse_consolidated_batches()` and `merge_static_properties()`.
**Configuration**: `ContentResolver` is injected into the pipeline context (via `AppState` or `PipelineContext`), selected at startup based on `content_cache.enabled` config.
## Acceptance Criteria
- [ ] HydrationStage calls ContentResolver when `content` property is requested
- [ ] org_id extracted from SecurityContext, root_namespace_id from traversal_path
- [ ] Content bytes merged into result rows as `content` property
- [ ] Works for both static and dynamic hydration plans
- [ ] Batch limit error propagated as structured pipeline error
- [ ] Integration tests with real ClickHouse + mock Gitaly
issue