feat: add winapp find-api — local Windows/WinRT API metadata search (port of winmd) - #744
feat: add winapp find-api — local Windows/WinRT API metadata search (port of winmd)#744Jaylyn Barbee (Jaylyn-Barbee) wants to merge 16 commits into
Conversation
Port the standalone winmd API-metadata search tool into the CLI as a first-class `winapp find-api` command group (issue #626), mirroring the find-ui port structurally. The bare form searches; sub-verbs (members, check-property, types, enums, namespaces, packages, stats, projects, refresh) drill in. The index is built from a project's restored packages under the global .winapp cache and auto-refreshes when project.assets.json changes. - ApiSearch engine (read side + cache builder) re-namespaced to WinApp.Cli.Services.ApiSearch; AOT-safe via System.Reflection.Metadata and source-gen JSON. - ApiMetadataService: cache-dir resolution, lazy auto-indexing with a file lock, and --project/--project-dir manifest resolution. - Command surface + shared emit/render plumbing; bounded, non-PII usage telemetry (FindApiUsageEvent). - DI wiring, JSON output models, root command "API Discovery" category. - Tests: engine (synthetic cache incl. global-namespace regression), service resolution, command routing/exit codes, telemetry. - Docs: usage.md, README, hand-written skill fragment; regenerated cli-schema.json and agent skills. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b442383b-d39a-4a3d-b08e-67ffed6abf50
… packaging) Security: - Invoke WinAppSDK runtime detection via absolute Windows PowerShell path instead of bare "powershell.exe" (binary-planting guard). - Add ApiCachePaths with namespace-filename sanitization and a cache-path containment check so untrusted namespaces / package Id/Version can't traverse outside the cache directory. Correctness: - check-property: only a Property counts as "found" (a like-named method or event no longer reports a false positive). - Members/enums: resolve short type names via ResolveType, matching the advertised help and check-property behavior. - ResolveManifest: an explicit --project-dir with no match reports "not indexed" instead of silently answering for a lone cached project. - RunIndexWithLock: narrow the IOException contention catch to lock acquisition only so real indexing I/O errors aren't misreported (and don't trigger a needless 30s wait). - refresh now forces a full rebuild (bypasses reused caches); project references (version "local") are always re-exported. CLI UX: - refresh progress respects --quiet. - refresh honors --project (refreshes the named project's recorded dir). Docs / packaging: - Add find-api to the hand-written agent command reference (+ .claude mirror). - generate-commands.mjs now emits invokable branch commands, so the bare `find-api <query>` wrapper is generated; regenerated winapp-commands.ts and docs/npm-usage.md. Tests: add regressions for the property-kind collision, explicit --project-dir no-match, short-name members/enums, and refresh force/--project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b442383b-d39a-4a3d-b08e-67ffed6abf50
…layout Brings winmd-port up to date with main (18 commits) and migrates the find-api plugin skill from the removed fragment-based layout to main's hand-authored per-skill layout: - Add plugins/winapp/skills/winapp-find-api/SKILL.md (no version frontmatter, hand-authored CLI reference + related-skills cross-links) - Remove old-layout .claude/, .github/plugin/skills/winapp-cli/, and docs/fragments/skills/ find-api files (removed on main) - find-api agent section preserved via main's agent-file rename - Regenerate cli-schema.json, winapp-commands.ts, npm-usage.md from the merged schema (find-api + embed-identity) - Take main's generate-llm-docs.ps1 (fragment skill generator removed) Validated: solution builds, 41 find-api tests pass, 230 npm tests pass, TS lint/format/compile clean, generate-commands/generate-docs --check in sync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a38f2203-35c1-4b34-ae9e-d3a46627705a
…dexing
Resolves the find-api/find-ui merge and consolidates the two discovery
commands where they had drifted apart.
Merge resolutions (kept both features):
- Root command registers find-api and find-ui under one "Discovery" help group.
- WinAppJsonContext registers both find-api and find-ui JSON output models.
- README / agent doc list both under a single "Discovery" heading.
- Regenerated cli-schema.json, npm-usage.md, and winapp-commands.ts.
Parallelize winmd indexing (find-api refresh):
- Resolve every project first, dedupe packages by cache directory, then export
the distinct packages in parallel, so a package shared by several projects is
parsed once per run instead of once per project.
- Parallelize .winmd parsing, XML-doc parsing, and per-namespace cache writes
within a package. Results are reassembled in file order, so output stays
byte-identical to the sequential build (verified against a full sample index).
- Serialize the progress callback; the console sink behind it is not thread-safe.
- Write package meta.json (the reuse sentinel) and project manifests last, so an
interrupted run never advertises a cache whose payload is missing.
- ~1.8x faster on a 9-package / 127-winmd WinUI project (4.34s -> 2.46s).
Consolidation between find-api and find-ui:
- Program.cs: the parse-error -> flat {"error":...} JSON bridge covered find-ui
only, so `find-api --max abc --json` printed human help text. Generalized to
both discovery commands (IsFlatJsonErrorCommand / EmitFlatJsonError).
- ApiCacheBuilder's private atomic-write helper now delegates to the shared
PathSafety helper (new sync AtomicWriteAllText next to the async one).
- docs/usage.md: find-api moved next to find-ui, with reciprocal "Related" notes
so the API-surface vs WinUI-sample vs running-app distinction is explicit.
- docs/telemetry.md documents the find-api usage event alongside find-ui.
- find-ui skill description now disambiguates itself from find-api.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
…eries from an unrelated project A `find-api` query from a directory with no project could be silently answered from whichever project happened to be the only one in the global cache, because `ResolveManifest` had a "lone cached project" fallback and the cache is global (~/.winapp/cache/find-api/). The answer therefore depended on unrelated global state: with one project cached the query returned confident results from the wrong project; with two it errored instead. No output model except `stats` carried the project name, so `--json` consumers could not detect the swap. Replace that fallback with an explicit machine-wide SDK scope. The Windows SDK and Windows App SDK metadata is already project-independent (NuGetResolver's SDK lookups never took a projectDir), so it can back a real scope of its own. Resolution precedence is now: --project <name> (or `sdk`) -> --project-dir -> project in cwd -> SDK scope A projectless query never consults the cached project list, so results cannot depend on unrelated global state. A --project-dir pointing at a real but unindexed project still errors rather than narrowing to the SDK, since narrowing would hide that project's NuGet packages and make its types look nonexistent. Every scoped payload now carries a `scope` field (`project` or `sdk`), stamped centrally in `WithManifest`, and text mode prints a note when the SDK scope answered. The SDK manifest is written to `sdk.json` as a sibling of `projects/` so it can never collide with a real project manifest or show up in `find-api projects`. Also extracts `NuGetResolver.FindSdkPackages` and `ApiCacheBuilder.ResolvePackageExports` to share logic with the new `BuildSdkCache`, and adds an `ISdkPackageSource` seam so tests do not depend on the machine's installed SDK. Known gap: the SDK index has no staleness check, so a newly installed Windows SDK needs `find-api refresh --project sdk`. Project scopes self-refresh off project.assets.json timestamps; there is no cheap equivalent signal for the SDK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
…ollide Manifests were written as <projectName>.json with no path component unless --scan was passed, so two projects with the same name in different directories overwrote each other in the shared cache. Resolution then compounded it: the --project-dir branch verified manifest.ProjectDir, but the current-directory branch, the --project-dir name fallback, and AutoIndexIfStale all matched on file name alone. A query could therefore be answered from a different project's index -- wrong package set, presented as authoritative -- and AutoIndexIfStale would treat the foreign manifest as "found" and skip re-indexing the project actually being queried. This is easy to hit in practice: monorepos, and any workflow that scaffolds the same template repeatedly. It was observed in a benchmark sweep where several trials built an identically-named app in different directories and one trial's manifest ended up pointing at another trial's directory. - ApiCacheBuilder.ManifestName() now always appends a short hash of the project's full path, so the cache key is unique per project location. --scan no longer changes naming; it only affects which projects are discovered. - ApiMetadataService resolves manifests by their recorded ProjectDir rather than by file name, via a shared FindManifestPathForDir helper used by the current-directory branch, the --project-dir branch, and AutoIndexIfStale. Matching on ProjectDir also keeps legacy unhashed manifests resolvable. - The --project-dir name fallback is removed outright: if no manifest claims that directory, the project is not indexed, and saying so is correct. - --project <name> now collapses candidates by ProjectDir and reports an ambiguity error listing the directories when several indexed projects share a name, instead of silently returning whichever was enumerated first. Adds four regression tests, including the same-name-different-directory case for both cwd and --project-dir resolution. Existing tests that gave a manifest a ProjectDir one level below the current directory were corrected to record the directory that actually holds the project, which is what indexing writes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
…rs/enums Three changes driven by an A/B benchmark of find-api against a baseline agent. Report which index answered. Only `packages` and `stats` carried a project name, and nothing carried a project directory, so a caller could see `scope: project` but not *which* project produced the result. Auditing the benchmark run had to infer this from cache-file timestamps. Every scoped payload now reports `projectName` and `projectDir`, stamped at the single existing scope-stamp site. Project names are not unique across directories, so the directory is the only reliable identity. Add `--filter` to `members` and `enums`. A benchmark trial ran `enums Symbol` seven times, grepping the 197-value dump differently each time, because the tool offered no way to narrow it. `--filter` is a case-insensitive substring match on the member/value name and reports the unfiltered totals alongside the narrowed list, so a filtered view is never mistaken for a small API. A filter that matches nothing still exits 0 -- that is "nothing matched", not "no such type". Update the skill to use find-api on compile errors. The benchmark showed 13 of 21 type/member errors were fixed by editing blind with no lookup, even though the tool was available and the errors were exactly claims about the API surface. The skill now maps CS0246/CS0117/CS1061/CS0104/XAML-unknown-member to the query that answers each, directs callers to filter rather than dump-and-grep, and states the case for check-property more firmly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
Benchmark iterations 1 and 2 showed find-api's marginal cost is dominated
by the conversation context re-sent on every turn, not by payload size.
The lever that actually works is issuing fewer calls, so this makes the
common verbs batchable and stops optimizing for bytes.
Batching. search, members, enums, and check-property now take multiple
subjects per invocation. One subject returns the exact payload it always
did (text and --json) so nothing existing breaks; two or more return a
{ count, results } envelope, with missingCount added for check-property.
check-property batches properties on one type -- type first, then every
property -- which covers the dominant case unambiguously. A batch exits 0
only when every subject resolved and was found, so batching can never
silently hide a miss. Verifying five properties goes from five turns and
775 chars to one turn and 299.
Attribution in text mode. --json was used once in twelve trials, so the
scope/projectName/projectDir work landed in a payload almost nobody read.
Every scoped verb now prints its source in text mode too.
check-property weight. 21 of 23 checks came back found:true, so the full
near-miss block was paid for on every call and used by almost none. In
batch mode a hit is one line; the full suggestion detail still prints on
a miss, where it is what resolves the question.
--filter guidance. 16 filtered enum calls cost ~3,434 tokens against ~582
for a single unfiltered dump -- 5.9x worse. Guidance now differentiates:
filter large member lists, dump enums whole, and never re-run the same
command with different filter text.
types, namespaces, and projects are hidden (not removed) after zero
invocations across 48 trial-runs. They still work when called explicitly.
Adds 15 tests covering batch fan-out, single-subject back-compat, batch
exit codes, JSON envelope shape, and verb visibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
…ol at a time Run5 measured 57% of find-api calls as single-subject (median 1), despite an overall mean of 3.36 subjects per call. The large batches come from agents front-loading before writing code; the reactive debug loop was still one lookup per symbol. Adds an explicit trigger for the second batching moment: read the whole build error list, collect every uncertain symbol across all of it, and verify in one call before editing. Fixing errors one at a time costs a lookup, an edit, and a full rebuild per symbol, and each rebuild tends to surface the next bad symbol that the same call could have caught. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
Brings in `winapp new` (#686), build-tool signature verification, and the other 15 commits on main since this branch diverged. Two conflicts: - Program.cs — main added a parse-error JSON bridge for `winapp new`, while this branch had generalized the find-ui bridge's `IsFindUi` predicate into `IsFlatJsonErrorCommand` so it also covers find-api. Both are wanted, so main's `new` block is kept ahead of the generalized discovery bridge. - docs/cli-schema.json — generated file. Regenerated from the merged build rather than hand-resolved; verified a strict superset of main's schema (nothing dropped, 7 find-api entries added). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
bd083aa to
4a9870e
Compare
A local Debug build self-reports 1.0.0, so regenerating the schema off one wrote that placeholder into the committed file. validate-llm-docs.ps1 normalizes the fresh schema's version to version.json (0.6.1) before comparing, so the committed 1.0.0 read as drift and failed validate-docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
Build Metrics ReportBinary Sizes
Test Results✅ 4641 passed, 5 skipped out of 4646 tests in 630.6s (+79 tests, -59.9s vs. baseline) Test Coverage✅ 85.4% line coverage, 78.4% branch coverage · CLI Startup Time59ms median (x64, Try This BuildInstalls the MSIX for your architecture, replacing any previously installed build. Needs the GitHub CLI — the command offers to install it and sign you in if it is missing. & ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) 744Switching between builds often?Put the tool on your PATH once: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPathThen this build is just: winapp-pr 744Run Updated 2026-08-13 21:07:23 UTC · commit |
The Release build treats warnings as errors, so the eight CollectionAssert
call sites passing inline \
ew[] { ... }\ literals failed CI's build and
build-and-package jobs. Debug only surfaced them as warnings, which is why
they were missed locally.
Follows the existing convention in the test project (ControlsFetchNoticeTests,
MsBuildPropertyReaderTests, RunCommandTests, and others).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
Search printed the absolute on-disk cache path for every namespace hit. It is a debugging aid rather than an answer, and it was 35-41%% of the response on queries where it appeared -- cost paid on every call, multiplied by batching. Default output is now 41-47%% smaller on those queries; --verbose still shows the paths for diagnosing a stale or unexpected index. The fake metadata service returned an empty file list, so no existing test could have caught this; it now returns a path and two tests pin the behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
Converts imperative accumulate-into-list loops into equivalent LINQ projections where the transform is a straightforward filter/map: - Scoring.allTermsMatch -> terms.All(...) - NuGetResolver.IsFrameworkPackage -> prefixes.Any(...) - NuGetResolver compile-entry, dll-path, and xmlDoc collection -> Select/Where/SelectMany - WinMdParser.ParseEnumValues and GetMethodParameters -> Select/Where - ApiQueryEngine namespace merge -> HashSet.UnionWith Behavior is unchanged; short-circuiting is preserved in the All/Any cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81234fca-8857-4447-805a-1971ff162b67
Re: the
|
| Site | Behavior on failure |
|---|---|
PathSafety.cs:240 |
Best-effort temp cleanup, then throw; — the original error is preserved and rethrown. Swallowing a cleanup failure here is required to avoid masking the real one. |
ApiCacheBuilder.cs:334 |
Atomic write fails -> falls back to a plain write. |
ApiCacheBuilder.cs:438 |
SDK-discovery subprocess fails -> returns null, caller handles it. |
ApiMetadataService.cs:266 |
Already catch (Exception ex) with LogWarning; auto-indexing is explicitly best-effort. |
ApiMetadataService.cs:428 |
Already catch (Exception ex) with LogWarning, returns ResolvedScope.Failed(...). |
ApiMetadataService.cs:446 |
Corrupt manifest -> null (treated as "no cache", triggers re-index). |
ApiQueryEngine.cs:330 |
Unreadable package meta -> reports that package as meta-unreadable instead of failing the whole listing. |
ApiQueryEngine.cs:361 |
Stats aggregation skips an unreadable package rather than aborting. |
ApiQueryEngine.cs:636, 648 |
Deserialize -> null on a corrupt cache file. |
Note that ApiMetadataService.cs:266 and :428 already catch a typed Exception and log it — the rule flags them regardless of the logging.
The LINQ findings from the same review were addressed in 88c025c (8 of 11). Three were declined on merit and called out there: Scoring.IsFuzzySubsequence is a stateful sequential scan with an early return (not a projection), and ApiQueryEngine.cs:245/543 use a side-effecting seen.Add(...) predicate that would depend on mutation during lazy enumeration.
Description
Adds
winapp find-api— a fully local, offline search over the Windows/WinRT API surface a project can actually see, resolved from its own restored NuGet/SDK packages. It answers "does this type exist, what's on it, and did I spell this property right?" from the real metadata instead of from memory.This is the port of the
winmdtool out of microsoft/win-dev-skills (src/tools/winmd-cli/) and intowinapp, per #652 — but not a 1:1 copy. The query engine (metadata parsing, XML-doc extraction, scoring, near-miss suggestions) came over largely intact; the surface around it was reworked to fitwinappconventions and to fix real defects found along the way.Why it matters to users: the answer is grounded in your project's referenced metadata, so it's correct for your package versions rather than for whatever version the model was trained on. It also fails loudly —
check-propertyexits non-zero on a miss, so it can gate a step instead of just printing advice. And it works before a project exists (--project sdk), so it's useful during scaffolding, not only after restore.Usage Example
Related Issue
Closes #652
Type of Change
Checklist
plugins/winapp/skills/(winapp-find-api/SKILL.md) andplugins/winapp/agents/, notdocs/fragments/skills/Additional Notes
What's new versus the
winmdtool this ports from. The query core is at parity by design; these are the deltas worth reviewing:search,members,enums, andcheck-propertyeach accept several subjects in one call. This is the biggest change in how the command gets used — five property checks go from five calls to one, and the combined output is ~60% smaller than the five separate responses. Two API contracts here are worth explicit scrutiny because they're hard to change later: (1) a single subject returns the original payload shape in both text and--json— the{ count, results }envelope only appears for two or more subjects, so existing single-subject callers are unaffected; (2) a batch exits0only if every subject resolved and was found.check-propertybatches N properties against one type (type first) — the dottedType.Propertymulti-type form was considered and rejected as too implicit.--project sdk). The original could only answer against an indexed project. Previously, a projectless query would get silently answered from some unrelated indexed project — that's now impossible; you either get the SDK scope or an explicit error.Project: <name> (<dir>)in text,scope/projectName/projectDirin--json). Without this you can't tell a wrong-project answer from a right one.--filteronmembers/enums. The original had--filterfor namespace prefixes only. Guidance is deliberately asymmetric: filter long member lists, dump enums whole — a filtered enum lookup usually costs more than just reading all the values.types,namespaces,projects). Still fully callable, just not surfaced in help or the CLI schema — they overlap withsearch/packagesand were adding noise to the command surface without earning it.check-propertybatch output — one line per confirmed property, full detail (near-miss suggestions, attached-property forms, other declaring types) only on a miss, which is the case where you actually need it.Scoped out, tracked separately. Search and ranking quality are unchanged from the ported implementation — this PR does not claim to find APIs better than
winmddid, only to make it cheaper to call, honest about which index answered, and usable outside a project. Acheck-fileverb — validate every API reference in a file you just wrote, before building — is filed as #743.Testing. 411/411 targeted find-api, CLI-schema, and help tests pass. Full suite: 4463/4469. The 6 failures were confirmed pre-existing by stashing this branch, rebuilding clean, and reproducing them identically — 5 Node/Electron E2E and
AnalyzeDumpAsync_ManagedDumpWithDeepStack(the known ARM64-host-analyzing-an-x64-dump environmental failure). End-to-end verified againstsamples/winui-app.Note for reviewers on diff size. This branch contains the initial port as well as the changes above, so the diff is large. The files that carry the design decisions are
Commands/FindApiShared.cs,Commands/FindApiVerbs.cs, andplugins/winapp/skills/winapp-find-api/SKILL.md.Follow-up for #652. Once this lands, the old
winmdreferences in microsoft/win-dev-skills (src/tools/winmd-cli/plus the skills/docs pointing at it) should be removed so there's no duplicated implementation across the two repos.