Subchapter 25.30
references/phases/discover/discover-assemble.mdMarkdown8 KBView on GitHub
Assembler unit. Runs after the discover fragments have written their contributions. It is the single creator of both phase artifacts and owns their final contract. See for how it is composed into the phase.
discover.mdSchema reference: references/shared/schema-discover-azure.md — consult for
complete field definitions, per-type config schemas, the drift record shape, and
the validation checklist.
Merge every fragment’s contributions into resources[], keyed by azure_id.
One entry per azure_id; never two.
Each entry carries at minimum azure_id, azure_type (canonical Microsoft.*),
resource_group, subscription_id, source, and config.
Write metadata: discovery_timestamp, discovery_sources (only sources that
actually contributed), subscriptions_discovered, total_resources, and
confidence.
Apply source precedence when two sources describe the same azure_id, highest
first: live az (current existence and configuration — it is now), then
RDfA (utilization, reservations, consumption; loses to live on state because
an archive may be days old, wins on measurement because live has no rollup), then
IaC (authoritative for provenance, module structure, and declared-but-
undeployed resources; authoritative for nothing about state), then billing
(fallback only, billing_inferred).
Every disagreement becomes a drift entry. Record both values, both sources,
and which won, so the report can say “your Terraform declares Standard_D2s_v3,
your tenant is running Standard_D4s_v3“ instead of quietly picking one.
Merge the fragments’ warnings[] into one top-level array on the inventory,
preserving every entry. The array is always present, [] when clean. Every code
comes from the closed vocabulary in schema-discover-azure.md § Warnings — do not
invent one, because an invented code makes the report’s grouping unstable and any
fixture assertion on a code unreliable.
Derive azure-resource-clusters.json per references/clustering/: seed one
candidate per resource group, split a candidate whose members have no edges
between them, merge candidates joined by a non-ambient crossing edge, then assign
tier, primary, member roles, and justification. clustering-algorithm.md is the
procedure; typed-edges-strategy.md says which edge types may merge and which are
ambient; classification-rules.md picks the primary; tiering.md assigns the tier.
Merge AI-profile contributions by producer. IaC only ->
metadata.profile_source: "iac_cognitive"; app-code only -> "application_code"; both ->
"merged", with code winning conflicts and infrastructure[] unioned by address.
Preserve both sources_analyzed flags. A strong IaC AI signal without an IaC profile
contribution is an assembly failure, not an optional absence.
Four tiers, set per resource and per mapping decision:
| Label | Meaning | Source |
|---|---|---|
deterministic | fixed 1:1 table lookup | the fast-path Direct Mappings table |
measured | rubric backed by observed utilization, not declared config | RDfA 31-day rollup or az monitor metrics list |
inferred | rubric from declared config only | IaC, or live CLI without metrics |
billing_inferred | billing-only fallback | Cost Management export |
measured is deliberately not named after one tool. Naming it rdfa_inferred would
mean the live path could never earn the tier even when it supplies the same
evidence. User-facing label: “Measured from your actual usage.”
Writes both artifacts from the single IaC fragment. Clustering is real as of build step 4: seed, split, merge, tier, primary, roles.
justification records WHICH of those produced each cluster, and it is what makes an
empty edges[] legitimate rather than a silent gap. A cluster that survived seeding
untouched has a real reason — its members share a resource group — and that reason is not
an edge. Without the field, “grouped by the seed” and “grouped for no recorded reason”
produce identical output, and the phase’s postcondition on the justifying edge set could
only pass by not being evaluated. split:* and merge:* require a non-empty edges[]
carrying the evidence.
| Lands in | What |
|---|---|
| step 2 | The merge-and-drift rules above, exercised once more than one source can contribute |
| step 4 | patterns.md and the cluster-level data-pipeline gate. Until it exists every cluster carries pattern_status: "catalog_absent" — a defined state, not a gap |
Resource group is a good seed and a bad final answer. It works when there is one
app per group; it splits nothing when there is one group per environment; it actively
separates things that belong together under horizontal groups (rg-databases,
rg-app); and it carries no signal at all in the single-group startup default. The
refinement is what makes clustering mean anything.
Azure’s edge data is richer than GCP’s and does not require IaC: ARM resource IDs
are embedded in resource properties, so edges survive every discovery source —
serverFarmId on a web app, subnetId, a private endpoint’s privateLinkServiceId,
a Key Vault reference in app settings, a managed identity plus its role-assignment
scope, and app= / workload= tags.
schema-discover-azure.md‘s checklist.ai-workload-profile.json was produced — i.e. nothing will produce any artifact
(matching gcp’s rule). When an IaC source contributed resources, write the inventory
and clusters as usual. When the run is app-code-only (the app-code fragment
produced ai-workload-profile.json but no IaC source was found), write ONLY the AI
profile and leave azure-resource-inventory.json / azure-resource-clusters.json
ABSENT — never write an empty inventory to satisfy a gate. Clarify detects the
app-code-only (AI-only) run by the inventory being absent while the AI profile is
present, and routes to clarify-ai-only.md.