Skip to content

feat: define the shared WinUI sample index contract (#703 Phase 0) - #806

Merged
Zach Teutsch (zateutsch) merged 10 commits into
mainfrom
jay/find-ui-indexes
Sep 8, 2026
Merged

Zach Teutsch (zateutsch) merged 10 commits into
mainfrom
jay/find-ui-indexes

Conversation

@Jaylyn-Barbee

@Jaylyn-Barbee Jaylyn Barbee (Jaylyn-Barbee) commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Description

#703 asks WinUI-Gallery and CommunityToolkit to publish machine-readable sample indexes so find-ui stops scraping their repository layouts. Neither team has ever been asked for this — research found zero prior art in either repo — so appetite is genuinely unknown, and that is the main risk on the issue.

This is Phase 0, which is entirely on our side of the fence. We are the consumer, so we define the contract first. The point is that each upstream ask arrives as "here is the schema, here is the file it produces, here is the tested code that produced it" rather than asking two maintainer teams to design a format for us.

Nothing here changes user-facing behavior.

What ships:

Piece What it is
docs/winui-sample-index.schema.json The published contract. JSON Schema 2020-12, schemaVersion pinned to const: 1.
SampleIndexParser Generic index → Scenario[]. Kind-checks every element, since this parses untrusted network content.
SampleIndexWriter Reference generator core. Deterministic output. Lives in the build-only baker, not the shipped CLI.
ReactorFetcher Rewired onto the shared parser: 151 → 67 lines, no parsing logic of its own.
--emit-index Opt-in flag on the snapshot baker that writes an index per source. Never committed, never shipped.

The vocabulary is deliberately Reactor's. microsoft-ui-reactor already publishes reactor-search-index.json, and this contract generalizes it rather than competing with it. Field names are the ones Reactor already ships, which means Reactor's live index validates against this schema today with no change on their side — that is the proof the contract is adoptable rather than aspirational, and ReactorFetcher reading it through the shared parser is the working demonstration.

Usage Example

Not a shipped CLI surface. The generator is opt-in on the build-only baker:

# Generate an index per source into a scratch directory (never committed)
dotnet run --project src/winapp-CLI/WinApp.Cli.SnapshotBaker -- --emit-index ./artifacts/indexes

Verified live against all three sources. This is a usable-only export: scenarios are sanitized first and any left with neither XAML nor C# are omitted, so gallery writes 305 of the 323 it fetches. Those 18 are already dropped before search today, so including them would hand upstream known-broken content to publish. "Lossless" here is a property of the schema — a scenario that survives is carried field for field — not a promise that every fetched scenario appears.

Source Controls / samples Size
gallery 106 / 305 375 KB
toolkit 26 / 48 98 KB
reactor 95 / 95 83 KB

Related Issue

Closes #808 — Phase 0 of #703.

Type of Change

  • ✨ New feature

Checklist

  • New tests added for new functionality — 18 in SampleIndexTests.cs
  • Tested locally on Windows
  • Main README.md updated — n/a, no user-facing surface
  • docs/usage.md updated — n/a, no CLI command changed; the baker is a build-only tool and, like the existing bake path, documents itself through its own usage output
  • Language-specific guides updated — n/a
  • Sample projects updated — n/a
  • Shipped skills updated in plugins/winapp/skills/ — n/a, no CLI workflow changed

Additional Notes

This PR needs the corpus-rebake-not-needed label, and here is the evidence

find-ui-corpus-check.yml fires on two changed paths — WinApp.Cli.SnapshotBaker/ and ReactorFetcher.cs — and this PR re-bakes nothing. That is correct behavior from the check and the right question to ask, so it was answered directly rather than waved off:

The full corpus was baked from origin/main and from this branch back to back, and the decompressed snapshots compared.

gallery  main=62016BF907F0  branch=62016BF907F0  720,910 chars  IDENTICAL
toolkit  main=53331F9368D4  branch=53331F9368D4  122,110 chars  IDENTICAL
reactor  main=2D1815A46E13  branch=2D1815A46E13   97,394 chars  IDENTICAL

Rewiring ReactorFetcher onto the shared parser (−97/+20 lines) is behavior-preserving. Gallery and Toolkit are the control: their fetchers are untouched and also came out identical, which confirms both bakes saw the same upstream state, so a reactor difference would have been attributable to this code rather than to upstream moving mid-test.

Separately and unrelated to this PR: the committed corpus is now behind upstream (gallery 321 → 323, reactor 93 → 95, baked 2026-08-11). That is exactly what find-ui-corpus-drift.yml exists to report, and it is deliberately not folded in here.

Two schema defects that only showed up against real data

Both were found by round-tripping all three live corpora, and both would have shipped as silent quality regressions once a source published an index.

1. Curated keywords are a separate ranking signal. ProviderData carries two dictionaries, not one: SearchEngine weights Keywords at 5.0 and Tags at 3.0. Only the Toolkit populates Keywords, from author-written markdown frontmatter. A single keywords field would have folded the Toolkit's best signal down to 3.0 and quietly re-ranked results.

keywords keeps its published meaning (Tags, 3.0) because Reactor already ships that field and re-pointing it would change live rankings for no reason. The new curatedKeywords carries the 5.0 slot. Against the live Toolkit corpus, 26 of 26 controls populate both — so this is load-bearing, not hypothetical.

2. details and xmlnsImports belong to the sample, not only the control. The writer took control metadata from the first scenario in each group. For description, apiNamespace and friends that is correct — they are control facts. For these two it is not: each Toolkit sample is its own markdown file with its own XAML, so siblings legitimately disagree. segmented has five samples with five descriptions and differing import counts (2/2/2/2/3). Collapsing them onto the first would have emitted XAML using a prefix the index never declared.

Both fields are now valid at either level. The writer hoists them to the control when every sample agrees and writes them per sample when they do not, so the common case stays compact — Gallery, where all samples share one description, grows 412 bytes; Toolkit grows 5.7 KB; Reactor not at all. The reader prefers the sample value and falls back to the control's.

Two findings about existing data, recorded rather than fixed

  • gallery/itemsrepeater has one scenario scraped with no control metadata at all — ApiNamespace, ControlDescription, Description, RelatedControls and Docs are all empty on itemsrepeater-7 while its six siblings have them. Round-tripping fills it from the siblings. That is a repair, not a loss, so the contract asserted in tests is "a populated value is never changed or dropped" rather than strict equality. Fixing the scrape is out of scope here and moot once Gallery publishes an index.
  • gallery-tags.json holds 13 entries for control ids that have no scenarios (scratchpad, listbox, wrappanel, jumplist-gallery, …). SearchEngine derives its control list from scenarios, so these are already dead today. The index cannot carry them and does not need to.

Design calls worth reviewing

  • additionalProperties: true throughout. Reactor's live index carries generatedFrom, category and galleryRoute, which this contract does not model. A closed schema would reject the one real-world index that already exists, which would be an odd contract to hand someone.
  • details is a new field rather than a redefinition of description. Reactor's published description already means the one-line summary. Gallery keeps both a short subtitle and a long-form description, so it needs a second field — taking description for the long form would have broken the one index in the wild.
  • An absent schemaVersion is accepted as v1; an unknown one is refused. A future v2 index should fail loudly rather than be half-parsed into wrong results.
  • Determinism is enforced, not incidental. Controls are sorted by id, generatedAtUtc is omitted by default, and JavaScriptEncoder.UnsafeRelaxedJsonEscaping keeps XAML < from becoming \u003C. A generated artifact that churns between runs cannot be diffed by a drift check and is unreviewable by the maintainers we are asking to host it.
  • The generator writes on demand and commits nothing. This follows the precedent set when feat: find-ui works offline — bake the corpus into the binary, refresh cache every 24h #770 merged with only the compressed blobs checked in. The existing positional bake invocation used by build-cli.ps1 and the drift workflow is deliberately byte-identical.

Review feedback

The writer moved out of the shipped CLI. --emit-index was always a baker flag, but SampleIndexWriter itself sat in WinApp.Cli/Services/Controls/, so the shipped binary carried code whose only caller is a build-time tool. It now lives in WinApp.Cli.SnapshotBaker/. The CLI keeps only SampleIndexSchema and SampleIndexParser — the parts that survive into a world where upstream publishes and we only ever read. Moving it also took the file out of the Services/Controls/ .editorconfig subtree, so it now meets the repo's brace and file-header rules.

A malformed language tag no longer yields C#. GetString collapsed absent and present-but-not-a-string into "", so "language": 42 parsed as C# and would have been offered as paste-ready code. IsCSharp now inspects the JsonElement directly and treats a non-string tag as not-C#. Covered for 42, null and "".

Zach's blocking finding is fixed in #815, not here. He flagged that --emit-index emits winapp-authored samples presented as Gallery content, and proposed filtering them at emit time. Investigating it showed the divergence was not an emit-time problem: GalleryFetcher was injecting and rewriting samples in the fetch path and stamping them Source = "gallery", so they were already reaching users mislabeled, long before any index was written. Filtering at emit would have hidden the symptom on the one surface we were about to show upstream. #815 removes the divergence at the source instead, which also makes the filter unnecessary. This PR is therefore unchanged in that respect and its byte-identical-corpus evidence below still stands.

Verification

  • 209/209 targeted tests pass (SampleIndex, ReactorFetcher, ScenarioSanitizer, Controls, FindUi, Gallery, Toolkit, Search, EmbeddedSnapshot).
  • Release build clean — 0 warnings, warnings-as-errors on.
  • A throwaway harness fetched all three sources live and round-tripped every scenario through writer and reader, asserting scenario-level equality, no lost live tag, determinism against real upstream data, and identical sanitizer behavior before and after. It is deleted; its durable conclusions are three hermetic tests covering per-sample divergence, control-level hoisting, and reader inheritance.
  • Schema_DeclaresExactlyTheFieldsTheReaderAndWriterUse asserts docs/winui-sample-index.schema.json and SampleIndexSchema cannot drift apart — the schema file is copied to the test output for that check.

Copilot AI balanced review requested due to automatic review settings September 2, 2026 14:03
@Jaylyn-Barbee Jaylyn Barbee (Jaylyn-Barbee) added the corpus-rebake-not-needed find-ui corpus freshness check: this PR's changes provably do not alter what a bake produces label Sep 2, 2026
Comment thread src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli.Tests/SampleIndexTests.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The schema currently validates samples that the parser drops or interprets as C#, weakening the core published contract.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Defines the Phase 0 shared WinUI sample-index contract for #703 without changing shipped CLI behavior.

Changes:

  • Adds the schema plus shared parser and deterministic writer.
  • Migrates Reactor parsing to the shared implementation.
  • Adds opt-in index generation and comprehensive contract tests.
File summaries
File Description
docs/winui-sample-index.schema.json Defines the published index contract.
SampleIndexSchema.cs Centralizes contract fields and version.
SampleIndexParser.cs Parses indexes into provider data.
SampleIndexWriter.cs Generates deterministic indexes.
ReactorFetcher.cs Delegates parsing to the shared parser.
SnapshotBaker.cs Adds per-provider index generation.
Program.cs Adds the build-only --emit-index option.
SampleIndexTests.cs Tests parsing, round trips, inheritance, and schema alignment.
WinApp.Cli.Tests.csproj Copies the schema into test output.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/winui-sample-index.schema.json
Comment thread docs/winui-sample-index.schema.json Outdated
Comment thread docs/winui-sample-index.schema.json Outdated
@github-actions

github-actions Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 43.74 MB 43.75 MB 📈 +6.5 KB (+0.01%)
CLI (x64) 43.73 MB 43.74 MB 📈 +6.5 KB (+0.01%)
MSIX (ARM64) 18.11 MB 18.11 MB 📈 +2.7 KB (+0.01%)
MSIX (x64) 19.20 MB 19.21 MB 📈 +7.1 KB (+0.04%)
NPM Package 37.68 MB 37.69 MB 📈 +4.4 KB (+0.01%)
NuGet Package 37.81 MB 37.82 MB 📈 +3.4 KB (+0.01%)

Test Results

✅ 4786 passed, 5 skipped out of 4791 tests in 740.3s (+22 tests, -118.6s vs. baseline)

Test Coverage

✅ 89% line coverage, 82.3% branch coverage · ⚠️ -0.3% vs. baseline

CLI Startup Time

53ms median (x64, winapp --version) · 📉 -11ms vs. baseline

Try This Build

Installs 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))) 806
Switching between builds often?

Put the tool on your PATH once:

& ([scriptblock]::Create((irm https://raw.githubusercontent.com/microsoft/winappCli/main/scripts/winapp-pr.ps1))) -AddToPath

Then this build is just:

winapp-pr 806

Run winapp-pr with no arguments to pick from a list of open PRs.


Updated 2026-09-04 13:55:07 UTC · commit e1fc2f9 · workflow run

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Generated Gallery indexes bypass sanitization and contain malformed XAML that violates the contract.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/winapp-CLI/WinApp.Cli.SnapshotBaker/SnapshotBaker.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Malformed language values are accepted as C#, and index generation can produce incomplete or mixed-generation artifacts.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/winapp-CLI/WinApp.Cli.SnapshotBaker/SnapshotBaker.cs:229

  • What is wrong: each provider's index is published directly to the final directory before the run is known to be complete. Show me: run successfully once, then rerun when one provider fails: successful providers are replaced while the failed provider's old index-*.json remains, so the directory still looks complete but mixes generations even though the command exits 1. Why it matters: a stale or internally inconsistent artifact set can be handed to upstream maintainers. Smallest fix: stage the whole index set and publish it only when failures is empty, as BakeAsync already does.
    src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs:119
  • What is wrong: GetString makes an absent language and a present value of the wrong JSON type indistinguishable. Show me: { "code": "auto x = 1;", "language": 42 } produces a scenario whose code is treated as C#, although the contract says only an absent tag or the string csharp is accepted. Why it matters: malformed network content can be presented as paste-ready C# instead of being rejected. Smallest fix: distinguish a missing property from a non-string property and accept the code only for missing or case-insensitive csharp; cover the non-string case in the parser tests.
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/winapp-CLI/WinApp.Cli.SnapshotBaker/SnapshotBaker.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The parser rejects the schema-valid schemaVersion representation 1.0, silently producing an empty corpus.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs:176

  • schemaVersion: 1.0 is valid under JSON Schema 2020-12 for both type: "integer" and const: 1, but JsonElement.TryGetInt32 rejects that lexical representation. A schema-valid index produced by a serializer that writes 1.0 therefore parses as an empty corpus instead of v1. Compare the numeric value (for example via TryGetDecimal) and add this representation to the version-gate test.
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We are the consumer, so we define the format. Before asking WinUI-Gallery or
CommunityToolkit to publish a sample index, this establishes the contract and
proves it can carry real sample data losslessly -- so each upstream PR arrives
as "here is the schema and the tested code that produces it" rather than a
request for someone else to design one.

The schema generalizes the index microsoft/microsoft-ui-reactor already
publishes, keeping its field names verbatim so that file stays valid without
the Reactor team changing anything. Additions cover XAML sources: `xaml`,
`xmlnsImports`, `docs[]`, and `details` for long-form prose (Reactor's
`description` already means the one-line summary, and Gallery keeps both).
`additionalProperties` stays open because the live Reactor index carries
`generatedFrom`, `category` and `galleryRoute` that this contract does not model.

ReactorFetcher is rewired onto the shared reader rather than left duplicating
it, dropping it from 151 to 67 lines. That is what makes this evidence instead
of speculation: the reader now runs against a real published index in
production. Behavior is unchanged, so CacheVersion is not bumped.

Two hardening wins fell out of the shared reader: non-object entries inside
`controls`/`samples` are skipped instead of throwing, and `Scenario.Source` is
stamped by the caller rather than read from the document, so a source cannot
label its samples as another source's.

SampleIndexWriter has no non-test caller yet by design -- it is the Phase 1
deliverable, and gains one when the reference generator is wired up (deferred
behind #770, which owns the corpus bake it will build on).

Verification: 193/193 targeted find-ui, controls, search and sample-index tests
pass; Release build of the full solution is clean with 0 warnings. Beyond the
12 hermetic tests, a throwaway run against the live Reactor corpus confirmed all
93 real scenarios survive write/parse field-for-field with byte-identical output
across runs, and `find-ui "flex layout" --source reactor` still returns Flex and
StackPanel end to end.

Refs #703

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…se 0)

Validating the Phase 0 contract against all three live corpora surfaced two
places where the schema could not represent the data losslessly. Both would
have shipped as silent quality regressions once a source published an index.

Curated keywords are a separate ranking signal
----------------------------------------------
ProviderData carries two dictionaries, not one: SearchEngine weights Keywords
at 5.0 and Tags at 3.0. Only the Toolkit populates Keywords, from author-written
markdown frontmatter. The contract had a single `keywords` field, so a Toolkit
index would have folded its best signal down to 3.0 and quietly re-ranked
results.

`keywords` keeps its published meaning (Tags, 3.0) because Reactor already
ships that field and re-pointing it would change live rankings. The new
`curatedKeywords` carries the 5.0 slot. Verified against the live Toolkit
corpus: 26 of 26 controls populate both.

Details and imports belong to the sample, not only the control
--------------------------------------------------------------
The writer took control metadata from the first scenario in each group. For
`description`, `apiNamespace` and friends that is correct - they are control
facts. For `details` and `xmlnsImports` it is not: each Toolkit sample is its
own markdown file with its own XAML, so siblings legitimately disagree.
`segmented` has five samples with five descriptions and differing imports
(2/2/2/2/3). Collapsing them onto the first would have emitted XAML using a
prefix the index never declared.

Both fields are now valid at either level. The writer hoists them to the control
when every sample agrees and writes them per sample when they do not, so the
common case (Gallery, where all samples share one description) stays compact:
the gallery index grows 412 bytes, the toolkit index 5.7 KB, reactor not at all.
The reader prefers the sample value and falls back to the control's.

Proving it against the real corpora
-----------------------------------
A throwaway harness fetched all three sources live and round-tripped every
scenario through writer and reader. Two findings worth recording:

- `gallery/itemsrepeater` has one scenario scraped without any control
  metadata. Round-tripping fills it from its siblings. That is a repair, not a
  loss, so the permanent contract is "a populated value is never changed or
  dropped" rather than strict equality.
- `gallery-tags.json` holds 13 entries for control ids that have no scenarios.
  SearchEngine derives its control list from scenarios, so they are already
  dead today; the index cannot carry them and does not need to.

Sanitizer behaviour is identical before and after the round trip - the same
sample ids are dropped either way, so the index changes nothing the corpus
boundary guard cares about.

The harness itself is deleted; its durable conclusions are three hermetic tests
covering per-sample divergence, control-level hoisting, and reader inheritance.

Also adds `--emit-index` to the snapshot baker so the reference generator can be
run on demand. Nothing is committed, following the precedent set when #770
merged with only compressed blobs checked in. The existing positional bake
invocation used by build-cli.ps1 and the drift workflow is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 406ab204-0471-4344-95a7-a91ae84d5a45
The schema promised things it did not enforce, so an upstream publisher
could pass validation and still silently lose samples:

- A sample's anyOf only checked that xaml or code was present, but the
  parser drops both on IsNullOrWhiteSpace. `{"code": "   "}` validated
  and then vanished. Require a non-whitespace character instead.
- `language` accepted any string while the parser always treats code as
  C#, so a sample tagged cpp would be pasted into a user's .cs file.
  Constrain it to csharp, and drop non-C# code in the parser so a
  publisher who skips validation still cannot inject another language.
- The xaml description told authors to declare prefixes on the control,
  which contradicts the sample-level xmlnsImports override our own
  Toolkit index relies on.

Verified all 466 samples across the gallery, toolkit and reactor
indexes still validate, and generator output is byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 406ab204-0471-4344-95a7-a91ae84d5a45
Three fixes to the generated index and the reader.

--emit-index wrote raw provider output. ScenarioSanitizer normally runs
later, in ControlsSearchService, so the reference artifact we intend to
hand upstream carried malformed XAML and unbalanced C# that every
consumer then drops -- the exact breakage a published index exists to
remove. Sanitize before serializing and skip samples left with neither
XAML nor C#: 18 unusable gallery samples now drop, and no malformed XAML
remains in any of the three indexes.

The control-level usings block is copied onto each of a control's
samples, so an oversized one multiplies past the capped fetch size.
Real usings are a few namespace names, so drop the prefix above a
generous ceiling.

Link the schema from llms.txt beside cli-schema.json, so the upstream
teams meant to implement the contract can find it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 406ab204-0471-4344-95a7-a91ae84d5a45
Sanitizing before serialization means the generator can write fewer
samples than it fetched, which reads as a contradiction next to the
Phase 0 claim that the schema carries the corpus losslessly. Both are
true, but they are different claims: losslessness is a property of the
schema -- a scenario that survives is carried field for field, which is
what the round-trip tests assert -- not a promise that every fetched
scenario appears. Drop that distinction into the remarks so the next
reader does not have to derive it from the counts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 406ab204-0471-4344-95a7-a91ae84d5a45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Index publication must be atomic, and parsing must accept every schema-valid representation of version 1.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/winapp-CLI/WinApp.Cli.SnapshotBaker/SnapshotBaker.cs
Comment thread src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs Outdated
Co-authored-by: Jaylyn-Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Comment thread src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs Dismissed

@zateutsch Zach Teutsch (zateutsch) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review (winappcli pr-review skill) — verify before acting.

Decision

Changes required — the generated Gallery index includes a winapp-authored sample and presents it as upstream Gallery content. The parser’s handling of malformed language values is also worth tightening, but it is non-blocking.

Must fix

Generated Gallery indexes misattribute locally authored samples

  • What is wrong: Index generation writes the post-processed Gallery corpus, including scenarios injected locally by winapp rather than supplied by WinUI Gallery.
  • Show me: Running WinApp.Cli.SnapshotBaker.exe --emit-index <dir> produced index-gallery.json containing Photo gallery: image grid (UniformGridLayout), which is added by GalleryFetcher.InjectMissing; expected: an upstream index containing only Gallery-owned samples.
  • Why it matters: The artifact intended for WinUI Gallery maintainers would ask them to publish code their repository does not own, making the proposed authoritative index inaccurate.
  • Smallest fix: Mark locally injected scenarios as synthetic and exclude them in EmitIndexesAsync, with a regression test proving emitted source indexes contain fetched source content only.
  • Location: src/winapp-CLI/WinApp.Cli.SnapshotBaker/SnapshotBaker.cs:211

Non-blocking

Malformed language values are accepted as C#

  • What is wrong: A missing language and a present non-string value both become an empty string, which the parser treats as C#.
  • Show me: {"code":"auto x = 1;","language":42} flows through GetString and is retained as paste-ready C#; expected: reject the code because a present language must be the string csharp.
  • Why it matters: Malformed upstream data can surface a wrong-language snippet that fails when pasted into a C# project. Current published Reactor data uses valid csharp values, so this is not blocking.
  • Smallest fix: Distinguish an absent property from a present non-string property and add that malformed case to parser tests.
  • Location: src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexParser.cs:121

What was exercised

  • dotnet build src\winapp-CLI\WinApp.Cli.SnapshotBaker\WinApp.Cli.SnapshotBaker.csproj -c Debug — succeeded with 0 warnings and 0 errors.
  • Filtered SampleIndexTests — all 21 passed.
  • Built WinApp.Cli.SnapshotBaker.exe --emit-index <scratch-dir> — fetched all three providers and reproduced the Gallery provenance defect in generated output.
  • scripts\build-cli.ps1 — compilation and generation progressed into the full test suite, then repeatedly failed downloading Microsoft.Windows.SDK.BuildTools and stopped producing output; the stalled run was stopped. Internal-feed authentication was unavailable in this shell, so the full suite did not complete.

… language tags

winapp reads published sample indexes at runtime but never writes one, so
SampleIndexWriter moves into the build-only SnapshotBaker alongside --emit-index.
The shipped CLI keeps only SampleIndexSchema and SampleIndexParser, which
ReactorFetcher already uses. Moving the file out of Services/Controls also takes it
out of that subtree's relaxed .editorconfig, so it now meets the repo's brace and
file-header rules.

SampleIndexParser no longer treats a malformed language tag as C#. GetString
collapses an absent property and a present non-string property into the same empty
value, so a sample carrying `"language": 42` was retained as paste-ready C#. The tag
is now read directly, and code is kept only when the tag is absent or names csharp.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 30e85b00-6b59-4df1-b2d7-5642c86c823a
@chiaramooney Chiara Mooney (chiaramooney) modified the milestone: 0.7 Sep 8, 2026
@zateutsch

Copy link
Copy Markdown
Contributor

🤖 AI-generated review (winappcli pr-review skill) — verify before acting.

Decision

Merge. This is a well-scoped Phase 0 for #703: a published sample-index schema, a shipped reader that Reactor already uses, and a build-only writer + --emit-index flag whose only job is to prove the schema round-trips. Build is clean, 22 tests pass, and no security, correctness, or ship-surface issues survived review. One minor, optional UX nit in an internal tool.

Must fix

None — mergeable as-is.

Non-blocking

Baker treats an unknown flag as the output directory

  • What is wrong: In the build-only SnapshotBaker, any positional arg that isn't --emit-index becomes the output directory, including a mistyped flag.
  • Show me: dotnet run --project src/winapp-CLI/WinApp.Cli.SnapshotBaker -- --bogus → starts the default network bake into a directory literally named --bogus → expected: usage error, exit 1.
  • Why it matters: A typo silently kicks off network/file work in the wrong place instead of failing fast. Low impact since this tool is maintainer-only, not shipped.
  • Smallest fix: Reject a positional value starting with - unless it is a supported flag, before assigning outputDirectory (still allows intentional paths like .\-out).
  • Location: src/winapp-CLI/WinApp.Cli.SnapshotBaker/Program.cs:27-43

What was exercised

  • dotnet build WinApp.Cli.SnapshotBaker.csproj -c Debug (pulls in WinApp.Cli) — succeeded, 0 warnings.
  • WinApp.Cli.Tests.exe --filter FullyQualifiedName~SampleIndexTests — 22/22 passed (round-trip fidelity, mixed per-sample vs hoisted details/xmlnsImports, non-C# language drop, oversized-usings guard, unknown schema-version refusal, non-object tolerance).
  • Reactor-parity regression check (old inline parser vs new SampleIndexParser delegation) — no scenario/tag dropped: Reactor's index has no schemaVersion (accepted as v1) and no language tags (accepted as C#).
  • Untrusted-input trace: parsed index values reach display-only markdown; ScenarioSanitizer strips terminal escapes on both read (ControlsSearchService) and write (EmitIndexesAsync) paths; no value reaches Process.Start or a file-path sink; docs.uri is never emitted.
  • Not exercised: a live --emit-index network run and the non-blocking finding's failure path — both confirmed by static read of Program.cs.

@zateutsch Zach Teutsch (zateutsch) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with one non blocking comment.

@zateutsch
Zach Teutsch (zateutsch) merged commit 42b2bed into main Sep 8, 2026
31 checks passed
@zateutsch
Zach Teutsch (zateutsch) deleted the jay/find-ui-indexes branch September 8, 2026 21:05
Jaylyn Barbee (Jaylyn-Barbee) added a commit that referenced this pull request Sep 11, 2026
…es them (#815)

## Description

`GalleryFetcher` post-processed the corpus it fetched from
WinUI-Gallery, then stamped the results `Source = "gallery"`. That is
the value `find-ui` renders as the `[gallery]` tag and uses to build ids
like `gallery-itemsrepeater-7` — so **winapp presented its own content
to users as WinUI Gallery's.**

Three mechanisms, found by decompressing the baked corpus rather than by
reading the code:

| Mechanism | What it did | State when found |
|---|---|---|
| `InjectMissing` — CommandBar | Appended a hand-written CommandBar
sample | **Dead.** Guarded on Gallery having no commandbar sample;
Gallery ships one now. |
| `InjectMissing` — `itemsrepeater-7` | Appended a hand-written
`UniformGridLayout` photo grid | **Live, unconditional.** No detection
logic at all. |
| `ApplyOverrides` — `tabview-1` | Rewrote upstream's `CreateNewTab` C#
body | **Live.** Guarded on `CSharp.Contains("Frame")`. |

Both live paths are removed and the dead one is deleted outright.
`FetchAsync` is now a passthrough.

This is wrong on its own terms, and it blocks
[#703](#703): we cannot ask
WinUI-Gallery to publish a sample index while quietly merging samples
they do not own into their data — still less hand them a *generator*
that does it. A mislabeled local file is a bug; a generator that
silently injects a third party's content into their published output is
something else.

**The guidance is kept and stays reachable.** It moves to a surface that
says who wrote it:

- The image-grid pattern moves to `Notes.cs`, so it appears in the
**Important** notes on every `ItemsRepeater` result instead of
masquerading as an upstream sample.
- The TabView advice was **already** in `Notes.cs`
(*"TabViewItem.Content can be ANY UIElement... use Frame only for page
navigation"*), so dropping that override loses nothing. It was redundant
when it was written.

This also follows the repo convention of stating each user-facing fact
once on its canonical surface.

## Usage Example

No CLI surface changes. The observable difference is that a sample
winapp wrote no longer appears under the `[gallery]` tag:

```console
$ winapp find-ui "photo grid"
```

Before, this returned `gallery-itemsrepeater-7` — a winapp-authored
sample labeled as Gallery's. Now it returns `GridView`, `ItemsView` and
`ItemsRepeater`, and the `UniformGridLayout` guidance reaches the user
through the **Important** notes on the `ItemsRepeater` result,
attributed to winapp.

## Related Issue

Unblocks [#703](#703).
Companion to #806, which defines the index contract; this PR makes the
data we would generate from that contract honest.

## Type of Change

- 🐛 Bug fix

## Checklist

- [x] New tests added for new functionality —
`GalleryCorpus_CarriesNoWinappAuthoredSample`
- [x] Tested locally on Windows
- [ ] Main README.md updated — n/a
- [ ] docs/usage.md updated — n/a, no command surface changed
- [ ] Language-specific guides updated — n/a
- [ ] Sample projects updated — n/a
- [x] Shipped skills updated in `plugins/winapp/skills/` —
`winapp-find-ui/SKILL.md`

## Additional Notes

### ⚠️ This PR *does* re-bake the corpus — do not apply
`corpus-rebake-not-needed`

Unlike #806, this one genuinely changes the baked blobs.
`snapshot-gallery.json.br`, `snapshot-reactor.json.br` and
`snapshot-manifest.json` are all updated.

It also bumps **`CacheVersion` 19 → 20**, so the manifest is re-stamped
and every existing on-disk cache is discarded. See below for why that is
load-bearing rather than housekeeping.

### Isolating the change from upstream drift

The committed corpus was stale, so a naive before/after would have
conflated our removal with upstream's own additions. Both were baked
back to back against the same live upstream:

```
main    323 scenarios
branch  322 scenarios
```

The only two differences:

- `itemsrepeater-7` — removed.
- `tabview-1` — C# restored to upstream's (939 → 1064 chars), confirming
the baked corpus really had been carrying winapp's `CreateNewTab` body.

The CommandBar injection produced no difference, which is the direct
confirmation it had gone dead.

Committed counts move **321 → 322** rather than 323 → 322 because
upstream added two samples since the last bake. Manifest now reads
gallery 322 / reactor 95 / toolkit 48.

### The one real gap this leaves

The `itemsrepeater-7` injection was not baseless — `UniformGridLayout`
inside `ItemsRepeater` is genuinely under-demonstrated upstream.
Verified against the corpus: four upstream samples use it, each
demonstrating something else. That gap is now addressed as **guidance in
`Notes.cs`** rather than as a fabricated sample.

The TabView override was a different matter: upstream ships ten
`tabview` samples and we rewrote one of them to taste. That is not a
gap.

### Regression guard

`GalleryCorpus_CarriesNoWinappAuthoredSample` fails if a winapp-authored
sample reappears in the Gallery corpus, so this cannot silently regress.

### Review feedback

**The stale cache was the real bug.** Removing the injection and
re-baking does not reach anyone who already has a cache. Caches are
matched on an exact version string, so a user who fetched with the
shipped CLI keeps a `19` cache holding the winapp-authored sample and
the rewritten `tabview-1`, and keeps being served them under the
`[gallery]` tag indefinitely — including offline. `CacheVersion.cs`
states the rule directly: bump whenever *"cleaning logic changes would
alter the cached output for the same input data."* That is exactly this
change, so `Current` is now `20`.

Worth noting the re-bake at version 20 produced **byte-identical Brotli
blobs** — the only diff is the manifest stamp. The corpus content was
already right after the first bake; what was missing was the instruction
to discard old caches.

**The provenance claim was overstated, in code and in the skill.** Both
said samples are reproduced "as fetched" / are "exactly what that
repository ships". That is not true: `FetchFromGitHub` cleans content,
compresses C#, truncates both languages at 2000 chars, and strips event
handlers with no emitted code-behind. The skill compounded it by telling
users to report anything wrong to WinUI-Gallery, so the wording could
have sent people upstream to file defects winapp's own extraction
introduced. Both now state the narrower invariant this PR actually
establishes — no injected scenarios, no bespoke per-sample overrides —
and say plainly that extraction is uniform and deliberately lossy.

**Moving the note to `Notes.cs` quietly cost discoverability.**
`Notes.Get` is consulted only when rendering a selected scenario
(`SearchEngine.cs:825`); notes are never part of the BM25 document. The
removed sample's header was the only text in the corpus carrying the
token `photo`, so deleting it made the guidance reachable only to
someone who already knew to look at `ItemsRepeater`:

```
before this PR   photo grid -> ItemsRepeater (the winapp-authored sample)
after removal    photo grid -> Grid, Image          <- guidance unreachable
after the fix    photo grid -> GridView, ItemsView, ItemsRepeater
```

Fixed in `Synonyms`, which is query-time only and needs no re-bake, so
the ranking change carries no corpus risk. `thumbnail` already mapped to
`Image` and now also offers the controls that lay thumbnails out.

**Not taken:** extending the regression guard to fingerprint the old
`tabview-1` body. The guard still covers only the injected sample, so
re-introducing that specific override would not fail the build.
### Verification

- **171** targeted tests pass (`Gallery`, `FindUi`, `Controls`,
`Search`, `EmbeddedSnapshot`, `Notes`, `Synonyms`, `CacheVersion`,
`Toolkit`, `Reactor`).
- Release build clean — **0 warnings**, warnings-as-errors on.
- `scripts/validate-plugin-package.ps1` clean.
- Live CLI run confirms the `ItemsRepeater` note renders in the
**Important** section.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Copilot-Session: 30e85b00-6b59-4df1-b2d7-5642c86c823a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

corpus-rebake-not-needed find-ui corpus freshness check: this PR's changes provably do not alter what a bake produces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 0: define the shared WinUI sample index contract

5 participants