feat(infra) Phase 4 W1-4 — flip deployAiSearch=true + --ensure-ai-search smoke probe#103
Merged
Merged
Conversation
…rch smoke probe
Phase 4 W1-4 per docs/build-spec.md § Phase 4 scope item 13. Unblocks
the H1 operational hand-off (Bicep apply → AI Search Basic provisioned
in dev) and the Wave 2 W2-3 embedding pipeline + index population
(ADR-0021 — index schema for `pinwiz-rag-v1`).
What this PR ships:
- `infra/main-shared.dev.bicepparam`: add `param deployAiSearch = true`.
AI Search Basic was deferred during Phase 3 H1 fix-up via the
`deployAiSearch=false` override in main-shared.dev.local.bicepparam
(East US 2 capacity exhausted on H1 day 2026-05-07; Phase 3 doesn't
consume AI Search). Phase 4 RAG ingestion is the consumer; flipping
the committed param makes the intent explicit. Operator follow-up:
remove the `param deployAiSearch = false` line from the local override
so the committed `true` takes effect.
- New `src/PinballWizard.Core/Configuration/AiSearchOptions.cs`: sealed
options class with `Endpoint` (the search service URL, [Url]-validated)
and `IndexName` (default `pinwiz-rag-v1` per ADR-0021). Mirrors the
AiFoundryOptions sectioning convention with a `EndpointKey` constant
for presence-checking from gating code.
- New `src/PinballWizard.Infrastructure/Integrations/AiSearch/`:
- `IAzureAiSearchSmokeProbe.cs` — the probe interface + result record.
- `AzureAiSearchSmokeProbe.cs` — connects via DefaultAzureCredential,
calls `SearchIndexClient.GetServiceStatisticsAsync` (lightest call
that exercises endpoint reachability + AAD auth), returns a
structured result. Mirrors AzureFoundrySmokeProbe — idempotent,
structured-result-on-failure, safe to re-run.
- `ServiceCollectionExtensions.cs` — `AddAzureAiSearchIntegration`
binds the options + registers the probe. Mirrors
`AddAzureFoundryIntegration` shape.
- New `--ensure-ai-search` CLI flag in `src/PinballWizard.Cli/Program.cs`:
resolves the probe from DI; exit-code-2 + remediation message when
AiSearch:Endpoint isn't configured; success message reports the
endpoint + expected index name. Mirrors the --ensure-azure-foundry
exit-code-2 pattern. The host's `CreateHost` gates
`AddAzureAiSearchIntegration` on AiSearch:Endpoint presence so absence
is a valid configuration in Phase 0/1/2/3 and in local dev before H1.
- New `Azure.Search.Documents` 11.7.0 NuGet (the GA major covering the
semantic-ranker + vector-field surface ADR-0021 locks). Wave 2 W2-3
extends consumption for index creation + document upsert; Wave 3
W3-3 adds hybrid-retrieval queries.
- 6 new unit tests (`AzureAiSearchSmokeProbeTests.cs`) exercising the
misconfiguration paths (empty endpoint, whitespace endpoint, malformed
URL, custom IndexName preserved through failure result, ctor null-arg
guards). Mirrors AzureFoundrySmokeProbeTests pattern. Test count
718 → 724.
Local review: 0 🔴 / 3 ⚠️ / 7 ✅. Two ⚠️ addressed:
1. Test now asserts `AiSearchOptions.EndpointKey` appears in the
malformed-URL error message (parity with the empty-endpoint test
already-asserted).
2. bicepparam comment now explicitly names which line to remove from
main-shared.dev.local.bicepparam (operator UX).
The third ⚠️ (the empty-endpoint code path is unreachable through DI's
ValidateOnStart but remains as defensive depth) is deferred per the
sibling-consistency principle — the Foundry sibling has the same
defensive branch.
Build: 0 warnings, 0 errors. Tests: 724 / 724.
Operator hand-off (H1) after this merges:
1. Remove `param deployAiSearch = false` from main-shared.dev.local.bicepparam.
2. Pre-flight East US 2 AI Search Basic capacity via portal; if
constrained, edit the bicep param to relocate to East US or Central
US (Phase 3 lesson 3 — Cosmos location stays unchanged).
3. `pwsh ./infra/scripts/Deploy-SharedResources.ps1 -Environment dev`.
4. `dotnet run --project src/PinballWizard.Cli -- --ensure-ai-search`.
5. Record apply timestamp in decision-log.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 4 W1-4 per
docs/build-spec.md§ Phase 4 scope item 13. Unblocks the H1 operational hand-off (Bicep apply → AI Search Basic provisioned in dev) and the Wave 2 W2-3 embedding pipeline + index population (per ADR-0021'spinwiz-rag-v1index schema).This is the smallest planned PR of Wave 1 — a 1-line param flip + the post-deploy smoke probe needed for H1 verification.
What ships
infra/main-shared.dev.bicepparam: addparam deployAiSearch = true. AI Search Basic was deferred during Phase 3 H1 fix-up via thedeployAiSearch=falseoverride inmain-shared.dev.local.bicepparam(East US 2 capacity exhausted on H1 day, 2026-05-07; Phase 3 doesn't consume AI Search). Phase 4 RAG ingestion is the consumer; flipping the committed param makes the intent explicit.AiSearchOptionsinCore/Configuration/:Endpoint([Url]-validated) +IndexName(defaultpinwiz-rag-v1); mirrorsAiFoundryOptionssectioning +EndpointKeyconstant for gating.IAzureAiSearchSmokeProbe+AzureAiSearchSmokeProbeinInfrastructure/Integrations/AiSearch/: connects viaDefaultAzureCredential, callsSearchIndexClient.GetServiceStatisticsAsync(lightest call exercising endpoint reachability + AAD auth), returns a structured result. Idempotent, structured-result-on-failure, safe to re-run. MirrorsAzureFoundrySmokeProbeline-for-line.AddAzureAiSearchIntegrationDI extension: binds options + registers the probe withValidateOnStart. MirrorsAddAzureFoundryIntegration.--ensure-ai-searchCLI flag inProgram.cs: exit-code-2 + remediation message whenAiSearch:Endpointisn't configured; success message reports the endpoint + expected index name. Gated DI viaCreateHostso absence is valid config in Phase 0/1/2/3 and in local dev before H1.Azure.Search.Documents11.7.0 NuGet — the GA major covering the semantic-ranker + vector-field surface ADR-0021 locks. Wave 2 W2-3 extends consumption for index creation + upsert; Wave 3 W3-3 adds hybrid-retrieval queries.AzureFoundrySmokeProbeTestsshape (empty/whitespace/malformed endpoint, custom-IndexName preservation through failure result, ctor null guards).Test Plan
dotnet build PinballWizard.slnx— 0 warnings, 0 errorsdotnet test PinballWizard.slnx— 724 / 724 passing (was 718; +6 fromAzureAiSearchSmokeProbeTests)Integrations/Foundry/AzureFoundrySmokeProbe— see "Local review" sectionOut of Scope
applyitself (operator hand-off H1 — see "Operator follow-up" below).AiSearchRagIndexer).AiSearchRagRetriever).Azure.ResourceManager.Search) which adds another tenant-scoped dependency for marginal value at this gate. The deploy script already reports the provisioned SKU. Tracked for revisit if H1 surfaces SKU mismatches.Operator follow-up after merge (H1 hand-off)
Per build-spec § Phase 4 § Operational hand-offs § H1:
param deployAiSearch = falsefrommain-shared.dev.local.bicepparamso the committedtruetakes effect.infra/main-shared.dev.bicepparamto relocate AI Search to a sibling region (East US, Central US — Phase 1 Cosmos location stays unchanged; cross-region search-from-app latency penalty is small per Phase 3 lesson 3).pwsh ./infra/scripts/Deploy-SharedResources.ps1 -Environment dev.dotnet run --project src/PinballWizard.Cli -- --ensure-ai-search. Expected output:Azure AI Search verified: endpoint reachable at <endpoint> (expected index: pinwiz-rag-v1; index creation lands in Wave 2 W2-3).docs/decision-log.md.Checklist
~/.claude/projects/c--projects-PinballWizard/memory/is now stale, it has been updated or removed — N/A (the wave 0 close + cleanup handoffs explicitly reference W1-4 as the next item; they remain accurate)TODO/FIXME/ commented-out code committed<NoWarn>without a comment — N/APre-push self-audit
Step 0 —
/local-review(qualitative)/local-reviewand addressed every 🔴 finding before pushMalformedEndpointtest now assertsAiSearchOptions.EndpointKeyappears in the error message (parity with the empty-endpoint test).AzureAiSearchSmokeProbe.ProbeAsyncis unreachable through DI becauseValidateOnStart()rejects empty endpoints at host construction. Kept as defensive depth — mirrors theAzureFoundrySmokeProbesibling pattern; the unit tests constructOptions.Create(...)directly and exercise the path. Cost of deferring: zero.main-shared.dev.local.bicepparam(operator UX).Step 1 — Mechanical checklist
*Optionsproperty has at least one real getter call insrc/—Endpointread inAzureAiSearchSmokeProbe.cs:38, 47, 51+ gated inProgram.cs;IndexNameread inAzureAiSearchSmokeProbe.cs:43, 52, 67, 87, 93, 98(every code path passes it into the result record'sExpectedIndexName)AzureFoundrySmokeProbeline-for-line (see local-review category 4): identical ctor null-checks, identicalUri.TryCreatemalformed-URL pattern, identical catch filterwhen (ex is not OperationCanceledException), identical log-message shape on success, identicalOptionsshape (SectionName + EndpointKey + [Url] + ValidateDataAnnotations + Validate + ValidateOnStart), identicalTryAddSingleton. Drift: the AI Search probe has one additional defensive test (PreservesCustomIndexNameInResult) covering the IndexName-in-failure-result round-trip — additive, not drift.catch { }— all catches scoped tocatch (Exception ex) when (ex is not OperationCanceledException)ISourceScraper? — N/A0 Warning(s), 0 Error(s)git log -1 --format='%an <%ae>'shows personal noreply, not work email — confirmedJim Keeley <94459922+jkeeley2073@users.noreply.github.com>