AdvisoryAI consolidation — S0 target design (SPRINT_20260722_013 AAI-1)
Status: APPROVED (owner, 2026-08-03 — ledger round 88, ruling C “ALL TEN S0 drafts APPROVED”); S2+S3 BUILT (2026-08-04, AAI-3 — see §2.3/§2.4); S4 DONE (2026-08-05, AAI-4 — the OpsMemory HTTP surface lifted byte-identically into
src/AdvisoryAI/__Libraries/StellaOps.OpsMemory.Application, the deadOpsMemoryDecisionHookdeleted with five build-boundary pins, and every question this design parked on AAI-4 ruled: see §2.2A, §3 and §4). Verification base: the sprint’s recorded dig rounds (12/13 RAG-store anatomy), the live migration baselines (tables re-enumerated fromsrc/this pass), and the 2026-08-03 build-boundary register. All ⚠ items are closed; §4 records which questions AAI-3 answered and which stay open with their owners.
1. Current → target shape
| Current | Target | |
|---|---|---|
| Deployables | advisory-ai-web + advisory-ai-worker, opsmemory-web | advisoryai-web + advisoryai-worker (OpsMemory becomes a MODULE of the family, not a deployable) |
| Source | src/AdvisoryAI/StellaOps.AdvisoryAI.*, StellaOps.OpsMemory.* | `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService |
| DAL | migrations in StellaOps.AdvisoryAI/Storage/Migrations + __Libraries/StellaOps.OpsMemory/Migrations | StellaOps.AdvisoryAI.Persistence (D12 name), fresh 001_*, database stellaops_advisoryai, env STELLAOPS_POSTGRES_ADVISORYAI_CONNECTION fail-closed |
2. Table inventory + dispositions (P13)
2.1 advisoryai (24 tables)
| Group | Tables | P13 class | Disposition |
|---|---|---|---|
| RAG store | kb_doc, kb_chunk, entity_alias | derived-rebuildable (embeddings need a declared retention class) | carry, but the index budget decision is the S1 headline: kb_chunk = 130 MB of index over 37 MB heap, 12 indexes incl. FIVE per-language tsvectors + two trigram; embeddings are float4[] (no pgvector). Decide ONCE with measurements: pgvector vs app-side similarity; per-language FTS vs one config + trigram (P13 named-reader rule) |
| Chat family | chat_sessions, chat_messages, chat_tool_invocations, chat_evidence_links, chat_policy_decisions, conversations, turns, runtime_chat_settings_overrides | operational + audit | Lineage question ANSWERED (code dig 2026-08-03): not duplicates — two ROLES. conversations/turns = the EF conversation STATE store (ConversationStore/PostgresConversationService); chat_* = the chat AUDIT lineage (PostgresAdvisoryChatAuditLogger via AdvisoryChatService). Both DI-wired and live. Carry BOTH with distinct P13 classes (operational vs audit); all empty live, so the baseline is free to normalize names |
| AI attestations | ai_run_attestations, ai_claim_attestations | evidence-adjacent (append-audit) | carry byte-exact |
| Consent | ai_consents | audit | carry |
| Runtime | runtime_runs, runtime_explanations, runtime_policy_intents | operational | carry |
| Search telemetry | search_events, search_history, search_feedback, search_quality_alerts | cache-bounded | carry with cache-bounded classes. (The draft row said “windowed” in the class column and “cache-bounded” in the disposition; cache-bounded is the canonical P13 class and is what AAI-3 stamped on all four.) |
| API registry | api_spec, api_operation | registry | carry — readers VERIFIED (2026-08-03): in-family only (KnowledgeSearch/UnifiedSearch stores + KnowledgeIndexer) |
doctor_search_projection | doctor search read model | derived | coordinate fate with 009 (doctor registry/aggregation succeeded the central probing; do not carry silently) |
2.2 opsmemory (1 table)
Code side (AAI-4, 2026-08-05): the OpsMemory HTTP surface — the six decision/suggestion/stats endpoints, OpsMemoryTenantResolver and OpsMemoryPolicies — moved byte-identically out of the retiring opsmemory-web host into src/AdvisoryAI/__Libraries/StellaOps.OpsMemory.Application. AAI-9 gate 9 now mounts them in the surviving advisoryai-web; AAI-10 still owns predecessor deletion. Program.cs and its Translations/*.json deliberately stay behind: the host registers the bundle from Assembly.GetExecutingAssembly(), so moving the resources with the code would have silently broken localization on a live service. The moved types still declare StellaOps.OpsMemory.WebService.* namespaces — renaming is AAI-10’s job with the host deletion, and keeping them is what makes the move auditable as a pure move.
decisions — the OpsMemory decision memory. P13 class: source-of-truth(stamped by AAI-3; the draft assigned this table no retention class, which P13 requires of every table). Source of truth rather than derived because an operator’s decision and its recorded outcome have no rebuild path from anything else in the estate. Carry into the consolidated baseline (module, not deployable).
2.2A Not in the table inventory: the second, private migration ledger
Found by AAI-3 (2026-08-04) and not enumerated by this design. Before the AAI-9 source cut, the live advisoryai schema carried a SECOND migration ledger beside the platform runner’s advisoryai.schema_migrations: PostgresKnowledgeSearchStore.EnsureSchemaAsync creates advisoryai.__migration_history, then loads every embedded .Storage.Migrations. resource from StellaOps.AdvisoryAI and applies whatever that ledger reports unapplied. It runs on every index pass (KnowledgeIndexer.cs:48, UnifiedSearchIndexer.cs:49 and :143), so it is a fourth migration discovery path in this family and a second migration authority over one schema — a §2.7 discipline defect the merge must not inherit.
The consolidated baseline does not reproduce it: one schema set, one ledger, pinned by OnlyOneMigrationLedgerExists_TheLegacyPrivateOneDoesNotCarry. It also cannot reach the staged baseline (it scans the legacy assembly for a .Storage.Migrations. resource path, and the new assembly is neither), so the AAI-3 staging invariant survives it.
Ruled by AAI-4 (2026-08-05) — the retirement is an AAI-9 step, not an S4 one (D-AAI4-2). Two facts moved it. The duplication is now proven exact: the collapsed legacy tree embeds exactly one resource, StellaOps.AdvisoryAI.Storage.Migrations.001_v1_advisoryai_baseline.sql (enumerated from GetManifestResourceNames() on the built assembly; everything else is under _archived/ and excluded by the csproj), and the host’s AddStartupMigrations(...) applies that same resource from that same assembly into advisoryai.schema_migrations — so the private ledger is a pure duplicate authority over one file. But the two authorities do not necessarily share a connection: the startup migrator rides PostgresOptions.ConnectionString (AdvisoryAI:Storage / ConnectionStrings:Default / Database:ConnectionString) while the store rides KnowledgeSearchOptions.ConnectionString (AdvisoryAI:KnowledgeSearch). Wherever those point at different databases, EnsureSchemaAsync is the only migrator for the store’s database, and deleting it early strips a live service of its schema convergence. There is also no consolidated code path to retire it in, because D-AAI3-2 forbids anything that ships from referencing StellaOps.AdvisoryAI.Persistence until AAI-9.
AAI-9 gates 1–2 source result (2026-08-22): DONE atomically, not deployed. Both runtime option paths now resolve the one service-owned connection; EnsureSchemaAsync, every call to it, its private-ledger implementation, and the active legacy embedded resource were removed in the same changeset. The web host registers the gated consolidated migration authority before either indexer; the worker registers only the runtime DAL. OnlyOneMigrationLedgerExists_TheLegacyPrivateOneDoesNotCarry and the fresh 24+1 convergence suite pin the result. The maintenance-window data copy, configuration arm, image build, and live verification remain AAI-9 work.
2.3 Baseline decisions APPLIED (AAI-3, 2026-08-04)
src/AdvisoryAI/__Libraries/StellaOps.AdvisoryAI.Persistence/Migrations/001_advisoryai_consolidated_baseline.sql creates 24 + 1 tables under ONE ledger in advisoryai.schema_migrations. Beyond the §2 table dispositions:
- Both schema names reproduced inside the one database
stellaops_advisoryai, because AAI-9 copiesadvisoryaiandopsmemoryas two units and gates on per-schema row counts. OpsMemory is a module of the family, not a deployable; the surviving schema name is a data-move artefact, not a service boundary. - RAG index budget carried VERBATIM — deliberately not decided here. §2.1 dispositions the RAG store as “carry” and AAI-2 recorded the pgvector-vs-
float4[]and five-FTS-configs-vs-one questions as measurement-gated on the benches the owner approved (review item A9). On a Postgres without pgvector the baseline creates exactly the 12 indexes this design measured, which is the shape AAI-4 benches against. The count reconciles precisely: six tsvector GIN indexes (body_tsvplus the five per-language columns) + two trigram +doc_id+kind+entity_keydomain.
- The pgvector column stays conditional — not an oversight. The live readers probe
information_schemaforembedding_vecand take the app-side array path when it is missing, so declaring it unconditionally would break convergence anywhere the extension is absent, which includes the live database. That absence is also why the measured anatomy is 12 indexes and not 13. - Authored in the runtime shape. Every column the legacy lineage bolted on through ten migrations of
ALTER TABLE ... ADD COLUMNis declared inline, and none of the legacy data-repairUPDATEbackfills carries. Exactly oneALTER TABLEsurvives in the whole file — the conditional pgvector column. search_feedbackis created once. The collapsed legacy baseline creates it twice, from005_search_analytics.sqland005_search_feedback.sql, the second silently a no-op underIF NOT EXISTS. Same shape both times, so nothing is lost by creating it once.- The P6 eventing core is NOT registered, diverging from all three wave-2 siblings. Notify registered it to retire
notify.locks(DC-35), Findings for the DC-29 overlay outbox, JobEngine for the fenced lease. This family has no lock, lease or outbox table and zero references toStellaOps.Eventinganywhere in its tree, so aneventingschema here would have no named reader — the P13 violation the baseline exists to avoid. AAI-4 adds it if theOpsMemoryDecisionHook(§3, find #7) is wired as a real event consumer; because that is the one shared P6 library, adding it later cannot fork a lineage. - Zero seed rows (§2.11), proven by both a fresh convergence and an explicit Seed-category run.
2.4 Staging: how the fresh baseline stays off the shared database
main stays deployable at every commit, and all three family containers run live today (advisory-ai-web, advisory-ai-worker, opsmemory-web) against the shared platform database. Four discovery paths exist and none reaches the staged baseline:
| Path | Scans | Why it cannot reach the baseline |
|---|---|---|
| advisory-ai-web / -worker | typeof(AdvisoryAiDataSource).Assembly, prefix-free (AdvisoryAiCoreRuntimePersistenceExtensions.cs:64) | binds the LEGACY assembly by type |
| opsmemory-web | typeof(IOpsMemoryStore).Assembly, prefix-free (OpsMemoryRuntimePersistenceExtensions.cs:102) | binds the LEGACY assembly by type |
| platform-web central migrator | AppDomain scan for IMigrationModulePlugin | already discharged: PAC-6b removed BOTH AdvisoryAiMigrationModulePlugin and OpsMemoryMigrationModulePlugin plus the Platform.Database project references; this assembly declares no plugin. 2026-09-14 (SPRINT_20260722_021 PLT-4): the scan and the plugin contract are deleted outright |
PostgresKnowledgeSearchStore.EnsureSchemaAsync (§2.2A) | legacy assembly, .Storage.Migrations. resource path | new assembly matches neither |
Because the DC-26 central-migrator removal was already done for this family, AAI-3 inherits the Findings situation rather than the JobEngine one, and — like Findings — needs no .Consolidated suffix, since the D12 target DAL name was never taken.
The AAI-3-specific part. The three siblings could stage safely on the assembly name alone, because the hosts they created shipped nowhere. Here the design’s target host projects already exist and are live (§1), so the invariant is a reference question instead: nothing that ships may reference StellaOps.AdvisoryAI.Persistence at all, and the consolidated persistence is therefore not wired into either host’s Program.cs. Wiring it now would make the next rebuild of a live image resolve STELLAOPS_POSTGRES_ADVISORYAI_CONNECTION — which no compose file sets — and fail closed on startup. The wire-up belongs to the AAI-9 cutover, and the invariant is enforced by NoShippingProjectReferencesTheConsolidatedPersistence.
3. Build-boundary dispositions (2026-08-03 register)
State after AAI-4 (2026-08-05). Two of the eight are gone.
excititordied with SPRINT_20260722_003 VULN-B1’s removal of theStellaOps.Excititor.Corereference (the provider it fed was inert), andfindings-ledgerdied with theOpsMemoryDecisionHookdeletion (D-AAI4-1).advisory-ai-webandadvisory-ai-workernow carry SIX pins each —attestor,concelier,evidence-locker,opsmemory,policy,scanner— andopsmemory-webcarries ZERO, all three of its pins having been reached through that same hook edge. The list below is the S0 inventory with its original dispositions; strike-through of resolved entries is noted inline.
Both keys carried the same EIGHT pins at S0 — the widest per-key foreign surface among the wave-2 programs, all inbound-to-AdvisoryAI-as-consumer:
concelier+excititor— the RAG/context readers over the old vuln plane; repoint to the hub SDK/APIs/corpus artifact (003 consumer family; AdvisoryAI is explicitly named in the 003 survey’sExcititor.Coretype-consumer cluster and holds a directConcelier.Persistenceedge — one of the HARD violations).opsmemory— intra-family; dies by consolidation.attestor+evidence-locker— repoint onto the 011 closed Evidence SDK (S0 there enumerates this family among the five inbound callers).policy,scanner,findings-ledger— exact-type inventory DONE (2026-08-03), and it is tiny: the whole non-test AdvisoryAI tree has THREE foreign usings.policy=LatticeRule/LatticeExpressionmodel types inPolicyStudio/PolicyBundleCompiler(pure-model shapes — the classification/extraction path applies).findings-ledger=OpsMemoryDecisionHookimplementing Findings’IDecisionHook— DEAD WIRING (find #7): no host registers it (the only estate registration ofIDecisionHookis findings-ledger-web’s own cache-invalidation hook). RESOLVED by AAI-4 (2026-08-05, D-AAI4-1): the hook and theStellaOps.Findings.LedgerProjectReferenceare deleted. That one edge was the shortest witness for FIVE pins —findings-ledgeron both advisory-ai keys and all threeopsmemory-webpins, sinceattestorandevidence-lockerwere reached through it — soopsmemory-webwent to zero. Re-wiring OM-007 is a later-stage DC-32 EVENT consumer; never by findings loading OpsMemory source, and never by OpsMemory loading findings source.scanner= zero direct usings; purely transitive baggage.
4. What S1 must decide
- Merge confirmed per ADR-039 D14 (no deviation found by this S0). CLOSED — AAI-2, owner via D14 + round-88 ruling C.
- The RAG index budget (§2.1) — with measurements, not taste. OWNER RULING (2026-08-03, review item A9): benches approved (“surely, do benches”) — the pgvector-vs-app-side and one-FTS-config-vs-five decisions follow the measured numbers. STILL OPEN; owner is a named measurement window, NOT AAI-4 (corrected by AAI-4, D-AAI4-4: a bench is only meaningful against the real corpus, whose 130 MB of index lives on the live database — an owner-gated surface no implementation stage may touch — and synthetic data answers a different question). AAI-3 carried the shape verbatim so the benches have a fixed baseline to measure against (§2.3); the baseline is editable in place while unapplied. Carried is not decided.
doctor_search_projectionfate (with 009). RULED by AAI-4 (2026-08-05, D-AAI4-3): CARRY, with the writer retiring at AAI-10 and the table only after that, in a separate destructive window. The 009 coordination still has not happened — a repo grep ofSPRINT_20260722_009finds no mention of the table — so AAI-4 did not invent 009’s ruling; it supplied the missing evidence instead, and the evidence sharpens the finding: the table has one writer and zero readers.PostgresKnowledgeSearchStore.cs:1044INSERTs on every index pass, there is no SELECT against it anywhere insrc/, andIKnowledgeSearchStoreexposes no doctor-read method. The doctor capabilityknowledge-search.md:38describes is actually served fromkb_chunkviaKnowledgeOpenDoctorAction. So this is a write-only derived table — a P13 named-reader violation, not merely an uncoordinated carry. It carries anyway for safety, not merit: dropping it is a destructive action needing its own window (ADR-004), and the live writer survives until AAI-10, so removing it from the baseline would breakadvisoryai-webthe moment AAI-9 repoints it. Same class as the JobEngine sibling’s D-JOB3-6 (scheduler.doctor_trends). Pinned byDoctorSearchProjection_IsCarriedLoudly_WithItsLiveWriter.- Chat-lineage overlap — ANSWERED (§2.1: two roles, carry both with distinct P13 classes). CLOSED and enforced: the baseline stamps
append-auditon thechat_*lineage andoperationalonconversations/turns, and a test asserts the two classes DIFFER, so a later edit cannot quietly collapse them into the “second full representation of one semantic fact” P13 forbids. - (New, raised by AAI-3.) Retiring
PostgresKnowledgeSearchStore.EnsureSchemaAsyncand the legacyStorage/Migrationsembedded resources — RULED by AAI-4 (2026-08-05, D-AAI4-2): owner is AAI-9, executed in the same commit that repoints the store. Rationale and the AAI-9 obligation in §2.2A. - (New, raised by AAI-4.)
IOpsMemoryClient/NullOpsMemoryClient— RESOLVED by AAI-9 gate 9 (2026-08-22, D-AAI9-4). The unused service-shaped abstraction and local Null fallback are gone;OpsMemoryDataProviderconsumes the in-processIOpsMemoryChatProvider, registered with the lifted store/playbook/chat services from the one owned AdvisoryAI DSN. No network seam or second migration authority was introduced. Amended 2026-09-07 (AAI-14): the data-provider registration is split by branch. BecauseIOpsMemoryChatProvideris registered only where the AdvisoryAI-owned DSN resolves, an unconditional registration ofOpsMemoryDataProvidermade a DSN-less local harness fail DI validation at host construction.AddAdvisoryChatDataProvidersnow keeps the concrete registration on the production branch — so a live host without OpsMemory storage still fails closed at construction — and on the local-harness branch (Development/TestingplusAdvisoryAI:RuntimeBindings:AllowLocalHarnessFakes=true) registers a factory that resolves the real provider when storage is present andNullOpsMemoryDataProviderwhen it is not. This does not reinstate the client seam D-AAI9-4 deleted: there is still noIOpsMemoryClient, no network indirection, and no production-path fallback. Verified-by:AdvisoryAiRuntimeBindingGuardTests.AdvisoryChat_OnTheProductionBranch_KeepsTheConcreteOpsMemoryRegistrationand its two local-harness siblings.
Remaining verification CLOSED (2026-08-03):
- Routes (7):
/api/v1/search,/api/v1/advisory-ai+/v1/advisory-ai,/api/v1/advisory/api/v1/advisories(alias pair),/api/v1/chat,/v1/evidence-packs— all toadvisoryai.stella-ops.local. (/api/v1/advisory-sources+/ingest/advisorybelong to Concelier, not this family.) CORRECTED by AAI-5 (2026-08-05): “alias pair” overstates it — see §5. The count of seven is right and the host is right, but that pair is DEAD (F-AAI5-3) and the inventory missed a wildcard-served surface (F-AAI5-2).
- Console:
advisory-ai.client.ts+advisory-ai-api.client(chat/advisory pages) ride the prefixes above; UI-E2 registry carries them as a group. CORRECTED by AAI-5: the registry carried only TWO of this family’s prefixes (advisoryAi,advisoryAiRuns); twenty literals across ten files bypassed it, including/api/v1/search,/api/v1/opsmemoryand/v1/evidence-packs— see §5. - Deploy reality: all THREE family containers run live (
advisory-ai-web,advisory-ai-worker,opsmemory-web) — note OpsMemory currently runs as its OWN deployable; the merge folds it to a module, so the compose delta RETIRES a running container (deploy window + drain, not just a key rename). CORRECTED by AAI-8 (2026-08-05): the retirement is NOT at S8. S8 stages the overlay OUTSIDE the defaultconfig_fileslist and changes nothing running;opsmemory-webmust keep serving through soak. AAI-9 gate 9 mounts the liftedStellaOps.OpsMemory.Applicationendpoints on advisoryai-web before any route/runtime cutover; AAI-10 owns the later predecessor retirement. See §6. - Pins’ exact types: §3 (three usings total; one dead hook; scanner transitive-only).
5. S5/S6 — API surface, gateway and Console (AAI-5, 2026-08-05)
Currency note (2026-09-04) — the route swap described below is RETIRED, not pending. The 2026-08-28 owner ruling (“all microservices needs to autopublish no router configuration for microservices endpoints”) removed the routes this swap replaced; RAR-7 (
8bebf33263) executed that and AAI-9 gate 6 was rewritten (F-AAI9-15).tools/scripts/aai5-swap-advisoryai-routes.ps1now exits 2 on every path —-Checkand a bare run alike — because installing a static^/api/advisoryai/v1(.*)group would out-rank the surface advisoryai-web already publishes. There is no-Checkdrift report to read any more. Confirm the end state withRouteSwapManifestStateTests; the operator procedure isdocs/runbooks/advisoryai/advisoryai-aai9-data-move.md(step 11). The manifest is retained for its route-table findings. Everything below is the 2026-08-05 design record, kept for those findings, and is not an instruction to install the group.
The swap package was the deliverable, not an applied change: manifest (tools/scripts/aai5-advisoryai-route-swap.json), apply script (tools/scripts/aai5-swap-advisoryai-routes.ps1), guard test reading the SAME manifest (AdvisoryAiConsolidatedRouteConfigTests), and window runbook. Nothing was installed live — per D8/DC-19 this family gets ONE routing change and it belonged to AAI-9.
The consolidated group: ^/api/advisoryai/v1(.*) → http://advisoryai.stella-ops.local/api/advisoryai/v1$1, Microservice, regex. Spelling is advisoryai, not the sprint’s advisory-ai (D-AAI5-1): every other target identifier in this program drops the hyphen — deployables advisoryai-web/-worker, database stellaops_advisoryai, env STELLAOPS_POSTGRES_ADVISORYAI_CONNECTION, container alias advisoryai.stella-ops.local, and the host’s own AddRouterMicroservice(serviceName: "advisoryai").
5.1 What the S0 route inventory did not record
- F-AAI5-1 — the target namespace is NOT free, and a grep says it is.
/api(index 199) is a bare non-regex ReverseProxy prefix toplatform.stella-ops.local; the resolver matches non-regex routes byStartsWith(path + "/"), so it captures every unmatched/api/*— including the whole consolidated plane. Appending the group hands it to platform-web. Ordering is therefore load-bearing; the script refuses to write a table where the group is outranked. - F-AAI5-2 — six OpsMemory endpoints have no gateway route and never had one.
/api/v1/opsmemory/**reachesopsmemory-webonly through^/api/v1/([^/]+)(.*), which derives the microservice hostname from the first path segment — it works purely because the deployable is namedopsmemory. AAI-8’s container retirement is what breaks it, silently. The removal set is seven routes PLUS this wildcard dependency. - F-AAI5-3 —
/api/v1/advisoryand/api/v1/advisoriesare DEAD, not an alias pair. Both translate to/api/v1/advisory$1on a host that has never mapped it: the AdvisoryAI service is entirely minimal-API (no[Route], noMapControllers), its only/api/v1/*group is/api/v1/chat, and a grep for"/api/v1/advisoracrosssrc/AdvisoryAIreturns zero hits. The G1 probe corroborates (ledger:134, 404). They are removed without a successor — giving them a home on the consolidated host would cement a misroute. - F-AAI5-4 —
^/api/v1/advisory(.*)has an unanchored tail and swallows/api/v1/advisory-sources/*. That is Concelier’s and Policy’s namespace, and there is no explicit route for it anywhere in the table, so this over-capture is the whole reason it is unreachable through the gateway. Removing route 83 does not fix advisory-sources; it stops AdvisoryAI absorbing another service’s namespace. Not this window’s to fix. - F-AAI5-5 —
/api/v1/advisorieshas a second claimant. It is in the vulnerabilities-hub B7 removal set (SPRINT_20260722_006/008), which ruled the hub is not a successor (“a false friend”). Whichever window lands first shrinks the other’s set; the script’s-Checkdrift report was the detector. Superseded 2026-09-04: that detector is gone with the swap (see the currency note above). Both spellings are now recorded insrc/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/retired-routes.json, so neither window has a set left to shrink. - F-AAI5-6 — the replaced set is type-MIXED, so four routes change TRANSPORT. Four are
ReverseProxy(direct HTTP to the DNS alias) and becomeMicroservice(Valkey router transport). SSE is proven safe across that boundary —/api/v1/chat/query/streamalready runs that way in production, and streamed bodies cross as chunkedResponseStreamDataframes.PayloadLimitsMiddlewareis not the risk (it already covers both route types via aUseWhen(!IsSystemPath)branch, andPayloadLimitsis request-direction only). What is unproven is a large buffered response: a non-streaming handler returns a singleFrameType.Responsecarrying the whole body as one message, where a ReverseProxy route would have streamed it./v1/evidence-packs/{packId}/exportmoves onto that path with no configured ceiling to point at, so AAI-9’s forcing function must include a realistically sized export. - F-AAI5-7 — three of the seven routes REWRITE the path in the gateway today (
/api/v1/search→/v1/search,/api/v1/advisory-ai→/v1/advisory-ai,/api/v1/advisories→/api/v1/advisory). The group forwards the tail verbatim, so those rewrites die with the routes and the host must serve the consolidated paths natively. AAI-9 gate 3 now supplies source-level native aliases for the carried AdvisoryAI surface while retaining the legacy paths; gate 9 now adds the six OpsMemory aliases. The window still owes a built-image direct probe. Applying the swap before that prerequisite yields a partial 404 farm with green route tests.
5.2 No change owed to OpenApiAggregator or src/Api
The aggregator derives its route→service mapping from the route table (OpenApiAggregator.cs:606-630), so it follows the swap unaided. Its two hardcoded switches (ResolveServiceHosts, ResolveOpenApiPaths) contain no AdvisoryAI entry and need none: the alias override exists only where the HELLO ServiceName and the container alias disagree (today, one entry: findingsledger→findings), and AdvisoryAI’s already agree. Unlike the Findings sibling, this family hands AAI-8 no aggregator re-keying. src/Api is likewise untouched — the family’s appearances in stella.yaml are scope names (advisory:read, advisory:ingest), not paths, and the scope catalog is unchanged by this stage.
5.3 Console (S6): six prefixes centralised, values unchanged
Twenty path literals across ten files now resolve through API_PATH_PREFIXES, so the AAI-9 Console work is a six-value edit. evidencePacks MOVED out of the “Evidence / attestation” block — it is served by advisory-ai-web (EvidencePackEndpoints.cs:41), not the Evidence family, and it flips with this group. Two new keys (advisoryAiSearch, advisoryAiApi, opsMemory) name surfaces the registry never carried.
The bypass class this family hides behind is not the interpolation the sibling guards hunt: three clients inject a full base-URL token that already resolves through the registry, so their call sites carry only a tail. What bypasses is the inject(TOKEN, {optional: true}) ?? '/v1/advisory-ai' fallback — registry-clean by inspection of the provider, and a hardcoded path that fires wherever the provider is absent. Two more of the same shape sit in app.config.ts’s own catch branches. tools/scripts/validate/check-console-advisoryai-prefix-literals.sh anchors on that shape and requires a delimiter after each surface name, because \b after advisory would also match Concelier’s advisory-sources.
/api/v1/chat has no Console consumer at all — only the CLI (ChatClient.cs, plus EvidenceCommandGroup.cs:1681 for evidence-pack export). The CLI repoint is handed to AAI-10 + the CLI owner: a shipped CLI must not target a route that does not exist until the window closes.
6. S7/S8 — doctor adoption, conformance and the staged stack (AAI-7/AAI-8, 2026-08-05)
6.1 Gated reference adoption and sole migration authority
Findings, JobEngine and the vulnerability hub all adopted doctor-check/v1 on hosts that ship nowhere. advisory-ai-web ships today (D-AAI3-1), and three things follow that no sibling had to handle:
- The doctor data source must not take the bare
NpgsqlDataSourceDI slot. The reference adopters registerAddSingleton<NpgsqlDataSource>and let the SDK’s context pick it up. AAI-7 identified the collision that the OpsMemory fold would create. AAI-9 resolves it structurally: the store uses a private typed pool over the AdvisoryAI-owned DSN, Doctor keeps its explicitServiceDoctorContext, and the bare slot remains free. Registration order is no longer part of the persistence boundary. - Every runtime store and
OwnDatabasebindSTELLAOPS_POSTGRES_ADVISORYAI_CONNECTION(orPostgres:AdvisoryAI:ConnectionString) and nothing else. The legacyAdvisoryAI:Storage/AdvisoryAI:KnowledgeSearchconnection values and genericConnectionStrings:Default/Database:ConnectionStringfallbacks cannot arm either runtime persistence or migrations. AAI-PA introduced the gated web reference; AAI-9 gates 1–2 then madeAdvisoryAiOwnDatabaseConnectionthe standard runtime resolver and kept the consolidated resolver constants equal under test. - Doctor registration cannot re-configure the host’s single auth client. A host gets exactly one
StellaOpsAuthClientOptions, and withAdvisoryAi:Authority:Enabled=true(the live posture) the LIVEADAPTER-17-002 block owns it asstellaops-advisory-ai-internalwith five internal HttpClients hanging off it. Copying the referenceAddStellaOpsAuthClientwould repoint every UnifiedSearch live adapter at the doctor client. The host now rides its existing identity and fails closed on a conflicting declaredClientId. Operator consequence: leaveDoctor__Registration__ClientIdunset and grant the existing clientplatform:doctor:register.
The worker uses the repointed runtime DAL but does not adopt the migration library. The JobEngine sibling’s stated reason (“a Worker host has no HTTP surface”) was checked and does not hold here — StellaOps.AdvisoryAI.Worker is a WebApplication already mapping MapWorkerHealthEndpoints(). The reason that does hold is duplication: both roles are replicas of one service over one database, and a second registration would be a second migration authority.
6.2 What the role-grant suite found: this baseline is the opposite of its siblings
AdvisoryAiRoleGrantTests proves the cross-owner refusal S7 asks for (a foreign role is rejected on read and write across both owned schemas and the migration ledger, with a granted-schema anti-vacuity twin; PUBLIC holds nothing; GRANT … ON ALL TABLES is a snapshot and this family has zero partitioned tables). It also found something AAI-9 must not inherit blindly:
The consolidated baseline creates NO database roles and enables NO row-level security, while 8+ of its tables carry a NOT NULL tenant_id. Findings ships FORCE RLS; JobEngine ships RLS plus a scheduler_admin role. So here there is no baseline role for AAI-9 to grant, and tenant isolation is application-enforced only — app.tenant_id / set_config appear nowhere in src/AdvisoryAI, which is why enabling RLS is a posture decision with an application cost rather than a one-line edit to a still-unapplied baseline. Measured from pg_roles, pg_class.relrowsecurity and information_schema.columns, so it fails loudly if a later migration changes any of it. AAI-9 owns the ruling.
6.3 S8 staging: what is staged, and the one thing that is deliberately not
devops/compose/docker-compose.advisoryai.yml stages advisoryai-web + advisoryai-worker outside the default config_files list, carrying the three advisory-ai-* volumes and their init step. Unlike the hub and JobEngine — and like Findings — this family gets no PostgreSQL cluster of its own: stellaops_advisoryai lives on the control-plane cluster. One DATABASE per service is the hard rule; one CLUSTER per service is not. Runbook: docs/runbooks/advisoryai/advisoryai-deploy-aai8-staged-stack.md.
No consolidated publish-helper or ownership-manifest keys, deliberately. Every sibling registered theirs at S8 and it was free, because their hosts were born new with zero violation pairs. These are continuation hosts: advisoryai-web would build from the same project as the live advisory-ai-web. Measured on the committed report — advisory-ai-web 5 violation pairs, advisory-ai-worker 5 — a second key over the same seed re-counts one source graph and turns those into ten register rows, five of them unpinned, which the shrink-only rule forbids. The rename rides AAI-9/AAI-10, where deleting the old key and adding the new one is net-zero and both files must move in one commit.
The D14 family fold DID execute here, as AAI-4’s manifest note and both register pins’ sunset: 013 S7/M2 gate scheduled: opsmemory-web joins the advisory-ai family as a third role, the three explicit opsmemory-owned ownership rules under src/AdvisoryAI are deleted (the surviving prefix rule covers them), and the two advisory-ai-* → opsmemory pins go with them. Measured 137 → 135 violation pairs, 0 unpinned / 0 stale / 0 grown, 53 keys; the report was regenerated once the tree went porcelain-clean (0010ba5995), where the AAI-7 doctor SDK also shows as +1 project and zero new violation pairs on advisory-ai-web — the SDK is domain-neutral-shared, which is what made adopting it on a live host free at the boundary.
