# Migrate Segw To Rap

> Reverse-engineer a SEGW-built OData V2 service (MPC/DPC/MPC_EXT/DPC_EXT) into a modern RAP V4 service — tables, CDS views (interface + projection), behavior definitions, draft entities, service definition + binding. Use when asked to "migrate this SEGW service to RAP", "convert OData V2 to V4 RAP", "modernize this gateway service for S/4", or "replace SEGW with RAP V4".

- Skill: `arc-mcp/migrate-segw-to-rap` (Agent Skill)
- Install (CLI): `npx skillmds@latest add arc-mcp/migrate-segw-to-rap`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arc-mcp/migrate-segw-to-rap/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: arc-mcp (https://skillmd.com/u/arc-mcp)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/arc-mcp/migrate-segw-to-rap

---


# Migrate SEGW OData V2 service ➜ RAP service

Reverse-engineer a classic SEGW-built OData V2 service (MPC/DPC/MPC_EXT/DPC_EXT) into a modern
RAP service (CDS root + projection + BDEF + SRVD + SRVB + behavior pool) on the same SAP system.
Runs side-by-side: the legacy service stays live; the new RAP service lands in a separate,
resettable package.

> **Domain example.** Templates in this skill use an illustrative `Project → Tasks → TimeEntries`
> domain (entity names like `ZR_DM_PROJECT`, `ZBP_DM_PROJECT`, `ZDM_PROJECT_D`) so the shape is
> concrete. Substitute the user's entities throughout — the LLM running this skill should
> rewrite every entity identifier to match the source service.

## Smart defaults (apply silently — do NOT ask before research)

| Setting | Default | Rationale |
|---|---|---|
| Discovery strategy | MPC class source (Tier 1) | ARC-1 reads it natively; richer than `$metadata` |
| Target package | User-provided. If absent, create a child of the source package via `SAPManage(action="create_package")`; default name `<source_package>_RAP`. Only fall back to `$TMP` if the user explicitly asks. | Keeps the migration output isolated and resettable. |
| Target transport | User-provided existing, or auto-created via `SAPTransport(action="create", description="RAP migration of <legacy_service>", package="<target_package>")`. | Required for non-`$TMP` packages. |
| OData version on target | V4 | Current SAP standard; FE-ready |
| RAP scenario | Managed with internal numbering | Simplest read-mostly; matches legacy SEGW behaviour |
| Draft | Off for read-mostly entities. ON when the legacy service exposed CUD (function imports, deep inserts, or sap:updatable=true on entity sets) and the user wants Fiori Elements compatibility. | Match the legacy service's behavior contract; FE list+OP work best with draft on. |
| Strict mode | `strict ( 2 )` | Current best practice |
| Projection layer | **Mandatory — always create a `ZC_*` projection alongside every `ZR_*` root view**. Service binding exposes the projection, never the root. | Run 1 of the skill skipped projections after a misread CDS error; spell out so the LLM never drops them. |
| `provider contract` on projections | **On the root projection only**, not on the root view, not on child projections. The contract sits at exactly one level. Use `transactional_query` when combining projection BDEF + `use draft` on 7.58; `transactional_interface` for read-mostly without draft. | Putting it on a root view → *"only valid on projection views"*. Putting it on child projections → *"inappropriate provider contract on …"*. Children are exposed via `redirected to parent` and inherit the BO contract from the root projection. Run 3 confirmed `_query` is needed for draft on 7.58. |
| CDS composition syntax (managed) | `composition [0..*] of <child> as <name>` — **no `on` clause**. Key linking is implicit via matching key fields / `with foreign key` in the child | 7.58 rejects `on` on managed compositions. The recovery is dropping `on`, not switching to `association to`. |
| `@Semantics.*` on 7.58 | `createdAt`, `lastChangedAt`, **and `localInstanceLastChangedAt`** are all valid on 7.58 — put `localInstanceLastChangedAt` on the local-instance etag field (`abp_locinst_lastchange_tstmpl`), exactly as SAP's own RAP generator does. `@Semantics.businessDate.from/to` also work. | Verified live on S/4HANA 2023 (758, 2026-06-12): standalone CDS views with `localInstanceLastChangedAt` and with `businessDate.from/to` both activate. An earlier "unknown annotation on 7.58" note was a misdiagnosis — draft annotations are genuinely absent only on pre-7.55 releases. |
| Draft table field names (when draft=ON) | Use BO-alias casing **without underscores** — e.g. `projectid`, `startdate`, NOT `project_id`, `start_date`. ABAP normalizes BDEF aliases (`ProjectId`, `StartDate`) to `PROJECTID`, `STARTDATE` — the draft table lookup is by that normalized name, not by the active table's snake_case. | Run 2: BDEF activation failed with *"key field PROJECTID expected at position 2, found PROJECT_ID"*. The active table can keep snake_case (BDEF mapping handles it); the draft table cannot — it has no mapping clause. |
| Naming | SAP-standard `Z<prefix>_<entity>`: `ZR_` root, `ZC_` projection, `ZI_<entity>_BEH` BDEF, `ZBP_` behavior pool, `ZUI_<service>_O4` V4 SRVB | Aligns with SAP-internal conventions; the leading `Z` is the customer namespace |
| Pre-write lint | On | Set `SAP_ABAP_RELEASE=<your_release>` in ARC-1 config (PR #255) so the lint preset matches the system release. The older `SAP_LINT_BEFORE_WRITE=false` workaround is no longer needed. |
| Pre-write SAP check | On for activation-blockers only | We rely on activation feedback, not `--check-before-write` |

## Input

The user must provide **at minimum**:

- **One of**: a SEGW service technical name (e.g. `Z<SOMETHING>_SRV`), a package containing
  the SEGW-generated classes, or an MPC class name directly. Ask if not given.
- **Target package** for the new RAP objects. Skill will create one (`<source_package>_RAP`)
  if not given and the source package is known; otherwise asks. Only defaults to `$TMP` if
  the user explicitly asks.
- **Transport** for non-`$TMP` writes — existing or auto-created (see Smart Defaults).

Optional:

- **RAP scenario** (default: managed with internal numbering).
- **Draft on/off** (skill infers from legacy CUD surface).
- **OData version** (default: V4).
- **Naming overrides** (default: SAP standard `Z<prefix>_<entity>`).

If only the legacy identifier is given, skill applies smart defaults and surfaces the resolved
plan in Phase 5 for user `ok` before any writes.

---

## Phase 0 — Authorization preflight

Run **before** any modelling work. Aborts cleanly if anything is missing, with the specific SAP
auth object the user needs.

### 0a. Server-side capability + auth probe

```text
SAPManage(action="probe")
```

Assert from the response:
- `systemType` ∈ {`onprem`, `btp`} — needed for naming + admin-field choices
- `rap.available == true` — fail with: *"This system doesn't expose RAP/CDS endpoints. Migration
  cannot proceed."*
- `transport.available == true` — fail with: *"Transport API unavailable; check ICF
  `/sap/bc/adt/cts/transports`."*
- `authProbe.searchAccess == true` — fail with: *"User cannot search ADT objects; need
  `S_DEVELOP` ACTVT=03 OBJTYPE=CLAS DEVCLASS=`<source-pkg>`."*
- `authProbe.transportAccess == true` — fail with: *"User cannot read transports; need
  `S_TRANSPRT` ACTVT=03."*

### 0b. Read the legacy MPC class — confirms read on source

```text
SAPRead(type="CLAS", name="<MPC>", method="*")
```

On HTTP 403: stop with *"Cannot read `<MPC>` — need `S_DEVELOP` ACTVT=03 OBJTYPE=CLAS
DEVCLASS=`<source-pkg>` P_GROUP=`<auth-group>`. Run SU53 in SAP GUI to see the missing field."*

### 0c. Confirm write on target package

```text
SAPTransport(action="check", objectType="DDLS", objectName="ZX_PRECHECK", package="<target_package>")
```

Then dry-run a write+delete on a throwaway DDLS:

```text
SAPWrite(action="create", type="DDLS", name="ZR_DM_PRECHECK",
         source="@AccessControl.authorizationCheck: #NOT_REQUIRED\ndefine root view entity ZR_DM_PRECHECK as select from t000 { client }",
         description="ARC-1 preflight",
         package="<target_package>",
         transport="<transport>")
SAPWrite(action="delete", type="DDLS", name="ZR_DM_PRECHECK")
```

On 403 from create: stop with *"Cannot write to `<target_package>` — need `S_DEVELOP` ACTVT=01,02,07
OBJTYPE=DDLS DEVCLASS=`<target_package>`."*

If all three pass, print **"Phase 0 ✓ Authorizations OK — proceeding to discovery."** and continue.


---

## Phase 1 — Discover the legacy service

### 1a. Resolve the MPC class name

If user gave a service name (e.g. `<legacy_service>`):
- Try convention: trim `_SRV` → append `_MPC` → `<MPC_class>`. Verify with
  `SAPRead`.
- If not found:
  ```text
  SAPQuery(action="sql", sql="SELECT obj_name, devclass FROM tadir
    WHERE pgmid = 'R3TR' AND object = 'CLAS'
      AND obj_name LIKE 'Z%_MPC' ORDER BY obj_name")
  ```
  and ask the user to pick.

If user gave a package (e.g. `<source_package>`):
- ```text
  SAPRead(type="DEVC", name="<package>")
  ```
  Filter for classes ending `_MPC`.

If user gave an MPC class directly: use it as-is.

Determine the corresponding `_MPC_EXT`, `_DPC`, `_DPC_EXT` by name convention.

### 1b. Confirm the four classes exist + record metadata

```text
SAPSearch(action="object", query="<base>_*", maxResults=10)
```

Expected: 4 hits. Record `objectName` + `packageName` for each.

Print to user:
```
Legacy service: <legacy_service>
  Package:      <source_package>
  MPC class:    <MPC_class>      (model)
  MPC_EXT:      <MPC_EXT_class>  (model overrides — usually empty)
  DPC class:    <DPC_class>      (data provider — generated)
  DPC_EXT:      <DPC_EXT_class>  (data provider — your custom code)
```


---

## Phase 2 — Extract the OData model from MPC source

The MPC class's private `DEFINE_*` methods carry the full model. Read them.

### 2a. Method inventory

```text
SAPRead(type="CLAS", name="<MPC>", method="*")
```

You're looking for:
- `DEFINE_<entity>` — one per entity type
- `DEFINE_ASSOCIATIONS` — all associations + nav properties
- `DEFINE_ACTIONS` — all function imports

### 2b. Per-entity read

For each `DEFINE_<entity>` method:

```text
SAPRead(type="CLAS", name="<MPC>", method="DEFINE_<ENTITY>")
```

Parse the body for:
- Entity type name + entity set name (look for `create_entity_type(...)` and
  `create_entity_set(...)`)
- Underlying ABAP DDIC bind (look for `bind_structure(...)` or `set_data_source(...)`)
- Each property: name, EDM type, max-length, is-key, sortable/filterable/creatable/updatable
  (look for `create_property(...)` followed by `set_*` calls)

### 2c. Associations

```text
SAPRead(type="CLAS", name="<MPC>", method="DEFINE_ASSOCIATIONS")
```

Parse for each `create_association(...)`:
- Association name
- Principal entity + cardinality
- Dependent entity + cardinality
- Navigation property name(s)
- Referential constraints (principal property ↔ dependent property)

### 2d. Function imports

```text
SAPRead(type="CLAS", name="<MPC>", method="DEFINE_ACTIONS")
```

Parse for each `create_action(...)`:
- Action / function-import name
- HTTP method
- Return type (entity / complex / primitive / void)
- Return cardinality
- Parameters: name, type, length, mode (in/out/inout)

### 2e. Compile into a structured model

After Phase 2, you should be able to print a complete table like:

```
=== Extracted OData model ===

Entity types (3):
  Project    bound to ZDM_PROJECT     key: ProjectId       12 properties
  Task       bound to ZDM_TASK        key: TaskId          15 properties (FK ProjectId)
  TimeEntry  bound to ZDM_TIMEENTRY   key: EntryId         15 properties (FKs TaskId, ProjectId)

Entity sets (3):  ProjectSet, TaskSet, TimeEntrySet

Associations (2):
  Project_Tasks       Project [1] ↔ Task [0..n]            ref: ProjectId ↔ ProjectId   nav on Project: Tasks
  Task_TimeEntries    Task [1]    ↔ TimeEntry [0..n]       ref: TaskId    ↔ TaskId      nav on Task:    TimeEntries

Function imports (1):
  ApproveProject  POST  in: ProjectId (Edm.String, len 10)  return: Project (Entity, 1)
```

Print it and ask the user **"Does this match what you expected?"** before proceeding.


---

## Phase 3 — Extract the behavior from DPC_EXT

### 3a. Method inventory

```text
SAPRead(type="CLAS", name="<DPC_EXT>", method="*")
```

Categorize each redefined method:

| Method pattern | Maps to RAP |
|---|---|
| `<EntitySet>_GET_ENTITYSET` | CDS view's read access — usually free in managed scenario |
| `<EntitySet>_GET_ENTITY` | Same — free in managed scenario |
| `<EntitySet>_CREATE_ENTITY` | BDEF `create` enabled (with optional determination/validation) |
| `<EntitySet>_UPDATE_ENTITY` | BDEF `update` enabled |
| `<EntitySet>_DELETE_ENTITY` | BDEF `delete` enabled |
| `/IWBEP/IF_MGW_APPL_SRV_RUNTIME~EXECUTE_ACTION` | BDEF static or instance `action(...)` per function import |

### 3b. Per-method body read (only the ones with logic)

For each redefined method, decide:

- **Trivial GET** (just `SELECT * FROM <table> WHERE <key>` + `MOVE-CORRESPONDING`): drop entirely
  — RAP managed scenario does this for free.
- **Filtered GET** (additional WHERE clauses, derived fields): translate to CDS view filters or
  field aliases.
- **Function imports**: read the body, identify the business effect (status update, calculation,
  external call), translate to BDEF action with appropriate behavior pool method.

```text
SAPRead(type="CLAS", name="<DPC_EXT>", method="<METHOD_NAME>")
```

For mostly-trivial methods, this is a 30-line read. For complex business logic, expect 100+
lines and plan to rewrite carefully.

### 3c. Compile into a behavior summary

```
=== Extracted behavior ===

Read-only (drop, free in RAP managed):
  PROJECTSET_GET_ENTITYSET, PROJECTSET_GET_ENTITY,
  TASKSET_GET_ENTITYSET, TASKSET_GET_ENTITY,
  TIMEENTRYSET_GET_ENTITYSET, TIMEENTRYSET_GET_ENTITY

Function imports → BDEF actions:
  ApproveProject(ProjectId)     ➜  static action ApproveProject parameter $self
                                   sets Status='A' + admin fields, returns Project
                                   (legacy did UPDATE + COMMIT WORK + reread; RAP managed
                                    handles persistence — no manual COMMIT needed)

Custom create/update/delete: none (read-only service)
```

If `_CREATE_ENTITY` / `_UPDATE_ENTITY` / `_DELETE_ENTITY` were present, list them with notes
on what would become BDEF determinations / validations.


---

## Phase 4 — Read underlying tables

For each table identified in Phase 2 (e.g. `ZDM_PROJECT`, `ZDM_TASK`, `ZDM_TIMEENTRY`):

```text
SAPRead(type="TABL", name="<table>")
```

Record:
- Field list with ABAP types + lengths
- Key fields
- Admin fields convention (classic `ERNAM/ERDAT/ERZET/AENAM/AEDAT/AEZET` vs modern `abp_*`)
- Foreign-key references (the `with foreign key` clauses)

The tables stay as-is — the new RAP CDS views read from them.


---

## Phase 5 — Design plan + user approval

Before any write operation, present the **complete RAP design** as a design plan, get explicit
user approval, then proceed.

### 5a. Naming

```
Legacy → New RAP

ZDM_PROJECT (table, untouched)        ─► ZR_DM_PROJECT  (CDS root view entity)
                                        ZC_DM_PROJECT  (CDS projection view, exposed via service)
                                        ZI_DM_PROJECT  (BDEF for root)
ZDM_TASK    (table, untouched)        ─► ZR_DM_TASK    + ZC_DM_TASK
ZDM_TIMEENTRY (table, untouched)      ─► ZR_DM_TIMEENTRY + ZC_DM_TIMEENTRY

(no SEGW project on the new side)     ─► ZUI_DM_PROJECTS  (service definition — exposes ZC_*)
                                        ZUI_DM_PROJECTS_O4 (service binding, OData V4 UI)
                                        ZBP_DM_PROJECT  (behavior pool class for actions)
```

### 5b. Composition tree

```
root: ZR_DM_PROJECT
  composition [0..*] of ZR_DM_TASK as _Tasks
                        composition [0..*] of ZR_DM_TIMEENTRY as _TimeEntries
```

Maps the SEGW associations 1:1 — except now they're **compositions** (parent owns children),
which gives RAP managed lifecycle semantics for free.

### 5c. Action

```
ZI_DM_PROJECT (BDEF):
  define behavior for ZR_DM_PROJECT alias Project
    persistent table zdm_project
    lock master
    authorization master ( instance )
    etag master last_changed_at
    
    {
      field ( readonly ) ProjectId;
      action approve_project result [1] $self;        ← matches SEGW ApproveProject
    }
```

The `approve_project` action → handler in `ZBP_DM_PROJECT` → sets `status='A'` + admin fields
via the framework's MODIFY ENTITIES interface (no manual UPDATE + COMMIT).

### 5d. Service binding URL

```
Old (SEGW V2):  /sap/opu/odata/sap/<legacy_service>
New (RAP V4):   /sap/opu/odata4/sap/zui_dm_projects_o4/srvd_a2x/sap/zui_dm_projects/0001
```

(or the FLP-bound URL if the service binding is published as UI.)

### 5e. Show + ask

Print the full plan to the user. Wait for explicit **"yes, proceed"** or modifications. If
the user wants changes (different naming, draft enabled, different RAP scenario), revise and
re-present.

> **Non-interactive mode.** If the user's initial prompt already supplied the Phase-5
> equivalent inputs (legacy service, target package, transport, scenario, draft on/off)
> AND said "run end-to-end" / "don't stop for review", skip the rhetorical *"yes, proceed?"*
> gate. Print the plan as a manifest and advance directly to Phase 5f / Phase 6. Only stop
> if Phase 5 surfaced a genuine conflict (missing input, contradictory scenario flags, etc.).
> See Run 6 findings — full-chain automations otherwise hit a phantom gate.

### 5f. Lock the artifact list as a Phase-6 contract

**Before leaving Phase 5, the skill MUST emit an artifact-list contract that Phase 6 will
echo back with status. Print this verbatim:**

```
=== Phase 6 will create + activate these artifacts (Phase 5 → Phase 6 contract) ===

Roots (CDS):           [ ] ZR_DM_PROJECT  [ ] ZR_DM_TASK  [ ] ZR_DM_TIMEENTRY
Projections (CDS):     [ ] ZC_DM_PROJECT  [ ] ZC_DM_TASK  [ ] ZC_DM_TIMEENTRY
                       (projections are MANDATORY — service binding never exposes roots directly)
Draft tables (TABL):   [ ] ZDM_PROJECT_D  [ ] ZDM_TASK_D  [ ] ZDM_TIMEENTRY_D
                       (only when draft is on — based on Phase 5d)
BDEFs:                 [ ] ZR_DM_PROJECT (root behavior)  [ ] ZC_DM_PROJECT (projection behavior)
Behavior pool (CLAS):  [ ] ZBP_DM_PROJECT (empty shell — generate_behavior_implementation will populate)
Service definition:    [ ] ZUI_DM_PROJECTS  (exposes projections, NOT roots)
Service binding:       [ ] ZUI_DM_PROJECTS_O4 (OData V4 UI)

Manual steps remaining after Phase 6 build:
  [ ] /IWFND/MAINT_SERVICE — register V4 service group ZUI_DM_PROJECTS_O4 (else 403 at runtime)
```

Phase 6 must walk through this list at the end and print the same checkbox list with each
item marked `✓` or `✗` (with the failing tool call args + response on `✗`). Do NOT declare
Phase 6 done until every box is `✓` or explicitly waived by the user.

---

## Phase 6 — Build the RAP stack

### 6a. Reset existing artifacts (idempotent re-runs)

If `<target_package>` package contains objects from a previous run, delete them first:

```text
SAPRead(type="DEVC", name="<target_package>")
```

For each object in **reverse dependency order** (SRVB → SRVD → BDEF → DDLS_C → DDLS → CLAS → TABL):

```text
SAPWrite(action="delete", type="<type>", name="<name>", transport="<transport>")
```

(SAP recommends releasing the transport before re-running, but for resettable demos we just
reuse `<transport>` — it's fine.)

**Plus a TADIR cross-check** (Run 1 found a stub draft table sitting in a different transport
that the package scan missed; Run 6 found that ADT 404s can coexist with TADIR ghost rows —
the "split-brain" failure mode). On ARC-1 ≥ 0.9.5 (PR #270), the canonical reset truth
comes from `source="both"` mode, which queries ADT and DB TADIR in one call and emits a
`splitBrain` warning array for any divergent names:

```text
SAPSearch(searchType="tadir_lookup",
          source="both",
          names=["ZDM_PROJECT_D","ZDM_TASK_D","ZDM_TIMEENTRY_D",
                 "ZR_DM_PROJECT","ZR_DM_TASK","ZR_DM_TIMEENTRY",
                 "ZC_DM_PROJECT","ZC_DM_TASK","ZC_DM_TIMEENTRY",
                 "ZI_DM_PROJECT_BEH","ZI_DM_TASK_BEH","ZI_DM_TIMEENTRY_BEH",
                 "ZBP_DM_PROJECT","ZUI_DM_PROJECTS","ZUI_DM_PROJECTS_O4"])
```

Read the response in this order:

1. **`results`** — names found via ADT discovery (the "is the object actually live"
   question). Empty = the package is clean from ADT's perspective.
2. **`splitBrain`** — names where ADT and DB disagree (the "ghost" cases). For each, the
   array entry tells you which source saw it. The pragmatic interpretation: if a name is
   in DB-only (TADIR row, no ADT object), it's a ghost — treat as already absent for the
   purposes of `SAPWrite create`. If it's in ADT-only (rare), there's a stale ADT cache —
   `SAPSearch` again after a few seconds.

`source="both"` requires `sql` scope (the DB leg uses the free-SQL path). If your profile
doesn't have it, fall back to `source="adt"` (default) for the live check + `SAPQuery` for
a broader package sweep:

```text
SAPQuery(action="sql", sql="SELECT obj_name, object, devclass, korrnum FROM tadir
  WHERE devclass = '<target_package>' AND obj_name LIKE 'Z%'")
```

If a planned name appears in a *different* package, it's a leftover stub — delete via its
actual transport before proceeding.

### 6a-gen. (Optional) Seed the root BO via SAP's official ABAP generator

If the **official SAP ABAP MCP server** is connected alongside ARC-1 (ships with ABAP Development
Tools for VS Code; enabled in Eclipse ADT 3.60+; appears as the `abap-mcp` server), its
*Generate ABAP Repository Objects* framework can scaffold the **root** managed+draft BO for you.
It does **not** replace this skill — Phases 1–5 (discovery + translation) and most of Phase 6 are
still ARC-1's job, because the generator is **single-entity and one-shot**:

> *"a maximum of one entity, no compositions or hierarchies"* and *"not intended for
> post-generation — subsequent changes require manual developer intervention."*

So it fits a SEGW migration **only** when the legacy service is effectively a **single root entity**,
managed+draft, whose table carries the modern timestamp fields `last_changed : abp_lastchange_tstmpl`
and `local_last_changed : abp_locinst_lastchange_tstmpl`. Most SEGW services are multi-entity with
classic `ERDAT/AEDAT` admin fields and function imports → **the generator does not apply; use the
manual build (6b) as normal.** Where it *does* apply, it only seeds the root; ARC-1 still adds the
children, compositions, actions, and field mappings afterward (the generator can't, being one-shot).

If you use it:

1. **Probe.** No `abap_generators-list_generators` tool (the `abap-mcp` server) → skip to 6b, build manually.
2. **Resolve the ID — release-specific, never hardcode.** Call `abap_generators-list_generators`,
   match the **"OData UI Service"** generator by display name, use the **returned `id`** (proven live:
   `uiservice` on SAP_BASIS 758 / S4 2023; `ui-service` on 816 / ABAP Platform 2025; 816 also has
   `x-ui-service` "from scratch"). Not listed → skip to 6b.
3. **Schema → generate.** `abap_generators-get_schema(generatorId, packageName="<target_package>",
   referencedObjectType="TABL", referencedObjectName="<root table, e.g. ZDM_PROJECT>")`, fill every
   required field from the **returned** schema, then `abap_generators-generate_objects(generatorId, …)`.
   This is a mutation — target `<target_package>` + `<transport>`, same guardrails as any write.
4. **Hand back to ARC-1.** Continue at **Step 2 onward in 6b** for the children/compositions, the
   projection contract level, the action (`approve_project`) + handler body, and activation —
   adapting the names the generator chose for the root. Reconcile its root CDS/BDEF naming with this
   skill's `ZR_*`/`ZC_*`/`ZI_*_BEH` plan, or accept the generator's names and update Phase 5f's contract.

State which path you took so the run stays auditable. **When in doubt, prefer the manual build (6b)** —
it is the proven path for the multi-entity SEGW shape and never depends on a second MCP server.

### 6b. Build order — strict, no improvisation

The order below was learned the hard way (Run 1, calls #22–33). **Do not reorder.** Each step
prevents a specific failure mode in the next.

#### Step 1 — Draft tables (only if Phase 5d.draft = ON)

```text
SAPWrite(action="batch_create", objects=[
  { type: "TABL", name: "ZDM_PROJECT_D",   source: "<draft table source — see template below>", package: "<target_package>", transport: "<transport>" },
  { type: "TABL", name: "ZDM_TASK_D",      source: "<...>", package: "<target_package>", transport: "<transport>" },
  { type: "TABL", name: "ZDM_TIMEENTRY_D", source: "<...>", package: "<target_package>", transport: "<transport>" }
])
SAPActivate(action="activate", objects=[ {type:"TABL", name:"ZDM_PROJECT_D"}, ... ])
```

**Draft table template** — three rules learned in Run 2:

1. **Field names use BO-alias casing without underscores** (`projectid`, `startdate`,
   not `project_id`, `start_date`). ABAP normalizes BDEF aliases to uppercase no-underscore;
   draft binding looks up by that name, not by the active table's snake_case.
2. **`creationtimestamp` and `lastchangedstamp` columns required** for `total etag` +
   `etag master` clauses in the BDEF to bind.
3. **`@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE` is mandatory** — without it
   the TABL save fails on 7.58.

```abap
@EndUserText.label : 'Demo: Project (draft shadow)'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #ALLOWED
define table zdm_project_d {
  " Field names match the BO aliases (no underscore) — required by RAP draft binding.
  " The active table ZDM_PROJECT can keep its snake_case names; the BDEF mapping bridges
  " them. The draft table has no mapping clause, so its names must align with the BO directly.

  key client            : abap.clnt not null;
  key projectid         : abap.char(10) not null;
  title                 : abap.char(100);
  description           : abap.char(255);
  status                : abap.char(1);
  startdate             : abap.dats;
  enddate               : abap.dats;
  erdat                 : abap.dats;
  erzet                 : abap.tims;
  ernam                 : abap.char(12);
  aedat                 : abap.dats;
  aezet                 : abap.tims;
  aenam                 : abap.char(12);

  " Required for `total etag CreationTimeStamp` + `etag master LastChangedStamp` in BDEF.
  " Computed in CDS root view via dats_tims_to_tstmp(...); persisted on draft instances.
  creationtimestamp     : abap.dec(15,0);
  lastchangedstamp      : abap.dec(15,0);

  include sych_bdl_draft_admin_inc;
}
```

`ZDM_TASK_D` mirrors with `taskid` + `projectid` (FK), all task-specific columns aliased
no-underscore. `ZDM_TIMEENTRY_D` mirrors with `entryid` + `taskid` + `projectid` (FKs).

#### Step 2 — CDS roots (`ZR_DM_*`)

Top-down: parent first, then children. Use `composition [0..*] of <child> as <name>` —
**no `on` clause** for managed scenario.

If your legacy DPC_EXT did Aedat → Erdat fallback (run 1 found this), bake it into the root
view's timestamp computation. On 7.58 use `@Semantics.systemDateTime.createdAt`,
`lastChangedAt`, **and `localInstanceLastChangedAt`** — the last annotates the local-instance
etag field (`abp_locinst_lastchange_tstmpl`) that a managed-draft BO needs, and is exactly what
SAP's own RAP generator emits on 758. **Verified live on S/4HANA 2023:** standalone views with
`localInstanceLastChangedAt` and with `@Semantics.businessDate.from/to` both activate. Only
genuinely old releases (< 7.55, before draft support) lack these — do NOT strip them on 7.58.

```cds
@Semantics.systemDateTime.createdAt : true
dats_tims_to_tstmp(
  zdm_project.erdat,
  zdm_project.erzet,
  abap_system_timezone($session.client, 'NULL'),
  $session.client,
  'NULL'
) as CreationTimeStamp,

@Semantics.systemDateTime.lastChangedAt : true
dats_tims_to_tstmp(
  case zdm_project.aedat when '00000000' then zdm_project.erdat else zdm_project.aedat end,
  case zdm_project.aedat when '00000000' then zdm_project.erzet else zdm_project.aezet end,
  abap_system_timezone($session.client, 'NULL'),
  $session.client,
  'NULL'
) as LastChangedStamp
```

> **Activation order for composition CDS.** The default `batch_create` activates each
> object inline in array order, so the parent `composition [0..*] of ZR_DM_TASK as _Tasks`
> activates **before** `ZR_DM_TASK` exists, hitting *"data source `ZR_DM_TASK` does not
> exist"* (Run 6 reproducer). Three safe paths, pick whichever fits the run:
>
> 1. **`batch_create` with `activateAtEnd: true`** (ARC-1 ≥ 0.9.5, PR #270). Writes
>    inactive drafts for every object, then issues one terminal `activateBatch` so SAP's
>    activator resolves the cross-references in a single pass. This is the recommended
>    pattern when ARC-1 supports it — single tool call, no recovery dance:
>    ```text
>    SAPWrite(action="batch_create", activateAtEnd: true, objects=[...])
>    ```
> 2. **Per-file `SAPWrite(action="create")`** for each root, then a **single
>    `SAPActivate(action="activate", objects=[...])` batch** at the end (after every DDL
>    source has landed). Works on every ARC-1 release.
> 3. Manual bottom-up: `SAPWrite create` parent → `SAPActivate` child first → activate
>    parent last. More tool calls; rarely needed.
>
> The same fix applies to **Step 3 projections** (composition chain mirrored). Activate the
> projection trio together at the end, not inline.

#### Step 3 — CDS projections (`ZC_DM_*`) — DO NOT SKIP

This is where Run 1 silently dropped the layer. **A `provider contract` error here means
"create projections", NOT "remove the contract".**

**Critical: `provider contract transactional_interface` goes on the *root* projection
ONLY (`ZC_DM_PROJECT`).** Child projections (`ZC_DM_TASK`, `ZC_DM_TIMEENTRY`) use bare
`as projection on …` and inherit the BO contract via `redirected to parent`. Putting
the contract on children yields *"inappropriate provider contract on …"* on activation
(Run 2).

##### Root projection — WITH contract

Pick the contract based on scenario:
- **Managed + draft + CUD** (this skill's default): `provider contract transactional_query`
  (Run 3 evidence on 7.58: combining projection BDEF + `use draft` rejects `transactional_interface`).
- **Read-mostly, no draft, no CUD**: `provider contract transactional_interface`.

```cds
@AccessControl.authorizationCheck : #NOT_REQUIRED
@EndUserText.label : 'Demo project — UI projection (root)'
@Metadata.allowExtensions: true
define root view entity ZC_DM_PROJECT
  provider contract transactional_query
  as projection on ZR_DM_PROJECT
{
  key ProjectId,
      Title,
      Description,
      Status,
      StartDate,
      EndDate,
      Erdat, Erzet, Ernam, Aedat, Aezet, Aenam,
      CreationTimeStamp,
      LastChangedStamp,
      _Tasks : redirected to composition child ZC_DM_TASK
}
```

##### Child projections — WITHOUT contract, with `redirected to parent`

```cds
@AccessControl.authorizationCheck : #NOT_REQUIRED
@EndUserText.label : 'Demo task — UI projection (child)'
@Metadata.allowExtensions: true
define view entity ZC_DM_TASK
  as projection on ZR_DM_TASK
{
  key TaskId,
      ProjectId,
      Title, Description, Status, Priority, DueDate, AssignedTo, EstimatedHours,
      Erdat, Erzet, Ernam, Aedat, Aezet, Aenam,
      _Project      : redirected to parent ZC_DM_PROJECT,
      _TimeEntries  : redirected to composition child ZC_DM_TIMEENTRY
}
```

Same shape for `ZC_DM_TIMEENTRY` (no contract, `_Task : redirected to parent ZC_DM_TASK`).

> Note `define view entity` (not `define root view entity`) on children — only the BO
> root has `define root view entity`.

#### Step 4 — Empty behavior pool (skeletons created automatically by Step 7)

Write only the empty global class shell. The local handler classes (`lhc_project`,
`lhc_task`, `lhc_timeentry`) are no longer pre-created here — Step 7's
`SAPWrite action=generate_behavior_implementation` (PR-C, ARC-1 ≥ post-2026-05-10)
auto-creates them when missing, then injects every required handler signature and stub
in one call. Run 2's mandatory ADT-paste pause for CCDEF/CCIMP is gone.

```text
SAPWrite(action="create", type="CLAS", name="ZBP_DM_PROJECT",
         source="CLASS zbp_dm_project DEFINITION
                   PUBLIC ABSTRACT FINAL
                   FOR BEHAVIOR OF zr_dm_project.
                 ENDCLASS.

                 CLASS zbp_dm_project IMPLEMENTATION.
                 ENDCLASS.",
         description="Behavior pool for ZR_DM_PROJECT BO",
         package="<target_package>", transport="<transport>")
SAPActivate(type="CLAS", name="ZBP_DM_PROJECT")
```

> **Expected activation behavior.** The `SAPActivate` here may return *"no behavior
> definition for `zr_dm_project`"* — that's **expected and OK**. The BDEF doesn't exist
> yet; it's created in Step 5. Don't treat this as a real failure. The class will activate
> cleanly once the BDEF is in place; Step 7 (`generate_behavior_implementation`) re-activates
> the class with handlers anyway. Either:
> - Skip activation here (write only) and rely on Step 7 to activate.
> - Or accept the warning and re-activate the class as part of Step 10's batch activate.

That's it. No CCDEF/CCIMP write here. No ADT pause. Continue to Step 5.

#### Step 5 — Root BDEF with the managed-with-draft rules baked in

Run 1 calls #22–31 discovered ten managed-with-draft rules the hard way. Bake them in:

```abap
managed implementation in class zbp_dm_project unique;
strict ( 2 );
with draft;

define behavior for ZR_DM_PROJECT alias Project
  persistent table zdm_project
  draft table zdm_project_d
  lock master
  total etag CreationTimeStamp                 " <- required by `with draft`
  etag master LastChangedStamp
  authorization master ( instance )
{
  create;
  update;
  delete;

  field ( readonly : update ) ProjectId;
  field ( readonly )          CreationTimeStamp, LastChangedStamp;

  " composition associations: only `create` is allowed in the inline block
  association _Tasks { create; with draft; }

  draft action Edit;
  draft action Activate optimized;
  draft action Discard;
  draft action Resume;
  draft determine action Prepare;              " <- 'determine' keyword required, not just 'draft action'

  action approve_project result [1] $self;     " <- DO NOT remove on activation failure;
                                               "    Step 7 (generate_behavior_implementation)
                                               "    auto-creates the lhc_project skeleton and
                                               "    injects the handler in one call.

  mapping for zdm_project { … all field maps … }
}

define behavior for ZR_DM_TASK alias Task
  persistent table zdm_task
  draft table zdm_task_d
  lock dependent by _Project                   " <- not _Project of root; use the cross-BO assoc you added in roots
  etag dependent by _Project
  authorization dependent by _Project
{
  update; delete;

  field ( readonly : update ) TaskId, ProjectId;   " <- lock-by reference fields MUST be readonly
  association _Project    { with draft; }
  association _TimeEntries { create; with draft; }

  " NO draft Edit/Activate/Discard/Resume — those belong on the root only.

  mapping for zdm_task { … }
}

define behavior for ZR_DM_TIMEENTRY alias TimeEntry
  persistent table zdm_timeentry
  draft table zdm_timeentry_d
  lock dependent by _Project                   " <- not by _Task — chain must reach a `lock master`
  etag dependent by _Project
  authorization dependent by _Project
{
  update; delete;

  field ( readonly : update ) EntryId, TaskId, ProjectId;
  association _Project { with draft; }
  association _Task    { with draft; }

  mapping for zdm_timeentry { … }
}
```

If TimeEntry can only reach `_Task` and not `_Project`, you must add a **cross-BO
`association to ZR_DM_PROJECT as _Project`** in `ZR_DM_TIMEENTRY` (root view) — the lock
chain has to terminate at the entity that holds `lock master`.

#### Step 6 — Projection BDEF

Declares the behavior alias for the projection. Run 6 found that 7.58 enforces two
non-obvious syntax rules in projection BDEFs — bake them in:

1. `use draft;` at the **top** (not inside the body) when the root BDEF declared
   `with draft`. Without it, `use action Edit/Activate/Discard/Resume/Prepare` is rejected.
2. Inside `use association _X { ... }` blocks, write **bare operation names** (`create;`),
   **not** `use create;` — the `use` keyword belongs at the top level only.

Canonical projection BDEF for the root projection (managed + draft + CUD scenario):

```abap
projection;
strict ( 2 );
use draft;

define behavior for ZC_DM_PROJECT alias Project
  use etag
{
  use create;
  use update;
  use delete;

  use action Edit;
  use action Activate;
  use action Discard;
  use action Resume;
  use action Prepare;
  use action approve_project;

  use association _Tasks { create; }     " <- bare 'create;' inside, NOT 'use create;'
}
```

Child projections (`ZC_DM_TASK`, `ZC_DM_TIMEENTRY`) need their own projection BDEF blocks
in the same DDLS or as separate definitions, each using
`use association _Project` / `_Task` to expose the upward composition for the lock-master
chain. Bare `create;` inside association bodies, same rule.

#### Step 7 — Generate the behavior implementation (one call, fully autonomous)

```text
SAPWrite(action="generate_behavior_implementation", type="CLAS", name="ZBP_DM_PROJECT")
```

That single call (PR-C, ARC-1 ≥ post-2026-05-10) does:

1. Reads class metadata → extracts `<class:rootEntityRef>` to auto-discover the bound
   BDEF (no need to pass `bdefName`).
2. Cross-validates that MAIN's `FOR BEHAVIOR OF zr_dm_project` and the BDEF's
   `managed implementation in class zbp_dm_project unique` agree. Refuses to mutate
   on mismatch.
3. Calls the same scaffold engine `scaffold_rap_handlers` uses, with `autoApply=true`:
   - Auto-creates missing `lhc_<alias>` skeletons (CCDEF + CCIMP).
   - Injects `METHODS …` signatures for every action / determination / validation /
     authorization the BDEF requires.
   - Injects empty `METHOD … ENDMETHOD.` stubs in CCIMP.
4. Writes CCDEF + CCIMP via the PR #257 include= path under one stateful lock.
5. Activates the class.

Returns a structured JSON report including `discovery`, `validation`, `scaffoldChanged`,
counts, and `activation.success`. If activation fails with the well-known "Local classes
of CL_ABAP_BEHAVIOR_HANDLER…" stale-active coupling, the response includes a guided
`activation.hint` with concrete recovery options instead of throwing — the just-written
CCDEF/CCIMP source remains useful for both recovery paths.

**If activation fails for any non-obvious reason**, run `SAPDiagnose(action="object_state",
type="CLAS", name="ZBP_DM_PROJECT")` (PR #254) to see active vs inactive divergence per
include before retrying. It surfaces the exact include that's out of sync without dumping
raw source.

**Lower-level alternative**: if you need to scaffold against an existing populated class
or want a dry-run-style preview without auto-activating, use `scaffold_rap_handlers`
directly with `autoApply=true|false` and `bdefName` explicit. Prefer
`generate_behavior_implementation` for fresh behavior pools.

#### Step 8 — Fill action body via `edit_method`

```text
SAPWrite(action="edit_method", type="CLAS", name="ZBP_DM_PROJECT",
         method="lhc_project~approve_project",
         source="
  METHOD approve_project.
    READ ENTITIES OF zr_dm_project IN LOCAL MODE
      ENTITY Project FIELDS ( ProjectId Status Aedat Aezet Aenam )
        WITH CORRESPONDING #( keys )
      RESULT DATA(projects).

    MODIFY ENTITIES OF zr_dm_project IN LOCAL MODE
      ENTITY Project
        UPDATE FIELDS ( Status Aedat Aezet Aenam )
        WITH VALUE #( FOR p IN projects (
          %tky    = p-%tky
          Status  = 'A'
          Aedat   = sy-datum
          Aezet   = sy-uzeit
          Aenam   = sy-uname
        ) ).

    READ ENTITIES OF zr_dm_project IN LOCAL MODE
      ENTITY Project ALL FIELDS WITH CORRESPONDING #( keys )
      RESULT DATA(updated).

    result = VALUE #( FOR u IN updated ( %tky = u-%tky %param = u ) ).
  ENDMETHOD.",
         transport="<transport>")
```

(No `COMMIT WORK` — the framework saves automatically on action commit.)

#### Step 9 — Service definition + binding

```text
SAPWrite(action="batch_create", objects=[
  { type: 

…(truncated)
