- name
- update-resolve-arm-resources
- description
- Update, debug, or review the typespec-azure-resource-manager resolveArmResources API, including versioned resource views, operation association, ARM state and cache isolation, projected TypeSpec types, and dependency-neutral logical naming hooks.
- allowed-tools
- shell
# Update `resolveArmResources`
Use this skill for changes to `resolveArmResources`, `ResolvedResource`, `Provider`, ARM resource
operation resolution, selected-version ARM metadata, or integrations that consume this metadata.
Read `packages/typespec-azure-resource-manager/proposal/resolve-arm-resources.md` for the base
resource graph contract and
`website/src/content/docs/docs/howtos/ARM/getting-resource-information.md` for the supported
consumer workflow before changing the API or its state and cache behavior.
## Purpose of the API
`resolveArmResources(program)` translates ARM decorators and HTTP metadata into a provider model
containing:
- declared ARM resources;
- resource lifecycle, list, action, and associated operations;
- non-resource provider operations;
- ARM resource type and instance-path identity;
- parent and scope relationships;
- singleton metadata; and
- references to the TypeSpec models and operations that produced the metadata.
The no-options API is a multi-version declaration view. A selected-version API must instead return
projected types and only declarations available in the requested API version.
## Core invariants
Preserve all of these invariants:
1. The no-options result remains backward compatible.
2. A selected-version result never mixes original types with types from a mutation realm.
3. Resource association is based on ARM and HTTP structural metadata before client-facing names
are applied.
4. Resource names, operation names, and operation-group names are logical metadata, not wire API.
5. Wire identity never changes through a naming hook.
6. Consumer-specific names never enter a shared provider cache.
7. A call for one version cannot affect another version or the default view.
8. Parent and resource-valued scope references point into the same returned resource graph.
9. Provider operations are selected from the same namespace graph as resources.
10. Decorator state keyed by a projected type is not trusted until embedded type references are
checked.
11. Program-wide ARM registration state is never cleared to prepare one selected version.
12. Repeated resolution of the same version reuses the same projected namespace and realm.
13. `ResolvedResource.kind` returns only `Tracked` or `Proxy`. Keep broader resource categories in
structural state and map every non-tracked category to `Proxy` during finalization.
## Implementation map
Read these files together before editing:
| File | Responsibility |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `packages/typespec-azure-resource-manager/src/resource.ts` | Public result types, main resolver, identity parsing, parent and scope resolution, provider operation traversal |
| `packages/typespec-azure-resource-manager/src/private.decorators.ts` | Resource registration and `listArmResources` |
| `packages/typespec-azure-resource-manager/src/operations.ts` | Resource operation registration and operation state |
| `packages/typespec-azure-resource-manager/src/namespace.ts` | Provider namespace registration and lookup |
| `packages/typespec-azure-resource-manager/src/state.ts` | ARM state keys |
| `packages/typespec-azure-resource-manager/test/resource-resolution.test.ts` | Resolver behavior and regression tests |
| `packages/typespec-client-generator-core/src/public-utils.ts` | TCGC `getLibraryName` policy |
| `packages/typespec-client-generator-core/src/decorators.ts` | TCGC `@clientName` lookup |
| `packages/typespec-client-generator-core/src/internal-utils.ts` | Existing version mutation usage |
| `core/packages/versioning/src/mutator.ts` | Snapshot mutator creation |
| `core/packages/compiler/src/experimental/mutators.ts` | Mutation and realm creation |
| `core/packages/compiler/src/experimental/realm.ts` | Realm ownership and state-map behavior |
## Resolution flow
The current resolver:
1. Finds the ARM provider namespace.
2. Checks the resolved-provider cache.
3. Enumerates decorator-registered resource models.
4. Resolves resource operation candidates and HTTP operations.
5. Detects resource identities from strict instance lifecycle paths.
6. Associates non-identity operations.
7. Resolves parent and scope relationships.
8. Collects unassociated provider operations.
9. Caches the provider result.
When modifying one stage, inspect its downstream assumptions. Resource names are logical metadata,
not wire API, but the current implementation uses them as an internal association signal.
External naming must therefore run after association.
## Versioning workflow
For a selected API version:
1. Resolve the original provider namespace.
2. Use `getVersioningMutators(program, providerNamespace)`.
3. Match the exact root version value.
4. Apply the snapshot with `unsafe_mutateSubgraphWithNamespace`.
5. Require and cache the returned namespace and non-null realm by program, provider namespace, and
version.
6. Resolve from the cached namespace and realm, not by looking up the provider again.
7. Filter enumerated ARM state to exact realm-owned keys.
8. Use the projected provider namespace as the derived provider-cache key.
9. Compute HTTP metadata from projected operations.
`getVersioningMutators` creates new mutator objects each time, and the compiler mutation cache keys
on mutator identity. Never recreate a snapshot on every resolver call, and never cache only the
mutator. Cache the projected namespace and its owning realm together.
The default view must explicitly exclude all realm-owned types. Qualified-name deduplication is not
enough because historical snapshots can rename a projected type.
Do not implement a selected-version view by filtering only the final provider. Projection is
required for renames, type changes, optionality changes, removed members, and operation return
types.
For a multi-service program:
1. Discover service namespaces with `listServices(program)`.
2. Select exact shared `Version` metadata, normally from `getAllVersions(program, namespace)`.
3. Validate that every selected namespace is an ARM provider namespace and every `Version` is the
registered object owned by that namespace.
4. Project and resolve each service independently because services can have unrelated version
enums while sharing one ARM provider string.
5. Filter realm registrations to types declared under the projected service namespace; mutating one
service can re-register sibling-service decorators in the same realm.
6. Finalize each service with its own `Version.value`, then concatenate resources and provider
operations in selection order.
7. Return no partial provider if any selection is invalid or cannot be projected.
Use namespace and `Version` references for programmatic selection. Do not identify a service by its
ARM provider string: Compute subservices all use `Microsoft.Compute`.
## State and cache classification
Treat these as registration state:
- `armResources`
- `armResourceOperations`
- `resourceOperationList`
- `armResourceOperationData`
- `armResourceNameExpression`
- `armProviderNamespaces`
- `armSingletonResources`
- `resourceBaseType`
- `armBuiltInResource`
- `customAzureResource`
Treat these as derived caches:
- `armResolvedResources`
- `armResourcesCached`
When adding a state key, document which class it belongs to. Selected snapshots do not clear these
maps: realm-owned keys and the projected provider namespace isolate their derived entries from the
declaration view.
Realm state maps can fall back to parent program state for original types. An entry visible from a
realm is not necessarily owned by that realm. Check
`unsafe_Realm.realmForType.get(type) === selectedRealm`.
## Naming integrations
The ARM package must not import TCGC.
Expose or use a dependency-neutral callback that receives:
- name kind, including separate `resource-model` and full `resource` phases;
- projected TypeSpec declaration;
- current ARM logical name;
- selected version;
- associated resource model when relevant;
- ARM resource type string when relevant; and
- resource instance path when the callback needs to distinguish multiple occurrences.
Apply naming only after structural resolution. All supported names are non-wire logical metadata:
- resource name;
- operation name; and
- operation-group name.
Never change:
- provider namespace;
- resource type path segments;
- resource instance path;
- HTTP path or method;
- serialized names;
- parent or scope identity; or
- singleton keys.
Do not describe resource names, operation names, or operation-group names as wire API. Only their
current use as internal resolver grouping data requires the post-resolution ordering.
Use `resourceModel` for the TypeSpec `Model` associated with an operation. Use `resourceType` for
the ARM resource type string formatted as `${provider}/${types.join("/")}`. Do not use
`resourceType` to refer to a TypeSpec model.
Keep `ArmResourceOperation.resourceName` and `resourceModelName` consistent with resource naming.
Retain a deferred expression at the decorator boundary instead of reconstructing or editing an
eager string. Expressions distinguish explicit values, literals, model operands, concatenation,
and legacy path algorithms. Extension and built-in formulas must retain every contributing model.
Evaluate the same expression with original model names for grouping and with `resource-model`
results for final output. The complete `resource` result runs afterward and has final precedence.
Explicit decorator and template names are opaque expressions. Treat undefined and empty template
sentinel values as omitted. If multiple identity operations contribute to an occurrence,
conservatively retain the explicit expression if any of them supplied the name.
Include both the ARM resource type string and instance path in resource naming requests because
one model and one resource type can produce several resolved resource occurrences. For example,
subscription-scoped and tenant-scoped resources can share the same resource type string while
having different instance paths. Resource type alone does not encode scope, parent identifiers, or
extension-resource targets. Public instance paths preserve named HTTP parameters such as
`{subscriptionId}`. Canonical `{}` segments are only for internal identity comparison and must not
replace parameter names in returned provider metadata or naming requests.
Track synthetic parents internally and invoke the name resolver with `type: undefined`. Current
synthetic parents reuse the child model in their stored `type` field, but that model is not an
authoritative declaration for the synthetic resource. Consumers can use `defaultName`,
`resourceType`, or `resourceInstancePath` to name synthetic occurrences explicitly.
Do not issue `resource-model` requests for synthetic resources.
For a TCGC integration test, let the consumer call `getLibraryName` or
`getClientNameOverride`. Do not duplicate TCGC precedence in ARM.
The implemented consumer adapter is:
```ts
resolveArmResources(program, {
version,
nameResolver: ({ kind, type }) =>
kind === "resource" || type === undefined ? undefined : getLibraryName(tcgcContext, type),
});
```
## Required tests
Start with failing tests for the requested behavior.
### Compatibility
- Existing unversioned result is unchanged.
- Existing multi-version result is unchanged when no version is supplied.
- Creating a TCGC context before resolution does not create duplicate resources.
### Selected versions
Use at least three versions and cover:
- resource added and removed;
- operation added and removed;
- model or operation renamed;
- property type changed;
- property made optional or required;
- operation return type changed;
- provider operation filtering; and
- parent and scope consistency.
Assert returned declared types belong to the selected realm.
### Multi-service selected versions
Use one small reusable TypeSpec fixture rather than copying a production specification. Cover:
- discovering ARM services and selecting the final declared version of each;
- independently versioned services that share one ARM provider string;
- one combined provider with resources and provider operations in selection order;
- a mixed-version selection and a single-entry selection;
- parent and resource-valued scope references into the returned combined graph;
- each naming request receiving the selected service's version value; and
- atomic diagnostics for empty, duplicate, non-ARM, and mismatched namespace/version selections.
Rely on the existing compatibility and call-order suites instead of duplicating them for the
multi-service entry point.
### Isolation
Run different call orders in one compiled program:
- default then V1 then V2;
- V2 then V1 then default;
- the same version twice;
- different name resolvers for the same version; and
- named then unnamed resolution.
Compare stable scalar projections rather than deeply comparing embedded TypeSpec graphs. Assert
realm ownership separately. No custom name may leak to another call.
### Naming
- Rename every model operand in standard, extension, built-in, and legacy derived names.
- Verify explicit names remain opaque and complete resource overrides have final precedence.
- Verify baseline grouping is unchanged by resolver-produced names.
- Verify a full, path-specific resource override wins over model-derived naming.
- Rename a resource model, operation, and operation interface.
- Verify all aliases of one operation receive the same name.
- Verify paths and resource type segments remain unchanged.
- Verify synthetic parents receive `type: undefined` and can be renamed by default name or path.
- Verify synthetic parents receive no `resource-model` request.
- Verify empty callback results produce the intended diagnostic behavior.
## Common failure modes
- Deduplicating projected and original resources by qualified name. This can choose the original
model for a selected version.
- Allowing a renamed realm type into the default view because its qualified name differs.
- Recreating version mutators and realms for every call.
- Caching a mutator without caching the projected namespace and its owning realm.
- Calling `resolveProviderNamespace(program)` after mutation. It can return the original namespace.
- Traversing all program operations instead of the selected provider namespace.
- Reusing `armResolvedResources` for a customized or versioned call.
- Mutating a cached `Provider` during post-processing.
- Checking only a state-map key while a value still references original types.
- Applying TCGC names before resource identity matching.
- Passing the reused child model as the authoritative type for a synthetic parent.
- Renaming ARM wire resource type segments with a client name.
- Clearing ARM registration maps program-wide.
- Adding an ARM runtime dependency on TCGC.
## Editing rules
- Keep the no-options path explicit and easy to compare with previous behavior.
- Prefer a small internal resolution context over optional parameters threaded independently
through many helpers.
- Use proper TypeScript types; do not use `any` to bypass realm or operation distinctions.
- Preserve existing resource graph references when copying results.
- Add comments only where realm ownership or cache isolation is non-obvious.
- Do not change unrelated resolver heuristics while adding version or naming support.
- If changing `.tsp` files, run `tsp format`.
- If changing decorator option models in `.tsp`, regenerate TypeScript types with `tspd`.
## Validation
Use mise from the repository root:
```powershell
mise exec -- pnpm -r --filter "@azure-tools/typespec-azure-resource-manager..." build
mise exec -- pnpm --filter "@azure-tools/typespec-azure-resource-manager" test
mise exec -- pnpm --filter "@azure-tools/typespec-client-generator-core" test
mise exec -- pnpm format
mise exec -- pnpm lint
```
During development, use targeted Vitest selectors first. Before a PR, add the required Chronus
change description for every affected package.
View on GitHub