---
name: iac-authoring
description: Load this FIRST, before your first reply, whenever someone wants something built, changed or reviewed in Azure - Terraform or Bicep - even when they name only the workload ("eine Chatapp in Azure mit RAG", chatbot, web app, API, data platform, landing zone) or a resource, or ask for Azure Infrastruktur, Terraform-Code, Bicep-Code, an infra-spec.yaml, or why a default was chosen. It runs the interview every Azure resource starts with; never answer such a request from memory.
---

# Authoring infrastructure from a question set

Each resource family under `resources/` holds one directory per resource, and its
`questions.yaml` carries the decisions that resource forces, grouped by concern. This file is the
protocol for turning a question set into a spec, and the spec into code. Follow it rather
than improvising — the value of the plugin is that two runs of the same skill ask the same
things and record them the same way.

This skill is the **entry point**: its description carries what people type when they
want Azure infrastructure — the targets, the workload words ("chat app with RAG" / "Chatapp mit RAG", web app,
API, landing zone) and a few resource names — because a request names what the system
does far more often than the resources it needs ("a chat app in Azure with RAG" / "eine Chatapp in Azure mit RAG"
must match, and a resource list alone does not). Behind it, three units kept apart on purpose: the
[azure-resources](../azure-resources/SKILL.md) skill carries the **resource terms** and
routes to the family, the family is the **routing** unit, the question set the
**knowledge** unit. They are separate because the skill listing has a shared character
budget, and a description that overflows it is dropped silently — the skill stays
installed and stops matching. A skill per resource would spend the budget at once; a skill
per family spent it by the sixth family; and the resource list lives in azure-resources so
that it can grow without crowding out this description.

## The pipeline

```
question set  ──interview──>  infra-spec.yaml  ──draw──>  infra-diagram.drawio.png  ──sign-off──>  render  ──validate──>  explained plan
                 [here]                          [iac-diagram]        (loop with the customer)    [iac-rendering]
```

The picture comes before the code, always: the customer signs off on the diagram, not on
the spec, and the render starts only once they have. This holds for every change of the
spec, including one made by editing it directly: redraw, then render.

The spec in the middle is the load-bearing part. It is the handover interface, not a
by-product: a backend renders *from* it, and the same spec gives Terraform and Bicep the
same decisions. Never generate code directly from the interview.

## Every run, whatever else it skips

The rules a run drops most easily, because each is one sentence in a long protocol. They hold
in every run, in this order; the sections below carry the detail.

1. **A request for a system starts the interview, not a build.** "A chat app with RAG", "an
   API with a database" asks for the infrastructure that system needs: this plugin writes no
   application code - no app, server, ingestion script or package - and no `.tf` or `.bicep`
   before the sign-off. In a run where nobody can answer, the first message is the
   interview's first questions, nothing built.
2. **An existing spec: the version gate first** - a spec from a newer plugin stops the run
   before anything is asked or written.
3. **An existing spec: the repository's `CLAUDE.md` section is brought up to date before the
   first question**, with the script or, without a shell, by hand. It is not skipped because
   the request is only a small change. A run that writes the first spec writes no
   `CLAUDE.md`: the render does, after the sign-off. Either way the section goes into the
   working directory the run was started in, never into a repository root found above it.
   **Without a shell, by hand** - no shell is never a reason to skip it:
   1. Read `<plugin>/core/iac-rendering/references/claude-md-block.md`; its text is the section.
   2. In `CLAUDE.md`, replace everything from the line starting
      `<!-- infrastructure-as-skills: begin` through the line
      `<!-- infrastructure-as-skills: end -->` with that text. Without such a section, append it.
      A section whose `rev=` is higher than the block's is left as it is.
   3. Leave every line outside the two markers exactly as it was.
4. **Every spec this run writes or changes carries `spec.plugin_version`** - the installed
   version, under the `spec:` key at the top, never under `deployment:`.
5. **No value nobody gave.** Region, subscription, resource group, address range: `open:`
   with a placeholder, never a plausible value. A default is not a guess: the stage stack
   `[development, test, production]` stays unless the customer named the stages.
6. **Spec, diagram, sign-off, then code.** No `.tf` or `.bicep` before the customer's yes,
   also when the request already said "then write the code".
7. **The turn ends on a question** - the draft question on the rough picture, the code
   question on the polished one - also when the diagram could not be drawn.

**Where `<plugin>` is.** Every `<plugin>/...` path in these skills is relative to the plugin's
root: two levels above this skill's base directory, which is shown when the skill loads
(`.../core/iac-authoring`). Read its files - `.claude-plugin/plugin.json`, the references, the
question sets - by that absolute path. Never search for them with Glob from the working
directory: the plugin is outside it, and a search there may be refused. A file that cannot be
read is no reason to skip the step that needs it; say which step and why.

### Without a shell

The scripts - `write_spec.py`, `check_spec.py`, `export_decisions.py`, the diagram scripts,
`sync_claude_md.py` - need Python and a shell. A session without one (a sandbox, a host that
grants no Bash) loses the tools, not the rules:

- The version gate and the `CLAUDE.md` section are done by hand with Read and Edit, as their
  sections say; the spec is written in its canonical form by hand and called unvalidated.
- The diagram cannot be drawn. Say so in one line, in `context.language`, with what would
  draw it, and close with the draft question on the decisions instead of the picture
  (the Phase 4 list, `infra-decisions.md` where it exists). No code before that yes either.
- A subagent has the tools of the session that starts it: never hand it a script in the
  hope that it has a shell.

## Before anything — find an existing spec

Search the repository for `**/infra-spec.yaml` first — the customer's repository, never
the plugin's own directory: nothing under it, its tests and fixtures included, is a
starting spec, a template or something to copy from, whatever its name resembles. If one
exists, this run **extends** it rather than starting over: its decisions are carried, Phase 0 asks what should be added
or changed, and only the delta is asked and reported. If none exists, the spec this run
writes goes into the repository's IaC root, or `infra/` when there is none, and is
committed with the code — it is what the next run extends. The rules for both, and for
several roots, are in [references/spec.md](references/spec.md#where-it-lives).

**Version gate, first.** Where a spec was found, run `python <plugin>/core/iac-authoring/scripts/check_plugin_version.py <spec>`
(without a shell: read `version` in `<plugin>/.claude-plugin/plugin.json` and `spec.plugin_version`
and compare them as numbers yourself; a spec without the field is not held back).
Exit 3 - or a spec version higher than the installed one - means the spec was written by a newer plugin than the installed one: **stop** - no sync, no
question of the interview, nothing written or rendered. Ask once with AskUserQuestion, in
`context.language`: the spec's and the installed version, and "Plugin jetzt aktualisieren?" / "Update the plugin now?" with
two options (*Ja, jetzt aktualisieren* / *Yes, update now*; *Nein, ich aktualisiere selbst* /
*No, I will update it myself*). The yes runs
`claude plugin update infrastructure-as-skills-azure@infrastructure-as-skills-azure` (`claude plugin list`
shows the id if that one is unknown; where `claude` is not on `PATH`, name the command instead of
running it), then says that Claude Code must be restarted for the update to load and the request
repeated - and stops. *Nein, ich aktualisiere selbst* names the command and stops. There is no
third option that carries on with the older plugin, and `plugin_version` is never lowered. Whatever
this run writes into the spec sets `spec.plugin_version` to the installed version
(`.claude-plugin/plugin.json`).

Then bring the repository's `CLAUDE.md` section up to date, before
anything else is asked: `python <plugin>/core/iac-rendering/scripts/sync_claude_md.py
<repo-root> --root <iac-root> --write`. It replaces only an older plugin section, leaves a
newer one alone and never fails the run (without a shell, apply the same rules by hand with
Read and Edit, from [claude-md-block.md](../iac-rendering/references/claude-md-block.md)); say in one line what it did. A section **newer**
than this plugin means the plugin is out of date: tell the customer so in the first message,
in `context.language` ("Das Plugin ist älter als der letzte Lauf in diesem Repo - bitte
aktualisieren" / "The plugin is older than the last run in this repo - please update"), and
carry on. It changes no code, so it is its own commit.

There is no platform profile file. The platform is asked in the interview, in plain words:
`landing_zone_exists`, and without a landing zone `central_logging`, `central_private_dns`
and `central_network`. The names behind a "yes" (hub, subnet, workspace, zone resource
group) are never asked: they are stage-file parameters or `open:` entries owned by the
platform team.

## Who is in the interview — the customer, always

The person answering is **the customer**: someone who can say what the system is for,
who uses it and how bad an outage or a loss would be, and who did not come to answer
questions about subnets. There is one person in the interview and no second mode. A question's `resolved_by`
([question-set-format](references/question-set-format.md#who-resolves-it-resolved_by))
decides what happens to it:

| `resolved_by` | What happens |
|---|---|
| `interview` | Put to the person: the module choice and the naming mechanism (asked first), the deployment set's context, posture, mandates and `requirements` group, and any resource question that carries it |
| `derived` | Never asked: follows from the interview's answers (`landing_zone_exists`, `central_*`, `deployment_scope`, `deploy_mechanism`, `network_requirements`, …), else its default; a `required` one is an `open:` entry owned by the platform team |
| *(absent)* | Its default, or derived from a requirement; a `required` one nobody can default is an `open:` entry owned by the workload owner |

Say so in the first message: "Ich frage dich nur, was das System braucht; die
technischen Entscheidungen setze ich nach den Vorgaben und zeige sie dir am Ende." / "I only
ask what the system needs; I set the technical decisions from the baseline and show them to
you at the end." There is
no second set of questions for someone technical. What the person knows beyond them they
say in prose - "ZRS", "subnet snet-pe-01", "TLS 1.2 is enough" ("TLS 1.2 reicht") - and Phase 0 records it as
`stated`, which outranks every default; what they want changed afterwards is an
`overridden` value in the spec, not a second interview.

No question with Azure vocabulary reaches the person, whatever it would cost to default
it. The cost of a wrong default is a line in Phase 4 that the person can object to; the
cost of a subnet question is a person who stops answering.

## Phase 0 — Ask how the code is built, then what is being built

**Everything said to the person is said in their language - from the very first question.**
Take it from the request ("I need a chat app..." is `en`, "Ich brauche..." is `de`) before
anything is asked; it becomes `context.language`. The blocks in this skill that are written
in German - the target, module and naming questions, the description prompt, the design
tick list, the working-mode question, the sign-off, the opening line - come with an English
block beside them. Use the block that matches `context.language`; for any other language
translate the English one, with the same options and the same meaning. Never put the German
text in front of someone who wrote English. Option labels and the `AskUserQuestion` header
follow the same rule.

Four structured questions open the interview, each through `AskUserQuestion`, each
skipped when the repository or the request already answers it; then one open question in
prose. In this order, because each one decides how the next is worded.

**The first question is the target**: Terraform or Bicep. The repository answers it when
it can - `*.tf` or `.terraform.lock.hcl` present → Terraform, `*.bicep` or
`bicepconfig.json` present → Bicep, and a carried `infra-spec.yaml` has it - and then it is
not asked. Both or neither → ask, once, two options and none marked recommended:

```
In welcher Sprache soll die Infrastruktur beschrieben werden?
  1. Terraform - Herstellerunabhängig, eine Sprache für Azure und alles andere; der Stand
     wird in einer State-Datei geführt
  2. Bicep - Microsofts eigene Sprache für Azure, ohne State-Datei; der Stand ist, was in
     Azure liegt
```

```
Which language should describe the infrastructure?
  1. Terraform - Vendor-neutral, one language for Azure and everything else; the state is
     kept in a state file
  2. Bicep - Microsoft's own language for Azure, no state file; the state is whatever is in
     Azure
```

It comes first because the naming question below exists only for Terraform, and because
the layout (Phase 1) and every rendering reference read it.

**The second question is the module choice**, before the description and before any resource
question: `deployment.module_source` — the code built from Azure Verified Modules (`avm`), or
from resources written out one by one (`authored`). It comes first because it decides what
every later decision renders to, and because it is the one choice a person can make without
knowing a resource. Ask it through `AskUserQuestion`, `avm` first and marked recommended;
when the repository already holds authored resource blocks, put `authored` first instead.
Word it without the product name in the label:

```
Wie soll der Code gebaut werden?
  1. Geprüfte Bausteine (empfohlen) - Aus Microsofts getesteten, gepflegten Bausteinen
     (Azure Verified Modules) - in Terraform und Bicep gleich aufgebaut
  2. Einzelne Ressourcen - Aus einzeln ausgeschriebenen Ressourcen - jede Zeile im eigenen
     Review, dafür mehr Code, den ihr selbst pflegt
```

```
How should the code be built?
  1. Verified building blocks (recommended) - From Microsoft's tested, maintained building
     blocks (Azure Verified Modules) - built the same way in Terraform and Bicep
  2. Individual resources - Written out resource by resource - every line in its own
     review, but more code that you maintain yourselves
```

**Skip it when it
is already answered**: the user said it ("without modules" / "ohne Module", "with AVM" / "mit AVM"), or a carried
`infra-spec.yaml` has it. A resource whose type has no Azure Verified Module renders
authored whatever the answer, recorded per module; say so when the person chose `avm`.

**The third question is the deployment scope**: `deployment.deployment_scope` - may the
infrastructure create its own resource groups (subscription level), or does it deploy only
into groups that exist (resource-group level)? Ask it right after the module choice, in the
same call when the target is already known, two options and none marked recommended - it is a
fact about the rights the organisation hands out. It decides the stages' separation
(`stage_isolation` is derived from it, never asked), whether `resource_groups_created` is asked,
the Bicep root's scope and the deployer's roles:

```
Auf welcher Ebene wird ausgerollt?
  1. Ressourcengruppen-Ebene - In Ressourcengruppen, die vorab existieren. Jede Stufe rollt
     mit einer eigenen Identität aus, die nur auf den Ressourcengruppen ihrer Stufe Rechte hat;
     keine Rechte auf der Subscription
  2. Subscription-Ebene - Die Infrastruktur legt ihre Ressourcengruppen selbst an. Jede Stufe
     braucht eine eigene Subscription und eine Identität mit Rechten auf dieser Subscription
```

```
At which level is it deployed?
  1. Resource-group level - Into resource groups that exist beforehand. Each stage deploys
     with an identity of its own whose roles are on that stage's resource groups only; no rights
     on the subscription
  2. Subscription level - The infrastructure creates its resource groups itself. Each stage
     needs a subscription of its own and an identity with rights on that subscription
```

**The fourth question, for a Terraform target only, is the naming
mechanism**: `deployment.naming_source`. Ask it right after the scope through
`AskUserQuestion`, with three options and **none marked recommended**: the
`LemurDaniel/naming` provider, the `Azure/naming/azurerm` module, or a custom mechanism
(the organisation's own locals or module). Word it like this, with the product names in
brackets - the one place a product name is allowed, because the options are products:

```
Wie sollen die Ressourcennamen erzeugt werden?
  1. Naming-Werkzeug des Plugins - Gesteuert über eine Namensdatei (naming.yaml), die ihr
     anpassen könnt (LemurDaniel/naming Provider)
  2. Microsofts Naming-Modul - Fertiges Modul mit CAF-Abkürzungen, keine eigene Namensdatei
     (Azure/naming/azurerm)
  3. Eigener Mechanismus - Der Mechanismus, den eure Organisation schon nutzt (eigene
     locals oder eigenes Modul)
```

```
How should the resource names be generated?
  1. The plugin's naming tool - Driven by a naming file (naming.yaml) that you can adapt
     (LemurDaniel/naming provider)
  2. Microsoft's naming module - Ready-made module with CAF abbreviations, no naming file of
     your own (Azure/naming/azurerm)
  3. Custom mechanism - The mechanism your organisation already uses (your own locals or
     your own module)
```

A Bicep target is not asked: it always assembles names from vars.

**Skip it when it is already answered**: the user said it, a carried spec has it, or the
repository already uses one - a naming provider block, a naming module call or naming
locals answers it. Which mechanism renders what is in
[naming.md](../iac-rendering/references/naming.md#the-mechanism).

Then the interview opens with one open question, not with the question set. Let the user
describe what they are building, and let that description answer as much as it can.
Only what it leaves open is asked afterwards.

The reason is what options cannot carry. What the system is for and who reaches it from
where are told in a sentence and are clumsy as options - and they decide which questions
the batch asks at all. Everything with fixed answers (stages, outage, recovery, residency,
mandates, cost) is the batch's, with its options; the opening question never announces a
default for one of them. What the description happens to settle there is `stated` and
drops out of the batch.

**Skip it when the request already is that description.** "Storage for the accounting
department's invoice archive, prod in West Europe, company network only, BSI C5" (German:
"Storage für die Rechnungsablage der Buchhaltung, Prod in West Europe, nur aus dem
Firmennetz, BSI C5") needs no opening question. A bare resource name — "I need a storage
account for terraform" (German: "ich brauch ein storage account für terraform") — is not a
description; it names the resource and nothing about the system around it.

Ask in plain text, not through `AskUserQuestion`: the point is free prose, and options
would turn it back into the structured interview it precedes. Two points and nothing
else - the rest comes as questions with options:

```
Beschreib kurz, was ihr braucht:
  - Wofür ist es da?
  - Wer arbeitet damit, und von wo (nur Mitarbeiter im Firmennetz, Partner, das Internet)?
Den Rest frage ich gleich mit Antwortmöglichkeiten ab.
```

```
Briefly describe what you need:
  - What is it for?
  - Who works with it, and from where (only staff on the company network, partners, the internet)?
I will ask the rest next, with options to choose from.
```

A point the request already answers is left out; a request that answers both skips the
question. The list is not extended for the request. A question about the system's parts - "is it only
the App Service, or also a language model, a database?" ("nur der App Service, oder auch ein
Sprachmodell, eine Datenbank?") - is not a requirement: what the request names is the design,
and what is missing is the customer's to add at Phase 4's list, which is where they see the
parts. A request that names its parts ("1 App Service") is not asked about others.

Then map the answer onto the deployment set and every routed question set. Everything it
settles is recorded with `source: stated` and is not asked again. Three rules for the
mapping:

- **A value is stated only if the user said it.** "Internal only" ("Nur intern") settles
  `network_exposure: private`; it does not settle an address range. An inferred value that
  the text does not carry belongs to the normal resolution order, not to `stated`.
- **A contradiction is asked, not resolved.** "Publicly reachable" ("Öffentlich erreichbar") next to
  "company network only" ("nur aus dem Firmennetz") is one question in Phase 3, quoting both halves.
- **Answers that cannot both hold are asked, once, after the batch.** The pairs that are
  known: `data_loss_tolerance: reproducible` with `backup_isolation: isolated` or
  `immutable` (data that can be regenerated needs no protected copy, or the data is not
  reproducible after all); `network_exposure: public` with `network_requirements: corporate`
  and nothing said about an exception; `outage_impact: tolerable` with `priority: protection`;
  `central_private_dns: yes` with `central_network: no` (zones in the platform resolve names only
  for networks linked to them, and a standalone network has no hub to be linked through: either
  the platform team links it, or one of the two answers is wrong); `network_exposure: private`
  with `network_requirements: free` when people use the system (the request names users, or an
  actor reaches it in `wiring.access`): a private system in a network with no connection to the
  company network is reachable by nobody - either the network is connected
  (`network_requirements: corporate`), or the part people open is public behind the company
  sign-in and the rest stays private (`network_exposure: private_with_exceptions`). A system
  with no human user (a batch job, an API only other Azure services call) is not affected.
  Quote both halves in one question with the two ways out; never pick one silently.
- **The description may add resources.** "…with logs to Log Analytics" ("…mit Logs nach Log Analytics") routes a second
  question set through [azure-resources](../azure-resources/SKILL.md) — the same request,
  one interview, one spec.
- **A default is not an assumption.** "Assume nothing that isn't stated" ("Nimm nichts an, was nicht dasteht") removes guesses,
  never defaults: the stack `[development, test, production]` stays, with a named stage
  joining it rather than replacing it, and so does every other default in the sets. What
  the instruction forbids is inventing a value no default exists for — a range, a
  subscription, a name.
- **A referenced platform part answers its central question.** A named hub network or
  shared private-endpoint subnet answers `central_network` as `yes`, a named central
  workspace `central_logging` as `yes`, a named zone resource group `central_private_dns` as
  `yes`, a named backup vault `central_backup` as `yes` - recorded `stated`, never an open item, because the reference is the answer. It does
  not answer `landing_zone_exists`: only the customer saying there is a landing zone does,
  and without one `landing_zone` stays `standalone` while each central answer sets its own
  ownership decision. An explicit answer beats an archetype's pre-answer.
- **A stated value holds in every stage, and posture is one per system.** "Publicly
  reachable is fine" ("Öffentlich erreichbar ist okay") said about the development stage is said about the system: the
  exposure is `public` in every stage, the network `none` in every stage. A run never
  splits a posture decision (exposure, local auth, keys, DNS, landing zone) by stage; only
  the customer's explicit word for a named stage does, recorded as `overridden` there.
- **An empty list is not always an answer.** Storage role assignments with shared keys
  off: an empty default renders an account nobody can use, so it is an `open:` entry with
  its owner rather than a decided value - read the question's `why` before recording `[]`.
- **Context values are bare.** `context:` holds values (`environments: [...]`,
  `workload: payments`, a per-stage map), not `value:`/`source:` records; the provenance
  of a context value is the deployment set's, under `deployment:`.

What remains open goes into Phase 3's batches. The description does not replace them; it
shrinks them by what it happened to settle.

## Phase 1 — Establish the layout

The target is known from Phase 0. The same reading of the repository that answered it
settles how the code is split, which the spec records under `composition.granularity`:

- An existing `modules/` directory → match what is there, per module and per file
- **Stages are part of the layout.** `context.environments` — `[development, test,
  production]` unless the user said otherwise — renders as one root and one parameter
  file per stage (`environments/<stage>.tfvars`, `main.<stage>.bicepparam`) with one
  state per stage (a backend key, never a workspace). A value marked `per_environment`
  in its question is a parameter whose value lives in the stage's file, never a literal
  in the template; a stage whose values nobody could give gets its file with placeholders
  (the all-zero GUID, `OPEN-<what>`) and an `open:` entry naming it; the pipeline refuses to
  deploy a file that still holds one. Only what `non_production_savings` relaxes differs between the
  stages; everything else is identical, which is what the stack is for. A value the
  customer stated holds in every stage unless they said which stage; the savings relax
  defaults and derived values only.
- An existing `infra-spec.yaml` → its `composition` is kept as it is; do not re-decide it
- A flat repository, or a single resource → `single`
- AVM with resources of more than one family → `per-family`
- Otherwise → `per-concern`

Deciding this before the interview matters because it is the one thing both backends must
agree on. If the split is settled per renderer, the same spec produces one Terraform root
and six Bicep files, and comparing the two outputs stops meaning anything. See
[references/spec.md](references/spec.md#modularity).

`deployment.module_source` is not read from the repository: it is asked first, in Phase 0,
and the repository only decides which answer is recommended (authored resource blocks
already there → `authored`). Under `avm` the split above is mostly decided too: one module
call per resource, the root holding the wiring, so `single` unless a pattern says
otherwise.

## Phase 2 — Resolve the deployment set, then every resource question

[`questions/deployment.yaml`](questions/deployment.yaml) is resolved **before** any
resource question set. It holds what is one decision per system rather than one per
resource, in two halves:

- **The context** — environment, region, workload, subscription, resource group. Only
  the environment is asked here; how the stages are separated (`stage_isolation`) follows
  from the deployment scope of Phase 0. Region, workload, subscription and resource group are
  `parameterized`: never asked, rendered as input variables, supplied at deploy time —
  a user who is only designing has no deployment target yet. Use them from the
  description if stated (`source: stated`; the stage file carries it, the template never
  does). **Not stated is `open:`, never a plausible value**: region, subscription id and
  the name of an existing resource group the description leaves out are each an `open:`
  entry (workload owner, `blocks: apply`), and the stage file holds the placeholder
  (`OPEN-location`, the all-zero GUID, `OPEN-resource-group`). A `westeurope` the run
  picked reads as a decision a year later. A group the infrastructure creates needs no
  name - the render computes it. The workload short name is taken from the description
  or the system's name and is never open.
  The environment is `required`, because nothing can be guessed and everything downstream reads them. A
  resource question with `answer: { context: <id> }` points here; without this group that field
  pointed at nothing, and a run could reach the renderer with no region and therefore no
  name.
- **The deployment decisions** — network exposure, local auth, who owns the virtual
  network and the workspace, mandates and what they force, DNS ownership, retention and
  the ingestion cap.

Resolve it once and every resource inherits it. A resource question carrying
`answer: { deployment: <id> }` is answered from there and never asked again — eight resources in
one run must not produce eight separate exposure answers, and a system is not private
because seven of them are.

**The compute service follows the application's technology, never the other way round.** A
design with an application of the workload's own (a chat app, a web front end, an API) asks
`app_runtime` - and, for a container, `app_containers` - and only then routes to a compute set:
code runs on App Service, the event-driven parts in the same language on a Function App, one
container on App Service for containers, several containers on Container Apps. Container Apps
is never chosen without `app_containers: several` or the customer naming it. A design whose
compute the request names ("auf Container Apps") records that as `stated` and still asks the
technology where the set needs it.

**Read every question set before Phase 3 asks anything.** That is the set of each resource
the request names **plus** every resource a deployment decision forces into the design: a
private network exposure brings the virtual network and its private-endpoint subnet,
`observability_ownership: create` brings a `log-analytics-workspace` resource whose set is
resolved and written like any other - its access and authentication decisions live there, not
in the deployment set - `application_monitoring:
enabled` with an application (App Service, Function App, Container Apps) brings one
`application-insights` component that every application's `application_insights` names - its
connection and authentication settings and its `Monitoring Metrics Publisher` role are rendered
with the application, they are infrastructure, not application code - a registry or
identity requirement brings its own resource. Work out the full resource list from the
deployment set first, then read all of the sets in one pass. A set read after the batch, or
after Phase 5 has begun, means its interview questions could not be asked and its defaults
are guesses.

**Read them through the digest**: `python3 <plugin>/core/iac-authoring/scripts/interview_digest.py
<family>/<resource>... --language <context.language>` prints the deployment set and every set
named - each interview question with its values and `forced_when`, every question that is
forced, derived or required, and each set's boundaries - at a fifth of the files'
size; `--why <id>` adds the reasoning where a default is justified. Never extract the
questions with a script of the run's own: one that keeps `ask` and `accepts` drops
`forced_when`, and a question a rule settles is asked anyway.

For each question, the answer then comes from the first source that has one:

| # | Source | `source` recorded as |
|---|---|---|
| 1 | What the user said — the request and the Phase 0 description | `stated`, or `overridden` where it changes a carried value |
| 2 | The existing `infra-spec.yaml` | The source it already has; `carried` only for what the repository holds without one (a pipeline, a `modules/` layout) |
| 3 | A pattern skill's pre-answers | `pattern` |
| 4 | The deployment set's answer: copied for an `answer: { deployment }` question, mapped through the question's table for an `answer: { question, map }` one | `deployment`, or `{ derived_from: deployment.<id> }` |
| 5 | The question's `default` | `default` |

A carried value outranks a pattern and a default on purpose: it was decided, and quite
possibly deployed. A re-run that let a pattern or a changed guardrail flip it would turn
"add a key vault" into a silent change to the storage account. Only the user changes a
carried value. Consequences are the exception — `deployment` and `forced` values are
re-derived from what they follow, not carried.

A question with `required: true` has no default and no safe guess. It is the only kind you
must ask. A question with `resolved_by: interview`
([question-set-format](references/question-set-format.md#who-resolves-it-resolved_by)) is
put to the person; one with `resolved_by: derived`, or none, is never put to a customer —
it resolves from the profile or the default, and a
`required` one nobody present can answer becomes an `open:` entry naming its owner.

**A forced value is pre-answered, not asked.** `key_protection: hsm` requires a Premium Key
Vault; `network_exposure: private` requires the private-endpoint subnet. Apply the
consequence, record it with the question that forced it, and say so in Phase 4 rather than
putting a question whose answer is already determined to the user. `public_parts` is such a
value when the design has one application people open: it is that application (its slots
are not parts of their own), and Phase 4's line is where the customer corrects it - asking "probably the app?"
is asking an answer back.
**`deployment_scope: resource_group` keeps every resource group `existing`**, and
`resource_groups_created` is then not asked: creating a group needs a role on the
subscription, and a deploying identity with one reaches every stage. The consequence is in the
option the person chose (Phase 0) and is said again in Phase 4 and the hand-over. With
`deploy_mechanism: local` one person's account deploys every stage, so the separation does not
hold against a wrong stage file; Phase 4 says so. `check_spec.py` fails on a `create` group or a
`subscription`-scoped deployer role under it.
`deployment.deployer_roles` is such a value, written last, once every resource is resolved:
the roles the deploying identity needs beyond `Contributor`, derived from `create` groups,
role assignments and secret placeholders (`forced_when` in the deployment set).

Defaults are not invented per resource — they come from
[references/guardrails.md](references/guardrails.md), which is also where their reasoning
lives. Read it before choosing any default or justifying one.

## Phase 3 — Ask, in one batch

Only what Phase 0's description left open reaches this phase, and only the questions the
mode allows. A question the user already answered in prose and is asked again tells them
the description was not read.

**Never asked in any mode, whatever the description leaves open: the workload short name,
the subscription id, the resource group and the region.** They are `parameterized` context
in the deployment set - input variables the stage file supplies, not decisions. A question
about them, "there is no default for these" included, is a bug in the interview: the person
is designing and has no deployment target yet; what the description leaves out of them is an
`open:` entry with a placeholder (Phase 2), reported in Phase 4 like any other open item. The group's *name* is that kind of value; the
`resource_groups` group of the deployment set is not. Two questions decide the split:
`resource_group_layout` - one group per stage, or spread over several - and, only on `split`,
`resource_group_scheme` - by responsibility (`concern`), by lifecycle (`lifecycle`), by protection
need (`protection`: data and secrets in a fenced group, the rest beside it) or by part of the
system (`component`). Under `component` the run names the parts from the description and
`meta.purpose` (short lowercase names: `chat`, `ingest`), puts what every part uses - network,
monitoring, identities - into `shared`, and places every other resource in its part
(`placement.resource_group`, `source: { forced_by: meta.purpose }`); the person renames, merges
or moves at the diagram sign-off. Which groups follow is **not asked and not written**: every
role of the scheme that a resource of the design is in - its placement, else its kind's role in
`resource-group-roles.yaml` - gets a group; the scripts derive it.
The person sees the result as frames in the diagram and moves a resource or drops a group
at the sign-off - a `placement.resource_group` or a changed split, `source: { overridden: <reason> }`. Then `resource_groups_created` (one list question, and the only record of it: a group on the
list is created, every other one exists; no mode per group is written), with every group
existing as the default. It reads the derived split, so it is asked in a call after the scheme, multi-select,
with **one option per group** - each role of the split, named by the scheme's words (a split has
no main group; a single layout asks for its one group),
never folded into one option ("the areas you split off"); five groups go into two questions of
the same call (the first four, the rest). Under `deployment_scope:
resource_group` it is not asked at all. Nor is the customer's language:
it is the language the request and the answers are written in, and Phase 0 writes it to
`context.language` (`en` or `de`) - the picture, the decision document and the
customer-facing text in the spec follow it.

**There is one batch**: the surviving interview questions — the
environment (four choices: the full stack of development, test and production; development and
production; a sandbox; other, where the customer names the stages - a sandbox answers the landing
zone and removes its questions), who reaches it and from where, the mandates, and the `requirements` group
(network requirements - may the network be chosen freely, or are ranges assigned or a
connection to the company network given -, outage impact, expected load, data-loss tolerance, access pattern, backup isolation, data
residency, priority - cost, balanced or protection first, which moves cost and hardening
knobs and never the security floor), fewer only where the description stated an answer. **The `requirements` group is asked in
full, every run**: a requirement that has a default is still asked, because the default
is the guardrail's guess and the customer is the only one who knows - skipping one because
the batch looks long is the failure this group exists to prevent. There is no cap on the
number of customer questions: the customer answers multiple choice, and a tenth option
row costs them less than a decision nobody asked them about. **The batch is
asked in topics, not as one list**: every interview question carries a `topic` (`platform`,
`network`, `security`, `resilience`, `data`, `application` - the headings and their order are in
[question-set-format.md](references/question-set-format.md#the-interviews-topics-topic)),
the questions of a topic stand together, the topics in that order, and a topic with no
surviving question is left out. The `AskUserQuestion` call is cut at the topic, not at a
count: it takes four questions per call, so a topic of up to four is one call whose `header`
is the topic's short name (*Rahmen*, *Netzwerk*, *Sicherheit*, *Ausfall*, *Daten*,
*Anwendung* / *Setup*, *Network*, *Security*, *Outage*, *Data*, *Application*; the header
chip holds twelve characters). A topic of more is several calls in a row, and then **every
header is numbered** - *Rahmen 1/3*, *Rahmen 2/3*, *Rahmen 3/3* - so the same word never
appears twice in a row without telling the person how far along they are; a topic of one call
carries no number; small neighbouring topics
share a call only when each of them is a single question. Before each call one line of
plain text names the topic and the person's place ("Netzwerk - Frage 1-2, danach Sicherheit,
Daten"); it is a message of its own, never part of a `question` text, which holds the question
and nothing else. Nothing reasoned or reported stands between the calls - the batch is never
trimmed to four to fit one call. **The options come from the question set, never from the run**: an enum's `accepts`
with their `plain_values`, an `int` or free `list` question's `offer` (`mandates`,
`log_retention_days`) with its labels; "Other" is the tool's and takes the rest. A question with
`accepts: { catalogue: ... }` (the Foundry models) offers only the catalogue entries the digest prints and says
the catalogue's date; a model, a version or a family from memory is never an option, since it is
one the catalogue has already moved past. The models are asked in two steps. First the family, a
multi-select of the three families with the most models, each option saying whether it runs in
the deployment type `data_residency` derives ("in der EU-Datenzone verfügbar" / "available in
the EU data zone", "nur mit weltweiter Verarbeitung" / "only with worldwide processing"); a family not in the catalogue (Gemini) is said to be unavailable in
Azure and the question put again. A chosen family the derived type does not offer is put back
once, before the models: another family, or the residency answer changed to worldwide by the
person - never widened silently. Then one multi-select per chosen family, in one call: its three
newest models in the derived type, with *Noch offen* / *Still open* as the first option, each
chosen model one deployment. The embedding model, where retrieval needs one, is a single choice
of three: an index holds one. A question with neither an `accepts` list or catalogue nor an `offer` is
asked in prose. An enum with more than four values (`app_runtime`) shows its first
four - the values are ordered by how common they are - and the description of the last one names
the rest ("Java, PHP: über „Other“ eintragen"). **"Other" is the tool's and cannot be switched
off**, so its answer is read, never guessed: a text that names a value of the question is that
value (`Java` -> `java`); a text that is a question or an objection is answered and the question
put again; a text naming something the question cannot take (`Pulumi`) is said to be outside the
plugin and the question put again with its options; "Other" with no text is asked once more in
prose, the options as a list. **A multi-select whose empty answer is valid carries it as an explicit
first option** - "Keine" / "None", "Alles in einer Gruppe" / "Everything in one group" - and
never as "choose nothing": the tool cannot submit an empty selection, and the answer comes back
as "Other" with no text. **No option in the batch is marked recommended**: an interview question asks for a fact about the organisation - whether the
system joins the company network, how bad a day's outage is, where the data may live - and
there is no better answer the plugin could know. The default is the guardrail's fallback for
an answer nobody gave, not advice, and marking it steers the person towards the guess the
question exists to replace. **Two exceptions.** `public_audience` marks `waf` recommended - an
entry with a web application firewall in front of a public app is a security recommendation,
not a guess about the organisation - and is worded the same every run, its three options from
the question set; it is asked for every application reachable from the internet. The second is the address range when the network is
free** (`network_requirements: free`): the batch offers four ranges out of a private block
nobody peers with - 10.100.0.0/25, /24, /23, /22 - with the size that fits the described system
marked recommended, because that recommendation is sizing the plugin derives from the
system (the sum of its subnets: endpoints /27, an integrated app /26, a Container Apps
environment), not a guess
about the organisation; the rule and the sizes are on `address_space` in the deployment
set, and each stage takes its own block of the chosen size. **It is the last question of the
interview**, after the application topic and after any contradiction is settled: the size is
the sum of the subnets, and the subnets follow from the compute (`app_runtime`,
`app_containers`) and from the exposure a contradiction may still change. Nothing else is asked. A question without `resolved_by` takes its default; a `derived`
question takes its derived value or its default; a `required` question of either
kind that nothing answers becomes an `open:` entry with its owner named, and Phase 4
says so. Put the batch in the customer's words — the `ask` texts of the `requirements`
group are written for that — and never a decision's vocabulary (`ZRS`, `TLS1_2`, a
subnet) in a question to a customer.

Phase 1 asks nothing: the target was Phase 0's first question and the layout is read
from the repository. A round trip per phase is the serial interrogation this protocol
exists to avoid.

**One exchange, every question in it.** Where the host offers `AskUserQuestion`, use it —
grouped options with the trade-off in the description beat prose. The deployment set is
resolved before the resource sets (Phase 2), so an interview question a resource set
carries is already filtered by the deployment answers when it reaches the batch -
`network_exposure: private` has removed the firewall questions before anyone sees them.

A serial interrogation ("and now, the TLS version?") is the one failure mode that makes
people abandon a tool like this, and it is entirely avoidable: the question set is known up
front, so there is no reason to discover questions one at a time.

If a question without `resolved_by: interview` survives into the batch, something is wrong: it
is asking a thing that should have a default, a platform value or an `open:` entry. Never
ask it; default it and say so in Phase 4, or open it with its owner.

**Never invent a value nobody gave.** Grouped options work for an `enum`, where the
values come from the question set. A reference (`accepts: { ref: ... }`), an address range, a subscription id, a
workspace name — anything only the environment knows — is never offered as an option and
never guessed: it comes from the stage file or from the person's
own prose in Phase 0, and otherwise it is an `open:` entry naming its owner. An
unassigned address range is a blocker to say out loud, not a gap to fill with
`10.0.0.0/16`; a workspace that does not exist yet is a boundary — something this run
does not create — and the honest answer is to record it as missing rather than to
reference a name nobody has.

**Ask in the user's language; keep the artefacts in English.** A question's `ask`,
`plain`, `plain_values` and `offer` carry the customer's wording per language: put the
line in `context.language` as it stands, and translate only where that language is
missing - including the trade-off in the options, which is the part that earns the
question. What stays English regardless: `infra-spec.yaml`, its `why` entries, code
comments, variable names and commit messages. They are the interface other backends,
reviewers and tools read, and a spec that is half German is a spec nobody can diff. Where
the user wants the reasoning in their language too, that is their call to make — ask once
rather than deciding it for them.

## Phase 4 — Show the design, report what was defaulted, then ask once

**First the design, as a list the customer ticks off**. Before
the account of defaults and before any file is written, say in the customer's words what
will be built: one line per part with what it is for, one line per connection, the handful
of decisions that cost money or change what the system can do. Nothing technical, no
attribute names - the customer recognises their system or says what is missing, which is
cheaper here than after the picture and far cheaper than after the code:

```
Das würde ich bauen - bitte abhaken, was stimmt:
  [ ] Eine Chat-Anwendung als Container, aus dem Internet erreichbar, Anmeldung mit Firmenkonten
  [ ] Ein Sprachmodell und ein Embedding-Modell (AI Foundry), nur in der EU
  [ ] Ein Suchindex über eure Dokumente (AI Search)
  [ ] Ein Speicher für die Dokumente und ein Datenbank für die Chatverläufe
  [ ] Ein Tresor für das eine Geheimnis, das die App braucht
  [ ] Ein eigenes Netz, 10.100.0.0/25, in dem alles außer der App nur privat erreichbar ist
  [ ] Logs aller Teile an einem Ort, 30 Tage
  Verbindungen: die App spricht mit Modellen, Suche, Speicher, Datenbank und Tresor; die
  Suche liest den Speicher und die Modelle. Nichts anderes spricht miteinander.
  Was Geld kostet oder euch einschränkt: eine Stufe (Sandbox), keine zweite Region, keine
  getrennte Sicherungskopie - alles so, wie ihr es beantwortet habt.
```

```
This is what I would build - please tick what is right:
  [ ] A chat application as a container, reachable from the internet, sign-in with company accounts
  [ ] A language model and an embedding model (AI Foundry), EU only
  [ ] A search index over your documents (AI Search)
  [ ] A store for the documents and a database for the chat histories
  [ ] A vault for the one secret the app needs
  [ ] A network of its own, 10.100.0.0/25, in which everything except the app is private only
  [ ] Logs of all parts in one place, 30 days
  Connections: the app talks to models, search, storage, database and vault; search reads
  the storage and the models. Nothing else talks to anything else.
  What costs money or limits you: one stage (sandbox), no second region, no separate
  backup copy - all as you answered it.
```

It is a list, not a summary. No preamble that names a phase ("Phase 3 is complete, let me
summarise" / "Phase 3 ist abgeschlossen, ich fasse zusammen"), no paragraph that strings the parts together with commas: the person
reads one line per part and ticks it, that is the whole point of the shape. The words are
the customer's: "logs", not "logging pipeline" ("Logs", not "Protokollierung"); "a store
for the documents", not "storage account with blob container" ("Speicher für die Dokumente",
not "Storage Account mit Blob-Container").

**A deployment slot named as a stage stays a slot.** "One stage and the slot as test" ("eine
Stufe und der Slot als Test") is `environments: [production]` and a slot in
`deployment_slots`, and the list carries one line for it: the slot runs on the production
plan with the production identity against the production data ("Der Test-Slot läuft auf
dem Produktions-Plan und arbeitet mit den Produktionsdaten" / "The test slot runs on the
production plan and works with the production data"). The customer ticks it or asks for a
test stage.

**A system with `network_exposure: private_with_exceptions` shows how each public part was
mapped**, as the lines of the list and nothing else: one line per part named in `public_parts`
with the resource that serves it and who may reach it, and one line that everything else stays
private. The mapping is the engine's reading of the customer's words, so it is the line the
customer is most likely to correct; a part that maps to no single resource is shown as open,
never to a guess. A mapped part is recorded with its `public_parts` entry as the reason.

```
  [ ] Die Webseite ist für jeden im Internet erreichbar, mit einem Schutzschild davor (Front Door)
  [ ] Die Partner-Schnittstelle ist nur für eure Partner erreichbar, über eine Liste ihrer Adressen
  [ ] Alles andere - Datenbanken, Nachrichten, Speicher, Tresor - ist nur privat erreichbar
```

```
  [ ] The website is reachable by anyone on the internet, with a protective layer in front (Front Door)
  [ ] The partner interface is reachable only by your partners, through a list of their addresses
  [ ] Everything else - databases, messages, storage, vault - is reachable privately only
```

Before the question comes the **security baseline**, as a block of its own and grouped by
what it protects, never as the one line it used to be: *Zugang* (keys and shared secrets
off, company accounts only, no public access), *Verschlüsselung* (TLS floor, encryption at
rest, infrastructure encryption), *Schutz vor Löschen* (soft delete, purge protection,
locks), *Identitäten* (which part has which identity, and that the rest has none) and
*Protokolle* (diagnostics, retention) / *Access*, *Encryption*, *Protection against deletion*,
*Identities*, *Logs*. Every row is a decision of the spec - `source: default` or `{ forced_by: ... }` in a
`security` or `identity` group - in the customer's words, and the block comes from the spec,
not from memory. Nothing in it is asked; it is shown so that nobody finds it in the code.

```
Sicherheit - nach den Vorgaben gesetzt:
  Zugang          Anmeldung nur mit Firmenkonten, keine Schlüssel, kein öffentlicher Zugang
  Verschlüsselung mindestens TLS 1.2, Daten im Ruhezustand verschlüsselt, doppelt beim Speicher
  Löschschutz     gelöschte Daten 7 Tage wiederherstellbar, Tresor gegen endgültiges Löschen geschützt
  Identitäten     Apps haben je eine eigene, Konten und Registry haben keine
  Protokolle      alle Teile schreiben in den zentralen Workspace, 30 Tage
```

```
Security - set by the baseline:
  Access          company accounts only, no keys, no public access
  Encryption      TLS 1.2 or higher, data encrypted at rest, twice for the storage
  Deletion        deleted data restorable for 7 days, vault protected against purging
  Identities      each app has its own, accounts and registry have none
  Logs            every part writes to the central workspace, 30 days
```

**The list, the security block and the account below are written as text of the message,
before the question is asked.** Thinking never reaches the person, and a dialog can cover
the text above it: a "Passt das Design?" / "Does the design fit?" with no design on screen
asks for a yes to something nobody has seen, and that yes is no sign-off. The tick list goes
into the dialog a second time, as the `preview` of the option *Ja, so bauen* / *Yes, build
it that way*, so it is in front of the person while they answer.

Close it with one `AskUserQuestion`: **Passt das Design?** / **Does the design fit?** - *Ja, so bauen* / *Yes, build it that way* (recommended
when nothing is open) / *Etwas fehlt oder ist zu viel* / *Something is missing or too much* (the person says what; the spec is
not written until the list is right). A part the customer strikes is removed, a part they
add goes through `azure-resources` like any other request, and the account below is
redone for the change. A security row the person wants different comes through the
dialog's free answer: only that row is opened and recorded
`source: { overridden: <reason> }`, the baseline stays as shown for the rest, and a row the guardrails refuse to relax -
anything security-shaped in a non-production stage - is said so, not applied.

**Then the account of what was decided for them.** Compact - not forty rows, group it. It
opens with what the person's answers decided, in their words and their language, and
keeps the technical rows for the section below it:

```
Was aus euren Antworten folgt:
  Ausfall wäre störend        -> Daten liegen in drei Rechenzentren der Region (ZRS)
  Eine Woche zurück           -> gelöschte Dateien und Ordner 7 Tage wiederherstellbar, alte Versionen bleiben
  Daten in laufender Nutzung  -> Standard-Speicherklasse, kein Aufpreis beim Lesen
  Nur aus dem Firmennetz      -> kein öffentlicher Zugang, Anbindung über private Endpunkte

Nach den Vorgaben gesetzt (ohne Rückfrage):
  Zugriff nur mit Firmenkonten, verschlüsselt, Logs in den zentralen Workspace

Offen (braucht jemanden vom Plattform-Team):
  Welche Anwendung greift zu und braucht welche Rechte?
```

```
What follows from your answers:
  An outage would be disruptive -> data sits in three datacenters of the region (ZRS)
  One week back                 -> deleted files and folders recoverable for 7 days, old versions stay
  Data in active use            -> standard access tier, no surcharge for reads
  Company network only          -> no public access, connected through private endpoints

Set by the baseline (no question asked):
  Access only with company accounts, encrypted, logs to the central workspace

Open (needs someone from the platform team):
  Which application accesses it and needs which permissions?
```

The technical rows follow:

```
Deployment-wide:
  exposure        private - every resource gets a private endpoint, no public surface
  network         own vnet, 10.42.0.0/16
  mandates        BSI C5 -> HSM key protection -> Key Vault Premium
  diagnostics     log-we-prod-platform-01

Defaulted (guardrail baseline):
  Security        TLS 1.2 floor, shared-key auth off, infrastructure encryption on
  Data protection blob soft-delete 7d, container soft-delete 7d, versioning on
  Observability   diagnostics -> Log Analytics, categories: audit + allLogs

Per stage (one file each; development and test hold placeholders until the platform team answers):
  production      sub 1111…, rg-invoicing-prod-we, hub vnet-hub-prod-we, ZRS
  development     sub ?, rg ?, hub ? - LRS (non_production_savings)
  test            sub ?, rg ?, hub ? - LRS

Asked:
  stages          development, test, production (default; "Produktion" named the production stage)
  location        westeurope
```

The deployment block comes first because it is what the rest hangs off, and a forced value
is shown with what forced it — `HSM -> Premium` is a cost consequence someone may want to
argue with, and they can only do that if they see the chain.

This is the step that keeps the plugin honest. Someone who disagrees with a default needs
to see it to disagree with it, and the ones they never look at cost one glance.

## Phase 5 — Working mode, spec, diagram loop, then the code question

Phase 5 runs in this order, for a new spec and for the extension of an existing one alike:
**working mode, spec, diagram loop, code question.** Nothing before the working mode is
written or drawn, and nothing after the diagram loop is generated until the code question
has been answered with a yes.

**First, ask how the rest of the run works - once, as the first step of Phase 5, before
the spec is written.** With the Agent tool available, ask through `AskUserQuestion`:
**Subagents** (recommended) or **Same session**. The answer covers both loops that follow -
the diagram loop below and the code loop of [iac-rendering](../iac-rendering/SKILL.md)
after the yes - and it is a working mode, not a decision: it is not written to the spec.
Say what it costs: agents check each other and work in the background, but each starts cold
and spends its own tokens; the same session is cheaper and does the steps itself. Only a
host without the Agent tool is not asked and runs in the same session. **A change or
extension of an existing spec enters Phase 5 again** - this question (unless already
answered in the session), spec, decision document, diagram loop, code question - it is not
a shortcut past any of them.

```
Wie soll ich weiterarbeiten?
  1. Mit Subagenten (empfohlen) - Du siehst zuerst einen Entwurf; passt der Inhalt, prüft ein
     Agent das Bild und ein zweiter schleift es; später rendert und testet je ein Agent den
     Code. Gründlicher, kostet mehr
  2. In dieser Sitzung - Ich mache alles selbst, nacheinander. Günstiger, keine zweite Prüfung
```

```
How should I continue?
  1. With subagents (recommended) - You see a draft first; once its content is right, one
     agent reviews the picture and a second polishes it; later one agent each renders and
     tests the code. More thorough, costs more
  2. In this session - I do everything myself, one step after the other. Cheaper, no second review
```

**Then write the spec.** `infra-spec.yaml` goes into the user's repository — the IaC root,
or `infra/` — is laid out with `write_spec.py` (order, group comments, the plugin version;
it changes no value and keeps every comment; without a shell, write it in that
[canonical form](references/spec.md#canonical-form) by hand) and validated with
`check_spec.py`. Its shape is
[references/spec.md](references/spec.md#shape), and that is the whole format: the plugin
ships no sample spec to copy, and the scripts are tools, not documentation — run
`write_spec.py`, `check_spec.py`, `new_diagram.py` and `export_decisions.py`, never read their source to
learn what a spec looks like. What `check_spec.py` rejects, it names.

**`naming.yaml` lands beside it in the same step**, for either target, when the directory
has none: `write_spec.py` writes a full copy of
[naming.default.yaml](../iac-rendering/references/naming.default.yaml) (through
`core/iac-rendering/scripts/write_naming.py`); a departure the user stated is then an edit to
that copy. Terraform's provider reads it at plan time, Bicep's name
`var`s are generated from it, so it belongs to the spec, not to the code — the names in the
diagram and every later render come from it, and a run that stops after the spec (the user
says "not yet" / "noch nicht") must still leave the convention in the repository. An
existing `naming.yaml` is left untouched. The rules are in
[naming.md](../iac-rendering/references/naming.md#namingyaml-in-the-users-repository--a-full-copy-both-targets).

**Record who reaches what in `wiring:`** when the spec holds more than one resource, from
what the request says — the diagram draws exactly this and nothing else
([spec.md](references/spec.md)). `wiring.access` is one `{ from, to, label }` per call the
request names ("the web app reads from the storage" / "die Web-App liest aus dem Storage" is `app-service` → `storage-account`),
with `users`, `internet` and `external:<name>` as the actors outside the deployment;
`resources.<key>.meta.purpose` is a few words per resource in the user's own terms ("Produktbilder"), and
only for a resource whose purpose the request says - one it names without saying what it is
for gets none, never a restatement of its type ("Speicher der App", "the app's database"),
which the picture already shows as the type;
A part the user calls optional is not built on spec: it is an `open:` entry ("Soll der Redis-Cache
gebaut werden?" / "Should the Redis cache be built?", `blocks: apply`) until someone decides,
then a resource or nothing. Never infer a call from the
resource types: two resources side by side say nothing about who talks to whom, and an
invented line in a picture the customer signs off on is a decision nobody made. A call
the request leaves out is an `open:` entry only where the spec cannot work without it;
otherwise the line is simply absent.

**Customer-facing text in the spec is written in the customer's language**, the one
`context.language` names - the one exception to the English spec: `wiring.access[].label`, `meta.purpose` and the
`owner` of every `open:` entry are printed verbatim into the picture, the `question` into
the decision document (the picture counts the open entries by owner and points at the
document), so an English label there is a `spec` finding of the review. Keys, values, `why`, `reason` stay English.

**Write the decision document in the same step**, in the user's language:

```sh
python3 <plugin>/core/iac-authoring/scripts/export_decisions.py ./infra-spec.yaml
```

It renders every decision in the spec as one line in the customer's words
(`infra-decisions.md` beside the spec): what follows from their answers, with the
decisions derived from each; what the rules set; what they decided differently; what is
still open and from whom. It is the Phase 4 account as a file someone can send, and it is
regenerated from the spec on every later run like the diagram. It is the
artefact the person signs off on; the spec is what the render reads. The script reads the
question sets' `plain` lines and falls back to English where a line lacks the language,
and says how many did — translate those in the conversation rather than editing the file.

**Then run the diagram loop, without asking.** Nobody reads the spec; the customer reads the picture.
Run [iac-diagram](../iac-diagram/SKILL.md) on the spec as soon as it validates and put
`infra-diagram.drawio.png` beside it - every run, for a single storage account as much as
for twelve resources, because the picture is the thing the person signs off on and the
question "should I draw a diagram?" was a question nobody ever answered with no. Where the PNG cannot be exported (no draw.io CLI), hand over the
`.drawio` and say so.

**Show the draft first and ask about the content.** The main session runs the script itself
(`new_diagram.py`, `verify_diagram.py`, `export_diagram.py --keep`: deterministic, seconds)
and shows the draft as soon as `verify_diagram.py` passes - with the decision document's first
section and the `open:` items - asking one question through `AskUserQuestion`: are the parts
and connections right? The layout is not polished yet and the message says so; what is asked
is the content. A content change is cheap here: correct the spec, re-export the decision
document, regenerate, show the new draft, ask again - no review runs on a draft the customer
may still change. An objection to the drawing itself ("the arrows overlap" / "die Pfeile
überlappen sich") is answered with the polish that follows, and noted for the review.

```
Entwurf (infra/infra-diagram.drawio.png) - die Darstellung ist noch roh, den Feinschliff
mache ich danach. Was ihr entschieden habt, steht in infra/infra-decisions.md.
Stimmen die Teile und Verbindungen?
  - Inhalt passt              -> Loop 1 polishes the picture; then the code question
  - Etwas ändern              -> the user says what; change the spec, redraw, ask again
  - Noch nicht                -> stop here; the spec and the draft stay, the next run extends them
```

```
Draft (infra/infra-diagram.drawio.png) - the layout is still rough; I polish it next. What
you decided is in infra/infra-decisions.md.
Are the parts and connections right?
  - Content is right          -> Loop 1 polishes the picture; then the code question
  - Change something          -> the user says what; change the spec, redraw, ask again
  - Not yet                   -> stop here; the spec and the draft stay, the next run extends them
```

**Then polish it - Loop 1 of [agents.md](../iac-rendering/references/agents.md)** - only after
"Content is right" ("Inhalt passt"). A **review agent** (read-only; with Subagents a
subagent, in the same session the session itself, with the same checklist) looks at the
PNG, works through [the checklist](../iac-diagram/SKILL.md#the-review-checklist) edge by
edge and reports a numbered list: a line through an icon or another line, overlapping or
wrapped labels, a label in the wrong language, a legend that contradicts a colour, a
number or a name that disagrees with the spec. **With Subagents the main session starts the
review agent itself, as its own Agent call on the confirmed draft** - and, only when the review
found an `error` with cause `render`, the **fix agent**. A subagent cannot start another, so a brief that tells one to "use a review
subagent" gets no independent review, and whoever edited the picture must never be the one that
checks. The review never shares an agent with the draft or the fix. Each finding carries a severity (`error`:
wrong, misleading or unreadable; `polish`: only looks worse than it could) and a cause
(`render`: the drawing; `spec`: the spec says it that way); a hit of the checklist is always
an `error`, never graded down to `polish`. An `error` with cause
`render` goes to the **fix agent**, which fixes it **by hand in the `.drawio`**
the way [iac-diagram](../iac-diagram/SKILL.md) says - the affected cells by id, a
waypoint, a geometry, a label - verifies, exports, and the review runs again. The script
is not touched in the loop: `new_diagram.py` exists to produce the first draft cheaply,
and the picture the customer signs off on is finished by hand. A fault the script would
repeat on the next spec is noted in the hand-over as a plugin todo, not fixed now. Three passes, then the customer
sees the picture as it is, with the open findings named. `polish` findings never start a pass:
the fix agent, running for an `error` anyway, takes them along and fixes those that are a
small edit of the cells they name; the rest go into the hand-over. **The review has ceilings**, because a reviewer always finds
more: at most **8 `error` and 3 `polish` findings per pass, errors first**; the same
fault on several cells is one finding that lists the cells; it reads the PNG and the spec
and opens the `.drawio` only for a coordinate it needs, and never samples pixels. When the
error list is full it stops and says where - the checklist item, and inside item 1 the last
edge traced. A later pass re-checks what the earlier passes named and what the fix touched,
then continues the checklist from that point - never a fresh sweep from the start, and never
done before the checklist has run to its end once. Three passes that do not finish it show
the picture with "reviewed up to item N" named as open. An `error` with cause `spec` is the main agent's: correct the
spec, re-export the decision document, regenerate the draft - **and do that before the
fix agent starts its hand edits**, because a regenerated draft loses them; the order
of a pass is spec findings first (the main session corrects the spec and runs the script
again), then the hand edits on the fresh draft, then the review again. A spec finding that
changes what the picture shows goes back to the draft question before the polish goes on: the
customer confirmed the content, not a correction they have not seen. Every finding names the cells it concerns by their draw.io ids (`e-6`,
`res-key-vault`), so the fix agent can act on it without re-deriving it. **Never show
the polished picture with an error you have already seen** - that is what the loop is
for.

**The code question.** Every polished diagram ends in this question, in the same message
that shows it - after the first polish, after every redraw in the loop, after a diagram-only
request, after an extension of the spec. The draft question never stands in for it: the
customer confirmed the content of a rough picture, and the code is built on the finished one. It is always asked: nothing the person said
earlier - "then write the code" / "dann schreib den Code", a stated target, an answer in the interview, a request
for a commit - stands in for it, and a run never skips it because the answer seems
obvious. A diagram shown without the question, or a
turn that ends on "diagram is done" or on a list of findings, is a missed step. It is asked with
`AskUserQuestion` - the interactive popup with the three options, as the call right after
the picture - and never replaced by prose like "let me know when I should render" or by
the question written out as text; the three options are the `AskUserQuestion` options.
Show the polished picture, name the review findings left open and what is still `open:` and
from whom, and ask one question. The German and English blocks below are the wording; it is
asked in `context.language`. The three options keep their meaning in any language: only the
yes starts the render.

```
Feinschliff fertig (infra/infra-diagram.drawio.png). Soll ich den Code erzeugen?
  - Ja, Code erzeugen         -> the picture is signed off; the code is generated next
  - Etwas ändern              -> the user says what; change the spec, back to the draft question
  - Noch nicht                -> stop here; the spec and the picture stay, the next run extends them
```

```
The picture is polished (infra/infra-diagram.drawio.png). Shall I generate the code?
  - Yes, generate the code    -> the picture is signed off; the code is generated next
  - Change something          -> the user says what; change the spec, back to the draft question
  - Not yet                   -> stop here; the spec and the picture stay, the next run extends them
```

"Change something" ("Etwas ändern") loops back to the spec, never into code: change the decision there, as
`overridden` where it replaces an answer, re-export the decision document, redraw, and show
the new draft with the draft question. An objection to the polished picture itself ("the
arrows overlap" / "die Pfeile überlappen sich") is a review finding that the review missed: it
goes to the fix agent like any other `error`, and the customer sees the picture again only
after the review passed. Stay in this loop as long as the person has objections - a round costs seconds
here and a regenerated module later. Offer the decision document as Word once, in the
first round, as a side line and not an option: the `.docx` is a conversion
(`pandoc infra-decisions.md -o infra-decisions.docx` where pandoc is on PATH; say so and
leave the Markdown where it is not).

**Only after "Yes, generate the code" ("Ja, Code erzeugen") does the render start** ([iac-rendering](../iac-rendering/SKILL.md)),
into the same directory, with its own loop of fmt, validate, tests and plan - Loop 2, in
the working mode chosen above. It is the
long step, and it runs on a picture the customer has agreed to, so nothing in it goes
back to the interview. Say that it takes a while and what will be there at the end.

**An earlier statement is not the answer.** "Dann schreibe den Terraform-Code" in the same
message as the spec, or a change of the spec the person asked for themselves, still goes
working mode, spec, draft, polish, question - the code is what they sign off on, not
what they ordered, and the question is put after the picture, never before it.
A run in which nobody can answer (no `AskUserQuestion`) ends after the draft with the
draft question as the last message, polishes nothing and writes no `.tf` or `.bicep`: a
sign-off nobody gave is not one, and the code would be built on a picture nobody has seen.

Never present the render as already decided. If the spec still holds `open:` entries or
`assumed` values, say so with the question — the code will then `plan` but not `apply` —
rather than letting "Ja, Code erzeugen" read as "ready to deploy".

A spec extended by a later run goes through the same loop; the diagram and the code are
regenerated from the whole spec, so the diff shows what the extension moved.

- Spec schema and the `source` field: [references/spec.md](references/spec.md)
- Drawing it: [iac-diagram](../iac-diagram/SKILL.md)
- Rendering it, for either target: [iac-rendering](../iac-rendering/SKILL.md)

## Phase 6 — Validate and explain

Generated code is not done when it is syntactically valid: `fmt` → `validate` →
`plan` / `what-if` → explain what will actually happen and what is risky about it. The
commands per target are at the end of each idiom file in
[iac-rendering](../iac-rendering/SKILL.md).

Do not skip the explanation on the grounds that the plan output is right there. A plan is a
list of attribute diffs; what a reviewer needs is which of those diffs are
service-affecting and which are noise, and that is a judgement the plan cannot make.

## Composition: how a pattern drives a resource

A pattern skill does not re-ask a resource's questions. It supplies a pre-answer block
keyed by resource, entering resolution at precedence 2:

```yaml
pre_answers:
  storage-account:
    public_network_access: disabled
    shared_access_key_enabled: false
    replication: ZRS
  key-vault:
    rbac_authorization: true
```

Two rules make this work:

- **A pattern may pre-answer, never contradict the user.** If the user asked for something
  the pattern pre-answers differently, the user wins — and you say what the pattern
  expected and what the departure implies for the rest of the system.
- **The wiring belongs to the pattern.** Private endpoints, DNS zone links, role
  assignments between the parts, diagnostic routing: no single resource question set can own a
  relationship, so the pattern renders it. This is the genuinely hard part and the main
  reason pattern skills exist.

A pattern may also fix the module split, since it is the only layer that knows which parts
get reused separately. It does that by supplying `composition.modules` entries, recorded
with `source: pattern` like any other pre-answer:

```yaml
composition:
  granularity: per-resource
  modules:
    - id: storage
      contains: [storage-account]
      outputs: [id, principal_id]
    - id: app
      contains: [app-service]
      needs: [storage]
```

The wiring between them stays in `wiring:` and renders into the root, not into a module —
a module that reaches into another module's resources is the failure this split exists to
prevent.

## Adding a resource

A resource goes into a family under `resources/` — `storage`, `network`, `observability`
today, `compute`, `data`, `security` planned. Families are not skills; the single
[azure-resources](../azure-resources/SKILL.md) skill routes into all of them. Inside the
family:

1. Create `<resource>/` in the family and write `<resource>/questions.yaml`. Copy an existing one and keep the group order:
   placement, networking, identity, security, data_protection, observability. The order is
   the review order, and a reviewer reading three resources should not relearn the layout.
2. Fill `why` and `renders` for every question. `why` is what a reviewer reads; `renders`
   names the attribute it drives, which is what lets a review map code back to a decision.
3. Take defaults from [references/guardrails.md](references/guardrails.md) rather than from the
   Azure portal. They differ often, and the portal default is not a baseline worth
   inheriting.
4. Mark `required: true` only where no safe default exists. Every extra required question
   is a tax on every future run.
5. Write `<resource>/notes.md` for what the question set cannot express:
   decisions fixed at creation, dependency order, the pitfalls worth naming out loud.
6. Write `<resource>/terraform.md` and `<resource>/bicep.md` — the
   rendering, not the decisions. Anything true for both belongs in the shared references
   instead.
7. Add a row to the family's `routing.md`, so the resource is reachable, and its icon to the
   `ICONS` table of [iac-diagram](../iac-diagram/SKILL.md#icons), so it is drawable.
8. If people would ask for it with a term the
   [azure-resources](../azure-resources/SKILL.md) description does not carry, add the term
   there, never here — resource terms are its job, workload and target terms are this
   description's, and this one stays short so the entry is never what gets pruned.
9. Validate with `scripts/check_question_set.py <resource-directory>` — enum defaults,
   `depends_on` and `answer` targets, the resolution fields, unknown fields —
   and with the repo's `check_repo_consistency.py` for routing reachability.

A genuinely new family gets a directory under `resources/` with a `routing.md`, and a row in
the azure-resources table. **Not** a skill directory and **not** a `plugin.json` entry:
`resources/` is deliberately undeclared, so nothing in it costs listing budget, and a
family that became a skill again would reintroduce the per-family budget cost that
overflowed at the sixth family.

## Reference files

Read as needed, not up front:

| File | When |
|---|---|
| [references/spec.md](references/spec.md) | Writing or reading `infra-spec.yaml` |
| [references/question-set-format.md](references/question-set-format.md) | Authoring or extending a `questions.yaml` |
| [references/guardrails.md](references/guardrails.md) | Choosing or justifying any default, or weighing an exception |
| `scripts/export_decisions.py` | Rendering the spec as `infra-decisions.md`, the customer's decision document |
| [iac-diagram](../iac-diagram/SKILL.md) | Drawing the spec as `infra-diagram.drawio.png` for the customer |
| [iac-rendering](../iac-rendering/SKILL.md) | Turning the spec into Terraform or Bicep — naming, layout, idiom |
