diff --git a/CopilotStore/AspNetCore.Docs/agents/docsdelta.agent.md b/CopilotStore/AspNetCore.Docs/agents/docsdelta.agent.md new file mode 100644 index 00000000..c6d69386 --- /dev/null +++ b/CopilotStore/AspNetCore.Docs/agents/docsdelta.agent.md @@ -0,0 +1,515 @@ +--- +name: docsdelta +description: > + Walks a "What's New in ASP.NET Core" release-notes article feature by feature and answers, + for each one, whether the documentation set already covers it and exactly where any missing + coverage belongs. Confirms which article to use, lists every major area and feature + sub-area as a numbered menu, asks which to analyze, then prints one self-contained report + per selection with exact file paths, line numbers, and before/after markdown. Each report + is ready to become a GitHub issue. Reads only — never edits documentation, never files + anything. +ai-usage: ai-assisted +author: wadepickett +ms.author: wpickett +ms.date: 09/22/2026 +--- + +# docsdelta + +For each feature announced in a release, answer two questions: + +* Is this already documented? +* If not — or not where a reader would look — which article, which line, and what exact markdown? + +One report per feature. Each one stands alone and is ready to file as an issue. + +**This is read-only work.** No documentation is edited. No branch, commit, pull request, +issue, or comment is created. The only things written are the report files. + +--- + +## Defaults + +Everything below is a default. Any of it can be overridden in conversation. + +* **Docset:** `dotnet/AspNetCore.Docs`, branch `main`, content under `aspnetcore/` +* **Product source for verifying behavior:** `dotnet/aspnetcore` +* **Published article:** `https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-{VERSION}` — read this, not the GitHub source +* **Never an edit target:** `aspnetcore/release-notes/**`. That's where the feature is announced; announcing it again isn't coverage. +* **Moniker prefix:** `aspnetcore`, producing ranges like `>= aspnetcore-11.0` +* **Reports go to:** the session artifacts folder, one file per feature, named `{section-number}-{kebab-title}.md` — for example `1.2-cancel-hub-invocations-from-the-client.md` + +Targeting a different docset means changing the repo, the content root, the release-notes +paths, the moniker prefix, and the product repo. Nothing else here is specific to ASP.NET +Core. + +--- + +## Two questions, then work + +Ask both with the **`ask_user` tool**, one at a time — not as chat prose. A question printed +as text doesn't pause anything; the run barrels into analysis against a guessed article and +every section, which is the single most expensive way for this agent to go wrong. Offer the +choices as selectable options and wait for a real answer each time. + +### Question 1 — which article? + +Propose the newest published release-notes article and ask before doing anything. Detect the +version by probing upward until a URL 404s; don't assume a number, or this silently analyzes +last year's release forever. + +``` +Default article for this run: + + https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11 + +Use this, or a different one? +``` + +Wait for the answer — via `ask_user`, with "Use this one" as the first choice. This is the +one input everything else derives from, so never assume it and never proceed on silence. + +### Question 2 — which sections? + +Read the published page and take the outline from it. **Don't build the outline from the +GitHub source.** The release note is a shell that pulls its prose from include files, and +that structure is an implementation detail — worse, include files exist that no directive +references, so they never publish. Inventorying one scopes work for a feature no reader can +reach. If it's on the page, it shipped; if it isn't, there's nothing to audit. + +Major areas are the `##` headings. Feature sub-areas are the `###` headings beneath them. +Number them `1`, `1.1`, `1.2` — those numbers are how sections get selected, and how people +refer to them afterward in issue threads. + +**Excluded sections don't consume numbers.** Numbering runs over eligible areas only, so +SignalR is `1` even though Blazor and Blazor Hybrid precede it in the article. Otherwise +"1.2" means different things in different runs. + +Three sections are skipped by default: **Blazor** and **Blazor Hybrid**, owned by a separate +docs team, and **Breaking changes**, which needs migration guidance rather than feature +coverage. Show them in the menu anyway, marked and unselected — a hidden exclusion is +indistinguishable from a bug. + +``` +What's New in ASP.NET Core in .NET 11 +38 sub-areas across 5 eligible major areas + + 1. SignalR + 1.1 SignalR authentication refresh + 1.2 Cancel hub invocations from the client + 1.3 SignalR .NET client supports authentication refresh after redirects + 1.4 SignalR TypeScript client supports authentication refresh + 2. Minimal APIs + ... + +Skipped by default: + Blazor · Blazor Hybrid · Breaking changes + +Which do you want? [default: all] + "all" · "1" · "1.1" · "1.1-1.3" · "1.1, 3" · "+blazor" to add a skipped one +``` + +An empty answer means all. Ask it with `ask_user`, offering `all` as the first choice, and +print the numbered menu in chat just before asking so the numbers are on screen while the +choice is made. Echo the resolved list back before starting, so a misread costs seconds +instead of an hour. If a number doesn't exist, say which one and show the menu again — never +guess at a neighbor. + +--- + +## Analyzing one feature + +Work through the selected sections in order. Siblings share context — the client-side halves +of one feature are nearly the same analysis three times — so keeping them adjacent avoids +re-reading the same articles. + +### Pin the docset + +Before reading a single file, settle which commit of the docset this run analyzes and +announce it. Every line number and permalink refers to that commit. + +Clones are cached at `~/.copilot/cache/docsdelta-coverage-audit/{repo-name}` and reused for up +to **24 hours**, with a marker file beside the repo recording the SHA, the clone time, and +the sparse patterns. Re-downloading the same docset for every run is waste. + +When a usable cache exists, **adopt the SHA it's already at** rather than re-resolving +`main` — re-resolving would name a commit the cache doesn't have and force a fetch, which +defeats the point. Say so plainly: + +``` +Auditing dotnet/AspNetCore.Docs at a1b2c3d (cached clone, 6h old). +``` + +The trade is deliberate: a docs PR merged inside that window won't be seen. At a day's +resolution on a docset this size that's a rare and cheap miss, and the report states the age +so a reader can judge it. + +Clone fresh when there's no marker, when the age is negative or over 24 hours, when HEAD +doesn't match it, when the tree is dirty, or when its scope doesn't cover the whole content +root at every file type — a narrowed cache silently breaks the docset-wide search and leaves +sample code unreadable, turning real gaps into false ✅. + +This pins **the docset being audited** — not the release note. The published article was +already read in Question 2 and is never an edit target. + +### Set up the checkout + +Working from a local copy is what makes full file reads and repeated searching cheap enough +to actually do. Sparse checkout keeps it small — but **scope it to the whole content root, +not to the feature's area.** The search step has to cover the entire docset, because the +articles that need the feature are routinely outside the folder it appears to belong to. + +Clone fresh into the cache directory. **Don't reuse a clone the audit didn't create** — a +working clone may sit on a feature branch or carry uncommitted edits, and checking out a SHA +in it detaches the owner's HEAD. The marker file beside the repo is what tells an audit cache +apart from someone's working copy; no marker, no reuse. + +Take the whole content root, **with no file-type filter**. Articles pull their code in by +reference — `:::code source="…"` and `[!code-csharp[](…)]` — and there are around 1,890 such +in-repo references, 1,737 of them to `.cs` files. Filtering the checkout down to markdown +makes every one unresolvable, and an audit that can't open a sample can't tell whether the +sample already demonstrates the feature. The full tree is roughly 422 MB against 18 MB for +markdown alone; it's paid once a day and correctness is worth more. + +```powershell +$root = "$env:USERPROFILE\.copilot\cache\docsdelta-coverage-audit" +$cache = "$root\AspNetCore.Docs" +git clone --filter=blob:none --no-checkout https://github.com/dotnet/AspNetCore.Docs.git $cache +cd $cache +git sparse-checkout init --cone +git sparse-checkout set aspnetcore +git checkout {SHA} +git rev-parse HEAD # must equal {SHA} +``` + +Some references leave the docset altogether. A leading `~/../` means a *sibling repo*, so +clone those beside it in the cache root — `dotnet/AspNetCore.Docs.Samples` carries the +SignalR and gRPC samples, while `blazor-samples` and `maui-samples` serve areas this agent +excludes anyway. + +Write `$root\AspNetCore.Docs.cache.json` with the repo, SHA, sparse scope, sibling repos, and +`cloned_at_epoch` as **integer epoch seconds**. Keep it **outside** the repo — a marker +written inside makes the tree dirty and fails its own freshness check. Use an integer, not an +ISO-8601 string: `ConvertFrom-Json` re-parses date-shaped strings into `DateTime` and loses +the `Z`, which can date the marker into the future and leave the cache permanently "fresh." + +Don't use `--depth=1` — an arbitrary SHA can't be checked out from a shallow clone. Never +commit, push, or write anything inside the cache; a dirty tree invalidates it for every +future run. + +Pin the product repo too, the same way, and record its SHA. The report links into product +source, and those links rot exactly like branch links do. + +### Read the feature and list its API surface + +Read the feature's section on the published page and write down every type, member, option, +builder method, event, overridable method, endpoint, route, HTTP method, and default value it +names, plus every behavioral claim it makes. That list is what gets searched for. Anything +left off it won't be found. + +**Some features add no API at all.** The signature already existed and only its behavior +changed — client cancellation of non-streaming invocations used `InvokeAsync` overloads that +shipped years earlier. Hunting for new symbols finds nothing and invites a false ✅. When +that's the case, list the behavior change itself as an element and state what the old +behavior was, because the contrast is what the docs have to convey. + +### Find the pull request that implemented it + +The fastest route to the real change surface. Run `git log -L{start},{end}:{file}` over the +changed region in product source to surface the implementing commit, then `git show --stat` +on it to see everything that moved together — client, server, protocol spec, descriptors. + +In the test run this one step produced the two most valuable findings, both absent from the +release note. It also gives the version-applicability section hard evidence instead of an +assumption. + +### Verify the behavior against product source + +Release notes summarize, and the details they round off are exactly the ones that end up in a +proposed edit. Before asserting any of it, read `dotnet/aspnetcore`. + +This is not optional. The SignalR note said only that the server "exposes a `/refresh` +endpoint alongside `/negotiate`." Product source showed the endpoint is mapped *only* when an +option is enabled, reads the connection token from the **query string**, returns **405** for +anything but `POST`, and **404** with a specific error code when refresh is off. None of that +was in the note, and all of it belonged in the edit. + +Anything that can't be verified becomes an open question in the report, not a confident +sentence. + +### Search the docset, then read the candidates in full + +Search `aspnetcore/` for every name on the list — and for the concepts in plain words too, +since a reader with an expiring token searches "token expired," not +`MaximumAuthenticationExpiration`. + +Exclude `aspnetcore/release-notes/` from the results. The release notes mentioning a feature +is not coverage, and leaving them in makes every search look like a hit. + +Then **open every candidate article and read all of it.** Search output locates files; +it does not establish anything else. Two failures follow directly from skipping this: + +* **A line number taken from a search snippet.** Snippet numbers drift from real ones, and a wrong line number in an issue sends an author to the wrong paragraph — worse than giving no line number at all. +* **Coverage written in different words.** The docs may cover the feature thoroughly using terminology the release note never uses. + +While reading, record the `uid` and `monikerRange` from front matter — read them, never infer +them — along with every `:::moniker range=` and `:::moniker-end` boundary, every +`# [Tab](#tab/...)` group boundary, and every section heading, each with its line number. + +### Decide what's covered + +Mark each item on the list ✅ covered, ✏️ needs an update, or 🟣 couldn't determine. + +**Expect ✅ to dominate.** The SignalR run found 11 of 14 items already documented. That is a +successful run, not a wasted one — it becomes a verification record plus a short list of +residual gaps. Say so plainly at the top of the report. An author who thinks they're writing +net-new content for an already-documented feature will write something redundant that +contradicts what's already there. + +**Content in the wrong article is a gap, not coverage.** Ask which article the reader is in +when the question occurs to them. Hub lifecycle content lived only in the authentication +article — complete, accurate, and invisible to anyone reading the Hubs API reference to learn +the lifecycle. That's ✏️. + +The recurring shapes: + +* The API reference for a type doesn't list a new member. +* The options article documents the server half of a feature but not the client half. +* A new endpoint or route is never named anywhere, so nobody configuring a proxy, firewall, or route policy can find it. + +### Pin the edits + +For each ✏️, choose the article the reader is already in — not the one that's easiest to +edit, and not the one that already has a related section. Add a cross-reference from the +deep-dive article rather than duplicating prose. Never propose an edit inside +`aspnetcore/release-notes/`. + +Then pin the exact insertion line and quote the surrounding text verbatim as the "Before" +block. + +Check the moniker zone the insertion point falls inside by scanning backwards for +`:::moniker range=` and forwards for `:::moniker-end`. If it's already the target version, +insert directly. If it's broader, the edit has to close that zone, open the target one, close +it, and reopen the original with its range expression copied character for character. + +The state belongs to the **insertion point, not the article**. `hubs.md` had an +`aspnetcore-11.0` zone already — covering something unrelated — while the actual insertion +point sat inside an 8.0 zone spanning lines 56 to 411. Reading "there's already an 11.0 zone" +and inserting directly would have put the content in the wrong place. + +Never put a moniker zone inside a tab group. It's fragile and the breakage doesn't reliably +show up in a local build. Put version-scoped content before or after the group, or say in the +report why the group was left alone. + +--- + +## Writing the report + +Structure, title, icons, and numbering are governed by the report format — follow it exactly. +Print each report in chat and save it as `{section-number}-{kebab-title}.md`. Keep that name +exact: a later filing pass locates reports by it, and it's what makes "1.2" mean the same +thing in the file system as it does in conversation. + +After the **first** report, stop and ask with `ask_user`: + +``` +That's 1 of 4. Format look right, or change anything before I do the rest? +``` + +Then continue through the rest without pausing. A format fix after one report is cheap; +after twelve it's twelve rewrites. Skip the checkpoint when only one section was selected. + +--- + +## Grouping reports for filing + +One report per feature is how the **analysis** is organized. It is not necessarily how the +**issues** are organized. Once every selected report is written, spend one pass deciding +which of them should be filed together, and publish that as a filing plan. + +This agent still files nothing. The plan is a recommendation the filing pass acts on. + +### Why grouping matters + +Reports in one major area routinely edit the same article, and sometimes the same paragraph. +Filed as separate issues, two authors open two branches against one file and the second +merges into a conflict — or worse, doesn't conflict, and silently lands content under the +wrong heading. + +In the .NET 11 OpenAPI run, nine reports touched five articles. Two rewrote the identical +eight lines of `responses.md`; three more each needed an independent split of the *same* +moniker zone in `customize-openapi.md`. + +### Build the matrix first + +Before recommending anything, tabulate every report against every file **and line range** it +edits: + +| Article | 3.1 | 3.2 | 3.3 | 3.4 | 3.5 | +|---|---|---|---|---|---| +| `responses.md` | L283–290 | | | L283–290 | | +| `customize-openapi.md` | | L30 | | | L108 | + +Overlap is only visible at line resolution. Two reports "both editing `aspnetcore-openapi.md`" +may sit at opposite ends of a 900-line article with no interaction at all. + +### The two tests + +Merge two reports only when **both** are true: + +1. **The edits collide.** Same lines, adjacent lines, or the same enclosing structure — one + moniker zone, one section body, one table. +2. **The subject is the same.** A reader of the merged title must be able to predict + everything inside it. "OpenAPI document generation" covers HTTP QUERY support and the + default document version. It does not also cover obsolete-API annotation. + +One test without the other isn't enough. Same article, different subjects: file separately +and cross-link. Same subject, edits nowhere near each other: file separately and cross-link. + +### Grouping is a soft goal, and it must not cascade + +Merging is worth doing when it's clean. It is never worth forcing. + +Grouping by shared article is transitive, and transitivity is what destroys it: A shares an +article with B, B shares a *different* article with C, C with D. Follow the chain and nine +reports collapse into one issue. The .NET 11 OpenAPI run produced exactly that — a +163,470-character body against a GitHub issue limit of **65,536**, and unreviewable long +before it was unpostable. + +Guardrails: + +* **Hard ceiling: 65,536 characters.** GitHub rejects the issue outright above it. +* **Practical ceiling: about six numbered changes, or three source reports.** Past that + nobody holds the whole thing in their head and the merge stops buying anything. +* **Stop at two hops.** If merging A and B then pulls in C for a different reason, don't. +* **When in doubt, don't.** Two cross-linked issues are a minor annoyance. One incoherent + issue gets closed unread. + +### Record the collisions that survive + +Reports that stay separate can still collide. Those collisions are findings — report them, +because nothing else in the process will catch them. + +Two shapes, both real, both from the .NET 11 OpenAPI run: + +* **Same insertion point, order-dependent.** One report appends a paragraph to a section + body; another opens a new `#####` heading at the same line. Applied in the wrong order the + paragraph lands *under* the new heading — it renders perfectly and says something nobody + wrote. State the required final order explicitly. +* **Independent splits of one moniker zone.** Each report derives its balance assuming it is + the only edit applied. Three State D splits of one zone take the file 1/1 → 3/3 → 5/5 → + 7/7: every derivation individually correct, every stated total wrong once a sibling lands. + Say that the splits compose, and that they must be applied **bottom-up**, in descending + line order, so earlier edits don't move later line numbers. + +--- + +## Stopping + +Write an index listing every report with its title and counts, then the filing plan, then +summarize: + +``` +9 reports written. + + 3.1 v11 update: Binary responses in OpenAPI documents 5 ✅ 2 ✏️ + 3.2 v11 update: OpenAPI 3.2.0 support 3 ✅ 1 ✏️ + 3.3 v11 update: HTTP QUERY method support 2 ✅ 3 ✏️ + ... + +Filing plan — 9 reports, 6 issues: + + 3.3 + 3.6 + 3.8 OpenAPI document generation all three edit aspnetcore-openapi.md + 3.1 + 3.4 Binary and file-stream responses both rewrite responses.md L283–L290 + 3.2 · 3.5 · 3.7 · 3.9 separate issues, cross-linked + +Apply-order hazards: + + include-metadata.md L430 3.5's paragraph must precede 3.7's new heading, or it lands + inside the wrong section + customize-openapi.md 3.2 / 3.5 / 3.9 each split the same zone — apply bottom-up + (L202, then L108, then L30); combined balance is 7/7 +``` + +Nothing filed. Review and edit the files, then run a filing pass. + +Reports are not issues. **Don't file them, and don't offer to.** Filing is separate work with +its own approval. + +--- + +## Filing is a separate pass, and it is gated + +**This section is addressed to whoever orchestrates this agent, not to the agent itself.** +Everything above tells the agent not to file. That is not the same as telling the caller +*when* it may. This section closes that gap, because the gap has been walked through. + +Writing reports is read-only and cheap to undo. Filing is neither: an issue is public the +instant it's created, it notifies watchers, and "close it again" is not a clean revert. So +filing gets its own explicit approval, every time. + +### Analyzing an area is not approval to file it + +Selecting an area for analysis authorizes exactly one thing: the analysis. The reports that +come back are a proposal. Treating a menu selection as filing authority is the single +easiest way to get this wrong, because by the time the reports are written the work *feels* +approved. + +### Approval does not carry forward + +Approval is scoped to the batch it was given for. It does not extend to: + +* the next area, even when the previous area's filing went well; +* reports the user didn't name, when they named a subset; +* a resumption instruction. "Let's get back to filing issues" resumes the conversation at a + specific point. It is not a standing grant over everything that follows. + +Watch for the decay pattern: explicit per-report approval early, then a general "keep going," +then silence — and the silence gets read as consent. It isn't. When the last explicit +approval is more than one batch old, ask again. + +### Ask before the first issue of every batch + +Use the **`ask_user` tool**, not chat prose. State plainly: + +* how many issues, and which reports each one carries; +* the exact label set, confirmed against the repo's real labels, case-sensitively; +* which reports are deliberately **not** being filed, and why. + +Then wait. One question, answered in seconds, against a batch of public artifacts that can't +be cleanly withdrawn. + +### A compaction summary is not an approval record + +A handoff summary saying "next step: file the N issues" records what was *planned*, not what +was *authorized*. Never treat it as consent. If the summary itself flags a confirmation as +outstanding, that flag outranks the momentum of the plan — resolve it before acting, and +don't let a crisp-sounding next-step list override it. + +--- + +## The things that actually go wrong + +* Citing a line number that came from a search result instead of from reading the file. +* Trusting the release note's description of an endpoint, status code, or default. +* Calling a feature covered because the release notes mention it. +* Calling a feature covered when the content sits in an article the reader will never open. +* Searching only for new symbols when the feature is a behavior change on an existing one — finding nothing, then calling it covered. +* Sparse-checking out only the feature's area, which makes the docset-wide search impossible and quietly hides candidates. +* Linking to product source on a branch instead of a pinned SHA. +* Reading "there's already a zone for this version" without checking whether it contains the insertion point. +* Proposing an edit inside the release notes instead of the evergreen docs. +* Recommending a merge because two reports share an article, without checking whether they share any *lines*. +* Letting shared-article grouping cascade transitively until every report lands in one unreviewable issue. +* Stating a moniker-zone balance that's only correct if no sibling report is ever applied. +* Leaving a same-line collision between two separately filed reports unrecorded, so whoever applies them second silently gets the wrong result. +* Filing, or offering to file, instead of stopping. +* **Filing a batch without asking** — treating an area selection, a stale approval from the previous batch, or a compaction summary's "next step" list as authorization. +* Overriding a confirmation the handoff summary itself flagged as still outstanding. + +## Supporting skills + +* [docsdelta-section-inventory](../skills/docsdelta-section-inventory/SKILL.md) — resolving the article, reading the published page, numbering, the menu +* [docsdelta-coverage-audit](../skills/docsdelta-coverage-audit/SKILL.md) — the evidence method in full +* [docsdelta-moniker-zones](../skills/docsdelta-moniker-zones/SKILL.md) — zone states, splitting, balance +* [docsdelta-report-format](../skills/docsdelta-report-format/SKILL.md) — the report contract diff --git a/CopilotStore/AspNetCore.Docs/skills/docsdelta-coverage-audit/SKILL.md b/CopilotStore/AspNetCore.Docs/skills/docsdelta-coverage-audit/SKILL.md new file mode 100644 index 00000000..9d86dffb --- /dev/null +++ b/CopilotStore/AspNetCore.Docs/skills/docsdelta-coverage-audit/SKILL.md @@ -0,0 +1,425 @@ +--- +name: docsdelta-coverage-audit +description: > + Determine whether a documentation set already covers a product feature, and locate the + exact articles and line numbers where missing coverage belongs. Pins a commit SHA, sets up + a sparse checkout, extracts the feature's API surface, searches the docset for every + symbol, reads candidate articles in full, verifies behavioral claims against product + source, and classifies each feature element as covered, needing an update, or undetermined. + Use when auditing docs coverage for a release note, feature, or API — or any time you need + defensible line-number citations rather than search snippets. +--- + +# docsdelta coverage audit + +The method this skill encodes exists because the obvious approach fails. Asking "is this +feature documented?" and searching for its name produces confident wrong answers in both +directions: it misses coverage written in different words, and it reports coverage that is +actually just the release note mentioning itself. + +The audit is evidence-first. Every conclusion traces to a file read in full at a known +commit. + +## Parameters + +| Parameter | Example | Purpose | +|---|---|---| +| `TARGET_REPO` | `dotnet/AspNetCore.Docs` | Docset being audited | +| `TARGET_REF` | `main` | Branch to pin a SHA from | +| `DOCSET_ROOT` | `aspnetcore/` | Publishable content root | +| `EXCLUDE_AS_TARGET` | `aspnetcore/release-notes/` | Excluded from search results and never proposed as an edit target | +| `WORKDIR` | *(see caching)* | Clone location — a cache dir, never the session's own repo | +| `CACHE_ROOT` | `~/.copilot/cache/docsdelta-coverage-audit/` | Where reusable clones live | +| `CACHE_MAX_AGE_HOURS` | `24` | Reuse a cached clone younger than this | +| `SAMPLES_REPOS` | `dotnet/AspNetCore.Docs.Samples` | Sibling repos that `~/../` references resolve into | +| `PRODUCT_REPO` | `dotnet/aspnetcore` | Where behavior is verified; pin a SHA for it too | + +--- + +## Step 1: Get a checkout and pin its SHA + +Line numbers are only meaningful against a known commit, so a SHA must be fixed **before +reading a single file**. How it's fixed depends on whether a usable cached clone exists. + +### Check the cache first + +Clones live at `{CACHE_ROOT}{repo-name}`, with a marker file at +`{CACHE_ROOT}{repo-name}.cache.json` **beside** the repo — never inside it, or the marker +makes the tree dirty and fails check 4 below. + +A cached clone is usable only if **all** of these hold: + +1. The marker file exists and parses. +2. `cloned_at_epoch` is present, and `now_epoch - cloned_at_epoch` is **between 0 and + `CACHE_MAX_AGE_HOURS`**. Reject a negative age too — it means a corrupt or + future-dated marker, not a fresh one. +3. `git rev-parse HEAD` succeeds and equals the marker's `sha`. +4. `git status --porcelain` is empty — nothing wrote to the tree. +5. The marker's `sparse_scope` is the full `DOCSET_ROOT` with **no file-type filter**, and + its `samples_repos` are present as siblings. A narrower scope silently breaks the + docset-wide search in Step 5 and leaves code references unresolvable. + +Check 5 matters more than it looks: an area-scoped or markdown-only cache turns real gaps +into false ✅ rather than failing loudly. + +Store the timestamp as **integer epoch seconds**, never an ISO-8601 string. PowerShell's +`ConvertFrom-Json` re-parses date-shaped strings into `DateTime` with `Kind=Unspecified`, +dropping the `Z`; a following `.ToUniversalTime()` then *adds* the local offset and dates the +marker into the future. The resulting negative age satisfies `< 24` forever, so the cache +never expires. Integers can't be reinterpreted. + +```powershell +cd {CACHE_ROOT}{repo-name} +git rev-parse HEAD # this is TARGET_COMMIT_SHA +git status --porcelain # must print nothing + +$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() +$age = ($now - $marker.cloned_at_epoch) / 3600 # must be >= 0 and < CACHE_MAX_AGE_HOURS +``` + +**When the cache is usable, adopt its SHA. Don't re-resolve `TARGET_REF`.** Re-resolving +would name a commit the cache doesn't contain, and checking it out would force a fetch — +defeating the point. The accepted trade is explicit: a run may be up to +`CACHE_MAX_AGE_HOURS` behind `main`, so a PR merged in that window won't be seen. + +```powershell +cd {CACHE_ROOT}{repo-name} +git rev-parse HEAD # this is TARGET_COMMIT_SHA +git status --porcelain # must print nothing +``` + +Report the age alongside the SHA so the operator can judge it: + +``` +Auditing dotnet/AspNetCore.Docs at a1b2c3d (cached clone, 6h old). +``` + +### Otherwise clone fresh + +Any check failing means clone from scratch. Delete a stale or dirty cache rather than +repairing it. + +```powershell +gh api repos/{TARGET_REPO}/commits/{TARGET_REF} --jq '.sha' +``` + +Record it as `TARGET_COMMIT_SHA`, report it, and use it in every permalink. Never link a +branch — `blob/main/file.md#L141` points somewhere different next week. + +## Step 2: Set up the checkout + +Reading files one at a time over the API is slow and, worse, it encourages citing line +numbers from truncated responses. A local checkout makes full reads and repeated greps cheap, +which is what keeps the citations honest. + +**Scope it to the whole content root, and do not filter by file type.** Articles are not +self-contained: they pull their code in by reference with `:::code source="…"` and +`[!code-csharp[](…)]`, so the `.cs`, `.csproj`, `.json`, `.razor`, `.cshtml`, and `.js` files +those directives point at are part of the evidence. On `dotnet/AspNetCore.Docs` there are +about 1,890 such in-repo references, 1,737 of them to `.cs` files. A markdown-only checkout +makes every one of them unresolvable, and an audit that can't open the sample can't tell +whether the sample already demonstrates the feature. + +That cost is real — roughly 422 MB and 19,500 files versus 18 MB for markdown alone — but +it's paid once per `CACHE_MAX_AGE_HOURS`, and correctness isn't the thing to trade away here. + +```powershell +$cache = "{CACHE_ROOT}{repo-name}" +git clone --filter=blob:none --no-checkout https://github.com/{TARGET_REPO}.git $cache +cd $cache +git sparse-checkout init --cone +git sparse-checkout set {DOCSET_ROOT} +git checkout {TARGET_COMMIT_SHA} +``` + +Then **verify HEAD matches the pinned SHA** before trusting anything: + +```powershell +git rev-parse HEAD +``` + +### Sibling sample repositories + +Some references escape the docset entirely. A leading `~/../` resolves to a *sibling repo*, +not to this one: + +| Reference root | Repository | Needed for | +|---|---|---| +| `~/../AspNetCore.Docs.Samples/` | `dotnet/AspNetCore.Docs.Samples` | SignalR, gRPC, and other non-Blazor areas | +| `~/../blazor-samples/` | `dotnet/blazor-samples` | Blazor only | +| `~/../maui-samples/` | `dotnet/maui-samples` | Blazor Hybrid only | + +Clone `SAMPLES_REPOS` as **siblings inside the cache root**, so `~/../` resolves by plain +path arithmetic: + +```powershell +git clone --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.Samples.git ` + "{CACHE_ROOT}AspNetCore.Docs.Samples" +``` + +If a `:::code source=` path can't be resolved on disk, the element is **🟣, not ✅** — an +unread sample is an unknown, and guessing what it contains is how a false ✅ gets written. + +Write the marker beside the repo so the next run can reuse it: + +```powershell +@{ + repo = "{TARGET_REPO}"; sha = "{TARGET_COMMIT_SHA}" + cloned_at_epoch = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + docset_root = "{DOCSET_ROOT}" + sparse_scope = "{DOCSET_ROOT}" # full content root, no file-type filter + samples_repos = @('dotnet/AspNetCore.Docs.Samples') +} | ConvertTo-Json | Set-Content "{CACHE_ROOT}{repo-name}.cache.json" +``` + +Notes: + +* Cone mode takes directory prefixes. Don't narrow it with `--no-cone` extension globs — + that's what made sample code unreadable. +* `--filter=blob:none` fetches blobs on demand, so the clone stays cheaper than the working + tree suggests. +* The cache lives under `CACHE_ROOT`, never in the session's own working repo. +* **Never reuse a clone the audit didn't create.** A developer's working clone may sit on a + feature branch or carry uncommitted edits, and checking out `TARGET_COMMIT_SHA` in it + detaches their HEAD. The marker file distinguishes an audit cache from a working copy — + no marker, no reuse. +* Do **not** use `--depth=1`; you can't check out an arbitrary SHA from a shallow clone. +* This is read-only. Never commit, push, create a branch, or write a file inside the cache — + a dirty tree invalidates it for every future run. + +### Pin the product repo too + +Check out `PRODUCT_REPO` the same way — same cache rules, same marker, same age limit — and +record its SHA as `PRODUCT_COMMIT_SHA`. Reports link into product source to justify +behavioral claims, and a link to `blob/main/...` rots exactly like any other branch link. A +local checkout is also what makes Step 4's history search possible. + +## Step 3: Extract the feature's API surface + +Read the source describing the feature — release note, spec, or PR — **in full**. From it, +build an explicit inventory. This list is what you search for, and anything not on it won't +be found. + +Capture every: + +* Type, member, property, and option name +* Builder or extension method +* Event and callback name +* Overridable virtual method +* Endpoint path, route template, and HTTP method +* Configuration key and default value +* Behavioral claim — "the connection stays open", "the negotiate response reports the lifetime" + +Also record what the source *doesn't* say. Gaps in the release note are often the most +important thing to document. + +### Element types + +Most elements are symbols. Not all of them are. + +| Type | Example | +|---|---| +| New API | A type, member, option, event, or overridable method | +| New endpoint | A route template plus its method, codes, and registration condition | +| **Behavior change on an existing API** | Same signature, different runtime behavior | +| Default change | A value that changed without any signature moving | + +**The behavior-change type is the one that gets missed**, because searching for new symbols +finds nothing and the feature looks absent — or worse, looks already covered because the +old signature is documented. Client cancellation of non-streaming hub invocations used +`InvokeAsync` overloads that had shipped years earlier; the entire feature was a behavior +change. + +When an element is a behavior change, record **what the old behavior was** alongside the +new one. The contrast is the thing the documentation has to convey, and it's also what +decides whether the change is breaking. + +## Step 4: Verify behavior against product source + +**This is the step that prevents publishing wrong guidance.** Release notes summarize, and +summaries round off exactly the details that end up in a proposed edit. + +Before asserting any of the following in a report, read `PRODUCT_REPO`: + +| Claim | What to confirm | +|---|---| +| An endpoint exists | Its route template, and whether registration is conditional on an option | +| An HTTP method | What the handler does with other methods | +| A status code | The actual code and error payload | +| A parameter's location | Query string vs. body vs. header | +| A default value | The field initializer, not the documentation | + +A .NET 11 SignalR audit found the release note said only "exposes a `/refresh` endpoint alongside +`/negotiate`". Product source showed the endpoint is registered *only* when an option is +enabled, takes the connection token from the **query string**, returns **405** for non-`POST`, +and **404** with a specific error code when refresh is disabled. None of that was in the +release note, and all of it belonged in the proposed edit. + +If you can't verify a claim, it becomes a 🟣, not a confident sentence. + +### Find the implementing pull request + +The highest-value move available, and the one most likely to be skipped. Locate the commit +that introduced the behavior, then look at everything it touched. + +```powershell +git log -L{start},{end}:{file} # history of just the changed region +git show --stat {commit} # the full change surface of that commit +``` + +One commit typically spans client, server, protocol specification, and internal descriptors. +Seeing them together reveals the parts of a feature the release note never mentions — in the +test run this surfaced that the server token is linked to `ConnectionAborted`, that +cancellation is cooperative and still sends a `Completion`, and that a descriptor change made +`CancellationToken` synthetic on *all* hub methods rather than only streaming ones. None of +that was in the note. + +It also settles version applicability with evidence. A single commit adding both halves of a +feature is proof it's `CURRENT-ONLY`; a backport or servicing commit is proof it isn't. + +Cite the PR in the report and record `PRODUCT_COMMIT_SHA` in every product-source link. + +## Step 5: Search the docset + +For every symbol from Step 3, search `DOCSET_ROOT`. Exclude `EXCLUDE_AS_TARGET` from results +— the release notes mentioning a feature is not coverage, and leaving them in makes every +search look like a hit. + +Search for more than exact symbols: + +* The symbol itself, and its `xref` UID form +* The concept in prose — a reader searching "token expired" won't type `MaximumAuthenticationExpiration` +* The feature's marketing name and its API name, which are often different + +**A hit is a candidate, not a finding.** Search output is for locating files. Nothing else. + +## Step 6: Read candidate articles in full + +Open every candidate at `TARGET_COMMIT_SHA` and read it — front matter to end. + +This is non-negotiable and it is the step most likely to be skipped under time pressure. +Skipping it produces the two failure modes this skill exists to prevent: + +1. **Citing a line number from a search snippet.** Snippet line numbers drift from real ones, and a wrong line number in an issue is worse than no line number — it sends an author to the wrong paragraph and quietly destroys trust in the whole report. +2. **Missing coverage that uses different words.** The docset may document the feature thoroughly under terminology the release note never uses. + +While reading, record: + +* `uid` and `monikerRange` from front matter — read them, never infer them +* Every `:::moniker range=` / `:::moniker-end` boundary with its line numbers +* Every `# [Tab](#tab/...)` group boundary +* Section headings with line numbers +* Exact insertion points, with the anchor text of the preceding paragraph +* Every `:::code source=` and `[!code-*[](…)]` reference in the sections you're judging + +### Then follow the code references + +Work outward in layers, and only as far as the question requires: + +1. **`toc.yml`** — establishes what exists and where a reader would look for it. +2. **The articles** — prose, headings, moniker zones, insertion points. +3. **The code each article pulls in** — only for sections you're actually classifying. + +Layer 3 is where a coverage call gets decided, because article prose routinely says "see the +following example" and lets the sample carry the API surface. A feature can be fully +demonstrated in a `Program.cs` the article never names in prose, which reads as a gap until +you open the file. + +Resolve the path before judging the element: + +| Path form | Resolves to | +|---|---| +| `~/../AspNetCore.Docs.Samples/…` | sibling repo at `{CACHE_ROOT}AspNetCore.Docs.Samples` | +| `~/…` | `DOCSET_ROOT` | +| relative (`generic-host/samples/…`) | the **article's own directory**, not the area root | + +The relative case is the one that trips people up. `aspnetcore/fundamentals/host/generic-host.md` +referencing `generic-host/samples/6.x/GenericHostSample/Program.cs` means +`aspnetcore/fundamentals/host/generic-host/samples/…` — resolve from the file's directory, +not from `fundamentals/`. + +Only part of a file usually publishes. `id="snippet_Host"` selects the span between +`// ` and `// ` markers in the source; `range="1-20"` selects +lines. Judge the span that actually publishes, not the whole file — a feature demonstrated +outside the included region is invisible to readers. + +If a path won't resolve, the element is **🟣**; never assume a sample's contents. + +## Step 7: Classify every feature element + +One row per element from Step 3. + +| Status | Criteria | +|---|---| +| ✅ | Documented in the evergreen docset, accurately, under the correct moniker | +| ✏️ | Absent, inaccurate, or present but undiscoverable from the article a reader would actually be in | +| 🟣 | Could not determine — needs a human or product-team answer | + +### Expect ✅ to dominate + +Documentation teams frequently ship coverage alongside the feature. The .NET 11 SignalR +authentication-refresh audit found **11 of 14** elements already documented. + +This is not a wasted run, and it must not be reported as one. Reframe the output explicitly: +it is a verification record plus a short list of residual gaps. Say so in the report's goal +section. An author who believes they are writing net-new content for an already-covered +feature will produce redundant, conflicting prose — the reframing is what prevents that. + +### Discoverability is a real gap + +Content that exists in one article but is missing from the article a reader would actually +be in is a ✏️, not a ✅. Ask: *which article is the reader in when this question occurs to +them?* In the .NET 11 SignalR audit, hub lifecycle content lived only in the authentication +article — complete, but invisible to anyone reading the Hubs API reference to learn the +lifecycle. + +Three recurring shapes: + +* **Canonical-reference gap** — the API reference article for a type doesn't list a new member. +* **Options-reference gap** — the options article documents the server half of a feature but not the client half. +* **Infrastructure gap** — a new endpoint or route is never named, so nobody configuring a proxy, WAF, or route policy can find it. + +### Record what you couldn't settle + +A 🟣 with a sharp question beats a guess. Phrase it so a product-team member can answer in +one reply, and put it in the report's review considerations with the evidence you did gather. + +## Step 8: Choose the target article + +For each ✏️, pick the article a reader is already in — not the one that's easiest to edit, +and not the one that already has a related section. + +* Prefer the canonical reference for the API being changed. +* Add a cross-reference from the deep-dive article rather than duplicating prose. +* Never propose an edit inside `EXCLUDE_AS_TARGET`. +* Check whether a sibling feature's audit already covers the content, and note the overlap + instead of proposing the same edit twice. + +Then pin the exact insertion point by line number, and quote the surrounding text verbatim +as the "Before" block. That quote is what a later pass uses to detect drift. + +--- + +## Audit validation checklist + +- [ ] A SHA was pinned before the first file read, and reported. +- [ ] The checkout's HEAD was verified to equal the pinned SHA. +- [ ] The checkout covers the whole `DOCSET_ROOT` at **every file type** — not markdown-only, not one area. +- [ ] `SAMPLES_REPOS` were cloned as siblings, so `~/../` references resolve. +- [ ] The clone is an audit cache with a valid marker, within `CACHE_MAX_AGE_HOURS`, clean, and not a developer's working clone. +- [ ] Cache age was reported alongside the SHA when a cached clone was reused. +- [ ] `PRODUCT_COMMIT_SHA` was pinned and used in every product-source link. +- [ ] The API surface inventory was built from a full read of the source. +- [ ] Behavior changes on existing APIs were listed as elements, with the old behavior recorded. +- [ ] The implementing commit was located and its full change surface reviewed. +- [ ] Every endpoint, status code, default, and parameter location was verified against `PRODUCT_REPO`. +- [ ] `EXCLUDE_AS_TARGET` was excluded from search results and never proposed as an edit target. +- [ ] Every candidate article was read in full at the pinned SHA. +- [ ] Code referenced by the sections being judged was resolved and read, including the specific `id=`/`range=` span. +- [ ] Any unresolvable `:::code source=` path was recorded 🟣 rather than assumed. +- [ ] No line number came from a search snippet. +- [ ] `uid` and `monikerRange` were read from front matter, not inferred. +- [ ] Every element has exactly one of ✅, ✏️, 🟣. +- [ ] Discoverability gaps were classified ✏️, not ✅. +- [ ] Unverifiable claims became 🟣 with a specific question. +- [ ] If coverage was largely complete, the report says so up front. diff --git a/CopilotStore/AspNetCore.Docs/skills/docsdelta-moniker-zones/SKILL.md b/CopilotStore/AspNetCore.Docs/skills/docsdelta-moniker-zones/SKILL.md new file mode 100644 index 00000000..39a995dc --- /dev/null +++ b/CopilotStore/AspNetCore.Docs/skills/docsdelta-moniker-zones/SKILL.md @@ -0,0 +1,214 @@ +--- +name: docsdelta-moniker-zones +description: > + Insert version-scoped content into Microsoft Learn articles that use moniker zones. + Covers determining which zone an insertion point falls inside, the four moniker states an + article can be in, the split pattern required when new content must be scoped more + narrowly than the zone containing it, why moniker zones must never be placed inside tab + groups, and how to verify zone balance. Use when proposing or applying an edit to a + versioned docset article that contains :::moniker range=::: directives. +--- + +# docsdelta moniker zones + +Moniker zones control which .NET version selector shows a block of content. Get them wrong +and the failure is silent and public: content appears under versions where the API doesn't +exist, or vanishes from the version it documents. + +This is the highest-risk mechanical part of a versioned docs edit. Everything here assumes +you have read the target article **in full** and recorded every zone boundary with its line +numbers. + +## Parameters + +| Parameter | Example | Purpose | +|---|---|---| +| `MONIKER_PREFIX` | `aspnetcore` | Produces `>= aspnetcore-11.0` | +| `MAJOR_VERSION` | `11.0` | The version new content is scoped to | +| `VERSION_FLOOR` | `8.0` | Oldest supported version; never create a zone below it | + +--- + +## The two version mechanisms + +| Mechanism | Scope | Where | +|---|---|---| +| `monikerRange` | The whole article | YAML front matter | +| `:::moniker range="..."` / `:::moniker-end` | A block within the article | Body | + +Front matter sets the article's outer bounds. A zone can only narrow that range, never widen +it. An article with `monikerRange: '>= aspnetcore-3.1'` cannot show content to .NET Core 2.1 +readers no matter what its zones say. + +--- + +## Step 1: Find the enclosing zone + +Before proposing any insertion, determine what zone the insertion point is already inside. + +From the target line, scan **backwards** for the nearest `:::moniker range=` and **forwards** +for the nearest `:::moniker-end`. That pair is the enclosing zone. If the backward scan +reaches the top of the body without a match, the insertion point is unzoned and governed +only by front matter. + +Record the zone's range expression and both boundary line numbers. You need all three. + +> Zones don't nest. A `:::moniker-end` closes the most recent opener, so a stray opener +> silently swallows the rest of the article. + +## Step 2: Identify the article's moniker state + +| State | Condition | What the edit must do | +|---|---|---| +| **A** | No front-matter range, no zones anywhere | Add a zone around the new content only | +| **B** | Front-matter range only, no zones in the body | Add the first zone around the new content | +| **C** | Insertion point is already inside a zone matching the target version | Insert directly — **no structural change** | +| **D** | Insertion point is inside a zone that's broader or different from the target version | **Split the zone** — see Step 3 | + +State C is the safe case. Sequence C edits first when an audit produces several, so early +progress carries no structural risk. + +State D is where mistakes happen. + +### Don't be fooled by an unrelated zone + +An article can contain a zone for your target version that has nothing to do with your +insertion point. In a .NET 11 SignalR audit, `hubs.md` had a `>= aspnetcore-11.0` zone at L49–L54 — but +it covered an unrelated feature, and the actual insertion point at L381 sat inside a +`>= aspnetcore-8.0` zone spanning L56–L411. That's **State D**, not State C. + +The state is a property of the **insertion point**, not of the article. + +## Step 3: The split pattern + +When content must be scoped to `>= {MONIKER_PREFIX}-{MAJOR_VERSION}` but sits inside a +broader zone, close the broader zone, open the narrow one, close it, and reopen the broader +one. + +Given an insertion inside a `>= aspnetcore-8.0` zone: + +````markdown +{last line of existing 8.0 content} + +:::moniker-end + +:::moniker range=">= aspnetcore-11.0" + +{new version-scoped content} + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0" + +{existing content resumes} +```` + +Four directives, always in that order. The net change to zone balance is zero: one opener and +one closer added to each range. + +Rules: + +* **Reopen with the original range expression, character for character.** `>= aspnetcore-8.0` + and `>= aspnetcore-8.0 <= aspnetcore-10.0` are different zones; retyping from memory + silently changes what the rest of the article applies to. +* **Blank lines around every directive.** A directive adjacent to prose or a list can fail to + parse. +* **Split at a section boundary where possible.** Splitting mid-list or mid-table produces + content that renders correctly but is unmaintainable. +* **Never create a zone below `VERSION_FLOOR`.** + +### Overlap is intentional + +`>= aspnetcore-8.0` and `>= aspnetcore-11.0` both match a .NET 11 reader, so the 11.0 block +appears in document order between two 8.0 blocks — exactly the intent. Don't "fix" this by +bounding the 8.0 zone to `<= aspnetcore-10.0` unless the surrounding content genuinely stops +applying at 10.0. Bounding it removes that content from .NET 11 readers, which is almost +never what's wanted. + +## Step 4: Never put a zone inside a tab group + +Tab groups look like this: + +```markdown +# [.NET](#tab/dotnet) +... +# [JavaScript](#tab/javascript) +... +--- +``` + +Placing `:::moniker:::` directives inside one is fragile and breaks rendering in ways that +don't reliably show up in a local build. + +If version-scoped content belongs near a tab group, put it **before or after** the group, or +zone the entire group. If that isn't editorially acceptable, say so in the report and leave +the group alone rather than shipping a fragile edit. + +In the .NET 11 SignalR audit this rule kept two client-options tables untouched. That turned out to be correct on +the merits too — the APIs in question were builder methods, not members of the type those +tables document. + +## Step 5: Verify balance + +Before delivering, verify directives across the whole file — not just the edited region. +Counting alone is not enough. Equal counts can still be interleaved, and a plain count also +matches `:::moniker` directives that appear inside fenced code blocks as examples — the +split pattern in Step 3 is one. The script below skips fenced blocks, checks ordering in the +same pass, and reports line numbers. + +```powershell +$lines = Get-Content {path} +$inFence = $false; $open = $null; $errors = @() +for ($i = 0; $i -lt $lines.Count; $i++) { + $l = $lines[$i] + if ($l -match '^\s*(```+|~~~+)') { $inFence = -not $inFence; continue } + if ($inFence) { continue } + if ($l -match '^\s*:::moniker range=') { + if ($open) { $errors += "L$($i+1): opener while zone from L$open is still open" } + $open = $i + 1 + } + elseif ($l -match '^\s*:::moniker-end') { + if (-not $open) { $errors += "L$($i+1): moniker-end with no open zone" } + $open = $null + } +} +if ($open) { $errors += "L${open}: zone never closed" } +$errors +``` + +The script must print nothing. Treat it as a backstop rather than a proof — the fence toggle +doesn't handle fences of differing lengths or indented code blocks — so still confirm by +reading that every opener closes before the next one opens. + +A file that's balanced but interleaved renders as nonsense, so the count alone isn't enough. + +--- + +## Choosing the version scope + +| Applicability | Meaning | Zone required | +|---|---|---| +| `CURRENT-ONLY` | New in this release | `>= {MONIKER_PREFIX}-{MAJOR_VERSION}` | +| `RETROACTIVE` | Also shipped in earlier supported versions | One zone per applicable range — cite evidence | +| `VERSION-AGNOSTIC` | Conceptual, no version dependency | None | + +Default to `CURRENT-ONLY` for a feature announced in a release note. Claim `RETROACTIVE` only +with evidence — a backport PR, a servicing note, or an API reference showing the earlier +version. "It probably works in 10 too" is not evidence. + +--- + +## Zone editing checklist + +- [ ] The article was read in full and every zone boundary recorded with line numbers. +- [ ] The enclosing zone of the insertion point was identified by scanning both directions. +- [ ] The moniker state was determined from the **insertion point**, not from the article. +- [ ] Any same-version zone elsewhere in the article was confirmed relevant before relying on it. +- [ ] State D edits use the full four-directive split. +- [ ] The reopened range expression matches the original character for character. +- [ ] Blank lines surround every directive. +- [ ] No zone was placed inside a `# [Tab](#tab/...)` group. +- [ ] No zone was created below `VERSION_FLOOR`. +- [ ] Opener and closer counts match across the whole file. +- [ ] Zones were confirmed non-interleaved by a top-to-bottom walk. +- [ ] `RETROACTIVE` claims cite evidence. diff --git a/CopilotStore/AspNetCore.Docs/skills/docsdelta-report-format/SKILL.md b/CopilotStore/AspNetCore.Docs/skills/docsdelta-report-format/SKILL.md new file mode 100644 index 00000000..0876b694 --- /dev/null +++ b/CopilotStore/AspNetCore.Docs/skills/docsdelta-report-format/SKILL.md @@ -0,0 +1,450 @@ +--- +name: docsdelta-report-format +description: > + The report contract for a per-feature documentation coverage analysis. Defines the exact + structure, heading order, status icons, change-numbering scheme, and title convention for + a report delivered in conversation and saved to a local file, ready to become a GitHub + issue. Repository-agnostic: the invoking agent supplies TARGET_REPO and + TARGET_COMMIT_SHA. Use when producing or consuming a What's New coverage report. +--- + +# docsdelta report format + +This is a **contract**. One report covers exactly **one feature sub-section** of a What's New +article. A reader must be able to act on it without opening the release notes or the +originating conversation. + +The report is written for two audiences at once: a human deciding whether the analysis is +right, and an AI author who will apply the changes. Every proposed edit must be paste-ready. + +## What every report needs to know + +Four things come from the analysis run and appear throughout the report: the **repo** being +analyzed, the **commit SHA** it was analyzed at, the **major version**, and the **moniker +prefix** for that docset. A fifth is needed whenever behavior was verified against product +source: the **product repo and its pinned SHA**, so that the References section links to +fixed code rather than to a moving branch. + +Links are built as `https://github.com/{repo}/blob/{sha}/{path}#L{start}-L{end}`. Always the +pinned SHA — a link to a branch points somewhere different next week. Never let a local +filesystem path into the report. + +The report is saved as `{section-number}-{kebab-title}.md`, matching the number it was +selected by: `1.2-cancel-hub-invocations-from-the-client.md`. A later filing pass finds +reports by that name. + +--- + +## Title convention + +``` +v{MAJOR_VERSION} update: {concise subject} +``` + +The title is the H1 of the report **and** the proposed GitHub issue title. Examples: + +* `v11 update: SignalR authentication refresh` +* `v11 update: OpenAPI 3.2 support` +* `v11 update: Minimal API async validation` + +Rules: + +* No emoji, no `Issue draft:` prefix, no trailing period. +* Subject names the **feature**, not the remedy. `v11 update: SignalR authentication refresh` — not `v11 update: add missing SignalR docs`. +* Keep it under about 60 characters so it reads cleanly in an issue list. + +--- + +## Status icons + +Exactly three. Use them in the coverage status summary table **and** on change headings — +the same vocabulary in both places, so a reader scanning the table and a reader scanning the +changes learn the same thing. + +| Icon | Meaning | +|---|---| +| ✅ | Already documented. No action. | +| ✏️ | Update needed. | +| 🟣 | Could not determine. Needs a human or product-team answer. | + +**Never use ❌ or any other status marker.** If a quoted table cell from the docset happens +to contain ❌ (the SignalR client feature matrix does), reword the surrounding prose to say +"unsupported" so the literal character isn't mistaken for a report status. + +--- + +## Change numbering + +Changes are numbered so people can refer to them in issue comments — "let's talk about 2" +or "I'd skip 1.1". + +| Form | Use | +|---|---| +| `N. Update` | A top-level change | +| `N.1 Update` | A sub-item of change `N` | +| `N.1 Update (Option A)` / `(Option B)` | Two valid approaches where the analysis genuinely cannot recommend one | + +Options are a last resort, not a hedge. Use them only when the choice depends on information +the analysis doesn't have — an editorial preference, a cross-repo sequencing cost, a team +convention. If one approach is clearly better, pick it and say why in the rationale. + +A change that needs no work is still numbered, but takes ✅ and a one-line body: + +```markdown +### ✅ 4. Update — TOC + +**No TOC change required.** All three target articles already have TOC entries, and no new article is proposed. +``` + +--- + +## Never include `ms.date` instructions + +The AI author assigned to the resulting issue already has standing instructions to update +`ms.date`. Restating it wastes a numbered slot and adds review noise. + +This means: + +* No `ms.date` change entry. +* No `ms.date` row in the affected files table. +* No `ms.date` step in the action plan. + +Front-matter changes that are **not** `ms.date` — a `monikerRange` widening, adding +`ai-usage: ai-assisted` — are still in scope and still get their own numbered change. + +--- + +## Section order + +Every report has these sections, in this order. Omit a section only when the rule below says +it's allowed. + +| # | Section | Required | +|---|---|---| +| 1 | H1 title + metadata block | Always | +| 2 | `## 🎯 Goal` | Always | +| 3 | `## ✅ Coverage status summary` | Always | +| 4 | `## 🔢 Version applicability` | Always | +| 5 | `## 📋 Coverage gap summary` | Always | +| 6 | `## 📁 Affected files` | Always | +| 7 | `## 📝 Proposed changes` | Always | +| 8 | `## ✅ Action plan` | Always | +| 9 | `## ⚠️ Review considerations` | Omit only when there are no 🟣 rows and nothing out of scope to note | +| 10 | `## 🔗 References` | Always | + +--- + +## Template + +Replace `{...}` placeholders. Preserve heading text and order. + +````````markdown +# v{MAJOR_VERSION} update: {concise subject} + +**Target repository:** `{TARGET_REPO}` +**Analyzed at commit:** `{TARGET_COMMIT_SHA}` {*(cached clone, {N}h old)* — only when reused} +**Product source verified at:** `{PRODUCT_REPO}` @ `{PRODUCT_COMMIT_SHA}` +**Source release note:** [{feature_heading}]({WHATS_NEW_URL}{published_anchor}) +**Proposed labels:** `{area-label}`, `{version-label}` {verified to exist in `{TARGET_REPO}`, case-sensitively} + +--- + +## 🎯 Goal + +{One short paragraph naming the outcome. If the feature turns out to be mostly documented +already, say so explicitly and reframe — an author who thinks they're writing net-new +content will write the wrong thing.} + +{Then a numbered list of the concrete gaps this report closes.} + +--- + +## ✅ Coverage status summary + +**Legend:** ✅ already documented · ✏️ update needed · 🟣 could not determine + +| # | Feature element from What's New | Status | Where | +|---|---|:--:|---| +| 1 | {element} | ✅ | [`{path}` L{a}–L{b}](permalink) | +| 2 | **{element}** | ✏️ | **{N}. Update** — [`{path}` L{a}–L{b}](permalink) | +| 3 | {element} | 🟣 | See *Review considerations* | + +--- + +## 🔢 Version applicability + +**Applies to:** `{CURRENT-ONLY | RETROACTIVE | VERSION-AGNOSTIC}` +**Target moniker:** `>= {MONIKER_PREFIX}-{MAJOR_VERSION}.0` +**Earlier versions affected:** {None — new in .NET {MAJOR_VERSION}. | .NET X, .NET Y — see evidence below.} + +| Article | `monikerRange` | Moniker state | +|---|---|---| +| `{path}` | `'>= {MONIKER_PREFIX}-2.1'` | **State {A|B|C|D}** — {what exists today and what the edit must do about it, with permalinked line ranges} | + +--- + +## 📋 Coverage gap summary + +{Two to four sentences naming the specific question a developer cannot answer from the +docset today, and which article they would have been reading when they failed to answer it. +Be concrete. "Coverage is thin" is not a finding; "a developer reading the Hubs API article +sees only two lifecycle overrides and has no way to learn a third exists" is.} + +**Feature announced in What's New:** +> {Quote the relevant sentences from the published What's New section.} + +**Breaking change:** {Yes — describe impact | No} + +--- + +## 📁 Affected files + +| Item | Path | Lines | Section | +|------|------|-------|---------| +| {N}. | [`{path}`](permalink) | {a}–{b} | "{Section heading}" | + +**Target article uids:** `{uid}`, `{uid}` + +--- + +## 📝 Proposed changes + +### ✏️ {N}. Update — `{path}`, {insert after line X | replace lines X–Y | split the moniker zone and insert after line X} + +**Applies to:** `{moniker range this change lands in}` +**Location:** [Line {X}](permalink), immediately after the paragraph beginning "{anchor text}". + +**Before (lines {a}–{b}):** + +```markdown +{Exact current text, verbatim from the file at TARGET_COMMIT_SHA.} +``` + +**After:** + +```markdown +{Exact proposed text, moniker-gated as required, ready to paste.} +``` + +**Rationale:** {One or two sentences. Say why *this article* rather than a different one.} + +--- + +### ✅ {N}. Update — TOC + +**No TOC change required.** {One clause explaining why.} + +--- + +## ✅ Action plan + +1. {Confirm the coverage status summary — especially that the ✅ rows genuinely need no edit.} +2. {Sequence the changes by risk. Self-contained inserts inside an existing moniker zone go first; zone splits go last.} +3. {Verify every `` resolves.} +4. {Build and confirm the content renders under the .NET {MAJOR_VERSION} selector **and does not appear** under earlier versions.} +5. {Resolve any OpenPublishing.Build warnings.} + +--- + +## ⚠️ Review considerations + +* 🟣 **{Open question}.** {What was checked, what remains unknown, and who can answer it.} +* **Out of scope for this issue** ({why — usually because a sibling sub-section covers it}): {list}. + +--- + +## 🔗 References + +* What's New section: [{feature_heading}]({WHATS_NEW_URL}{published_anchor}) +* Product source: {links to the files read to verify API behavior, permalinked at `PRODUCT_COMMIT_SHA`} +* Implementing PR: {link, when the commit that introduced the behavior was identified} +```````` + +--- + +## When one issue carries several reports + +The default is one report, one issue. When a filing pass merges two or three reports — +because their edits collide *and* their subject is the same — the merged body is still a +report and still follows this contract. What changes: + +* **One H1**, naming the shared subject. Not a concatenation: + `v11 update: OpenAPI document generation — HTTP QUERY support and the default 3.2 document version`, + not `v11 update: HTTP QUERY and document version and obsolete APIs`. +* **One coverage status summary** combining every feature element from every source report, + with an added column naming the source sub-area so each row still traces back to the + release note. +* **Changes renumbered from 1, in ascending line order per file.** The source reports' + numbers are gone. Line order is what makes the action plan applicable as a sequence + instead of three sequences that contradict each other. +* **One authoritative moniker balance derivation** covering all the changes together, showing + the running total. Each source report derived its balance in isolation; those numbers are + wrong the moment they're combined. +* **A `> [!IMPORTANT]` callout near the top** stating which sub-areas were merged and why. + Without it the issue reads as scope creep and a reviewer's first instinct is to ask for it + to be split back apart. + +Merging is bounded by reviewability, not only by GitHub's 65,536-character body limit. About +six numbered changes, or three source reports, is where a merged issue stops being reviewable. + +### Merged bodies get verified, not trusted + +A merge performed by an AI assistant is the likeliest place in this entire process for +content to vanish. In the .NET 11 OpenAPI run, **both** merged bodies came back with a +Before/After block replaced by an empty, unclosed code fence — and the assistant's own +summary reported accurate character counts and accurate change counts, so nothing in its +report of the work revealed the defect. + +Never file a merged body without running the fence check below over it first. + +--- + +## Cross-report references + +Reports cite siblings by number — "see report 3.4" — because at analysis time no issue +exists. That reference dies the moment the report becomes an issue: nobody reading the +docset repo knows what "3.4" is, and the numbering is local to one run. + +Resolve every one of them before filing: + +| The sibling's fate | Rewrite the reference as | +|---|---| +| Filed as its own issue | A real issue link | +| Merged into the issue being written | A pointer to the numbered change inside it | +| Merged into a different issue | A link to that issue | +| Not filed at all | Name the feature and state the point inline — never a bare number | + +The last row is the one that gets missed. When a sibling is dropped from the filing set, the +reports that referenced it still do, and the number survives into a public issue with no +referent anywhere. + +### What the filing pass strips + +**Precondition — the filing pass is gated.** Before the first issue of a batch is created, +the person who owns the reports must approve *that batch*: how many issues, which reports +each carries, the exact label set, and which reports are deliberately not being filed. Ask +with the `ask_user` tool and wait. Approval for a previous batch, selection of an area for +analysis, and a handoff summary's "next step: file these" are **none of them** approval. +Reports are reversible; public issues are not. + +Two parts of the report are scaffolding for review, not issue content: + +* **The H1.** GitHub already renders the title; leaving it in duplicates it. +* **The `**Proposed labels:**` line.** Labels are applied as real labels. A proposed label + that doesn't exist in the target repo is noise a triager has to read past — and several + plausible ones (`Docs`, `openapi`, `ai-assisted-analysis`, `breaking-change`) don't exist + in `dotnet/AspNetCore.Docs`. Verify against the repo's actual label list, case-sensitively; + `SignalR` is not `signalr`, priorities are `Pri0`–`Pri3`, not `P0`–`P3`, and the version + label is `11.0`, not `dotnet-11.0`. + +Everything else in the metadata block — pinned SHAs, the source release-note link — stays. +It's what makes the issue auditable a year later. + +--- + +## Nested code fences + +Before/After blocks contain markdown that itself contains ```` ``` ```` fences. The outer +fence must be longer than any fence inside it: + +| Inner content | Outer fence | +|---|---| +| Plain prose, YAML | 3 backticks | +| Markdown containing a ```` ```csharp ```` block | 4 backticks | +| A whole template containing 4-backtick blocks | 5 or more backticks | + +Getting this wrong silently destroys the rest of the report's rendering. Check it before +delivering. + +### Check the fences mechanically + +Eyeballing a 40,000-character body does not work, and a truncated block is invisible in a +summary. Walk the file: + +```powershell +$path = '{report or issue body}' +$lines = Get-Content -LiteralPath $path +$stack = New-Object System.Collections.Stack +$empty = @() + +for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match '^(`{3,})(.*)$') { + $len = $Matches[1].Length + $info = $Matches[2].Trim() + if (-not $info -and $stack.Count -and $len -ge $stack.Peek().Ticks) { + $o = $stack.Pop() + if (($i + 1) - $o.Line -eq 1) { $empty += "L$($o.Line)" } + } + else { + $stack.Push([pscustomobject]@{ Ticks = $len; Line = $i + 1 }) + } + } +} + +"chars : {0}" -f (Get-Content -Raw -LiteralPath $path).Length +"unclosed : {0}" -f ((($stack.ToArray() | ForEach-Object { 'L' + $_.Line }) -join ', ') -replace '^$', 'none') +"empty : {0}" -f ((($empty) -join ', ') -replace '^$', 'none') +"stale refs: {0}" -f (Select-String -Path $path -Pattern '[Rr]eports?\s+\d+\.\d+' -AllMatches).Count +``` + +A fence carrying an info string is always an opener. A bare fence closes the stack top only +when it's at least as long; otherwise it opens a new block. Three signals matter: + +* **An unclosed fence** — everything after it is swallowed. +* **An empty fence**, an opener immediately followed by its closer — a Before/After block + that lost its body. This is the shape AI-assisted merges produce. +* **Stale sibling references** that still name a report number. See *Cross-report references*. + +Moniker directive counts will **not** balance in a report, and that is expected: Before/After +blocks quote partial zones by design. Don't chase it. Only unclosed and empty fences indicate +real truncation. + +--- + +## Evidence rules + +* **Never cite a line number from a search snippet.** Read the full file at + `TARGET_COMMIT_SHA` and count. A wrong line number is worse than no line number. +* **Every "Before" block must match the file byte-for-byte.** It is the drift check that a + later filing pass depends on. +* **Verify API and endpoint claims against product source**, not against the docs or the + release note. The release note is a summary and may round off details that matter — route + templates, HTTP methods, status codes, and whether something is conditionally registered. +* **State what you could not determine.** A 🟣 row with a clear question is more useful than + a confident guess. Put the question in *Review considerations* with enough context that a + product-team member can answer it in one reply. +* **Expect "already covered" to be a common outcome.** Docs teams often ship coverage with + the feature. When that happens the report is still valuable — it becomes a verification + record plus a short list of discoverability gaps. Say this plainly in the Goal so nobody + writes redundant content. + +--- + +## Report validation checklist + +- [ ] H1 is `v{MAJOR_VERSION} update: {subject}` — no emoji, no `Issue draft:` prefix. +- [ ] Only ✅, ✏️, and 🟣 appear as status markers. +- [ ] Every ✏️ row in the summary table points at a numbered change, and every ✏️ numbered change has a row. Standing ✅ changes such as the TOC entry are exempt — they aren't feature elements. +- [ ] Changes use `N.` / `N.1`; Options appear only where a recommendation was genuinely impossible. +- [ ] No `ms.date` instruction anywhere. +- [ ] Section 5 is titled **Coverage gap summary**. +- [ ] Every permalink uses `TARGET_REPO` + `TARGET_COMMIT_SHA`, not a branch; product-source links use `PRODUCT_COMMIT_SHA`. +- [ ] When the checkout came from a cache, the metadata block states its age. +- [ ] The file is saved as `{section-number}-{kebab-title}.md`. +- [ ] Every "Before" block was verified against the file at that SHA. +- [ ] Every change entry names the moniker range it lands in. +- [ ] Moniker zone splits are balanced — each `:::moniker-end` has a matching opener. +- [ ] No `:::moniker:::` zone was placed inside a `# [Tab](#tab/...)` group. +- [ ] Nested fences are correctly sized. +- [ ] The mechanical fence check ran clean — zero unclosed fences, zero empty fences. +- [ ] No bare cross-report reference (`report 3.4`) survives in a body about to be filed. +- [ ] A merged body renumbers changes from 1 in ascending line order, combines the coverage tables, derives one moniker balance for the whole set, and carries the `> [!IMPORTANT]` merge rationale. +- [ ] No local filesystem path and no hard-coded repo name outside `TARGET_REPO`. + +--- + +## Related + +* **[docsdelta-section-inventory](../docsdelta-section-inventory/SKILL.md)** — enumerates the sections that each become one report. +* **[docsdelta-coverage-audit](../docsdelta-coverage-audit/SKILL.md)** — produces the evidence this report presents. +* **[docsdelta-moniker-zones](../docsdelta-moniker-zones/SKILL.md)** — the zone mechanics behind every version-scoped change entry. diff --git a/CopilotStore/AspNetCore.Docs/skills/docsdelta-section-inventory/SKILL.md b/CopilotStore/AspNetCore.Docs/skills/docsdelta-section-inventory/SKILL.md new file mode 100644 index 00000000..208ae60a --- /dev/null +++ b/CopilotStore/AspNetCore.Docs/skills/docsdelta-section-inventory/SKILL.md @@ -0,0 +1,245 @@ +--- +name: docsdelta-section-inventory +description: > + Builds a numbered inventory of every major area and feature sub-area in a "What's New in + ASP.NET Core" release-notes article by reading the published Microsoft Learn page, applying + the standing exclusions (Blazor, Blazor Hybrid, Breaking changes), and presenting a + selection menu that defaults to everything eligible. Use when scoping a What's New doc gap + analysis, or any time you need to enumerate and number the sections of a release-notes + article before processing them. +--- + +# docsdelta section inventory + +Produces the numbered work list that drives a section-by-section analysis. One inventory +entry becomes one unit of work — and, for a doc gap run, one report. + +## Parameters + +| Parameter | Required | Default | Description | +|---|---|---|---| +| `WHATS_NEW_URL` | No | *(latest — see below)* | Published Learn URL for the article | +| `MAJOR_VERSION` | No | *(derived)* | Derived from the URL's `aspnetcore-{N}` segment | + +--- + +## Step 1: Resolve the article + +### The published page is the source of truth + +**Read the published Learn article. Not the GitHub source.** + +The release note is a shell that pulls its prose from include files, and that structure is an +implementation detail of the release-notes repo. It is not what readers see, and walking it +introduces a correctness bug: include files exist that no `[!INCLUDE]` directive references, +so they never publish. Inventorying one means scoping work for a feature no reader can find. +Reading the rendered page makes that class of error impossible — if it's on the page, it +shipped; if it isn't, there's nothing to audit. + +The page also carries clean anchors and headings in true reading order, so nothing has to be +derived or spot-checked. + +### Default URL + +Propose the **latest** release-notes article and ask the operator to confirm or replace it: + +``` +https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-{MAJOR_VERSION} +``` + +Don't hard-code a version — a fixed number means the skill silently analyzes last year's +release forever. Probe upward from the version you believe is current until a URL 404s, and +take the highest one that resolves. + +Present it for confirmation before doing any work: + +``` +Default What's New article for this run: + + https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11 + +Use this, or give me a different URL? +``` + +Accept an operator-supplied URL for any version, and derive `MAJOR_VERSION` from its +`aspnetcore-{N}` segment. + +### Record the article's source commit + +The page's metadata reports the commit it was rendered from: + +``` +git_commit_id: 0a1b2c3d4e5f60718293a4b5c6d7e8f901234567 +source_path: aspnetcore/release-notes/aspnetcore-11.md +``` + +Record that as `RELEASE_NOTE_SHA` and use it for permalinks back to the release note. It's a +better pin than resolving `main` yourself, because it's the exact version the reader saw. + +> This is **not** the SHA the coverage audit runs against. That one is pinned separately +> against the docset being audited. Two different sources — don't collapse them. + +--- + +## Step 2: Build the outline + +Read the article top to bottom, preserving order: + +1. Every `##` heading is a **major area**. +2. Every `###` heading beneath it is a **feature sub-area**. +3. A major area with no `###` beneath it is itself a single unit of work. + +Record for each entry: heading text, heading level, published anchor, and the enclosing major +area. + +### Anchors + +Take the anchor from the page rather than deriving it. Learn disambiguates duplicate headings +by appending `-1`, `-2`, and so on, which slugification won't predict. + +`SignalR .NET client supports authentication refresh after redirects` +→ `#signalr-net-client-supports-authentication-refresh-after-redirects` + +### If the page is truncated + +Long release notes exceed a single fetch. Page through with an increasing start index until +the article ends, and confirm the last major area you captured is the one that actually ends +the page. A truncated fetch silently drops whole areas from the inventory. + +--- + +## Step 3: Apply standing exclusions + +Three sections are excluded by default. They're owned elsewhere or need a different kind of +analysis: + +| Section | Reason | +|---|---| +| `## Blazor` | Owned by a separate documentation team. | +| `## Blazor Hybrid` | Same. | +| `## Breaking changes` | Not a coverage gap. Breaking changes are tracked through the breaking-changes docset and need migration guidance, not feature coverage. | + +Also excluded as a **target** for proposed edits: + +| Path | Reason | +|---|---| +| `aspnetcore/release-notes/**` | The release notes are where the feature is announced, and the published article is what this inventory reads. Announcing it again is not coverage. Proposed changes must land in the evergreen docset. | + +**Show excluded sections in the inventory anyway**, marked and unselected, in the trailing +"Excluded by default" block of the menu. A hidden exclusion is indistinguishable from a bug, +and the operator may deliberately want one. + +**Excluded sections do not consume numbers.** Numbering runs over eligible areas only, +preserving their relative order on the page. Blazor and Blazor Hybrid are the first two H2s +on the .NET 11 page, so SignalR — the third H2 — is major area `1`. + +This matters more than it looks. If exclusions consumed numbers, `1.2` would mean one thing +in a run that audited everything and another in a run that skipped Blazor, and every issue +thread referring to a number would become ambiguous. + +--- + +## Step 4: Present the numbered menu + +Number major areas `1`, `2`, `3`; sub-areas `1.1`, `1.2`. These numbers are how the operator +selects work and how everyone refers to it afterward, so keep them stable for the run. + +``` +What's New in ASP.NET Core in .NET 11 +https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11 +Published from 0a1b2c3 · 38 sub-areas across 5 eligible major areas + + 1. SignalR + 1.1 SignalR authentication refresh + 1.2 Cancel hub invocations from the client + 1.3 SignalR .NET client supports authentication refresh after redirects + 1.4 SignalR TypeScript client supports authentication refresh + 2. Minimal APIs + 2.1 ... + 3. OpenAPI + 3.1 ... + 4. Authentication and authorization + 4.1 ... + 5. Miscellaneous + 5.1 ... + +Excluded by default: + ➖ Blazor (separate docs team) + ➖ Blazor Hybrid (separate docs team) + ➖ Breaking changes (not a coverage gap) + +Which do you want to analyze? [default: all of 1–5] + · "all" every eligible sub-area, in order + · "1" a whole major area + · "1.1" one sub-area + · "1.1, 3, 4.2" any mix + · "1.1-1.3" a range + · "+blazor" opt an excluded section back in +``` + +### Interpreting the reply + +| Reply | Means | +|---|---| +| Empty, `all`, `yes`, `go` | Every eligible sub-area, in inventory order | +| `1` | Every sub-area under major area 1 | +| `1.1` | That sub-area only | +| `1.1-1.3` | Inclusive range within one major area | +| `+blazor`, `+breaking` | Add an excluded section to the selection | +| `-4` | Everything except major area 4 | + +Echo the resolved selection as an explicit list before starting work, so a misparse is caught +before it costs an hour: + +``` +Selected 4 sub-areas: + 1.1 SignalR authentication refresh + 1.2 Cancel hub invocations from the client + 1.3 SignalR .NET client supports authentication refresh after redirects + 1.4 SignalR TypeScript client supports authentication refresh +``` + +If a number doesn't exist, say which and re-show the menu. Never silently drop it, and never +guess at an adjacent number. + +--- + +## Step 5: Emit the work list + +Each selected entry carries everything downstream needs: + +```yaml +- id: "1.1" + major_area: SignalR + title: SignalR authentication refresh + published_anchor: "#signalr-authentication-refresh" + published_url: "https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11#signalr-authentication-refresh" + excluded: false +``` + +Process in inventory order unless told otherwise. Order matters: sibling sub-areas of one +major area share context — the SignalR client-refresh sub-areas are nearly the same analysis +three times — and processing them together avoids re-reading the same articles. + +--- + +## Notes on behavior + +* **A section may be covered by a sibling's analysis.** Before writing a report for `1.4`, + check whether `1.1`'s analysis already located that content. Note the overlap rather than + duplicating the finding. +* **Never inventory from the GitHub source.** Unreferenced include files exist and don't + publish. Scoping work for one means analyzing a feature no reader can reach. +* **Don't renumber mid-run.** If the article changes while a run is in progress, finish + against the version you captured and report the drift. +* **The page's heading order is the article's order.** Preserve it *among eligible areas* — + their relative sequence must match the published page, because operators navigate by + position. Excluded areas are pulled out into the trailing "Excluded by default" block and + don't occupy a slot in the numbered list. + +--- + +## Related + +* **[docsdelta-coverage-audit](../docsdelta-coverage-audit/SKILL.md)** — analyzes each selected entry. +* **[docsdelta-report-format](../docsdelta-report-format/SKILL.md)** — the report produced for each selected entry.