From 415193ee0260678437c2a0dda5f5323ddb2ae01a Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Mon, 6 Jul 2026 13:20:11 +0200 Subject: [PATCH 1/4] Add architecture files for ai-core and recommendations, document ai already has one --- cds-feature-ai-core/docs/architecture.md | 165 ++++++++++++++++++ .../docs/architecture.md | 130 ++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 cds-feature-ai-core/docs/architecture.md create mode 100644 cds-feature-recommendations/docs/architecture.md diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md new file mode 100644 index 0000000..8569c50 --- /dev/null +++ b/cds-feature-ai-core/docs/architecture.md @@ -0,0 +1,165 @@ +# Architecture: `cds-feature-ai-core` + +## Table of Contents + +- [Purpose](#purpose) +- [Dependencies](#dependencies) +- [Feature](#feature) + - [CDS Model](#cds-model) + - [Public API](#public-api) + - [Key Infrastructure Classes](#key-infrastructure-classes) + - [Multi-Tenancy](#multi-tenancy) + - [Key Flows](#key-flows) + - [Tenant Subscribe](#tenant-subscribe) + - [Tenant Unsubscribe](#tenant-unsubscribe) + - [Inference Client Resolution](#inference-client-resolution) +- [Tests](#tests) +- [Quality Tools](#quality-tools) + +--- + +## Purpose + +Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing resource group management, deployment lifecycle, and inference client resolution as a standard CAP `RemoteService`. At the time of writing, `com.sap.ai.sdk:ai-core` offered no CAP integration — only raw REST API clients — so this plugin fills that gap. + +→ [README](../README.md) + +--- + +## Dependencies + +| Dependency | Why | +|---|---| +| `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. | +| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | +| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core — needed because resource group creation is eventually consistent. | +| CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | + +--- + +## Feature + +### CDS Model + +Defined in `src/main/resources/cds/`: `AICore.cds` + +```cds +// @protocol: 'none' — programmatic access only, never exposed as OData/REST +service AICore { + @cds.persistence.skip entity resourceGroups { ... } // AI Core resource group + @cds.persistence.skip entity deployments { ... } // AI Core deployment + action stop() + @cds.persistence.skip entity configurations { ... } // AI Core configuration + + // Events: + event resourceGroup // in: (optional: tenantId — falls back to UserInfo.getTenant() from request context) → out: resourceGroupId + event deploymentId // in: resourceGroupId + ModelDeploymentSpec → out: deploymentId + event inferenceClient // in: resourceGroupId + deploymentId → out: ApiClient +} +``` + +Entities are `@cds.persistence.skip` — they have no database tables and are backed entirely by the AI Core REST API at runtime. + +--- + +### Public API + +→ [Programmatic Usage in README](../README.md#programmatic-usage) + +--- + +### Key Infrastructure Classes + +| Class | Role | +|---|---| +| `AICoreServiceConfiguration` | extends `CdsRuntimeConfiguration` — wires all handlers, clients, and caches at startup; detects AI Core binding | +| `AICoreConfig` | Immutable config record populated from `cds.ai.core.*` YAML properties | +| `AICoreClients` | Holds `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and the raw `AiCoreService` from the AI SDK | +| `DeploymentResolver` | Thread-safe resolver with two Caffeine caches (`tenantId → rgId`, `rgId::configName → deploymentId`) and `ConcurrentHashMap` per-key locks (prevents duplicate deployments under concurrency); Resilience4j backoff on 403/404/412 | +| `AICoreApiHandler` | `@On` handler for the three custom events: `resourceGroup`, `deploymentId`, `inferenceClient` | +| `AICoreSetupHandler` | `@After(LATE) SubscribeEvent` / `@Before(EARLY) UnsubscribeEvent` — creates/deletes resource groups during MTX tenant lifecycle | +| `AbstractCrudHandler` | Base for all entity CRUD handlers; provides `resolveResourceGroup()` and `ensureResourceGroupAccessible()` (tenant isolation guard) | + +--- + +### Multi-Tenancy + +→ [Multi-Tenancy in README](../README.md#multi-tenancy) + +--- + +### Key Flows + +#### Tenant Subscribe + +``` +CAP MTX DeploymentService + | + | SubscribeEvent @After(LATE) + v +AICoreSetupHandler + | + | resolveResourceGroup(tenantId) + v +DeploymentResolver + | + | GET /v2/admin/resourceGroups?labelFilter=CDS_TENANT_ID=tenantId + v +SAP AI Core + | + | (if absent) POST /v2/admin/resourceGroups + v +resourceGroupId (cached 1h after last access — subsequent calls skip the AI Core management API; + if a resource group is deleted or reassigned externally, the plugin won't notice until the cache expires after 1h or the app restarts) +``` + +#### Inference Client Resolution + +```mermaid +flowchart TD + A["aiCoreService.inferenceClient(rgId, deploymentId)"] --> B["DeploymentResolver: check tenantResourceGroupCache"] + B -->|cache hit| E["check deploymentCache"] + B -->|cache miss| C["GET /v2/admin/resourceGroups — find by tenantId label"] + C --> D["PUT in tenantResourceGroupCache (1h TTL)"] + D --> E + E -->|cache hit| V["validateCachedDeployment: GET /v2/lm/deployments/{id}"] + V -->|valid| H["emit InferenceClientContext → ApiClient"] + V -->|invalid — invalidate cache entry| F + E -->|cache miss| F["GET /v2/lm/deployments — match by ModelDeploymentSpec"] + F -->|not found| G["POST /v2/lm/configurations + POST /v2/lm/deployments — poll until RUNNING"] + F -->|found| I["PUT in deploymentCache (1h TTL)"] + G --> I + I --> H +``` +*Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`).* + + +#### Tenant Unsubscribe + +``` +CAP MTX DeploymentService + | + | UnsubscribeEvent @Before(EARLY) + v +AICoreSetupHandler + | + | DELETE /v2/admin/resourceGroups/{id} + v +SAP AI Core + | + | invalidateTenant(tenantId) — evicts tenantResourceGroupCache and deploymentCache (which was filled on first call to resolveDeployment) entries for this tenant + v +(done) +``` +--- + +## Tests + +Unit tests for `AICoreServiceConfiguration`, `AICoreServiceImpl`, `AICoreSetupHandler`, and the CRUD handlers live in `src/test/` within this module (`mvn test`). + +End-to-end integration tests against a real AI Core instance live in [`integration-tests/spring/`](../../integration-tests/README.md) (`AICoreServiceTest`, `DeploymentTest`, `ResourceGroupTest`, `MultiTenancyTest`, and others). MTX lifecycle tests (subscribe/unsubscribe/tenant isolation) live in [`integration-tests/mtx-local/`](../../integration-tests/README.md). + +--- + +## Quality Tools + +→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md new file mode 100644 index 0000000..e6e1b62 --- /dev/null +++ b/cds-feature-recommendations/docs/architecture.md @@ -0,0 +1,130 @@ +# Architecture: `cds-feature-recommendations` + +## Table of Contents + +- [Purpose](#purpose) +- [Dependencies](#dependencies) +- [Feature](#feature) + - [CDS Model](#cds-model) + - [Configuration](#configuration) + - [Public API / Handlers](#public-api--handlers) + - [Key Infrastructure Classes](#key-infrastructure-classes) + - [Multi-Tenancy](#multi-tenancy) + - [Key Flows](#key-flows) + - [Recommendation Pipeline (OData GET on draft entity)](#recommendation-pipeline-odata-get-on-draft-entity) + - [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation) +- [Tests](#tests) +- [Quality Tools](#quality-tools) + +--- + +## Purpose + +Automatically injects AI-powered field recommendations from the SAP RPT-1 tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. + +→ [README](../README.md) + +--- + +## Dependencies + +| Dependency | Why | +|---|---| +| [`cds-feature-ai-core`](../../cds-feature-ai-core/README.md) | Provides the `AICore` CDS service and `AICoreService` API used to resolve the resource group, deployment ID, and inference `ApiClient` for the RPT-1 model. Recommendations cannot function without an active AI Core connection. | +| `@cap-js/ai` (Node.js CDS plugin) | At CDS build time, the plugin adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields. Without this (or a manual CDS extension), predictions are computed but not serialized in OData responses. | +| CAP Java `ExtensibilityService` | `RecommendationModelChangedHandler` listens to `EVENT_MODEL_CHANGED` to invalidate the per-tenant entity cache when a tenant's CDS model is upgraded via MTX. | + +--- + +## Feature + +### CDS Model + +No dedicated CDS model file — the plugin relies on the `AICore` service model provided by `cds-feature-ai-core`, and on the `SAP_Recommendations` navigation property injected by the `@cap-js/ai` Node.js plugin (or added manually by the application). + +The Node plugin will automatically detect fields annotated with a value list, see [`README`](../README.md#enabling-recommendations). + +### Configuration + +Wired by `RecommendationConfiguration` (extends `CdsRuntimeConfiguration`) at startup. It detects whether an AI Core binding is present and selects production vs. mock mode accordingly — no manual activation is required. + +### Public API / Handlers + +No Java API — the plugin is entirely annotation-driven. Extend or annotate your CDS model to control which fields receive recommendations, see [`README`](../README.md#enabling-recommendations). +Currently, it is not possible to hook into the recommendation result from application code to observe the injected output, nor to override the inference call itself ([#110](https://github.com/cap-java/cds-ai/issues/110)). + +### Key Infrastructure Classes + +| Class | Role | +|---|---| +| `RecommendationConfiguration` | extends `CdsRuntimeConfiguration` — wires all handlers at startup; selects production vs. mock based on AI Core binding presence | +| `FioriRecommendationHandler` | `@After` read handler on all app services (`entity="*"`) — cross-cutting read interceptor; orchestrates the full recommendation pipeline | +| `RecommendationContextBuilder` | Reads CDS annotations to determine which fields are prediction targets and which columns supply training context | +| `RptModelSpec` | Static factory for the `ModelDeploymentSpec` targeting `sap-rpt-1-small`; used as the cache key for deployment resolution | +| `RptInferenceClient` | Calls RPT-1 `/predict` endpoint; handles the synthetic `SAP_RECOMMENDATIONS_ID` index column for composite/non-string keys | +| `RecommendationResultParser` | Type-coerces RPT-1 string output back to CDS primitive types; resolves `@Common.Text` descriptions from the database | +| `RecommendationModelChangedHandler` | `@On(EVENT_MODEL_CHANGED)` — invalidates per-tenant entity cache on MTX model upgrade | + +### Multi-Tenancy + +→ [Multi-Tenancy in cds-feature-ai-core README](../../cds-feature-ai-core/README.md#multi-tenancy) + +Tenant isolation is inherited from `cds-feature-ai-core`: each prediction call resolves the resource group and deployment for the current request's tenant. No additional MT configuration is required in this module. + +**Per-tenant entity cache** (in `FioriRecommendationHandler`): + +``` +Cache<":", Boolean> 10k max, no TTL + → entities with no prediction columns are recorded and skipped on every future read + → invalidated by RecommendationModelChangedHandler on model change +``` + +### Key Flows + +#### Recommendation Pipeline (OData GET on draft entity) + +```mermaid +flowchart TD + A["OData GET — IsActiveEntity=false"] --> B["FioriRecommendationHandler @After(entity='*') afterRead(...)"] + B --> C{Entity in no-prediction cache?} + C -->|yes — skip| Z["Return response unchanged"] + C -->|no| D{Draft row? Single result?} + D -->|no| Z + D -->|yes| E["RecommendationContextBuilder: identify prediction fields + context columns"] + E --> F{Does this entity have any prediction fields?} + F -->|no — add entity to skip cache| Z + F -->|yes| G["DB query: up to 2000 context rows (ORDER BY modifiedAt DESC)"] + G --> H["cds-feature-ai-core: resolveResourceGroup → resolveDeploymentId → inferenceClient"] + H --> I["RptInferenceClient.predict(predictRow, contextRows, columns) POST /v2/inference/deployments/{id}/predict"] + I --> J["RecommendationResultParser: type-convert + resolve @Common.Text descriptions"] + J --> K["Inject SAP_Recommendations into response row"] + K --> L["Return enriched response"] +``` + +#### No-prediction-cache Invalidation + +``` +ExtensibilityService + | + | EVENT_MODEL_CHANGED (tenantId) + v +RecommendationModelChangedHandler + | + | evict all entries in no-prediction-cache for tenantId + v +Next read re-evaluates +``` + +--- + +## Tests + +Unit tests for `FioriRecommendationHandler`, `RptInferenceClient`, and `RecommendationConfiguration` live in `src/test/` within this module (`mvn test`). + +End-to-end integration tests covering the full recommendation pipeline against a real AI Core instance live in [`integration-tests/`](../../integration-tests/README.md) in the outer module (`RecommendationTest`, `NonStandardKeyRecommendationTest`). + +--- + +## Quality Tools + +→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) From 95b1fc222e032bb970caa0a934a20bd1b9f03e2a Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 8 Jul 2026 11:09:18 +0200 Subject: [PATCH 2/4] Update cds-feature-ai-core/docs/architecture.md Co-authored-by: Marvin L Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 8569c50..e420c68 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -32,7 +32,7 @@ Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing |---|---| | `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. | | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | -| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core — needed because resource group creation is eventually consistent. | +| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | | CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | --- From 1947f56b3bdba66f29d0e11ec3dd2757ac097deb Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 9 Jul 2026 14:45:10 +0200 Subject: [PATCH 3/4] Incorporate Marvins comments --- cds-feature-ai-core/docs/architecture.md | 65 +++++++++++++++---- .../docs/architecture.md | 4 +- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index e420c68..93464c4 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -33,7 +33,7 @@ Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing | `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. | | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | | `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | -| CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | +| `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | --- @@ -114,24 +114,61 @@ resourceGroupId (cached 1h after last access — subsequent calls skip the AI Co #### Inference Client Resolution +##### Event 1: resourceGroup + +```mermaid +flowchart TD + A1["emit ResourceGroupContext (tenantId)"] + A1 --> A2{"multiTenancy enabled
AND tenantId != null?"} + A2 -->|no| A3["return config.defaultResourceGroup()"] + A2 -->|yes| A4{"tenantResourceGroupCache
lookup by tenantId"} + A4 -->|cache hit| A5["return cached resourceGroupId"] + A4 -->|cache miss| A6["GET /v2/admin/resourceGroups
labelSelector: ext.ai.sap.com/tenant={tenantId}"] + A6 --> A7{"found?"} + A7 -->|yes| A8["cache result (expireAfterAccess 1h)"] + A7 -->|no| A9["POST /v2/admin/resourceGroups
(handle 409 Conflict = already exists)"] + A9 --> A8 + A8 --> A5 +``` + +##### Event 2: deploymentId — invoked with `resourceGroupId` + ```mermaid flowchart TD - A["aiCoreService.inferenceClient(rgId, deploymentId)"] --> B["DeploymentResolver: check tenantResourceGroupCache"] - B -->|cache hit| E["check deploymentCache"] - B -->|cache miss| C["GET /v2/admin/resourceGroups — find by tenantId label"] - C --> D["PUT in tenantResourceGroupCache (1h TTL)"] - D --> E - E -->|cache hit| V["validateCachedDeployment: GET /v2/lm/deployments/{id}"] - V -->|valid| H["emit InferenceClientContext → ApiClient"] - V -->|invalid — invalidate cache entry| F - E -->|cache miss| F["GET /v2/lm/deployments — match by ModelDeploymentSpec"] - F -->|not found| G["POST /v2/lm/configurations + POST /v2/lm/deployments — poll until RUNNING"] - F -->|found| I["PUT in deploymentCache (1h TTL)"] - G --> I - I --> H + B1["emit DeploymentIdContext
(resourceGroupId, ModelDeploymentSpec)"] + B1 --> B2["acquire per-key lock
(ConcurrentHashMap)"] + B2 --> B3{"deploymentCache
lookup by rgId::configName"} + B3 -->|cache hit| B4["validateCachedDeployment:
GET /v2/lm/deployments/{id}"] + B4 --> B5{"status RUNNING or PENDING?"} + B5 -->|yes| B6["return cached deploymentId"] + B5 -->|no / 404| B7["invalidate cache entry"] + B7 --> B8 + B3 -->|cache miss| B8["findOrCreateDeployment (under lock)"] + B8 --> B9["queryDeploymentsUntilReady (with retry):
GET /v2/lm/deployments?scenarioId=..."] + B9 --> B10{"match by configName
+ matchesExisting() + RUNNING/PENDING?"} + B10 -->|found| B11["cache deploymentId (expireAfterAccess 1h)"] + B10 -->|not found| B12["findOrCreateConfiguration:
GET /v2/lm/configurations?scenarioId=..."] + B12 --> B13{"config with matching name exists?"} + B13 -->|yes| B14["reuse existing configId"] + B13 -->|no| B15["POST /v2/lm/configurations"] + B15 --> B14 + B14 --> B16["POST /v2/lm/deployments (with retry for 403/412)"] + B16 --> B17["pollUntilRunning:
GET /v2/lm/deployments/{id}
(exponential backoff)"] + B17 --> B11 + B11 --> B6 ``` + *Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`).* +##### Event 3: inferenceClient — invoked with `resourceGroupId` and `deploymentId` + +```mermaid +flowchart TD + C1["emit InferenceClientContext
(resourceGroupId, deploymentId)"] + C1 --> C2["clients.sdkService()
.getInferenceDestination(rgId)
.usingDeploymentId(depId)"] + C2 --> C3["return ApiClient.create(destination)"] +``` + #### Tenant Unsubscribe diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index e6e1b62..b883f01 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -32,7 +32,9 @@ Automatically injects AI-powered field recommendations from the SAP RPT-1 tabula |---|---| | [`cds-feature-ai-core`](../../cds-feature-ai-core/README.md) | Provides the `AICore` CDS service and `AICoreService` API used to resolve the resource group, deployment ID, and inference `ApiClient` for the RPT-1 model. Recommendations cannot function without an active AI Core connection. | | `@cap-js/ai` (Node.js CDS plugin) | At CDS build time, the plugin adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields. Without this (or a manual CDS extension), predictions are computed but not serialized in OData responses. | -| CAP Java `ExtensibilityService` | `RecommendationModelChangedHandler` listens to `EVENT_MODEL_CHANGED` to invalidate the per-tenant entity cache when a tenant's CDS model is upgraded via MTX. | +| `com.sap.ai.sdk.foundationmodels:sap-rpt` (SAP AI SDK) | Provides the RPT-1 model client used to call the `/predict` endpoint. | +| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for the per-tenant entity skip cache (10k max, no TTL). | +| `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | --- From 5b60823e7a7a6b081cd2810b863962836eb3d699 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 9 Jul 2026 14:46:19 +0200 Subject: [PATCH 4/4] Update links to CI checks --- cds-feature-ai-core/docs/architecture.md | 2 +- cds-feature-recommendations/docs/architecture.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 93464c4..13cd192 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -199,4 +199,4 @@ End-to-end integration tests against a real AI Core instance live in [`integrati ## Quality Tools -→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) +→ [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index b883f01..b740d57 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -129,4 +129,4 @@ End-to-end integration tests covering the full recommendation pipeline against a ## Quality Tools -→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) +→ [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks)