feat: add winapp find-api — local Windows/WinRT API metadata search (port of winmd) - #744
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✅ 5804 passed, 18 skipped out of 5822 tests in 1025.7s (+396 tests, +262.4s vs. baseline) Test Coverage✅ 87.3% line coverage, 80.7% branch coverage · CLI Startup Time63ms 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-09-11 02:28:34 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.
… twins
Search() walked every package cache and scored each type it found, with no
dedupe -- unlike LoadAllTypes(), which every other verb goes through and which
has always kept a `seen` set. Because a type routinely ships in more than one
package in the same graph (Microsoft.UI.Xaml.Controls.Button is in both
WinAppSdkRuntime and Microsoft.WindowsAppSDK.WinUI), the same fully-qualified
name was emitted once per package.
That produced two user-visible defects:
- Ambiguity candidate lists repeated the same fully-qualified name, so the
accompanying "use the fully-qualified name to disambiguate" advice could not
actually disambiguate them. On a WinUI project, 44% of the candidates
reported for "NavigationView" were exact duplicates.
- Per-namespace Matches is capped with .Take(5), so duplicates crowded genuine
results out of the output entirely.
Also excludes CsWinRT's generated ABI.* projection twins from search. They are
marshalling internals nobody writes code against, and since "ABI.Microsoft.UI.
Xaml.Controls" counts as a distinct namespace prefix, they could fabricate a
CS0104 ambiguity warning for a type that really lives in one namespace.
Both filters run before scoring, so they cut work as well as output. Filtering
is scoped to Search() only; LoadAllTypes() is deliberately left unfiltered so
`find-api members ABI.Foo` still resolves for interop debugging.
Measured on samples/winui-app, JSON output size:
NavigationView 38,322 -> 17,001 chars (-56%), 105 -> 38 candidates
Button 70,636 -> 42,215 chars (-40%), 168 -> 62 candidates
AppWindow 12,863 -> 8,792 chars (-32%), 24 -> 8 candidates
acrylic brush 1,743 -> 964 chars (-45%)
StorageFile 7,356 -> 6,786 chars (-8%)
Zero duplicate and zero ABI candidates remain in any of them. Adds 5 regression
tests covering multi-package collapse, the Take(5) crowd-out, ABI exclusion from
search, ABI-only types not raising false ambiguity, and ABI types still being
reachable by exact name.
Deduping inherited members on the substituted signature let one declared member hide another declared by the same type. A base declaring both M(T) and M(String), reached as Base<String>, substitutes both to `void M(String item)`, so the second was dropped and the member vanished from output with no warning. Hold a supertype's dedup keys back until the whole type has been walked. Two members of one declaring type can no longer hide each other, while a derived type that redeclares an inherited member still hides the base copy, because its keys are recorded before the base is dequeued. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
A rendered signature spells types and names the same way, so rewriting
every matching identifier renamed anything spelled like a type parameter.
`Base<T>.Add(T T)` reached through `Base<String>` rendered as
`void Add(String String)`, and a property named `T` became `String String
{ get; set; }`. ApiMemberOutput carries no separate parameter list, so the
signature is the whole of what a caller sees — there was no correct field
left to fall back on.
Substitute the head of the signature, which is the only part that carries
types, and rebuild a parameter list from the parameters themselves rather
than parsing it back out of the string. A signature whose declared names
cannot collide keeps the plain rewrite verbatim, so output for real
metadata is unchanged: a sweep of 668 indexed SDK types (15,376 members)
produced byte-identical results.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
Nikola Metulev (nmetulev)
left a comment
There was a problem hiding this comment.
🤖 AI-generated review (winappcli pr-review skill) — verify before acting.
Thanks for the fixes. I’ve narrowed this to the changes that matter most: useful package-based discovery, clear first use, and trustworthy answers. No term aliases, generated tutorials, or universal NuGet catalog are requested.
1. Show usage for bare find-api
winapp find-api currently errors and exits 1. Please show short usage and examples instead; keep errors for genuinely malformed arguments. Entering the command should help someone discover how to use it.
2. Search package descriptions and meaningful words
winapp find-api "text generation" finds nothing, although the package describes LanguageModel as providing text generation and embeddings.
Meanwhile, llm matches letters inside scroLLMode and returns scrolling APIs:
Please search the available type/member descriptions and improve identifier-word matching while preserving exact lookup. A package-only prototype improved useful first-three results from 15/28 queries to 24/28, including 5/11 to 9/11 on held-out queries.
There is no requirement to recognize “Phi Silica” or “LLM” when those terms are absent from package information. An honest miss is better than unrelated matches.
3. Keep answers concise without losing important meaning
Show a few strong matches with their purpose, owning package/version, and the matching member signature when relevant. Keep filtered drill-down. Emit coverage warnings once, and do not count a framework plus its runtime-specific restore entry as two frameworks.
These remaining correctness cases should also be addressed:
| Current behavior | Requested change | Why |
|---|---|---|
| A metadata file named in the restore inventory disappears; refresh succeeds, then InfoBar is “not found” without explaining the missing file. | Mark coverage incomplete and recommend restore. | Missing evidence is not evidence that the API is absent. |
Getter-only attached Level gets the same green confirmation as writable Count; structured writability is omitted. |
Label the read-only case and provide writable: false. |
Finding a property does not make assignment valid. |
| Rename Before.csproj to After.csproj, restore and refresh; the tool still offers both projects. | Ignore or remove the obsolete manifest. | A normal rename should not leave a phantom project that blocks queries. |
4. Preserve the local-read safety boundary
Repository-selected paths still have routes around the checks: an assembly name such as ..\redirect\Poison reaches file enumeration; external lockfile paths bypass reparse checks; and the direct assets path checks its parent rather than the final file.
Please require a literal assembly filename and validate repository-supplied paths, including the final assets file, before reading them. A local metadata query must not follow a repository-selected link to an unintended location or network share.
Source: assembly name · external paths · assets file.
These paths were reconfirmed in current source. Earlier traversal used loopback SMB; no credential capture or exfiltration was performed.
5. V1 proposal: separately cached latest-stable SDK discovery
This is a feature addition, not a defect in local-only lookup. An older project already has LanguageModel but lacks GenerateStructuredJsonResponseAsync; a newer SDK contains it.
Please allow discovery in a cached latest-stable Windows App SDK catalog, clearly separate from project references:
GenerateStructuredJsonResponseAsync
Project index (AI 1.8.53): no matching member
SDK catalog (Windows App SDK 2.4.0 / AI 2.4.4): found
The experiment acquired about 175 MB of selected SDK/API packages and produced a roughly 4.9 MB metadata cache. Make the initial download explicit and reuse the cache; do not modify the project or install a runtime. “Found in version X” must not imply a minimum version, compilation success, or device compatibility.
Images show rendered captures of actual command output from 9fdb3af1, not native terminal-window screenshots. Paths are shortened and excerpts labeled. Fixed generic-substitution, XML-reader, restore-ownership and npm-documentation findings have been removed.
A cloned repository chooses these values, so reading through them without a check lets a clone steer local metadata reading. <AssemblyName> became a search pattern, and a search pattern is not confined to the directory it is rooted at: Directory.GetFiles(bin, @"..\redirect\Poison.dll") returns that file, and `..\..\*.dll` climbs further. Require a literal file name, and refuse wildcards for the same reason the caller takes only the referenced project's own output. project.assets.json was reached after checking `obj` but not the file itself, so an ordinary directory holding a linked assets file was read. Check the final path, which is what CrossesReparsePoint already walks. A path outside the repository root skipped the reparse check by design, because a package cache relocated with a junction is a normal setup. That is kept: only a link resolving off the machine is refused now, which is the outbound SMB reach the lexical network check cannot see. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
Typing `winapp find-api` is how someone finds out what the command does, and answering that with a red error and exit 1 turns a discovery moment into a failure. Show a short usage block with the shapes people reach for first, and exit 0. A caller that passed --json is making a programmatic call with a required argument missing, which is a real error, so that keeps the structured failure — handing it usage text would only break its parser. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
An attached property with a GetXxx but no SetXxx cannot be assigned, in markup or in code, yet check-property confirmed it with the same plain tick a settable one gets and left `writable` out of the JSON entirely. A caller acts on that and writes an assignment that does not compile. The detail view did say "(read-only)" in the accessor prose, but led with the tick a reader skims; the compact view — what a batch check shows — dropped the fact completely. Detection already knows whether a setter exists, so carry that out as the same `writable` answer a plain property gives and let both views use the marker they already have for it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
--json is what an agent reads, so the structured field is the part of the read-only fix that has to stay. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
Rename Before.csproj to After.csproj, restore, and refresh: a manifest is written for After and Before's is left behind, still naming the same directory. The directory now reads as holding two indexed projects, so every query there fails with "pick one" — and one of the two choices cannot be selected, because the project is gone. Drop a manifest whose project file is no longer on disk. That is enough for a rename to look like what it is, and it costs nothing when the project is still there. The stale cache entry is left alone rather than deleted; it is harmless once nothing offers it. The test fixtures wrote manifests without ever creating the project file they named, which is not a state the indexer can produce. They create it now, so a missing file means what it says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
…gone Both restore inventories silently dropped metadata files that were selected for the build but are no longer on disk: project.assets.json filtered its chosen compile assets through File.Exists, and the winmds lockfile skipped entries whose .winmd files were missing. A cleared NuGet cache or a partial sync therefore produced an index that answered "no such API" for everything those files defined, with nothing to attribute the gap to. Both paths now name the missing files through the existing coverage- caveat channel, so every later answer is qualified and points at the restore command that fixes it. The list is capped at five names because a wholly unrestored project can name hundreds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
Two ways the coverage warnings misrepresented what they described. A RID-specific restore entry (net8.0-windows10.0.26100.0/win-x64) sits next to the framework it belongs to in project.assets.json, and both matched the Windows moniker. An ordinary single-framework project was therefore told it targets two Windows frameworks and that its answers may not hold for the other one. Frameworks are now counted by the part before the RID. A caveat is usually about the machine or a shared package rather than one project, so in a solution every project raises the same sentence. Refresh printed it once per project. It is now printed once per run; each project still keeps its own copy, so answers stay qualified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
Searching for 'llm' returned scrolling APIs. 'llm' is a substring of scroLLMode and a subsequence of much of the API surface, so the contains and fuzzy bands both matched letters that start no word. A confident unrelated answer is worse than nothing, because the caller has no way to tell it apart from a real one. Every band below prefix now requires the match to begin where a word begins, using PascalCase, digit-run and separator boundaries. 'Mode' still finds ScrollMode, 'Stream' still finds IOStream and a typo like 'Buton' still finds Button; 'llm' now finds neither. The bands are also read by check-property, which keeps any suggestion scoring 40 or more, so the matches those callers depend on are locked down by tests first. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
'text generation' found nothing, although the documentation for LanguageModel says in as many words that it provides text generation. Someone searching that way is describing the task, not naming the type, and the summary is the only place that intent is written down. A type or member whose name does not match at all is now scored against its summary, requiring every word of the query to appear so one incidental word cannot drag a summary in. The band sits below every name band, including fuzzy, so a named API always outranks a prose match and this only surfaces when nothing was named. Descriptions were already merged into the cache during indexing, so no cache format change and no refresh are needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
A search returned a ranked list of names, so choosing between them meant opening each one. Two facts already in the index answer that in place: the documentation summary says what the API is for, and the package id and version say what the answer is true of - the same type name ships from more than one package in a normal graph. Each match now carries both. The summary is cut to its first sentence and bounded, because a result list is only concise if each line stays a line. The per-namespace grouping and the member signature on a member-driven match are unchanged, so the drill-down still works the same way. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
The solution build enables CA1861, which per-project builds do not, so a constant array argument added with the stale-manifest test failed the full build while the targeted test run stayed green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
The usage page and the shipped skill still described search as matching type and member names only, and showed output that no longer matches what the command prints. Both now describe word-level name matching, the description fallback and its limits, the package/summary line on every hit, and the bare invocation that prints usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 87a80d05-6e3a-435b-b715-261310ad03a4
Everything else from that review has been fixed. |
Jaylyn Barbee (@Jaylyn-Barbee) You're right about AI 1.8.53: it has no API XML documentation. Our example used
I should have made that version distinction explicit in the review. This is also a useful example of why searching a latest-SDK catalog alongside
The proposal is to discover with both sources, then use the referenced metadata This remains entirely package-derived: no aliases, invented documentation or |
So this will be resolved by follow up issue #832, if I am understanding correctly? |
Read immediate reparse targets without opening their destinations, validate local chains with cycle and hop bounds, and keep Windows short-name normalization from probing unchecked paths. Validate redirects before resolver containment checks and cover local cache relocations, relative links, hidden redirects, and malformed chains. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3b52aab2-c023-4a3b-9a45-fe8e298e2814
Recognize spaced identifier words before prefix and namespace matches so LanguageModel stays visible when Windows SDK metadata is indexed first. Preserve literal exact matches, result caps, and alias-free package-based discovery; cover both package orders and helper types crowding the result list. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3b52aab2-c023-4a3b-9a45-fe8e298e2814
Wait for first-frame readiness and prove same-owner input completes while recording remains pinned and another owner queues, instead of comparing process startup time with capture duration. Preserve cleanup behavior with scoped disposal and narrow filesystem exception handling. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3b52aab2-c023-4a3b-9a45-fe8e298e2814








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
Closes #774
Closes #796
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. 319/319 targeted find-api, CLI-schema, and help tests pass. Beyond unit tests, reference resolution and property reporting are verified end to end against real
dotnet new/dotnet buildfixtures — a multi-project solution (App -> Middle -> Leaf), a project with aSystem.*PackageReference, and a library withinit,set, and get-only properties — by running the built CLI and asserting on its--jsonoutput. 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.