Skip to content

Never serve sample code that still contains a substitution placeholder - #852

Merged
Jaylyn Barbee (Jaylyn-Barbee) merged 2 commits into
mainfrom
jay/sample-placeholder-contract
Sep 15, 2026
Merged

Jaylyn Barbee (Jaylyn-Barbee) merged 2 commits into
mainfrom
jay/sample-placeholder-contract

Conversation

@Jaylyn-Barbee

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

Copy link
Copy Markdown
Contributor

find-ui promises code you can paste. For Gallery-style samples it could not keep that promise: those snippets carry $(Name) substitution placeholders that the Gallery resolves at runtime from live option controls, and nothing downstream was checking for them.

The two ways this went wrong

On the scraping path, a placeholder in an attribute value was rewritten to the literal "...". That keeps the markup well-formed, which is why it survived review as "cosmetic" — but it is not pasteable. Decompressing the previous bake and counting shows 140 attributes set to "...", and they are overwhelmingly typed properties:

Property Count Why "..." fails
StrokeThickness, Height, Width, RadiusX/Y, Value 24 not a double
Orientation, SelectionMode, PaneDisplayMode 13 not an enum member
IsChecked 2 not a bool

Every one of those is a compile error the moment someone pastes it.

On the index path, nothing looked for placeholders at all, so a sample could be served with its tokens intact.

What this changes

A shared detector, SampleSubstitutionPlaceholder, is now the single definition of what a placeholder is. Both paths use it.

Scraping path. GalleryFetcher.NormalizeMarkupSubstitutions resolves a token by the position it occupies rather than blanket-replacing it:

  • attribute-name position (<Button $(Background)/>) — the whole attribute is removed
  • content position — becomes a comment
  • value position (Value="$(X)") — the whole attribute is dropped, so the property falls back to its own default

The last one is the behaviour change. Dropping the attribute is what the Gallery-side exporter already does, and it is the only option that compiles.

Two supporting fixes were needed to make that actually take effect:

  • ExtractInlineCode flattened XAML tokens to "..." before CleanGalleryContent ran, so the position-aware pass never saw them. It no longer pre-flattens.
  • CleanGalleryContent now runs the known IsOpen / Severity rewrites before normalization, so those real values survive instead of having their attribute dropped.

Index path. SampleIndexParser suppresses a language block that still contains a placeholder and drops the sample only if neither block survives. ScenarioSanitizer enforces the same rule on the way out, so a placeholder cannot reach a caller through either route.

Cache. CacheVersion goes to 22. Same input, different output, so without the bump an existing cache keeps serving the broken attributes this change exists to remove — the trap entry "20" already documents. The snapshot is re-baked accordingly.

Result

Measured against a fresh bake, decompressing each snapshot and parsing the JSON:

Source Scenarios XAML Attributes set to "..." Raw tokens
gallery 330 310 0 (was 140) 0
toolkit 48 48 0 0
reactor 95 0 0 0

No content was lost making this safe: gallery XAML went up, 302 → 310, because fragments that previously failed the sanitizer's well-formedness check now survive.

Schema

The contract gains the three gallery fields the WinUI Gallery exporter actually emits, which were undocumented:

  • xamlPlaceholdersDropped — tokens already removed; the XAML pastes as published
  • xamlOmittedAsMalformed — XAML withheld because it did not parse (12 samples upstream)
  • codePlaceholdersPresent — tokens still present in code; it does not compile as published

Read the first and last as opposites, not companions. One records cleanup already done, the other warns that cleanup was not possible. A consumer conflating them pastes broken C#, which is what the field exists to prevent — the description says so explicitly.

C# gets no deletion equivalent, and that is a property of the language rather than unfinished work: no C# construct yields a default by being absent. The tokens that actually occur are identifier fragments (BadgeNotificationGlyph.$(SelectedGlyph)), fixed-arity arguments (SetBorderAndTitleBar($(HasBorder), $(HasTitleBar))), and whole statements. None can be cut out without a syntax error or removing the sample's subject.

Testing

Full suite: 5437 tests, 12 failures, all pre-existing and environmental — 11 NuGet live-source tests that need api.nuget.org, and 1 crash-dump test. Same 12 fail on a clean checkout.

Directly affected classes re-run green after the schema change: SampleIndexTests 25/25 (includes the schema/reader drift guard), GalleryFetcherCleanContentTests 7/7, FindUiSearchTests 27/27, ScenarioSanitizerTests 23/23, EmbeddedSnapshotTests 19/19.

New tests cover each token position, the attribute-drop behaviour, and a guard that the IsOpen/Severity rewrites still win over attribute dropping — the regression the reordering could have caused.

Related

Related to microsoft/WinUI-Gallery#2232, which publishes the sample index and guarantees its XAML is placeholder-free. The two are technically independent and can merge in either order — the Gallery's index validates against this repo's schema both before and after this PR, because the contract sets additionalProperties: true at every level. Confirmed with ajv (draft 2020-12) against both the current main schema and the one here: valid in both cases.

What this PR adds is description rather than permission. On main, sample.gallery declares no properties at all, so the three fields that carry the pasteability contract are tolerated but unexplained. Landing this first simply means codePlaceholdersPresent is described by the published schema as soon as the Gallery starts emitting it, rather than shortly after.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Value-position substitution tokens were rewritten to the literal "...",
which keeps markup well-formed but is not pasteable: a measured 140
attributes in the previous bake were typed properties (StrokeThickness,
Height, Width, Orientation, SelectionMode, IsChecked and similar) where
"..." is a compile error. Drop the whole attribute instead so the
property falls back to its own default, matching how the gallery-side
exporter already handles the same case.

ExtractInlineCode flattened XAML tokens before CleanGalleryContent ran,
which bypassed the position-aware pass entirely. Let the token reach
that pass instead. Reorder CleanGalleryContent so the known IsOpen and
Severity rewrites run before normalization, so those values survive.

Bump CacheVersion to 22 and re-bake: same input, different output, so
an existing cache would keep serving the broken attributes this change
removes. Broken attributes now measure 0 across gallery, toolkit and
reactor, with no scenario loss.

Also document xamlOmittedAsMalformed, which the gallery emits on 12
samples but the schema never described, and add codePlaceholdersPresent
so a consumer can tell that a sample's C# still carries unresolved
tokens. C# cannot use the XAML deletion trick: the tokens appear as
identifier fragments, fixed-arity arguments or whole statements, none
of which can be removed without a syntax error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 15, 2026 19:01
Comment thread src/winapp-CLI/WinApp.Cli.Tests/GalleryFetcherCleanContentTests.cs Dismissed

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Note

This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.

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.

🟢 Approval recommended

Placeholder handling is consistently enforced across ingestion and output paths with focused regression coverage.

Review details
  • Files reviewed: 13/14 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 46.12 MB 46.12 MB 📈 +1.5 KB (+0.00%)
CLI (x64) 46.03 MB 46.03 MB 📈 +1.5 KB (+0.00%)
MSIX (ARM64) 19.04 MB 19.04 MB 📈 +3.7 KB (+0.02%)
MSIX (x64) 20.18 MB 20.19 MB 📈 +1.6 KB (+0.01%)
NPM Package 39.61 MB 39.62 MB 📈 +6.2 KB (+0.02%)
NuGet Package 39.75 MB 39.75 MB 📈 +4.1 KB (+0.01%)

Test Results

✅ 5816 passed, 18 skipped out of 5834 tests in 1023.9s (+7 tests, +179.6s vs. baseline)

Test Coverage

✅ 87.5% line coverage, 80.8% branch coverage · ✅ no change vs. baseline

CLI Startup Time

51ms median (x64, winapp --version) · ✅ -10ms 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))) 852
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 852

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


Updated 2026-09-15 19:36:53 UTC · commit 8968cd5 · workflow run

@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.

Approving with one non blocking item from AI review, we should probably remove this unless we have a reason for having it:

Published gallery schema object has no producer or consumer

  • What is wrong: The PR adds a sample.gallery object (xamlPlaceholdersDropped, xamlOmittedAsMalformed, codePlaceholdersPresent) to the published sample-index schema, but nothing writes those fields and nothing reads them. It documents a diagnostic contract that doesn't exist yet.

  • Show me: git grep xamlPlaceholdersDropped → only docs/winui-sample-index.schema.json. SampleIndexSchema.Gallery is referenced only in SampleProperties, which is consumed only by a test parity assertion (SampleIndexTests.cs:500). No producer emits it; the parser suppresses placeholder blocks without reading it.

  • Why it matters: A published schema is effectively forever. An external producer may implement these fields believing they're honored, and future maintainers must preserve or explain a contract the code never implemented.

  • Smallest fix: Drop the gallery object from docs/winui-sample-index.schema.json and remove Gallery from SampleProperties until a real writer/reader needs it. Keep the pasteability wording changes to xaml/code.

  • Location: docs/winui-sample-index.schema.json:148, src/winapp-CLI/WinApp.Cli/Services/Controls/SampleIndexSchema.cs:69,86

@Jaylyn-Barbee
Jaylyn Barbee (Jaylyn-Barbee) merged commit 379cbf8 into main Sep 15, 2026
32 checks passed
@Jaylyn-Barbee
Jaylyn Barbee (Jaylyn-Barbee) deleted the jay/sample-placeholder-contract branch September 15, 2026 20:36
Nikola Metulev (nmetulev) added a commit that referenced this pull request Sep 22, 2026
## Description

Replaces the WinUI Gallery scraper with the machine-readable sample
index now published by microsoft/WinUI-Gallery#2232 at
`catalog/windows-samples.json`.

The old path reconstructed the corpus by downloading Gallery metadata
and hundreds of individual source files. This change consumes the
upstream index in one request and deletes `GalleryFetcher.cs` entirely.
A shared `SampleIndexFetcher` now handles the Gallery and Reactor
indexes.

The published index contains Gallery source verbatim, so
`GalleryProvider` preserves the pasteability normalization users relied
on: Gallery-private page types and namespaces become app-local
placeholders, calls to Gallery's internal `UIHelper` are removed, and
XAML event attributes are retained only when the emitted C# defines the
handler. Samples remain complete rather than being truncated; `find-ui`
retrieves one selected sample at a time, and measurement showed
recovering a truncated sample costs more tokens than serving it whole.

Gallery `xmlnsImports` are now rendered in the `**Setup:**` line instead
of being discarded by the Toolkit-only formatter gate.

## Usage Example

```powershell
winapp find-ui "animated icon"
winapp find-ui --id gallery-animatedicon-2
```

The selected sample now includes the namespace declaration published by
Gallery:

```text
**Setup:** `xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"`
```

A cold corpus refresh fetches the Gallery index once rather than making
up to 433 requests.

## Related Issue

Closes: #809

Upstream index: microsoft/WinUI-Gallery#2232

Depends on the index-consumption foundation from #852.

## Type of Change

- ♻️ Refactor
- ⚡ Performance
- 🐛 Bug fix

## Checklist

- [x] New tests added for the published-index corpus contract and
pasteability guards
- [x] Tested against the live WinUI Gallery index
- [x] Tested locally on Windows
- [x] Full build and packaging completed
- [x] No command syntax or workflow documentation changes required

## Additional Notes

**Before → after:**

| Metric | Scraper | Published index |
|---|---:|---:|
| Gallery HTTP requests on a cold refresh | up to 433 | 1 |
| Scenarios | 330 | 327 |
| Controls | 114 | 115 |
| XAML samples | 295 | 290 |
| C# samples | 107 | 116 |
| Scenarios with no usable code | 5 | 0 |

The three removed scenarios are upstream omissions rather than parser
loss. Search enrichment preserves exact tag parity across all 127
controls represented by the shared Gallery tag data.

**Validation:**

- `scripts/build-cli.ps1 -SkipTests` completed successfully, including
NativeAOT, npm, NuGet, and MSIX packaging.
- 179/179 find-ui, Gallery, sample-index, snapshot, formatter, Toolkit,
and Reactor tests passed.
- A fresh live bake produced 327 Gallery, 48 Toolkit, and 95 Reactor
scenarios at cache version 23.
- Runtime checks confirmed unbacked handlers and Gallery-private helpers
are removed, Gallery namespace imports are printed, and Toolkit/Reactor
output remains intact.

The full CLI suite was not used as the local signal because unrelated
network/UI-dependent tests block on this machine; PR CI remains the
complete repository gate.

## AI Description

<!-- ai-description-start -->
_This section is auto-generated by AI when the PR is opened or updated.
To opt out, delete this entire section including the marker comments._
<!-- ai-description-end -->

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Nikola Metulev <nmetulev@users.noreply.github.com>
Copilot-Session: 034ffdc8-3308-4286-96d8-e02406110630
Copilot-Session: 883032eb-8c56-4340-b3d7-4c38bb1916b8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants