Delphi-to-Angular Conversion
Converts Delphi VCL views from the P2 codebase into Angular components for the POLYPOINT saas app.
Usage
/delphi-to-angular analyze /path/to/P2/delphi/pep/fEditMitarbeiter.dfm
/delphi-to-angular analyze /path/to/P2/delphi/pep/fEditMitarbeiter.dfm /path/to/screenshot.png
/delphi-to-angular analyze /path/to/P2/delphi/pep/fEditMitarbeiter.dfm /path/to/P2/delphi/language/soluling/PolylangSoluling.ntp
/delphi-to-angular generate
Any argument ending in .ntp is treated as the Soluling translation project file, regardless of its position. If omitted, the skill probes the default path <p2-repo>/delphi/language/soluling/PolylangSoluling.ntp and, failing that, asks the user.
Phase Routing
If $ARGUMENTS[0] is analyze — run the Analyze Phase below.
If $ARGUMENTS[0] is generate — run the Generate Phase below.
Otherwise, show usage examples above and stop.
Analyze Phase
Input: $ARGUMENTS[1] is the full path to the Delphi .dfm file. The .pas file is derived from the same path with a .pas extension. Optional $ARGUMENTS[2] is a screenshot path.
Step 1: Read Delphi source files
- Read
$ARGUMENTS[1] (the .dfm file)
- Read the same path with
.pas extension
- If screenshot path provided, read it for visual reference
- For Delphi patterns and file structure, see references/delphi-patterns.md
Step 2: Follow references
From the PAS uses clause, resolve referenced files from the same directory as the input file:
- Read any frames referenced (
fr*.pas + .dfm)
- Read any interface files (
intf*.pas) to understand data contracts
- Read any data modules (
dm*.pas + .dfm) for SQL queries
- Read the base class. If the form derives from a domain-specific base (
TfKnotenGen, TfMitarbBase, etc. — anything beyond TForm / TDBParForm / TFrame / TDataModule), open the base PAS + DFM. Base classes typically contribute fields, tabs, and validation that the child silently inherits. See references/delphi-patterns.md for inherited DFM merging and base-class reading.
- Trace every
ShowModal and CreateForm call in the PAS. Each one opens a sub-dialog that needs its own Angular dialog component. List the targets up front so the conversion plan accounts for them.
- Read string-resource units. Any unit in the
uses clause that looks like a strings container (strres, str*, *Strings, *Const, etc.) holds resourcestring constants the form/frame references in code (snackbars, dialog titles, validation messages). Read those .pas files so every German string the form can produce is in scope for the Soluling file located in Step 2.6 (the lookup itself happens in generate Step 2.5).
- Resolve every server-side routine. For each
StoredProcName = 'X' in a DFM and each BEGIN X(:…); END; in the PAS, open <p2-repo>/shared/db/<schema>/procs/X.sql. For each table the form writes, open its Liquibase TABLE_*.xml, INDEX_*.xml, and CONSTRAINT_*.xml in the saas repo. This is where derivations, unique keys, and cascades live — none of them appear in the DFM or PAS.
Step 2.5: Reuse pass
Before designing new helpers, search the workspace for existing utilities:
libs/shared/ (or wherever the workspace keeps shared libs) for builders, translation services, a11y helpers, snack-duration constants, date/Temporal helpers, etc.
- The most-similar existing feature in the app (e.g. an already-converted hierarchy or detail dialog) for tree helpers, lock services, or domain-specific utilities.
Reuse before generating. The point is zero duplication of cross-feature utilities.
Step 2.6: Locate the Soluling translation file
The P2 Delphi apps all share a single Soluling translation project: <p2-repo>/delphi/language/soluling/PolylangSoluling.ntp. This file is the authoritative source for every German UI string and its existing translations across all locales (de-CH, en, fr, …). The Angular conversion reuses those translations rather than re-translating from scratch.
- Resolve the path. Use the first source available, in this order:
- Any
$ARGUMENTS value ending in .ntp.
- The default
<p2-repo>/delphi/language/soluling/PolylangSoluling.ntp derived from the input DFM path (walk up to the p2 repo root).
- Ask the user for the path if neither of the above resolves to an existing file. Do not proceed to Step 3 without it.
- Read the file.
PolylangSoluling.ntp is a Soluling project file (XML-like text). Each translatable string appears with a stable key/ID, a source value (typically German), and translated values per locale. Skim once to learn the key shape and the set of locales present — this is the shape you will look up against in the Generate phase.
- Note the file path so the Generate phase can re-read it without re-asking.
Step 2.7: Detect runtime-generated UI (fields that exist in no DFM)
Some forms render fields that appear in no .dfm/.pas at all — they are built at runtime from database definitions and are often license-gated; skipping this check has dropped entire field groups from past conversions. Check every form for the signals listed in references/delphi-patterns.md § Runtime-generated UI:
frFeldItems in the uses clause, a TFeldItemsFrame field, or .Anbindung := assignments
- a
TTabSheet with no child controls in the DFM (something populates it at runtime)
- SQL touching
FELDITEMTYP / FELDITEMS, GetDfId calls, or TDfEd controls
Lizenz.*Active checks in the PAS (license-gated UI that a demo system may not show)
When detected, enumerate the concrete fields from the Liquibase seed DATA_FELDITEMTYP.csv (filter by the form's ANBINDUNG, note BEZEICHNUNG, DATENTYP, AUSWAHLWERTE, REQUIRED_KEY) and carry them into the Runtime-generated / license-gated fields section of the conversion plan. Do not proceed to Step 3 with an empty tab left unexplained.
Step 3: Parse DFM structure
Extract from the DFM file:
- Component tree (parent/child nesting)
- Component types and key properties (dimensions, captions, alignment, visibility)
- Data bindings: which
TDataSource links to which TOraQuery
- Embedded SQL from
TOraQuery.SQL.Strings
- Column definitions from
TDBGrid.Columns
- Tab structure from
TPageControl + TTabSheet
Step 4: Parse PAS logic
Extract from the PAS file:
- Event handlers and their logic (OnClick, OnChange, OnCreate, etc.)
OnDblClick handlers — list each one separately. Double-click does not exist in the web app (undiscoverable, no touch support, invisible to screen readers); each handler's action must be re-homed to an explicit affordance (row action button, kebab/context menu entry, or selection + toolbar action). See component-mapping.md § Events.
- Validation logic — empty-checks before save,
raise / ECMessage guards, iChecker.DoCheck chains. These identify the mandatory fields (→ [required] asterisk) and the validate() rules; validation fires only after the first save attempt (see angular-conventions.md § Signal Forms). Message-bearing checks are a minority of the rules — Step 4.5 covers the silent ones (derived columns, unique keys, triggers, WHERE-clause eligibility). Do not treat this bullet as the rule inventory.
- Private fields (
F prefix = state, i prefix = interface)
Sync* methods — these become computed() signals
- Filter methods (
Apply*Filter) — these become signal-based filtering
- Public API: properties, setup methods
- Interface dependencies for the data contract
Step 4.5: Business-rule extraction
The DFM/PAS sweep in Steps 3–4 finds rules that produce a message. It is structurally blind to rules enforced by an Oracle procedure, a unique index, a trigger, a derived column, or a WHERE clause — which in P2 is most of them. Skipping this step is how a rule reaches neither the Angular app (it is not UI) nor the backend team (the MSW handler stays plain CRUD). Full method, greps, worked example, and tooling caveats: references/business-logic-extraction.md.
Set export LC_ALL=C first. P2 sources are ISO-8859-1; under a UTF-8 locale grep silently skips most of the tree (measured: 592 vs 1,708 matching files).
Run sweep 0 first (exclude rules already externalized to the cloud API — ls rest/*.pas, grep -rn 'PPAPI'; don't re-port those), then all seven sweeps below. Record the result of each — including "none found", so the reviewer knows the sweep ran:
- Reported rules — grep
Strres.pas (or the form's strings unit) for rule verbs (darf, muss, kann nicht, nicht erlaubt, bereits, ueberschneid, ungueltig), then grep each rs_* id back to its enforcement site. Note the ECMessage button set: [mbOK] is a hard stop, [mbYes,mbNo] is user-overridable — that distinction is part of the rule.
- Server-side logic —
grep -rn "StoredProcName" --include='*.dfm' --include='*.pas', grep -rnE "BEGIN +[A-Z0-9_]+\(:", and inline Oracle functions in DFM SQL. For every routine found, open <p2-repo>/shared/db/<schema>/procs/<NAME>.sql and read it. These have no Delphi source; a form-only sweep sees nothing at all.
- DB constraints — for every table the feature writes, open the Liquibase
INDEX_*.xml (unique="true") and CONSTRAINT_*.xml change-sets. A unique key on a start date with an unconstrained end date means the end date is derived — find the procedure that derives it.
- Triggers —
grep -rn -i '\btrigger\b' --include='*.pas' (German comments naming the trigger). 431 of 602 ECBERN triggers short-circuit for JDBC Thin Client — they do not fire for the Java backend, so every rule they carry must be re-homed explicitly.
- Embedded + dynamic SQL — DFM
SQL.Strings and SQL.Text := / MacroByName. Classify each predicate: validity resolution, status-code filter, eligibility, derivation, ordering. Re-join DFM ' + line continuations before matching.
- Sentinels, config, and permissions —
cInfiniteDate (= 2999-12-31, not NULL), -1 = "rule disabled", PEPOptions. / objSpital. / IPEPConflictSettings.ReadFor(nodeId) (which rules are active is per-tenant DB data), RightManager.HasRight*, LizenzChecker, SPERRUNG locking, DbIncreaseVersionId.
- Existing spec —
ls tests/PolyTest.Tests.*.pas for DUnit units covering this domain; port their cases before the implementation.
Then, for each rule found, classify it with the decision tree in the reference doc and write a rule card in the format defined there (@backend-rule … @confidence). Assign ids <FEATURE>-001, -002, … in discovery order; ids are permanent.
Representability gate. Before finishing, check each backend-invariant card against the data model about to be designed: can the wire model and the mock store express the rule's @scope tuple? If the model collapses a dimension the rule ranges over (a singular field where the rule needs a list, a store keyed on a subset of the scope tuple), that is a model defect, not a documentation problem — raise it as a -000 card and fix the model. A rule whose scope the model cannot represent cannot be documented, mocked, or tested; it can only be lost.
Do not proceed to Step 5 with an unresolved model-blocked card that has no -000 counterpart.
Step 5: Build conversion plan
For each mapping decision, consult references/component-mapping.md.
Translate all German identifiers to English using the domain glossary in component-mapping.md. Flag any German terms not in the glossary and ask the user to confirm the translation before proceeding.
Present the conversion plan in this format:
## Conversion Plan: <delphi-name> -> <angular-name>
### Components to generate
1. <ComponentName> (<routed|child|dialog>, <purpose>)
- Mapped from: <Delphi class> (<base class>)
- Layout: <Delphi layout> -> <Angular layout approach>
- Contains: <child components, PDX components>
2. <ChildComponentName> (child, <purpose>)
...
### Connected dialogs
For each `ShowModal` / `CreateForm` target traced from the PAS, list the resulting Angular dialog component:
1. <DialogComponentName> — opens from <event/button>, <purpose>
2. ...
### Double-click replacements
For each `OnDblClick` handler found in the PAS, state the explicit affordance that replaces it (never a `(dblclick)` binding):
1. <Delphi control>.OnDblClick (<action>) -> <row kebab menu entry | inline action icon-button | selection + toolbar button>
2. ...
### Runtime-generated / license-gated fields
For each field discovered in Step 2.7 (user-defined FELDITEMTYP fields, license-gated controls), list: label, control type derived from `DATENTYP`, picklist values, license keys from `REQUIRED_KEY` / `Lizenz.*` checks, and the conversion decision (first-class form field | generic user-defined-fields UI | out of scope — confirm with the user). If Step 2.7 found nothing, state "none detected" so the reviewer knows the check ran:
1. <label> (<control type>, license key <id|none>) -> <decision>
2. ...
### Reused utilities
List anything found in the reuse-pass that the generated code will import (test-data builders, translation service, lock service, tree helpers, etc.) so the plan doesn't re-invent them.
### Store
- <StoreName> (signalStore)
- State: { <property>: <type>, ... }
- Methods: <method1>, <method2>, ...
- Optimistic UI: <yes/no — list mutations that need snapshot/rollback>
- Locking: <yes/no — list resources wrapped in withLock>
### Service
- <ServiceName>
- <method>(params) -> mocked via MSW / test-data builder, TODO: <HTTP method> <endpoint path>
- ...
### Backend contract / business rules
Rules extracted in Step 4.5. This section is the handoff to the Java backend team — the MSW handler generated in Generate Step 2.6 carries these annotations verbatim, and the backend team transcribes them into OpenAPI + Java weeks later.
**Model representability**
- Scope tuples the wire model must support: <e.g. `(paCode, nodeId, validFrom)` — a LIST of validity windows per shift per node, not a single `validity` field>
- Model changes required before any rule can be stated: <or "none">
**Rules to enforce in the mock handler** (`@mock implemented`)
| Id | Statement (one line) | Kind | Error | Delphi evidence |
| ------ | -------------------- | --------- | ---------- | ---------------- |
| XX-001 | ... | invariant | 409 `CODE` | `<file>:<lines>` |
**Rules carried but not enforceable in the mock** (`@mock documented-only` / `model-blocked`)
| Id | Statement | Why not enforceable | Delphi evidence |
| ------ | --------- | ------------------- | ---------------- |
| XX-004 | ... | ... | `<file>:<lines>` |
**Rules the frontend also checks** (immediate feedback only — never authoritative)
| Id | Field(s) | Validator | Backend remains the gate |
| --- | -------- | --------- | ------------------------ |
**Error contract**
| Rule id | Status | `code` | `field` | i18n key |
| ------- | ------ | ------ | ------- | -------- |
Never let the store infer meaning from the status alone. A 409 on save can mean duplicate, over-length, a stale lock, or a referential failure — the frontend must branch on `code`.
**Sentinels and conventions crossing the wire**
- Open-ended date: <`cInfiniteDate` 2999-12-31 vs `null` — state the decision and where it is normalised>
- Disabled-rule sentinel, status codes, persisted enum ordinals that must not be reordered.
**Rules needing user confirmation** (`@confidence inferred | assumed`)
- XX-00N: <question>
**Server-side artefacts with no Angular counterpart** — Oracle procedures, JDBC-bypassed triggers, cache-version bumps, locks. List each with its Delphi path so the backend team can see them.
### Form model (if applicable)
- <formName>: signal<{ <field>: <type>, ... }>
- Mandatory fields: <fields derived from PAS empty-checks — get `[required]` asterisk + gated validator>
- Validation: <validate() rules — all gated on the `submitted` signal, errors appear only after the first save attempt>
- Drives: <what computed signals depend on it>
### Route
- /<route-path> (lazy loaded)
### Translations (Soluling)
- Soluling source: <resolved .ntp path>
- Locales present in .ntp: <de-CH, en, fr, …>
Strings found in Soluling — will be written to every locale JSON during generate:
- `<feature.key>` <- "<German source>" (from <DFM component | strres constant>)
- ...
Strings NOT found in Soluling — will be intentionally skipped so the human translator catches them in Transifex:
- "<German source>" (from <DFM component | strres constant>) — no matching .ntp entry
- ...
### Translation decisions needed
- "<German term>" -> "<proposed English>"?
- (List any term not in the glossary — wait for user confirmation before generating.)
Wait for user approval before proceeding to generate.
Generate Phase
Precondition: The analyze phase was run in this session and the user approved the conversion plan. If not, ask the user to run analyze first.
Target location
All files go under the workspace's main app feature directory — typically apps/<app>/src/app/<feature-name>/. Verify the exact path by inspecting an existing feature in the same workspace.
For Angular conventions, code patterns, and styling rules, see references/angular-conventions.md. For PDX component recipes (buttons, inputs, form scaffold, layout pitfalls, icons), see references/pdx-recipes.md.
Step 1: Create feature directory
mkdir -p <app-feature-root>/<feature-name>
And subdirectories for any child components.
Step 2: Generate files in dependency order
Generate the files required by the approved conversion plan, following the patterns in references/angular-conventions.md. Component files, component specs, SCSS placeholders, and translation keys are mandatory for generated UI. Store, service, mocks, route, and e2e files are generated only when the plan calls for async data, routed navigation, backend interaction, or routed feature coverage.
Apply the smart-vs-dumb component rule: the parent owns the store when a store exists; children take a slice via input() and emit via output(). A child must not both inject(FeatureStore) and receive an input() for the same data.
- TypeScript interfaces — for the data model (from Delphi field types and interface contracts).
- Service (
<feature>.service.ts) — only when backend/data access is needed; @Injectable({ providedIn: 'root' }), with TODO endpoint comments.
- Service spec (
<feature>.service.spec.ts) — only when a service is generated; Vitest, mock HttpClient.
- Store (
<feature>.store.ts) — only when shared/async feature state is needed; signalStore(withState(...), withMethods(...)), rxMethod for async. Apply optimistic UI / locking patterns where the Delphi source needs them (see angular-conventions.md).
- Store spec (
<feature>.store.spec.ts) — only when a store is generated; Vitest, mock service or builder.
- Tree / immutable helpers if applicable (
<feature>.tree.utils.ts) — pure functions for addChildToTree, removeNodeFromTree, etc.
- Child components (dumb) — bottom-up, each with .ts, .html, .scss, .spec.ts.
data-testid on every interactive surface.
- Parent component (
<feature>.component.ts, .html, .scss) — imports children, wires store, owns data-testid host attribute.
- Parent spec (
<feature>.component.spec.ts) — Vitest. Include the axe a11y assertion (see angular-conventions.md).
Step 2.5: Populate translation keys from Soluling
For every user-visible string the new components introduce, add a translation key under a feature namespace (hierarchy.title, hierarchy.add_child). Values for each locale come from the Soluling project file located in the analyze phase — not from re-translation.
- Discover the workspace's i18n folder once. Commonly under
apps/<app>/public/assets/i18n/ (Nx) or src/assets/i18n/ (CLI). Search for an i18n directory containing locale JSON files (find . -type d -name i18n).
- List every locale file present (
de-CH.json, en.json, fr.json, etc.).
- Open
PolylangSoluling.ntp at the path resolved in analyze Step 2.6. For each German source string the generated components or strres-derived constants need, look it up by source value (and, where applicable, by Delphi component / resourcestring name) to find the matching Soluling entry.
- For each Soluling match: write the Angular feature key into every locale JSON file, using the Soluling translation for that locale. If the .ntp has
de-CH and fr but the app also ships en, copy the values for the locales that exist in both, and skip the locales the .ntp does not provide.
- For each German string with no Soluling match: do not write any locale entry for that key — not even an English placeholder, not even the German source. Leaving the key absent across all locale files is intentional: the next Transifex sync flags it as a new missing string so the human translator authors it once. Track these in the post-generate summary so the developer knows what to expect in Transifex.
- Keys in locale JSON files are flat —
{ "hierarchy.title": "…" }, not { "hierarchy": { "title": "…" } }. Match the project's existing casing (snake vs kebab, feature-prefixed or not) and reuse shared namespaces like common.save / common.cancel rather than minting feature-local duplicates.
Do not hardcode any user-visible string in templates or TypeScript — TranslatePipe for templates, TranslateService.instant(...) for snackbars / dynamic labels. The component templates still reference keys ('hierarchy.title' | translate) whether or not the locale file has a value yet — ngx-translate falls back to the key itself when a value is missing, which is exactly the signal the human translator needs.
Step 2.6: Generate mock API, fixtures, and rule enforcement
The MSW handler is not a stub — it is the specification the Java backend team reads. The OpenAPI spec and the Java service are hand-written from the mock and its fixtures weeks later, by someone who did not write the mock. Anything absent from the handler is absent from the backend.
If the generated feature has service-backed data, search the workspace for an existing MSW setup (mock-api, msw/handlers, setupWorker).
- If MSW exists, mirror the existing handler pattern: handler file per feature, in-memory store seeded with realistic fixtures, registered in the existing index file. Realistic data: 10+ tree nodes, 4+ list items, real-looking domain names.
- If MSW is not present, generate a builder in the workspace's shared test-data lib (
libs/shared/test-data/ or similar — discover the location), exported and shared by the service mock and every spec. No inline duplication.
Then apply the four rule-carrying requirements below. First find the workspace's best rule-carrying handlers and mirror them: grep the handlers directory for conflict responses (grep -rln "status: 409\|code:" <handlers-dir>) and pick the ones that enforce a domain rule and return a coded body — not the plain-CRUD ones. Rule kinds to look for in an exemplar: a temporal "at most one per period" rule with its cascade, system-row immutability with 409 {code:'IN_USE'}, lock and child-reference conflicts (see the reference doc for the exemplars found at the time of writing). Full annotation format and rationale: references/business-logic-extraction.md.
File-level JSDoc. Every handler opens with which Delphi form and which DB table it stands in for — it is the first thing the backend team reads.
One @backend-rule block per rule from the conversion plan, immediately above the code that implements it (or above the store/type it concerns, when documented-only). Use all rule-card fields from the reference doc's format (@backend-rule <id> <title>, @statement, @scope, @kind, @layer, @delphi, @p2-behaviour, @error, @i18n, @mock, @openapi, @test, @confidence, and @question on inferred/assumed cards). Rules the mock cannot execute still get a block, with @mock documented-only: <reason> or @mock model-blocked: <reason + blocking id>. Never omit a rule because the mock cannot enforce it — an unwritten rule and a nonexistent rule are indistinguishable in a handler file, and that indistinguishability is the entire failure mode this step exists to prevent.
Implement every rule marked @mock implemented, returning the typed RuleViolation conflict body defined in the reference doc ({ code, ruleId, field?, message } — code is a stable enum like 'DUPLICATE_VALID_FROM', ruleId joins handler ↔ spec ↔ i18n ↔ the eventual Java guard, message is English for logs and never rendered). The Angular store must branch on code, never on the bare status, and the resulting error path must be wired into the UI now: the app needs it anyway the moment the real backend enforces the rule, and inferring meaning from a bare 409 is a live bug (an over-length title rejected by the DB rendered to the user as "title already in use").
Generate <feature>.handlers.spec.ts with one test slot per rule id. @mock implemented rules get a real it('<ID> <statement>', ...) derived from @test — plain vitest, no custom tooling. documented-only and model-blocked rules get it.todo('<ID> <statement>') so the rule occupies a named, visible slot in every test run. Keep the handler and the spec in step when adding or removing a rule — PR review is the gate, deliberately: the handler plus its spec IS the handoff artifact, and a bespoke sync script was tried and retired because the ceremony cost more than the drift it prevented. Do not generate a separate rules document — everything the backend team needs lives in the handler they already read.
Step 3: Add route
For routed features only, add a lazy-loaded route to the workspace's main routes file (typically app.routes.ts — find it by inspecting an existing routed feature):
{
path: '<feature-path>',
loadComponent: () => import('./<feature-name>/<feature-name>.component').then((c) => c.<ComponentName>),
},
Step 4: Generate e2e + a11y tests
For every routed feature, generate a paired e2e spec at the workspace's e2e app (typically apps/<app>-e2e/src/<feature>.spec.ts — find it by inspecting an existing e2e spec). Dialog-only child components do not need their own e2e spec unless the approved plan explicitly asks for one. At minimum:
- route loads and the host testid is visible
- every top-level testid renders
- no console errors on initial load
- if the project uses cookie-based mock auth in e2e, set the cookie the e2e harness expects — inspect existing e2e specs in the workspace for the cookie name, value, and domain.
Selectors must be locale-resilient: getByTestId(...) first; never query by translation key, translated label, or aria-label that comes from a translation.
For every component spec, include the axe a11y assertion (see the spec template in angular-conventions.md). Project policy is zero a11y violations.
Step 5: Post-generate quality passes
Run two lint passes over the generated code before reporting done. Both are documented in references/angular-conventions.md.
- Signal-store / reactivity lint pass. Catch mirror-signal+effect copies, dep-less
computed(), missing untracked() around state writes inside effect(), granular dirty flags no one reads, and stores exposing raw objects when only a label is consumed.
- Convention check pass. Visibility modifiers on every member (including
input() / output()), named constants over magic literals, no dead injections, comments explain WHY not WHAT, smart-vs-dumb component check (no child both inject()s a store and exposes an input() for the same data), no (dblclick) bindings (every Delphi double-click action re-homed to an explicit affordance), form validators gated on the submitted signal, mandatory fields carry [required].
Step 6: Validate
Read package.json for the project's validation script and run it. Common names:
bun run check:strict
bun run check
npm run validate
Fix any failures (format, lint, test, build, e2e) before presenting the result. If no validation script is defined, fall back to bun run lint && bun run test.
Step 7: Present result
List all generated files with a one-line description of each. Highlight:
- Decisions made during generation (translation choices, optimistic UI scope, locking applied).
- TODO items that need backend work (HTTP endpoints, real auth).
- PDX library gaps: every workaround applied around a
pp-* component's behavior (CSS fighting internals, DOM queries into component markup, re-implemented inputs/outputs, sizing/focus patches) — name the component, the missing behavior, the workaround, and whether the behavior should be implemented in the library instead, so the user can decide to file a PDX improvement. See pdx-recipes.md § Report PDX library gaps.
- Soluling outcome: translation keys populated from
PolylangSoluling.ntp (by locale) vs. keys intentionally left absent across all locale files so the human translator authors them in Transifex. List each absent key with its German source so the developer can sanity-check before the next Transifex push.
- Business rules carried to the backend: count of rules enforced in the mock, count carried as
documented-only / model-blocked (with reasons), and every rule whose @confidence is inferred or assumed — list these as explicit questions for the user, since a wrong rule in an executable mock propagates faster than a wrong comment.
Supporting Files
Reference Files
- references/component-mapping.md — Delphi VCL → Angular component mapping tables and German-English domain glossary. Load during analyze phase.
- references/delphi-patterns.md — P2 Delphi codebase structure, file naming, DFM/PAS anatomy,
inherited keyword, base-class reading, ShowModal tracing, runtime-generated UI (FeldItems / license gating). Load during analyze phase.
- references/business-logic-extraction.md — where P2 hides business logic (18 location classes), the seven-sweep extraction pass, tooling caveats (encoding, DFM line continuation), the frontend/backend classification tree, the rule-card format, and the MSW
@backend-rule annotation + error contract. Load during analyze Step 4.5 and generate Step 2.6.
- references/angular-conventions.md — Angular patterns (component, store, signal forms incl. validation-after-first-submit and required-asterisk rules, i18n, testing, a11y, smart-vs-dumb components, optimistic UI, locking, mat-tree, post-generate lint passes). Load during generate phase.
- references/pdx-recipes.md — PDX component recipes, icon naming, layout pitfalls, width-control rule, two-column layout, right-rail aside, row-actions (double-click replacement), snackbar. Load during generate phase.
Example Files
- examples/sample-conversion.md — Complete worked example converting fChooseMonthRange to choose-month-range.
1---2name: delphi-to-angular3description: Use when converting Delphi VCL views (.dfm/.pas) from the P2 codebase to Angular components. Handles forms, frames, data modules, grids, trees, tabs, and dialogs. Produces Angular components plus any needed store, service, mocks, routes, and tests matching the POLYPOINT saas repo stack. Extracts business rules from Delphi code, Oracle procedures, and DB constraints into rule cards that are enforced in the MSW handlers handed off to the backend team.4---56# Delphi-to-Angular Conversion78Converts Delphi VCL views from the P2 codebase into Angular components for the POLYPOINT saas app.910## Usage1112```13/delphi-to-angular analyze /path/to/P2/delphi/pep/fEditMitarbeiter.dfm14/delphi-to-angular analyze /path/to/P2/delphi/pep/fEditMitarbeiter.dfm /path/to/screenshot.png15/delphi-to-angular analyze /path/to/P2/delphi/pep/fEditMitarbeiter.dfm /path/to/P2/delphi/language/soluling/PolylangSoluling.ntp16/delphi-to-angular generate17```1819Any argument ending in `.ntp` is treated as the Soluling translation project file, regardless of its position. If omitted, the skill probes the default path `<p2-repo>/delphi/language/soluling/PolylangSoluling.ntp` and, failing that, asks the user.2021## Phase Routing2223If `$ARGUMENTS[0]` is **`analyze`** — run the Analyze Phase below.24If `$ARGUMENTS[0]` is **`generate`** — run the Generate Phase below.25Otherwise, show usage examples above and stop.2627---2829## Analyze Phase3031**Input:** `$ARGUMENTS[1]` is the full path to the Delphi `.dfm` file. The `.pas` file is derived from the same path with a `.pas` extension. Optional `$ARGUMENTS[2]` is a screenshot path.3233### Step 1: Read Delphi source files34351. Read `$ARGUMENTS[1]` (the `.dfm` file)362. Read the same path with `.pas` extension373. If screenshot path provided, read it for visual reference384. For Delphi patterns and file structure, see [references/delphi-patterns.md](references/delphi-patterns.md)3940### Step 2: Follow references4142From the PAS `uses` clause, resolve referenced files from the same directory as the input file:4344- Read any frames referenced (`fr*.pas` + `.dfm`)45- Read any interface files (`intf*.pas`) to understand data contracts46- Read any data modules (`dm*.pas` + `.dfm`) for SQL queries47- **Read the base class.** If the form derives from a domain-specific base (`TfKnotenGen`, `TfMitarbBase`, etc. — anything beyond `TForm` / `TDBParForm` / `TFrame` / `TDataModule`), open the base PAS + DFM. Base classes typically contribute fields, tabs, and validation that the child silently inherits. See [references/delphi-patterns.md](references/delphi-patterns.md) for `inherited` DFM merging and base-class reading.48- **Trace every `ShowModal` and `CreateForm` call** in the PAS. Each one opens a sub-dialog that needs its own Angular dialog component. List the targets up front so the conversion plan accounts for them.49- **Read string-resource units.** Any unit in the `uses` clause that looks like a strings container (`strres`, `str*`, `*Strings`, `*Const`, etc.) holds `resourcestring` constants the form/frame references in code (snackbars, dialog titles, validation messages). Read those `.pas` files so every German string the form can produce is in scope for the Soluling file located in Step 2.6 (the lookup itself happens in generate Step 2.5).50- **Resolve every server-side routine.** For each `StoredProcName = 'X'` in a DFM and each `BEGIN X(:…); END;` in the PAS, open `<p2-repo>/shared/db/<schema>/procs/X.sql`. For each table the form writes, open its Liquibase `TABLE_*.xml`, `INDEX_*.xml`, and `CONSTRAINT_*.xml` in the saas repo. This is where derivations, unique keys, and cascades live — none of them appear in the DFM or PAS.5152### Step 2.5: Reuse pass5354Before designing new helpers, search the workspace for existing utilities:5556- `libs/shared/` (or wherever the workspace keeps shared libs) for builders, translation services, a11y helpers, snack-duration constants, date/Temporal helpers, etc.57- The most-similar existing feature in the app (e.g. an already-converted hierarchy or detail dialog) for tree helpers, lock services, or domain-specific utilities.5859Reuse before generating. The point is **zero duplication of cross-feature utilities**.6061### Step 2.6: Locate the Soluling translation file6263The P2 Delphi apps all share a single Soluling translation project: `<p2-repo>/delphi/language/soluling/PolylangSoluling.ntp`. This file is the authoritative source for every German UI string and its existing translations across all locales (`de-CH`, `en`, `fr`, …). The Angular conversion reuses those translations rather than re-translating from scratch.64651. **Resolve the path.** Use the first source available, in this order:66 1. Any `$ARGUMENTS` value ending in `.ntp`.67 2. The default `<p2-repo>/delphi/language/soluling/PolylangSoluling.ntp` derived from the input DFM path (walk up to the `p2` repo root).68 3. **Ask the user** for the path if neither of the above resolves to an existing file. Do not proceed to Step 3 without it.692. **Read the file.** `PolylangSoluling.ntp` is a Soluling project file (XML-like text). Each translatable string appears with a stable key/ID, a source value (typically German), and translated values per locale. Skim once to learn the key shape and the set of locales present — this is the shape you will look up against in the Generate phase.703. **Note the file path** so the Generate phase can re-read it without re-asking.7172### Step 2.7: Detect runtime-generated UI (fields that exist in no DFM)7374Some forms render fields that appear in **no** `.dfm`/`.pas` at all — they are built at runtime from database definitions and are often license-gated; skipping this check has dropped entire field groups from past conversions. Check every form for the signals listed in [references/delphi-patterns.md](references/delphi-patterns.md) § Runtime-generated UI:7576- `frFeldItems` in the `uses` clause, a `TFeldItemsFrame` field, or `.Anbindung :=` assignments77- a `TTabSheet` with no child controls in the DFM (something populates it at runtime)78- SQL touching `FELDITEMTYP` / `FELDITEMS`, `GetDfId` calls, or `TDfEd` controls79- `Lizenz.*Active` checks in the PAS (license-gated UI that a demo system may not show)8081When detected, enumerate the concrete fields from the Liquibase seed `DATA_FELDITEMTYP.csv` (filter by the form's `ANBINDUNG`, note `BEZEICHNUNG`, `DATENTYP`, `AUSWAHLWERTE`, `REQUIRED_KEY`) and carry them into the **Runtime-generated / license-gated fields** section of the conversion plan. Do not proceed to Step 3 with an empty tab left unexplained.8283### Step 3: Parse DFM structure8485Extract from the DFM file:8687- Component tree (parent/child nesting)88- Component types and key properties (dimensions, captions, alignment, visibility)89- Data bindings: which `TDataSource` links to which `TOraQuery`90- Embedded SQL from `TOraQuery.SQL.Strings`91- Column definitions from `TDBGrid.Columns`92- Tab structure from `TPageControl` + `TTabSheet`9394### Step 4: Parse PAS logic9596Extract from the PAS file:9798- Event handlers and their logic (OnClick, OnChange, OnCreate, etc.)99- **`OnDblClick` handlers** — list each one separately. Double-click does not exist in the web app (undiscoverable, no touch support, invisible to screen readers); each handler's action must be re-homed to an explicit affordance (row action button, kebab/context menu entry, or selection + toolbar action). See component-mapping.md § Events.100- **Validation logic** — empty-checks before save, `raise` / `ECMessage` guards, `iChecker.DoCheck` chains. These identify the mandatory fields (→ `[required]` asterisk) and the `validate()` rules; validation fires only after the first save attempt (see angular-conventions.md § Signal Forms). **Message-bearing checks are a minority of the rules** — Step 4.5 covers the silent ones (derived columns, unique keys, triggers, WHERE-clause eligibility). Do not treat this bullet as the rule inventory.101- Private fields (`F` prefix = state, `i` prefix = interface)102- `Sync*` methods — these become `computed()` signals103- Filter methods (`Apply*Filter`) — these become signal-based filtering104- Public API: properties, setup methods105- Interface dependencies for the data contract106107### Step 4.5: Business-rule extraction108109The DFM/PAS sweep in Steps 3–4 finds rules that **produce a message**. It is structurally blind to rules enforced by an Oracle procedure, a unique index, a trigger, a derived column, or a WHERE clause — which in P2 is most of them. Skipping this step is how a rule reaches neither the Angular app (it is not UI) nor the backend team (the MSW handler stays plain CRUD). Full method, greps, worked example, and tooling caveats: [references/business-logic-extraction.md](references/business-logic-extraction.md).110111**Set `export LC_ALL=C` first.** P2 sources are ISO-8859-1; under a UTF-8 locale grep silently skips most of the tree (measured: 592 vs 1,708 matching files).112113Run sweep 0 first (exclude rules already externalized to the cloud API — `ls rest/*.pas`, `grep -rn 'PPAPI'`; don't re-port those), then all seven sweeps below. Record the result of each — including "none found", so the reviewer knows the sweep ran:1141151. **Reported rules** — grep `Strres.pas` (or the form's strings unit) for rule verbs (`darf`, `muss`, `kann nicht`, `nicht erlaubt`, `bereits`, `ueberschneid`, `ungueltig`), then grep each `rs_*` id back to its enforcement site. Note the `ECMessage` button set: `[mbOK]` is a hard stop, `[mbYes,mbNo]` is user-overridable — that distinction is part of the rule.1162. **Server-side logic** — `grep -rn "StoredProcName" --include='*.dfm' --include='*.pas'`, `grep -rnE "BEGIN +[A-Z0-9_]+\(:"`, and inline Oracle functions in DFM SQL. **For every routine found, open `<p2-repo>/shared/db/<schema>/procs/<NAME>.sql` and read it.** These have no Delphi source; a form-only sweep sees nothing at all.1173. **DB constraints** — for every table the feature writes, open the Liquibase `INDEX_*.xml` (`unique="true"`) and `CONSTRAINT_*.xml` change-sets. A unique key on a _start_ date with an unconstrained end date means the end date is derived — find the procedure that derives it.1184. **Triggers** — `grep -rn -i '\btrigger\b' --include='*.pas'` (German comments naming the trigger). **431 of 602 ECBERN triggers short-circuit for `JDBC Thin Client`** — they do not fire for the Java backend, so every rule they carry must be re-homed explicitly.1195. **Embedded + dynamic SQL** — DFM `SQL.Strings` and `SQL.Text :=` / `MacroByName`. Classify each predicate: validity resolution, status-code filter, eligibility, derivation, ordering. Re-join DFM `' +` line continuations before matching.1206. **Sentinels, config, and permissions** — `cInfiniteDate` (= 2999-12-31, **not** NULL), `-1` = "rule disabled", `PEPOptions.` / `objSpital.` / `IPEPConflictSettings.ReadFor(nodeId)` (which rules are active is per-tenant DB data), `RightManager.HasRight*`, `LizenzChecker`, `SPERRUNG` locking, `DbIncreaseVersionId`.1217. **Existing spec** — `ls tests/PolyTest.Tests.*.pas` for DUnit units covering this domain; port their cases before the implementation.122123Then, for each rule found, **classify** it with the decision tree in the reference doc and write a **rule card** in the format defined there (`@backend-rule` … `@confidence`). Assign ids `<FEATURE>-001`, `-002`, … in discovery order; ids are permanent.124125**Representability gate.** Before finishing, check each backend-invariant card against the data model about to be designed: can the wire model and the mock store express the rule's `@scope` tuple? If the model collapses a dimension the rule ranges over (a singular field where the rule needs a list, a store keyed on a subset of the scope tuple), that is a **model defect, not a documentation problem** — raise it as a `-000` card and fix the model. A rule whose scope the model cannot represent cannot be documented, mocked, or tested; it can only be lost.126127Do not proceed to Step 5 with an unresolved `model-blocked` card that has no `-000` counterpart.128129### Step 5: Build conversion plan130131For each mapping decision, consult [references/component-mapping.md](references/component-mapping.md).132133Translate all German identifiers to English using the domain glossary in component-mapping.md. **Flag any German terms not in the glossary** and ask the user to confirm the translation before proceeding.134135Present the conversion plan in this format:136137```markdown138## Conversion Plan: <delphi-name> -> <angular-name>139140### Components to generate1411421. <ComponentName> (<routed|child|dialog>, <purpose>)143 - Mapped from: <Delphi class> (<base class>)144 - Layout: <Delphi layout> -> <Angular layout approach>145 - Contains: <child components, PDX components>1461472. <ChildComponentName> (child, <purpose>)148 ...149150### Connected dialogs151152For each `ShowModal` / `CreateForm` target traced from the PAS, list the resulting Angular dialog component:1531541. <DialogComponentName> — opens from <event/button>, <purpose>1552. ...156157### Double-click replacements158159For each `OnDblClick` handler found in the PAS, state the explicit affordance that replaces it (never a `(dblclick)` binding):1601611. <Delphi control>.OnDblClick (<action>) -> <row kebab menu entry | inline action icon-button | selection + toolbar button>1622. ...163164### Runtime-generated / license-gated fields165166For each field discovered in Step 2.7 (user-defined FELDITEMTYP fields, license-gated controls), list: label, control type derived from `DATENTYP`, picklist values, license keys from `REQUIRED_KEY` / `Lizenz.*` checks, and the conversion decision (first-class form field | generic user-defined-fields UI | out of scope — confirm with the user). If Step 2.7 found nothing, state "none detected" so the reviewer knows the check ran:1671681. <label> (<control type>, license key <id|none>) -> <decision>1692. ...170171### Reused utilities172173List anything found in the reuse-pass that the generated code will import (test-data builders, translation service, lock service, tree helpers, etc.) so the plan doesn't re-invent them.174175### Store176177- <StoreName> (signalStore)178 - State: { <property>: <type>, ... }179 - Methods: <method1>, <method2>, ...180 - Optimistic UI: <yes/no — list mutations that need snapshot/rollback>181 - Locking: <yes/no — list resources wrapped in withLock>182183### Service184185- <ServiceName>186 - <method>(params) -> mocked via MSW / test-data builder, TODO: <HTTP method> <endpoint path>187 - ...188189### Backend contract / business rules190191Rules extracted in Step 4.5. This section is the handoff to the Java backend team — the MSW handler generated in Generate Step 2.6 carries these annotations verbatim, and the backend team transcribes them into OpenAPI + Java weeks later.192193**Model representability**194195- Scope tuples the wire model must support: <e.g. `(paCode, nodeId, validFrom)` — a LIST of validity windows per shift per node, not a single `validity` field>196- Model changes required before any rule can be stated: <or "none">197198**Rules to enforce in the mock handler** (`@mock implemented`)199200| Id | Statement (one line) | Kind | Error | Delphi evidence |201| ------ | -------------------- | --------- | ---------- | ---------------- |202| XX-001 | ... | invariant | 409 `CODE` | `<file>:<lines>` |203204**Rules carried but not enforceable in the mock** (`@mock documented-only` / `model-blocked`)205206| Id | Statement | Why not enforceable | Delphi evidence |207| ------ | --------- | ------------------- | ---------------- |208| XX-004 | ... | ... | `<file>:<lines>` |209210**Rules the frontend also checks** (immediate feedback only — never authoritative)211212| Id | Field(s) | Validator | Backend remains the gate |213| --- | -------- | --------- | ------------------------ |214215**Error contract**216217| Rule id | Status | `code` | `field` | i18n key |218| ------- | ------ | ------ | ------- | -------- |219220Never let the store infer meaning from the status alone. A 409 on save can mean duplicate, over-length, a stale lock, or a referential failure — the frontend must branch on `code`.221222**Sentinels and conventions crossing the wire**223224- Open-ended date: <`cInfiniteDate` 2999-12-31 vs `null` — state the decision and where it is normalised>225- Disabled-rule sentinel, status codes, persisted enum ordinals that must not be reordered.226227**Rules needing user confirmation** (`@confidence inferred | assumed`)228229- XX-00N: <question>230231**Server-side artefacts with no Angular counterpart** — Oracle procedures, JDBC-bypassed triggers, cache-version bumps, locks. List each with its Delphi path so the backend team can see them.232233### Form model (if applicable)234235- <formName>: signal<{ <field>: <type>, ... }>236 - Mandatory fields: <fields derived from PAS empty-checks — get `[required]` asterisk + gated validator>237 - Validation: <validate() rules — all gated on the `submitted` signal, errors appear only after the first save attempt>238 - Drives: <what computed signals depend on it>239240### Route241242- /<route-path> (lazy loaded)243244### Translations (Soluling)245246- Soluling source: <resolved .ntp path>247- Locales present in .ntp: <de-CH, en, fr, …>248249Strings found in Soluling — will be written to every locale JSON during generate:250251- `<feature.key>` <- "<German source>" (from <DFM component | strres constant>)252- ...253254Strings NOT found in Soluling — will be intentionally skipped so the human translator catches them in Transifex:255256- "<German source>" (from <DFM component | strres constant>) — no matching .ntp entry257- ...258259### Translation decisions needed260261- "<German term>" -> "<proposed English>"?262- (List any term not in the glossary — wait for user confirmation before generating.)263```264265**Wait for user approval before proceeding to generate.**266267---268269## Generate Phase270271**Precondition:** The analyze phase was run in this session and the user approved the conversion plan. If not, ask the user to run analyze first.272273### Target location274275All files go under the workspace's main app feature directory — typically `apps/<app>/src/app/<feature-name>/`. Verify the exact path by inspecting an existing feature in the same workspace.276277For Angular conventions, code patterns, and styling rules, see [references/angular-conventions.md](references/angular-conventions.md). For PDX component recipes (buttons, inputs, form scaffold, layout pitfalls, icons), see [references/pdx-recipes.md](references/pdx-recipes.md).278279### Step 1: Create feature directory280281```bash282mkdir -p <app-feature-root>/<feature-name>283```284285And subdirectories for any child components.286287### Step 2: Generate files in dependency order288289Generate the files required by the approved conversion plan, following the patterns in [references/angular-conventions.md](references/angular-conventions.md). Component files, component specs, SCSS placeholders, and translation keys are mandatory for generated UI. Store, service, mocks, route, and e2e files are generated only when the plan calls for async data, routed navigation, backend interaction, or routed feature coverage.290291Apply the **smart-vs-dumb component rule**: the parent owns the store when a store exists; children take a slice via `input()` and emit via `output()`. A child must not both `inject(FeatureStore)` and receive an `input()` for the same data.2922931. **TypeScript interfaces** — for the data model (from Delphi field types and interface contracts).2942. **Service** (`<feature>.service.ts`) — only when backend/data access is needed; `@Injectable({ providedIn: 'root' })`, with TODO endpoint comments.2953. **Service spec** (`<feature>.service.spec.ts`) — only when a service is generated; Vitest, mock HttpClient.2964. **Store** (`<feature>.store.ts`) — only when shared/async feature state is needed; `signalStore(withState(...), withMethods(...))`, `rxMethod` for async. Apply optimistic UI / locking patterns where the Delphi source needs them (see angular-conventions.md).2975. **Store spec** (`<feature>.store.spec.ts`) — only when a store is generated; Vitest, mock service or builder.2986. **Tree / immutable helpers** if applicable (`<feature>.tree.utils.ts`) — pure functions for `addChildToTree`, `removeNodeFromTree`, etc.2997. **Child components** (dumb) — bottom-up, each with .ts, .html, .scss, .spec.ts. `data-testid` on every interactive surface.3008. **Parent component** (`<feature>.component.ts`, `.html`, `.scss`) — imports children, wires store, owns `data-testid` host attribute.3019. **Parent spec** (`<feature>.component.spec.ts`) — Vitest. Include the axe a11y assertion (see angular-conventions.md).302303### Step 2.5: Populate translation keys from Soluling304305For every user-visible string the new components introduce, add a translation key under a feature namespace (`hierarchy.title`, `hierarchy.add_child`). Values for each locale come from the Soluling project file located in the analyze phase — **not** from re-translation.3063071. **Discover the workspace's i18n folder once.** Commonly under `apps/<app>/public/assets/i18n/` (Nx) or `src/assets/i18n/` (CLI). Search for an `i18n` directory containing locale JSON files (`find . -type d -name i18n`).3082. **List every locale file present** (`de-CH.json`, `en.json`, `fr.json`, etc.).3093. **Open `PolylangSoluling.ntp`** at the path resolved in analyze Step 2.6. For each German source string the generated components or strres-derived constants need, look it up by source value (and, where applicable, by Delphi component / resourcestring name) to find the matching Soluling entry.3104. **For each Soluling match:** write the Angular feature key into **every** locale JSON file, using the Soluling translation for that locale. If the .ntp has `de-CH` and `fr` but the app also ships `en`, copy the values for the locales that exist in both, and skip the locales the .ntp does not provide.3115. **For each German string with no Soluling match:** do **not** write any locale entry for that key — not even an English placeholder, not even the German source. Leaving the key absent across all locale files is intentional: the next Transifex sync flags it as a new missing string so the human translator authors it once. Track these in the post-generate summary so the developer knows what to expect in Transifex.3126. **Keys in locale JSON files are flat** — `{ "hierarchy.title": "…" }`, not `{ "hierarchy": { "title": "…" } }`. Match the project's existing casing (snake vs kebab, feature-prefixed or not) and reuse shared namespaces like `common.save` / `common.cancel` rather than minting feature-local duplicates.313314Do not hardcode any user-visible string in templates or TypeScript — `TranslatePipe` for templates, `TranslateService.instant(...)` for snackbars / dynamic labels. The component templates still reference keys (`'hierarchy.title' | translate`) whether or not the locale file has a value yet — `ngx-translate` falls back to the key itself when a value is missing, which is exactly the signal the human translator needs.315316### Step 2.6: Generate mock API, fixtures, and rule enforcement317318The MSW handler is not a stub — **it is the specification the Java backend team reads.** The OpenAPI spec and the Java service are hand-written from the mock and its fixtures weeks later, by someone who did not write the mock. Anything absent from the handler is absent from the backend.319320If the generated feature has service-backed data, search the workspace for an existing MSW setup (`mock-api`, `msw/handlers`, `setupWorker`).321322- **If MSW exists**, mirror the existing handler pattern: handler file per feature, in-memory store seeded with realistic fixtures, registered in the existing index file. Realistic data: 10+ tree nodes, 4+ list items, real-looking domain names.323- **If MSW is not present**, generate a builder in the workspace's shared test-data lib (`libs/shared/test-data/` or similar — discover the location), exported and shared by the service mock and every spec. No inline duplication.324325Then apply the four rule-carrying requirements below. First find the workspace's best rule-carrying handlers and mirror them: grep the handlers directory for conflict responses (`grep -rln "status: 409\|code:" <handlers-dir>`) and pick the ones that enforce a domain rule and return a coded body — not the plain-CRUD ones. Rule kinds to look for in an exemplar: a temporal "at most one per period" rule **with its cascade**, system-row immutability with `409 {code:'IN_USE'}`, lock and child-reference conflicts (see the reference doc for the exemplars found at the time of writing). Full annotation format and rationale: [references/business-logic-extraction.md](references/business-logic-extraction.md).3263271. **File-level JSDoc.** Every handler opens with which Delphi form and which DB table it stands in for — it is the first thing the backend team reads.3283292. **One `@backend-rule` block per rule from the conversion plan**, immediately above the code that implements it (or above the store/type it concerns, when `documented-only`). Use all rule-card fields from the reference doc's format (`@backend-rule <id> <title>`, `@statement`, `@scope`, `@kind`, `@layer`, `@delphi`, `@p2-behaviour`, `@error`, `@i18n`, `@mock`, `@openapi`, `@test`, `@confidence`, and `@question` on inferred/assumed cards). Rules the mock cannot execute still get a block, with `@mock documented-only: <reason>` or `@mock model-blocked: <reason + blocking id>`. **Never omit a rule because the mock cannot enforce it** — an unwritten rule and a nonexistent rule are indistinguishable in a handler file, and that indistinguishability is the entire failure mode this step exists to prevent.3303313. **Implement every rule marked `@mock implemented`**, returning the typed `RuleViolation` conflict body defined in the reference doc (`{ code, ruleId, field?, message }` — `code` is a stable enum like `'DUPLICATE_VALID_FROM'`, `ruleId` joins handler ↔ spec ↔ i18n ↔ the eventual Java guard, `message` is English for logs and never rendered). The Angular store must branch on `code`, never on the bare status, and the resulting error path must be wired into the UI now: the app needs it anyway the moment the real backend enforces the rule, and inferring meaning from a bare 409 is a live bug (an over-length title rejected by the DB rendered to the user as "title already in use").3323334. **Generate `<feature>.handlers.spec.ts` with one test slot per rule id.** `@mock implemented` rules get a real `it('<ID> <statement>', ...)` derived from `@test` — plain vitest, no custom tooling. `documented-only` and `model-blocked` rules get `it.todo('<ID> <statement>')` so the rule occupies a named, visible slot in every test run. Keep the handler and the spec in step when adding or removing a rule — PR review is the gate, deliberately: the handler plus its spec IS the handoff artifact, and a bespoke sync script was tried and retired because the ceremony cost more than the drift it prevented. Do not generate a separate rules document — everything the backend team needs lives in the handler they already read.334335### Step 3: Add route336337For routed features only, add a lazy-loaded route to the workspace's main routes file (typically `app.routes.ts` — find it by inspecting an existing routed feature):338339```typescript340{341 path: '<feature-path>',342 loadComponent: () => import('./<feature-name>/<feature-name>.component').then((c) => c.<ComponentName>),343},344```345346### Step 4: Generate e2e + a11y tests347348For every routed feature, generate a paired e2e spec at the workspace's e2e app (typically `apps/<app>-e2e/src/<feature>.spec.ts` — find it by inspecting an existing e2e spec). Dialog-only child components do not need their own e2e spec unless the approved plan explicitly asks for one. At minimum:349350- route loads and the host testid is visible351- every top-level testid renders352- no console errors on initial load353- if the project uses cookie-based mock auth in e2e, set the cookie the e2e harness expects — inspect existing e2e specs in the workspace for the cookie name, value, and domain.354355Selectors must be locale-resilient: `getByTestId(...)` first; never query by translation key, translated label, or aria-label that comes from a translation.356357For every component spec, include the axe a11y assertion (see the spec template in angular-conventions.md). Project policy is zero a11y violations.358359### Step 5: Post-generate quality passes360361Run two lint passes over the generated code before reporting done. Both are documented in [references/angular-conventions.md](references/angular-conventions.md).362363- **Signal-store / reactivity lint pass.** Catch mirror-signal+effect copies, dep-less `computed()`, missing `untracked()` around state writes inside `effect()`, granular dirty flags no one reads, and stores exposing raw objects when only a label is consumed.364- **Convention check pass.** Visibility modifiers on every member (including `input()` / `output()`), named constants over magic literals, no dead injections, comments explain WHY not WHAT, smart-vs-dumb component check (no child both `inject()`s a store and exposes an `input()` for the same data), no `(dblclick)` bindings (every Delphi double-click action re-homed to an explicit affordance), form validators gated on the `submitted` signal, mandatory fields carry `[required]`.365366### Step 6: Validate367368Read `package.json` for the project's validation script and run it. Common names:369370- `bun run check:strict`371- `bun run check`372- `npm run validate`373374Fix any failures (format, lint, test, build, e2e) before presenting the result. If no validation script is defined, fall back to `bun run lint && bun run test`.375376### Step 7: Present result377378List all generated files with a one-line description of each. Highlight:379380- Decisions made during generation (translation choices, optimistic UI scope, locking applied).381- TODO items that need backend work (HTTP endpoints, real auth).382- **PDX library gaps:** every workaround applied around a `pp-*` component's behavior (CSS fighting internals, DOM queries into component markup, re-implemented inputs/outputs, sizing/focus patches) — name the component, the missing behavior, the workaround, and whether the behavior should be implemented in the library instead, so the user can decide to file a PDX improvement. See pdx-recipes.md § Report PDX library gaps.383- **Soluling outcome:** translation keys populated from `PolylangSoluling.ntp` (by locale) vs. keys intentionally left absent across all locale files so the human translator authors them in Transifex. List each absent key with its German source so the developer can sanity-check before the next Transifex push.384- **Business rules carried to the backend:** count of rules enforced in the mock, count carried as `documented-only` / `model-blocked` (with reasons), and every rule whose `@confidence` is `inferred` or `assumed` — list these as explicit questions for the user, since a wrong rule in an executable mock propagates faster than a wrong comment.385386---387388## Supporting Files389390### Reference Files391392- **[references/component-mapping.md](references/component-mapping.md)** — Delphi VCL → Angular component mapping tables and German-English domain glossary. Load during analyze phase.393- **[references/delphi-patterns.md](references/delphi-patterns.md)** — P2 Delphi codebase structure, file naming, DFM/PAS anatomy, `inherited` keyword, base-class reading, ShowModal tracing, runtime-generated UI (FeldItems / license gating). Load during analyze phase.394- **[references/business-logic-extraction.md](references/business-logic-extraction.md)** — where P2 hides business logic (18 location classes), the seven-sweep extraction pass, tooling caveats (encoding, DFM line continuation), the frontend/backend classification tree, the rule-card format, and the MSW `@backend-rule` annotation + error contract. Load during analyze Step 4.5 and generate Step 2.6.395- **[references/angular-conventions.md](references/angular-conventions.md)** — Angular patterns (component, store, signal forms incl. validation-after-first-submit and required-asterisk rules, i18n, testing, a11y, smart-vs-dumb components, optimistic UI, locking, mat-tree, post-generate lint passes). Load during generate phase.396- **[references/pdx-recipes.md](references/pdx-recipes.md)** — PDX component recipes, icon naming, layout pitfalls, width-control rule, two-column layout, right-rail aside, row-actions (double-click replacement), snackbar. Load during generate phase.397398### Example Files399400- **[examples/sample-conversion.md](examples/sample-conversion.md)** — Complete worked example converting fChooseMonthRange to choose-month-range.