# Using the winget COM API syncwingetlink uses the winget **COM API (`Microsoft.Management.Deployment` namespace)** as the first choice for enumerating installed portable packages. When it is unavailable, it automatically falls back to filesystem scanning (`FsScanSource`). This page documents what `src/core/WingetComSource.cpp`, `src/core/ComApartment.cpp`, `src/core/PackageSourceError.cpp`, and `src/core/PackageSourceFactory.cpp` actually do, not the original design intent recorded in `docs/PLAN.md` ยง3 โ€” the two diverged after M6 (issue #56) changed apartment ownership, and this page had not been updated since. Every claim below is backed by a named file, or by a live run recorded in `docs/adr-phase-8.md` ADR-0037 and marked as such. ๐Ÿ“– ๆ—ฅๆœฌ่ชž็‰ˆใฏ [`com-api_ja.md`](./com-api_ja.md) ใ‚’ๅ‚็…งใ—ใฆใใ ใ•ใ„ใ€‚ ## Why the COM API - It is a **stable, versioned public interface** provided separately from the CLI. - It is usable from Win32 desktop apps via C++/WinRT. - The sqlite (`PortableIndex`) internal schema is undocumented and may change, so a direct read is avoided and kept as a last-resort fallback. ## Build-time projection The `Microsoft.Management.Deployment` C++/WinRT projection is not committed; it is generated by a dedicated MSBuild target on every non-design-time build (`props/syncwingetlink.winget-projection.targets`, imported only by `src/syncwingetlink.core.vcxproj`; see `docs/adr-phase-2.md` ADR-0008): 1. A `powershell.exe -NoLogo -NoProfile -NonInteractive` step runs `Get-AppxPackage -Name Microsoft.DesktopAppInstaller | Sort-Object Version -Descending | Select-Object -First 1` and resolves `Microsoft.Management.Deployment.winmd` inside that package's install location. The build fails with a clear MSBuild error if no such package is installed. 2. `cppwinrt.exe` is invoked as `-input -reference $(WindowsTargetPlatformVersion) -output $(IntDir)generated -overwrite -prefix`. The `cppwinrt.exe` chosen is keyed off the **host** architecture (`$(PROCESSOR_ARCHITECTURE)`/`$(PROCESSOR_ARCHITEW6432)` == `ARM64` selects the SDK's `arm64` compiler; every other host uses the SDK's `x64` one) โ€” not the target `$(Platform)` being built. Cross-building `ARM64` from an x64 host therefore still uses the x64 `cppwinrt.exe`; the generated headers are architecture-independent (ADR-0008 consequence). 3. Output goes to `$(IntDir)generated` (per-project/platform/configuration, since `$(IntDir)` already varies by all three) and is added to `ClCompile`'s `AdditionalIncludeDirectories`. 4. The target is skipped when `$(DesignTimeBuild) == 'true'` (IntelliSense passes). **What is pinned and what is not.** The Windows SDK is pinned: `WindowsTargetPlatformVersion = 10.0.26100.0`, `PlatformToolset = v145` (`Directory.Build.props`). The **winmd is not pinned at all** โ€” it always comes from whichever Desktop App Installer version happens to be newest on the build machine, and is regenerated (`-overwrite`) on every build. A verification run recorded a specific App Installer version at a point in time (see ADR-0037); that figure is evidence of what was tested, never a build requirement, and a later reader's own build will project whatever is installed on their machine at build time. Only `syncwingetlink.core.vcxproj` imports this target. `syncwingetlink.vcxproj` (the executable, `main.cpp` only) and `tests/syncwingetlink.tests.vcxproj` (an MSTest DLL that only `ProjectReference`s core) do not, which is why `WingetComSource.h` never names a winrt type โ€” every winrt type stays behind a pimpl in `WingetComSource.cpp` โ€” and why any future header that needs one must follow the same pattern. ## Activation > Confirmed against the code as of `WingetComSource.cpp` (M6, issue #56) and, where noted, > against a live run recorded in ADR-0037. Reference the generated > `Microsoft.Management.Deployment` projection for exact method signatures; the > requirements below are the part that does not come from the winmd and is easy to get > wrong. **`winrt::init_apartment()` is not used, anywhere in this codebase.** It throws when the calling thread already has a different concurrency model initialized (`RPC_E_CHANGED_MODE`). `src/core/ComApartment` uses `CoInitializeEx(nullptr, COINIT_MULTITHREADED)` directly instead, and tolerates that specific failure: | `CoInitializeEx` result | `ComApartment` behavior | |---|---| | `S_OK` | Owns the apartment; destructor calls `CoUninitialize()` | | `S_FALSE` (already initialized, same model) | Also treated as owned; destructor calls `CoUninitialize()` | | `RPC_E_CHANGED_MODE` | Not an error. Reuses the existing apartment; destructor does **not** call `CoUninitialize()` | | any other failure | Throws `PackageSourceError(PackageSourceErrorKind::Unknown, "CoInitializeEx failed", hresult)` โ€” note this path does **not** go through `mapHresultToKind` below | Apartment ownership is a **process-wide** concern, constructed **once** by `main.cpp` before any other core call (`docs/adr-phase-5.md` ADR-0024) โ€” `WingetComSource` does not construct its own (it did until #56; a stale forward-reference to that arrangement in `docs/adr-phase-2.md` ADR-0009 is corrected by a dated amendment note, not by rewriting the original decision). Any code path that constructs a `WingetComSource` is responsible for the process already having an initialized apartment. Separately, `rules/RuleSet.cpp::parse()` constructs its **own** `ComApartment` for `winrt::Windows::Data::Json` (`docs/adr-phase-2.md` ADR-0011) โ€” this applies even to a `--source fs` or `test-rule` invocation that never touches `WingetComSource` at all, so it is not solely a stand-in for `main.cpp`'s process-wide one. **Every activatable class in this namespace needs its own fixed CLSID when called from an unpackaged desktop process** โ€” not just `PackageManager`. None of them are registered for ordinary WinRT activation (`T t{};`) outside a package graph; each is exposed as a separately registered out-of-process `ExeServer` class in the installed Desktop App Installer package's `AppxManifest.xml`: | Class | CLSID | |---|---| | `PackageManager` | `C53A4F16-787E-42A4-B304-29EFFB4BF597` | | `FindPackagesOptions` | `572DED96-9C60-4526-8F92-EE7D91D38C1A` | | `PackageMatchFilter` | `D02C9DAF-99DC-429C-B503-4E504E4AB000` | All three are activated via a direct `::CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER, winrt::guid_of(), &raw)` call (the file-local `createInstanceNoThrow()` helper in `WingetComSource.cpp`) โ€” `CLSCTX_LOCAL_SERVER` only, never `CLSCTX_INPROC_SERVER` or `CLSCTX_ALL`; see "Out-of-proc vs in-proc" below for what that choice implies. `winrt::guid_of()` requests `T`'s *typed default interface* (e.g. `IPackageManager`), not `IUnknown` โ€” this matters concretely; see the `APPMODEL_ERROR_NO_PACKAGE` note below. This is functionally the same activation attempt `winrt::create_instance(clsid, CLSCTX_LOCAL_SERVER)` performs internally, just without the C++ exception that call raises on failure (`docs/adr-phase-9.md` ADR-0040) โ€” `mapHresultToKind()` and the diagnostic text both need the resulting `HRESULT`, so the throw-free `CoCreateInstance` route was chosen over `winrt::try_create_instance()`, which discards it. `FindPackagesOptions` in particular cannot be constructed as `FindPackagesOptions{}` โ€” that fails with `REGDB_E_CLASSNOTREG`. Two small build requirements exist only because this is an unpackaged, unusual consumer of a WinRT namespace: - `WIN32_LEAN_AND_MEAN` (set project-wide) excludes `` from ``, which is where `CLSCTX_LOCAL_SERVER` and `CoInitializeEx`/`CoUninitialize` live โ€” `WingetComSource.cpp` and `ComApartment.cpp` both include `` explicitly rather than relying on a transitive include. - `` is header-only with no `#pragma comment(lib, ...)` of its own. `WingetComSource.cpp` adds `#pragma comment(lib, "runtimeobject.lib")`. A linker directive embedded in an `.obj` only propagates through the static-library archive for a link that actually pulls that `.obj` in, so `RuleSet.cpp` โ€” which also activates WinRT runtime classes (`winrt::Windows::Data::Json`) and can be linked (e.g. by tests) without ever touching `WingetComSource.cpp` โ€” carries its own copy of the same `#pragma comment` rather than relying on `WingetComSource.cpp`'s. ```cpp #include using namespace winrt::Microsoft::Management::Deployment; constexpr GUID kPackageManagerClsid = { /* see table above */ }; constexpr GUID kFindPackagesOptionsClsid = { /* see table above */ }; constexpr GUID kPackageMatchFilterClsid = { /* see table above */ }; // CoInitializeEx(nullptr, COINIT_MULTITHREADED) must already have been called on this // process (tolerating RPC_E_CHANGED_MODE) - main.cpp does this once, process-wide. // // Simplified here as winrt::create_instance for readability; the actual code // (createInstanceNoThrow() in WingetComSource.cpp) calls ::CoCreateInstance directly // with winrt::guid_of() and branches on the HRESULT instead of letting // this throw, so an activation failure - reproducibly hit on some hosts, issue #143 - is // reported through PackageSourceCreation rather than as a first-chance C++ exception // (docs/adr-phase-9.md ADR-0040). The activation attempt itself, and its result, are // identical either way. PackageManager manager = winrt::create_instance(kPackageManagerClsid, CLSCTX_LOCAL_SERVER); auto catalogRef = manager.GetLocalPackageCatalog(LocalPackageCatalog::InstalledPackages); auto connectResult = catalogRef.Connect(); if (connectResult.Status() != ConnectResultStatus::Ok) { // Any non-Ok status (including SourceAgreementsNotAccepted, which has no dedicated // kind) is treated as PackageSourceErrorKind::CatalogError - see "Failure and // fallback" below. } auto catalog = connectResult.PackageCatalog(); // FindPackagesOptions/PackageMatchFilter are activated once and reused across every // enumeratePackages() call - see "What happens when" below. ``` ## Out-of-proc vs in-proc This codebase activates all three classes with **`CLSCTX_LOCAL_SERVER` only** โ€” there is no `CLSCTX_INPROC_SERVER` fallback anywhere. This is a deliberate, load-bearing choice, not an oversight, and it has consequences worth naming explicitly (the checklist item this section exists to satisfy): - **The out-of-process server (`WindowsPackageManagerServer.exe`, launched by COM from the Desktop App Installer package) must be launchable for `--source com` to work at all.** There is no in-process COM DLL this code can fall back to. If the server cannot be launched or reached, activation fails at the very first COM call this code makes - `createInstanceNoThrow(kPackageManagerClsid, manager)` - before anything else runs. - **Every call is genuinely cross-process and marshalled**, not a same-process vtable call. That is why `FindPackagesOptions`/`PackageMatchFilter` are activated **once**, in `WingetComSource::Impl`'s constructor, and the same instances are reused across every subsequent `enumeratePackages()` call rather than being rebuilt per call โ€” the comment in the code is explicit that "the object carries no per-call state (it is a filter list the server reads)." - **A single bad package cannot corrupt the whole enumeration**, which matters more for an out-of-process, marshalled API than an in-process one: `GetMetadata()` throwing `winrt::hresult_error` for one field is treated as an empty field, not a failure (`getMetadataOrEmpty`); a `winrt::hresult_error` thrown while processing one `CatalogPackage` match skips that package and continues to the next. - **Live evidence that this constraint is real, not theoretical**: a verification run recorded in ADR-0037 reproduced an environment where an unpackaged process could successfully `CoCreateInstance` the bare `PackageManager` CLSID (requesting `IUnknown`) but **failed** to activate it through the typed `IPackageManager` interface โ€” exactly the interface `winrt::guid_of()` requests โ€” with `HRESULT_FROM_WIN32(APPMODEL_ERROR_NO_PACKAGE)` (`0x80073D54`, "the process has no package identity"). See "Failure and fallback" for how that specific failure is classified and what a user sees. Whether this reflects a permanent constraint of unpackaged out-of-proc activation for this interface, or a version-dependent behavior of the App Installer build tested, was not resolved by this verification and is not asserted either way here. ## What happens when Every COM activation `WingetComSource` performs โ€” `PackageManager`, `GetLocalPackageCatalog`, `Connect`, and building `FindPackagesOptions`/ `PackageMatchFilter` โ€” happens in `WingetComSource::tryCreate()`, via the private `Impl::initialize()` it calls. Only `PackageCatalog::FindPackages` happens in `enumeratePackages()`. **`tryCreate()` reports failure by returning null and setting an out-parameter, never by throwing** (`docs/adr-phase-9.md` ADR-0040, issue #143) โ€” this is different from every other failure path in this codebase, and exists specifically so a host where COM activation is expected to fail every time does not raise a first-chance C++ exception on every `scan`/`fix` run. This split has a direct, user-visible consequence: - **`--source com`** calls `WingetComSource::tryCreate()` inside `createPackageSource()` (`PackageSourceFactory.cpp`). A construction failure is converted into a thrown `PackageSourceError` right there, at the boundary between the non-throwing `tryCreate()`/`PackageSourceCreation` contract and `createPackageSource()`'s own throwing one for this explicit case (`requireSource()`). A `--source com` run that gets past construction has already done all of its COM activation โ€” only `FindPackages` itself remains, deferred to the first `scan`/`fix` call into `enumeratePackages()`. Either failure point is **not** degraded: the user named `com` explicitly, so the `PackageSourceError` propagates. There is no attempt to also fall back to FS. - **`--source auto`** wraps both source factories in `AutoPackageSource` (`PackageSourceFactory.cpp`). Its `enumeratePackages()` calls the COM factory (which internally calls `tryCreate()`) and branches on the returned `PackageSourceCreation` without throwing; if construction succeeded, it then calls the resulting source's `enumeratePackages()` inside an ordinary `try` block. A failure at either the construction step (activation, reported not thrown) or the `FindPackages` step (thrown, caught) degrades identically to a filesystem scan. ## Enumeration ``` PackageManager โ””โ”€ GetLocalPackageCatalog(LocalPackageCatalog.InstalledPackages) โ””โ”€ PackageCatalogReference.Connect() โ†’ PackageCatalog โ””โ”€ FindPackages(FindPackagesOptions) โ†’ [CatalogPackage] โ””โ”€ CatalogPackage.InstalledVersion (PackageVersionInfo) โ†’ Id / Name / Version / InstalledLocation (no per-file alias) ``` `FindPackagesOptions` carries **exactly one filter**: `Field = Id`, `Option = ContainsCaseInsensitive`, `Value = L""` โ€” "Id contains the empty string," which matches every package without narrowing the result set. This works around `FindPackagesResultStatus::InvalidOptions` having been observed from an empty options object (no filters, no selectors) against some winget versions. `Selectors()` is never populated and no `ResultLimit` is set. `CreateCompositePackageCatalog`/ `CompositeSearchBehavior` are **not used anywhere in this codebase** โ€” only `GetLocalPackageCatalog(LocalPackageCatalog.InstalledPackages)` is called. Results are sorted ascending by `Id` before being returned, for determinism. For each match: 1. `CatalogPackage.InstalledVersion()`; a falsy result is skipped. 2. `GetMetadata(PackageVersionMetadataField::InstallerType)` is read (empty on any `hresult_error`) and compared against `L"portable"` via exact-length + `CompareStringOrdinal(..., bIgnoreCase = TRUE)` โ€” ordinal, not `_wcsicmp`/`towlower`, because a locale-dependent fold (e.g. the Turkish dotless-i) must not change the answer. No prefix, substring, or trim matching. A non-match is skipped. 3. `GetMetadata(PackageVersionMetadataField::InstalledLocation)` is read the same way. - **Empty**: the package is dropped entirely โ€” nothing to scan for executables โ€” rather than reconstructing a path from the `Packages\_` naming convention `FsScanSource` uses; that convention is a filesystem-source heuristic, not something COM's own metadata implies (`docs/adr-phase-2.md` ADR-0009). - **Non-empty**: used **verbatim**, with **no existence check, no `is_directory` check, no canonicalization, no reparse-point check**. A stale or nonexistent `InstalledLocation` therefore produces an `InstalledPackage` with zero executables (`collectExecutables` returns empty for a root that does not exist or is not a directory) โ€” it is **not** dropped unless `--include`/`--exclude` were supplied, since `PackageFilter::apply` only removes zero-executable packages when filtering is active. 4. `Id`, `Name`, and `InstalledVersion().Version()` are copied verbatim into the result. `PackageExe` (`src/core/Model.h`) has a single field, `path` โ€” there is no `metadataAlias` member. (`docs/adr-phase-2.md` ADR-0009's decision text says `WingetComSource` "never populates `PackageExe::metadataAlias`"; that field has since been removed from the model entirely โ€” see the amendment note on that ADR.) There is no COM-metadata alias tier at all: `PackageVersionMetadataField` has exactly six members (`InstallerType`, `InstalledScope`, `InstalledLocation`, `StandardUninstallCommand`, `SilentUninstallCommand`, `PublisherDisplayName`), and `IPackageVersionInfo2/3/4` add only version comparison, publisher, and installer metadata โ€” nothing alias-related. Alias resolution is entirely the job of the M3 regex rules in `docs/rules.md`. ## Failure and fallback `WingetComSource` never lets a raw `winrt::hresult_error` escape from any COM call site โ€” this is structurally enforced (`docs/adr-phase-9.md` ADR-0040 and ADR-0041): the three COM activations are performed through the non-throwing `createInstanceNoThrow()` helper instead of `winrt::create_instance`, and the two public entry points (`tryCreate()` and `enumeratePackages()`) own a final exception boundary that translates any escaping `winrt::hresult_error` into `PackageSourceError`. Unrelated `std::exception` or foreign exception types still propagate unchanged; they are programming/unexpected failures, not a reason to silently degrade into a filesystem scan. Failure is therefore reported as a `PackageSourceError`, classified by one of three independent rules below - **thrown** from every site except the three activations inside `tryCreate()`/`Impl::initialize()`, which **return** it instead (see "What happens when" above). **HRESULT โ†’ `PackageSourceErrorKind`** (`mapHresultToKind`, `PackageSourceError.cpp`), used for `PackageManager` activation, `GetLocalPackageCatalog`, `Connect` (when it throws rather than returning a non-`Ok` status), `FindPackagesOptions`/ `PackageMatchFilter` activation, and `FindPackages` (when it throws): | HRESULT | Kind | |---|---| | `REGDB_E_CLASSNOTREG` | `AppInstallerMissing` | | `CO_E_SERVER_EXEC_FAILURE` | `AppInstallerMissing` | | `CLASS_E_CLASSNOTAVAILABLE` | `AppInstallerMissing` | | `E_ACCESSDENIED` | `AccessDenied` | | `HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE)` (`0x800706BA`) | `ServerUnavailable` | | `RPC_E_DISCONNECTED` | `ServerUnavailable` | | `RPC_E_SERVER_DIED` | `ServerUnavailable` | | `HRESULT_FROM_WIN32(APPMODEL_ERROR_NO_PACKAGE)` (`0x80073D54`) | `PackageIdentityRequired` | | anything else | `Unknown` | `RPC_S_SERVER_UNAVAILABLE` is a Win32 error code (`1722`), not an HRESULT; a COM call surfaces it wrapped via `HRESULT_FROM_WIN32`. An earlier version of `mapHresultToKind` compared against the raw Win32 constant and never matched a real COM failure (fixed by `docs/adr-phase-9.md` ADR-0039). `PackageIdentityRequired` covers a case distinct from `AppInstallerMissing`: the winget COM server *is* registered and reachable (`winget` itself may work fine at the same time), but activating the typed WinRT `PackageManager` interface from this unpackaged, out-of-process caller is rejected. See `docs/adr-phase-9.md` ADR-0039 and issue #143 for the observed reproduction; the root cause of *why* activation is rejected on some hosts and not others remains open. `mapHresultToKind` is pure and winrt-independent, so it is unit-tested with synthetic HRESULTs (`tests/PackageSourceErrorTests.cpp`) without winget installed. **`FindPackagesResultStatus` โ†’ `PackageSourceErrorKind`** (`classifyFindPackagesStatus`, `WingetComSource.cpp`), used when `FindPackages` returns rather than throws, and its status is not `Ok`: | Status | Kind | |---|---| | `BlockedByPolicy` | `PolicyBlocked` | | `AccessDenied` | `AccessDenied` | | `CatalogError` | `CatalogError` | | `InternalError`, `InvalidOptions`, `AuthenticationError` (default) | `Unknown` | **`ConnectResultStatus`**: not a switch. **Any** non-`Ok` status โ€” including `SourceAgreementsNotAccepted`, which has no dedicated kind โ€” is hard-coded to `PackageSourceErrorKind::CatalogError`, with the HRESULT taken from `connectResult.ExtendedErrorCode()`. An `hresult_error` swallowed by `getMetadataOrEmpty` or by the per-match `catch` in the enumeration loop never reaches any of the above โ€” it is not a failure at all, just an empty field or a skipped package. **The `--source` contract**: | Value | Behavior | |---|---| | `com` | Construct and use `WingetComSource` only. Any `PackageSourceError` โ€” from activation or from `enumeratePackages()` โ€” propagates to the caller undegraded. | | `fs` | Use `FsScanSource` only. `WingetComSource` is never constructed. | | `auto` (default) | Try COM (construction **and** enumeration); on **any** `PackageSourceErrorKind`, degrade to `FsScanSource`. An empty COM result (zero portable packages found) is **not** a failure and does **not** degrade. | Contrary to a narrower reading, `auto` does not only degrade on activation-step failures โ€” `AutoPackageSource::enumeratePackages()` wraps both the constructor call and `enumeratePackages()` in one `try`/`catch (const PackageSourceError&)`, so a failure inside `FindPackages` mid-enumeration degrades exactly the same way as a failure activating `PackageManager`. The discriminator is the **exception type**, not the kind: a non-`PackageSourceError` exception (`std::bad_alloc`, a logic error) propagates untouched, by design. On degrade, the CLI prints (to stderr, at normal importance โ€” so suppressed by `--quiet` but not by default): ``` warning: --source auto fell back to a filesystem scan: ``` and, additionally under `--verbose`: ``` verbose: package source - requested: auto, used: filesystem (degraded: ) ``` When package enumeration ultimately fails (for example `--source com`, `--source fs`, or an `auto` run whose filesystem fallback also fails), `cli::run()` prints: ```text hint: ``` The hint text points to the public troubleshooting page at . A successful `--source auto` degradation does **not** print this hint: only the existing warning/verbose messages above. See also [`troubleshooting.md`](./troubleshooting.md). (or `requested: auto, used: com` / `requested: com, used: com` / `requested: fs, used: fs` when no degrade occurred โ€” these strings are derived from `options.source` alone and are not independently cross-checked against which source actually ran). **Exit codes**: every one of the eight `PackageSourceErrorKind` values maps to `ExitCode::PackageEnumerationFailed` (`4`) in `src/cli/Dispatch.cpp::exitCodeFor()`. `ExitCode::InsufficientPermission` (`2`) is never reached from a package-source error โ€” `AccessDenied` and `PolicyBlocked` both map to `4`, the same as every other kind. `2` is reserved for `SymlinkService` permission failures (Developer Mode / privilege), a different subsystem entirely. ## Capabilities / permissions What is directly observable from this codebase: `src/app.manifest` is a plain unpackaged manifest (`asInvoker`, `longPathAware`), with no AppX capability declared โ€” there is nowhere in an unpackaged manifest to declare one. The out-of-process server activates via a fixed CLSID rather than any packaged-identity mechanism. A live verification run (ADR-0037) reproduced a concrete, reproducible failure on one tested machine and App Installer version: `winrt::create_instance` (as the code was at the time; see "Activation" above for the equivalent throw-free call the code makes today, ADR-0040) failed with `HRESULT_FROM_WIN32(APPMODEL_ERROR_NO_PACKAGE)` (`0x80073D54`) even though `winget list` (the real winget CLI) worked normally and a bare `CoCreateInstance` of the same CLSID requesting only `IUnknown` succeeded โ€” the failure was specific to activating the **typed** `IPackageManager` interface out-of-process from this unpackaged caller. See ADR-0037 for the exact reproduction, and ADR-0039 for `mapHresultToKind` gaining the dedicated `PackageIdentityRequired` kind so this case's diagnostic message names the actual cause instead of the generic activation-failure wording. This is recorded as an **observed, environment- and possibly version-specific data point**, not a general rule: it was not established whether every unpackaged process on every supported Windows build and every App Installer version hits this, and no capability string or integrity-level requirement is asserted as fact here, because none could be confirmed from source or from Microsoft's own public documentation of this API. Treat `--source com` as something to verify empirically on the machine you care about (`scan --source com --verbose`), not as guaranteed by this document. Out-of-process COM server activation may also fail in environments without an interactive window station/desktop (headless CI, some sandboxes) โ€” a process-level failure at the activation step is equivalent to `AppInstallerMissing`/`ServerUnavailable`/`PackageIdentityRequired` for `--source auto` purposes: whichever kind `mapHresultToKind` assigns, `AutoPackageSource` degrades to the filesystem scan the same way. Do not assume an automated test proves `--source com` works in every environment; verify it in the environment that actually matters to you. ## Extending this code - Public headers in `src/core/` must never name a winrt type. Only `syncwingetlink.core.vcxproj` imports the projection-generation target, so `tests/syncwingetlink.tests.vcxproj` has no include path to the generated headers. `WingetComSource.h` confines all winrt types to `WingetComSource.cpp` behind a pimpl (`struct Impl`); follow the same pattern for any new COM-backed type. - Any new file that activates a WinRT class from this namespace needs `` explicitly (see "Activation" above) and, if it is the first translation unit in the static library to reference `winrt/base.h` machinery not already covered, may need its own `#pragma comment(lib, "runtimeobject.lib")` โ€” though in practice one is already enough for the whole archive. - `FindPackagesOptions`/`PackageMatchFilter`-style objects that carry no per-call state should be built once and reused, not rebuilt per call โ€” see "Out-of-proc vs in-proc." ## Known caveats - **There is no per-file alias mapping in this API at all** โ€” not "may not fully return it." Alias resolution is entirely the job of the M3 regex rules in `docs/rules.md`; see `docs/adr-phase-2.md` ADR-0009/ADR-0012. - A portable package whose `InstalledLocation` comes back **non-empty but stale** is kept in the enumeration result with zero executables rather than dropped, unless `--include`/`--exclude` filtering is active โ€” see "Enumeration" above. - The winmd this code projects against is never pinned to a specific App Installer version (see "Build-time projection"); behavior across versions is not guaranteed identical, and the `APPMODEL_ERROR_NO_PACKAGE` finding under "Capabilities / permissions" is a concrete instance of that risk. The FS fallback via `--source auto` is effectively required in practice, not merely a nice-to-have.