From 4d5fe131391325a685a3bdecfe41ce6c882d65ec Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Sat, 11 Jul 2026 19:41:13 +0200 Subject: [PATCH 1/3] Fix release-notes classifier tagging newest skia-sync PR as internal _files_by_commit() prefixed each commit with a \x1e record separator and parsed by matching a leading \x1e per line. But run() returns stdout.strip(), and \x1e is whitespace to Python's str.strip(), so the first record's leading separator was stripped. Since git log lists commits newest-first, the newest commit in every range lost its file list and was classified as 'internal' by _pr_category() (empty file set). On an active line the newest commit is very often the latest automated [skia-sync] submodule bump, so those product PRs (they touch externals/skia) were repeatedly mis-tagged 'internal' and dropped from release notes. Parse by splitting on \x1e instead, which recovers every record whether or not its leading separator survived strip(). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- scripts/infra/docs/release-notes-data.py | 28 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/scripts/infra/docs/release-notes-data.py b/scripts/infra/docs/release-notes-data.py index a1fbacf03e8..40479dfefcb 100644 --- a/scripts/infra/docs/release-notes-data.py +++ b/scripts/infra/docs/release-notes-data.py @@ -1371,17 +1371,31 @@ def _files_by_commit(from_ref, to_ref): One ``git log --name-only`` call (no pathspec filter — we want each PR's FULL file set to classify it product vs internal). A ``\\x1e`` record separator prefixes each hash line so file lines can never be mistaken for a hash. + + We SPLIT on ``\\x1e`` rather than matching a leading ``\\x1e`` per line: git + lists commits newest-first, and ``run()`` returns ``stdout.strip()`` — and + ``\\x1e`` is whitespace to Python's ``str.strip()``, so the FIRST (newest) + record loses its leading separator. A ``startswith("\\x1e")`` scan would then + silently drop that newest commit's file list, tagging it ``internal`` — which + is exactly how the latest automated ``[skia-sync]`` submodule bump (usually the + newest commit in the range) kept getting mis-tagged. Splitting recovers every + record whether or not its leading separator survived. """ out = run(["git", "log", "--no-renames", "--name-only", "--format=%x1e%H", "{}..{}".format(from_ref, to_ref)]) files_by = {} # type: dict - cur = None - for line in out.split("\n"): - if line.startswith("\x1e"): - cur = line[1:].strip() - files_by[cur] = set() - elif line.strip() and cur is not None: - files_by[cur].add(line.strip()) + for record in out.split("\x1e"): + record = record.strip() + if not record: + continue + # First line is the commit hash; the rest (past the blank line git emits + # before --name-only output) are the touched paths. Merge commits have no + # --name-only output, so they map to an empty set (still `internal`). + lines = record.split("\n") + commit = lines[0].strip() + if not commit: + continue + files_by[commit] = {ln.strip() for ln in lines[1:] if ln.strip()} return files_by From fc98373214f857314b9101ffe395cb1569ddcf07 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Sun, 12 Jul 2026 07:55:20 +0200 Subject: [PATCH 2/3] Add a mixed tier for native/ and docs bumps; scope externals product to skia MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The classifier tags each PR product / mixed / internal by touched files. Refine the package-affecting-but-not-API paths and the externals/ split: - product is the strong signal: binding/ + source/ (managed API & Views) and externals/skia (the native Skia submodule, with its vendored HarfBuzz). The prefix is exact 'externals/skia', not 'externals/', so it does NOT sweep in the sibling externals/depot_tools build-toolchain submodule or externals/.gitignore. - native/ (per-platform build config: compile flags / gn args) shapes the shipped native binaries, so it is not internal — but a native/-only PR is usually infra, not a consumer-facing change, so it is 'mixed' (Polish judges from the title) rather than firm 'product'. Also covers the shipped WinUI native subtree under native/winui/. - docs (the SkiaSharp-API-docs submodule, mdoc XML) ships as the packages' IntelliSense docs, so it is not internal — but a docs bump is doc content, not an API/behaviour change, so it is 'mixed' too (moved up from internal). A submodule bump appears in the parent as the bare 'docs' gitlink, so the prefix is slash-less (like externals/skia) and does not collide with 'documentation/' (the docs site, still internal). - externals/depot_tools (build toolchain) and externals/.gitignore are build infra that never ships and does not shape the binary, so they stay internal. Docs (SKILL.md, dev spec) updated to match. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .agents/skills/release-notes/SKILL.md | 4 +- .../dev/release-notes-and-api-diffs.md | 34 +++++++++++------ scripts/infra/docs/release-notes-data.py | 37 ++++++++++++++----- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/.agents/skills/release-notes/SKILL.md b/.agents/skills/release-notes/SKILL.md index d7ac4839506..a2c4d972f3e 100644 --- a/.agents/skills/release-notes/SKILL.md +++ b/.agents/skills/release-notes/SKILL.md @@ -23,8 +23,8 @@ to read. If yes, write about it. If no (CI tweaks, internal refactors, doc/workflow plumbing, test infra), leave it out — the renderer already collapses that noise. `data.json` tags every PR `product` / `mixed` / `internal`; treat `internal` as -invisible unless it changed shipped behaviour, and for `mixed` judge from the -title. +invisible unless it changed shipped behaviour, and for `mixed` (build config in +`native/`, or a `docs` API-docs bump) judge from the title. ## Running the full pipeline (prepare → write prose → render) diff --git a/documentation/dev/release-notes-and-api-diffs.md b/documentation/dev/release-notes-and-api-diffs.md index c96076a2892..05d6c660caf 100644 --- a/documentation/dev/release-notes-and-api-diffs.md +++ b/documentation/dev/release-notes-and-api-diffs.md @@ -987,7 +987,7 @@ built from. It is timestamp-free and includes, at minimum: - `harfbuzz` on released pages — `{ "version", "api_diff_link", "prs" }` for the co-shipped HarfBuzzSharp section (§4.5). It is absent on `-unreleased` pages. - `prs` — the flat PR map, including title, URL, author, `community`, and the - deterministic `tag` (`product`, `mixed`, `internal`). + deterministic `tag` (`product`, `mixed`, or `internal`). - `contributors` — the authoritative non-maintainer, non-bot roster the renderer uses for the community table. - `previews` — per-preview/RC buckets, when present. Each carries a `key`, the human @@ -1035,17 +1035,27 @@ principles are fixed here. API, runtime behavior, native binary, or NuGet package — changed for a consumer. Prepare makes this deterministic: `release-notes-data.py` tags every PR in `data.json` as **`product`**, **`mixed`**, or **`internal`** by the files it changed. - - **`product`** — touches shipped code (`binding/`, `externals/`, `source/`). Written up. - - **`internal`** — touches none of those (CI, workflows, agent skills, docs site, - tests, samples, build/meta). Dropped into the one collapse line. - - **`mixed`** — touches only build config (`native/`): it may change the shipped - binary via a compile flag or be pure infra. Polish judges from the title/context - already in `data.json`; it does not open the PR. - - `native/` is deliberately **not** treated as shipped code: it is build - configuration, and the thing that actually ships is `externals/skia/`. Polish drops - `internal`, writes up `product`, and inspects `mixed`; moving the classification out - of the LLM makes product-focus reliable run-to-run. + - **`product`** — touches shipped code with a real API / behaviour / native change: + `binding/` + `source/` (managed API & Views) and `externals/skia` (the native Skia + submodule, with its vendored HarfBuzz). Written up. + - **`mixed`** — affects the shipped package but is not itself an API/behaviour change, so + Polish judges from the title: `native/` (per-platform build config — compile flags/gn + args that shape the native binaries, usually infra) and `docs` (the mdoc API-docs + submodule that ships as IntelliSense XML — doc content, not behaviour). + - **`internal`** — touches none of those (CI, workflows, agent skills, docs *site*, + tests, samples, build/meta, and the `externals/depot_tools` build-toolchain submodule). + Dropped into the one collapse line. + + `native/` shapes the shipped binaries and `docs` ships as doc XML, so neither is + `internal`; but neither is a direct API/behaviour change, so both are `mixed` (inspected + from the title) rather than firm `product`. `docs` and `externals/skia` are submodules, so + in the parent repo they appear as bare gitlink paths (`docs`, `externals/skia`) and the + prefixes match those exactly — the `externals/skia` prefix is deliberately not just + `externals/`, which would sweep in the sibling `externals/depot_tools` build-toolchain + submodule (internal) and `externals/.gitignore`; the `docs` prefix is slash-less so it hits + the gitlink without colliding with `documentation/`. Polish drops `internal`, writes up + `product`, and inspects `mixed`; moving the classification out of the LLM makes + product-focus reliable run-to-run. 2. **Highlights are a hook, not a summary.** The `## Highlights` section always exists and is assembled by `release-notes-render.py`. The prose targets ~80 words and is hard-capped at 100 words total across `highlights_headline` + `highlights_body`, naming only the diff --git a/scripts/infra/docs/release-notes-data.py b/scripts/infra/docs/release-notes-data.py index 40479dfefcb..e055506c4d7 100644 --- a/scripts/infra/docs/release-notes-data.py +++ b/scripts/infra/docs/release-notes-data.py @@ -1305,16 +1305,33 @@ def determine_diff_range(branch): # can FOCUS on product, INSPECT mixed, and MENTION internal (§4.4) — a lexical filter # instead of re-judging every PR from its title each run (the source of leak variance). # -# product — touches SHIPPED code (managed API, native code, Views). A real change a -# consumer can see. Companion test/benchmark/generated files are ignored. -# mixed — touches only BUILD config (native/): may change the shipped binary via a -# compile define (e.g. a rasteriser flag), or may be pure infra (Docker -# image, SDK pin). Polish guesses from the title/context in data.json -# (it does not open the PR) — surface a behaviour change, drop pure infra. -# internal — touches NEITHER: a pure repository process (CI, workflows, agent skills, -# docs site, tests, samples, build/meta files). Dropped into the collapse line. -_SHIP_PATH_PREFIXES = ("binding/", "externals/", "source/") -_BUILD_PATH_PREFIXES = ("native/",) +# product — touches SHIPPED code: a real API / behaviour / native change a consumer +# can see. Companion test/benchmark/generated files are ignored. +# binding/ — managed SkiaSharp/HarfBuzzSharp API + NativeAssets packages +# source/ — the Views / integration packages +# externals/skia — the native Skia submodule (with its vendored HarfBuzz): +# THE native product. A bump shows up in the parent as the +# bare `externals/skia` gitlink, so the prefix is exact — +# `externals/` alone would wrongly sweep in the sibling +# build-tooling submodule and `externals/.gitignore`. +# mixed — affects the shipped PACKAGE but is not itself an API/behaviour change, so +# Polish judges from the title/context in data.json (it does not open the PR) +# — surface a real behaviour change, drop pure infra / doc churn: +# native/ — the per-platform build config (compile flags / gn args). It +# shapes the shipped native binaries (so not internal), but a +# native/-only PR is usually infra, not a consumer-facing change. +# docs — the SkiaSharp-API-docs submodule (mdoc XML) that ships as the +# packages' IntelliSense docs: shipped content, but a doc-text +# change, not an API/behaviour change. It is a submodule, so a +# bump shows up as the bare `docs` gitlink — hence the slash-less +# prefix (a `docs/` prefix would miss it, and `docs` does not +# collide with `documentation/`). +# internal — touches none of the above: a pure repository process (CI, workflows, agent +# skills, docs *site*, tests, samples, build/meta files, and the +# `externals/depot_tools` build-toolchain submodule). Dropped into the +# collapse line. +_SHIP_PATH_PREFIXES = ("binding/", "externals/skia", "source/") +_BUILD_PATH_PREFIXES = ("docs", "native/") def _pr_category(files): From 3743a1adcd9199b8ae92230afe395d7943d01ee6 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Sun, 12 Jul 2026 17:45:35 +0200 Subject: [PATCH 3/3] Extract PR path classification into release-notes-paths.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The product/mixed/internal path mapping was hardcoded in release-notes-data.py as _SHIP_PATH_PREFIXES / _BUILD_PATH_PREFIXES tuples. Move it into a committed, data-driven config so the mapping is edited in one deterministic place instead of in script logic. - scripts/infra/docs/release-notes-paths.json: ordered tiers (each a tag + patterns) plus a default. Self-documenting via description/why/notes fields. - release-notes-data.py: _pr_category now loads the tiers (cached) and returns the first tier with a matching file, else the default. A pattern matches by prefix (str.startswith, unchanged behaviour) or, if it contains a glob metacharacter (* ? [), by fnmatch.fnmatchcase — so paths, prefixes, and wildcards are all supported. A missing/malformed config is a hard error (no silent fallback to a guessed mapping). Behaviour is identical to the previous hardcoded tuples (verified across the prefix cases and real sync/docs/depot_tools PRs). Dev spec updated with the config-file inventory entry and a pointer to the JSON as the edit surface. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../dev/release-notes-and-api-diffs.md | 19 ++-- scripts/infra/docs/release-notes-data.py | 92 ++++++++++++------- scripts/infra/docs/release-notes-paths.json | 30 ++++++ 3 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 scripts/infra/docs/release-notes-paths.json diff --git a/documentation/dev/release-notes-and-api-diffs.md b/documentation/dev/release-notes-and-api-diffs.md index 05d6c660caf..0f2b9ff24f3 100644 --- a/documentation/dev/release-notes-and-api-diffs.md +++ b/documentation/dev/release-notes-and-api-diffs.md @@ -342,6 +342,8 @@ scripts/infra/docs/ (all doc engines, together) prunes stale -unreleased pages and retires HarfBuzz hub pages release-notes-schema/prose.schema.json the prose contract the agent fills versions.json supersession + baseline config (§1); shared repo-wide + release-notes-paths.json PR path→tag classification (§4.4): the deterministic, + editable product/mixed/internal path map read by release-notes-data.py docker/ reproducible image + run.sh wrapper; `api-diffs` invokes the `docs-api-diff` target directly @@ -1034,7 +1036,12 @@ principles are fixed here. every PR is whether the **shipped SkiaSharp/HarfBuzzSharp library** — its public API, runtime behavior, native binary, or NuGet package — changed for a consumer. Prepare makes this deterministic: `release-notes-data.py` tags every PR in `data.json` as - **`product`**, **`mixed`**, or **`internal`** by the files it changed. + **`product`**, **`mixed`**, or **`internal`** by the files it changed. The path→tag + mapping is **not** hardcoded in the script — it lives in + [`scripts/infra/docs/release-notes-paths.json`](../../scripts/infra/docs/release-notes-paths.json), + the single deterministic place to edit it. It is a short list of ordered tiers (each a + `tag` + `patterns`) plus a `default`; the first tier with a matching file wins. A pattern + matches by prefix (`str.startswith`), or as a glob if it contains `* ? [`. - **`product`** — touches shipped code with a real API / behaviour / native change: `binding/` + `source/` (managed API & Views) and `externals/skia` (the native Skia submodule, with its vendored HarfBuzz). Written up. @@ -1042,9 +1049,9 @@ principles are fixed here. Polish judges from the title: `native/` (per-platform build config — compile flags/gn args that shape the native binaries, usually infra) and `docs` (the mdoc API-docs submodule that ships as IntelliSense XML — doc content, not behaviour). - - **`internal`** — touches none of those (CI, workflows, agent skills, docs *site*, - tests, samples, build/meta, and the `externals/depot_tools` build-toolchain submodule). - Dropped into the one collapse line. + - **`internal`** — the `default`: touches none of those (CI, workflows, agent skills, docs + *site*, tests, samples, build/meta, and the `externals/depot_tools` build-toolchain + submodule). Dropped into the one collapse line. `native/` shapes the shipped binaries and `docs` ships as doc XML, so neither is `internal`; but neither is a direct API/behaviour change, so both are `mixed` (inspected @@ -1054,8 +1061,8 @@ principles are fixed here. `externals/`, which would sweep in the sibling `externals/depot_tools` build-toolchain submodule (internal) and `externals/.gitignore`; the `docs` prefix is slash-less so it hits the gitlink without colliding with `documentation/`. Polish drops `internal`, writes up - `product`, and inspects `mixed`; moving the classification out of the LLM makes - product-focus reliable run-to-run. + `product`, and inspects `mixed`; moving the classification out of the LLM (and into the + JSON) makes product-focus reliable run-to-run. 2. **Highlights are a hook, not a summary.** The `## Highlights` section always exists and is assembled by `release-notes-render.py`. The prose targets ~80 words and is hard-capped at 100 words total across `highlights_headline` + `highlights_body`, naming only the diff --git a/scripts/infra/docs/release-notes-data.py b/scripts/infra/docs/release-notes-data.py index e055506c4d7..acdaa35afd1 100644 --- a/scripts/infra/docs/release-notes-data.py +++ b/scripts/infra/docs/release-notes-data.py @@ -94,7 +94,7 @@ from __future__ import annotations -import argparse +import fnmatch import hashlib import json import re @@ -141,6 +141,12 @@ VERSIONS_JSON_PATH = Path("scripts/infra/docs/versions.json") +# PR path-classification config (loaded lazily from +# scripts/infra/docs/release-notes-paths.json). The single deterministic place that +# owns which touched paths map to which release-note tag — see _pr_category(). +PATH_TAGS_JSON_PATH = Path("scripts/infra/docs/release-notes-paths.json") +_PATH_TAGS_CONFIG = None # type: Optional[tuple] + # HarfBuzz-owned paths (spec §1.5/§4.5). A HarfBuzz family page lists ONLY the # commits touching these — the HarfBuzzSharp binding + its native assets, the # native libHarfBuzzSharp build, and HarfBuzz tests — NEVER any SkiaSharp.* source @@ -1305,43 +1311,61 @@ def determine_diff_range(branch): # can FOCUS on product, INSPECT mixed, and MENTION internal (§4.4) — a lexical filter # instead of re-judging every PR from its title each run (the source of leak variance). # -# product — touches SHIPPED code: a real API / behaviour / native change a consumer -# can see. Companion test/benchmark/generated files are ignored. -# binding/ — managed SkiaSharp/HarfBuzzSharp API + NativeAssets packages -# source/ — the Views / integration packages -# externals/skia — the native Skia submodule (with its vendored HarfBuzz): -# THE native product. A bump shows up in the parent as the -# bare `externals/skia` gitlink, so the prefix is exact — -# `externals/` alone would wrongly sweep in the sibling -# build-tooling submodule and `externals/.gitignore`. -# mixed — affects the shipped PACKAGE but is not itself an API/behaviour change, so -# Polish judges from the title/context in data.json (it does not open the PR) -# — surface a real behaviour change, drop pure infra / doc churn: -# native/ — the per-platform build config (compile flags / gn args). It -# shapes the shipped native binaries (so not internal), but a -# native/-only PR is usually infra, not a consumer-facing change. -# docs — the SkiaSharp-API-docs submodule (mdoc XML) that ships as the -# packages' IntelliSense docs: shipped content, but a doc-text -# change, not an API/behaviour change. It is a submodule, so a -# bump shows up as the bare `docs` gitlink — hence the slash-less -# prefix (a `docs/` prefix would miss it, and `docs` does not -# collide with `documentation/`). -# internal — touches none of the above: a pure repository process (CI, workflows, agent -# skills, docs *site*, tests, samples, build/meta files, and the -# `externals/depot_tools` build-toolchain submodule). Dropped into the -# collapse line. -_SHIP_PATH_PREFIXES = ("binding/", "externals/skia", "source/") -_BUILD_PATH_PREFIXES = ("docs", "native/") +# The path→tag mapping is NOT hardcoded here: it lives in the committed +# scripts/infra/docs/release-notes-paths.json, the single deterministic place to edit it +# (ordered tiers of prefixes/globs + a default). See that file's `description`/`notes` +# and spec §4.4 for the rationale (why `externals/skia` is exact, why `docs` is +# slash-less, why `native/` is mixed, etc.). + + +def _load_path_tags(): + # type: () -> tuple + """Load release-notes-paths.json as ``([(tag, patterns), ...], default)`` (cached). + + Ordered tiers + a default tag — the deterministic PR path-classification config + (spec §4.4). Read once and memoised; a missing/malformed file is a hard error (the + file is committed and required — do not silently fall back to a guessed mapping). + """ + global _PATH_TAGS_CONFIG + if _PATH_TAGS_CONFIG is None: + if not PATH_TAGS_JSON_PATH.exists(): + raise FileNotFoundError( + "Missing %s — the PR path-classification config (spec §4.4)." + % PATH_TAGS_JSON_PATH) + with open(PATH_TAGS_JSON_PATH) as f: + cfg = json.load(f) + tiers = [(t["tag"], tuple(t.get("patterns", []))) + for t in cfg.get("tiers", [])] + _PATH_TAGS_CONFIG = (tiers, cfg.get("default", "internal")) + return _PATH_TAGS_CONFIG + + +def _path_matches(path, pattern): + # type: (str, str) -> bool + """Does a file ``path`` match a config ``pattern``? + + Plain patterns are PREFIXES (``str.startswith`` — so ``binding/`` catches everything + under it and a bare submodule gitlink like ``externals/skia`` / ``docs`` matches + itself). A pattern containing a glob metacharacter (``* ? [``) is matched with + ``fnmatch.fnmatchcase`` (case-sensitive, platform-independent) instead. + """ + if any(c in pattern for c in "*?["): + return fnmatch.fnmatchcase(path, pattern) + return path.startswith(pattern) def _pr_category(files): # type: (set) -> str - """Classify a PR ``product`` / ``mixed`` / ``internal`` by touched files (§4.4).""" - if any(f.startswith(_SHIP_PATH_PREFIXES) for f in files): - return "product" - if any(f.startswith(_BUILD_PATH_PREFIXES) for f in files): - return "mixed" - return "internal" + """Classify a PR ``product`` / ``mixed`` / ``internal`` by touched files (§4.4). + + Tiers from release-notes-paths.json are tried in order; the first tier with any + matching file wins, else the configured default. + """ + tiers, default = _load_path_tags() + for tag, patterns in tiers: + if any(_path_matches(f, p) for f in files for p in patterns): + return tag + return default # Automation accounts — never credited as human contributors (§4.5). The workflow diff --git a/scripts/infra/docs/release-notes-paths.json b/scripts/infra/docs/release-notes-paths.json new file mode 100644 index 00000000000..117c8c3db6e --- /dev/null +++ b/scripts/infra/docs/release-notes-paths.json @@ -0,0 +1,30 @@ +{ + "description": "Path-based PR classification for the release notes. release-notes-data.py reads this file to tag every merged PR by the files it touched (behaviour spec: documentation/dev/release-notes-and-api-diffs.md, section 4.4). This JSON is the single deterministic place to edit the mapping - the script does not hardcode which paths are product/mixed/internal.", + "matching": "Tiers are evaluated top-to-bottom; the FIRST tier with any file matching any of its patterns wins, otherwise `default`. A pattern matches a file path by PREFIX (str.startswith) - so `binding/` catches everything under it, and a bare submodule gitlink such as `externals/skia` or `docs` matches itself. A pattern that contains a glob metacharacter (* ? [) is matched with fnmatch instead (e.g. `native/*/libHarfBuzzSharp/**`).", + "tiers": [ + { + "tag": "product", + "why": "Shipped code - a real API / behaviour / native change a consumer can see. Written up in the notes.", + "patterns": [ + "binding/", + "source/", + "externals/skia" + ] + }, + { + "tag": "mixed", + "why": "Affects the shipped package but is not itself an API/behaviour change; Polish judges from the PR title/context and surfaces only a real behaviour change, dropping pure infra / doc churn.", + "patterns": [ + "native/", + "docs" + ] + } + ], + "default": "internal", + "notes": [ + "`externals/skia` is intentionally exact, NOT `externals/`: the sibling `externals/depot_tools` (build toolchain) and `externals/.gitignore` are build infra and must stay `default` (internal).", + "`docs` is the mono/SkiaSharp-API-docs submodule (mdoc XML that ships as the packages' IntelliSense docs). It is slash-less so it matches the bare `docs` gitlink and does not collide with `documentation/` - the docs site, which stays internal.", + "`native/` covers the per-platform build config (gn args / compile flags) and the shipped WinUI native subtree under `native/winui/`.", + "Anything not matched (CI, workflows, agent skills, the `documentation/` docs site, tests, samples, build/meta files, and `externals/depot_tools`) falls through to `default`." + ] +}