Skip to content

Commit 1298929

Browse files
mattleibowCopilot
andauthored
[api-docs] Fix false-positive extraction and add merge guard (#4030)
[api-docs] Fix false-positive extraction and add merge guard (#4030) Context: mono/SkiaSharp-API-docs#115 Companion: mono/SkiaSharp-API-docs#116 Review of the automated docs output (PR #115) revealed three systemic pipeline issues: the extract regex matched legitimate prose containing "to be added" (e.g. SKPath.AddPath's "elements to be added to the current path"), the merge step had no guard against agent-invented fields, and the writer produced wrong domain facts (gamma 2.8 vs 2.2 for BT.470, contradictory bit-packing for Bgra10101010XR). docs-tool.ps1: * Anchor extraction regex to `^\s*To be added\.?\s*$` — only full-text placeholder matches trigger extraction * Record `_extractedKeys` metadata during extract so the merge phase knows which fields were originally placeholders * Add merge guard that rejects any agent-added fields not present in the original extract (prevents invented documentation) * Exclude manifest.json from merge processing * Fix PowerShell falsy-empty-array check (`@()` is falsy in boolean context; use .PSObject.Properties existence test instead) SKILL.md: * Writer prompt: JSON integrity rules (never add/remove/rename fields), trust hierarchy for native type facts (header > reference > knowledge) * Factual verifier prompt: standard value verification against skia-patterns.md reference file * Phase 2: simplified for automated workflow awareness Validated across 8 workflow runs — 0 tooling regressions post-fix. Co-authored-by: Matthew Leibowitz <mattleibow@live.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 69bff95 commit 1298929

2 files changed

Lines changed: 78 additions & 8 deletions

File tree

‎.agents/skills/api-docs/SKILL.md‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ STEPS:
8787
c. Fill all "To be added." fields following the rules below
8888
d. Write the completed JSON back to the same path
8989
90+
JSON INTEGRITY RULES (NON-NEGOTIABLE):
91+
- NEVER add new keys to the JSON that are not already present in the file
92+
- NEVER add new entries (members) to the JSON that were not extracted
93+
- Only modify values of EXISTING keys — the extract tool determines scope
94+
- The "_extractedKeys" array in each entry lists exactly which fields you may fill
95+
- If a member has only "remarks" extracted, fill ONLY "remarks" — do NOT add
96+
"summary", "params", or other fields even if you think they could be improved
97+
9098
WRITING RULES:
9199
- NEVER invent API calls — verify every method/overload exists by reading C# source
92100
- NEVER guess numeric values — read MemberValue from JSON, cross-reference source
@@ -95,6 +103,14 @@ WRITING RULES:
95103
- Use <see langword="null" /> not <see langword="default" /> for nullable params
96104
- Use <see langword="true" /> and <see langword="false" /> for boolean literals in prose
97105
106+
STANDARD VALUES (CICP, Vulkan, OpenType, etc.):
107+
- For enum members referencing external standards, you MUST read the C/C++ header
108+
in externals/skia/ where the enum is defined and verify numeric values
109+
- Do NOT rely on your own knowledge of standards for specific numeric values
110+
(gamma values, bit depths, transfer function parameters, etc.)
111+
- If you cannot find the header, state "value from standard" without inventing
112+
specific numbers (e.g., say "assumed display gamma" not "assumed gamma 2.2")
113+
98114
REMARKS RULES:
99115
- Type-level entries (memberType=type) have remarksRequired=true with a CDATA template.
100116
Complete it fully — never leave [bracketed placeholders]. Include description,
@@ -157,6 +173,18 @@ SPECIFIC CHECKS:
157173
- "Gets or sets" vs "Gets": check if property has { get; set; } or only { get; }
158174
- Cross-library: SkiaSharp and HarfBuzzSharp are DIFFERENT libraries with different conventions.
159175
176+
STANDARD VALUE VERIFICATION (CRITICAL):
177+
- For enum members that cite external standards (CICP, ITU-T, Vulkan, OpenType),
178+
you MUST locate and read the C/C++ header where the enum is defined (check
179+
externals/skia/include/ or binding/ generated files)
180+
- Cross-reference the MemberValue in JSON against the enum constant in source
181+
- If documentation claims a specific numeric property of a standard (e.g., "gamma 2.2",
182+
"10-bit precision", "64-bit packed"), verify it is consistent:
183+
- Does the bit math add up? (e.g., "64-bit with 10+10+10+10 packed" = only 40 bits → ERROR)
184+
- Does the gamma match the correct CICP value number?
185+
- If you cannot find the header, flag the claim as UNVERIFIED rather than assuming correct
186+
- Provide file path + line number for every standard value you verify
187+
160188
TRUST HIERARCHY for native type facts (bit layouts, byte orders, macro expansions):
161189
1. Native C/C++ header in repo (if you can find and read it) — AUTHORITATIVE
162190
2. skia-patterns.md reference file — PRE-VERIFIED, trust it if header unavailable
@@ -259,7 +287,10 @@ After fixing all CRITICAL review issues, merge the JSON back into XML:
259287
pwsh .agents/skills/api-docs/scripts/docs-tool.ps1 merge output/docs-work/
260288
```
261289

262-
The merge tool has built-in safety checks — it counts `MemberSignature` and `TypeSignature` elements before and after merging each file and aborts with a fatal error if any were lost. It also validates the output XML is well-formed.
290+
The merge tool has built-in safety checks:
291+
- **Signature preservation** — counts `MemberSignature` and `TypeSignature` elements before and after merging each file and aborts with a fatal error if any were lost
292+
- **Extract guard** — rejects any field not listed in `_extractedKeys` (prevents agents from overwriting existing documentation with "improved" versions). Rejected fields emit a warning.
293+
- **XML validation** — validates the output XML is well-formed after save
263294

264295
Then run formatting to clean up:
265296

‎.agents/skills/api-docs/scripts/docs-tool.ps1‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,24 +195,37 @@ function Extract-DocsBlock([System.Xml.XmlElement]$docs) {
195195

196196
switch ($child.LocalName) {
197197
"param" {
198-
if ($text -match "To be added") {
198+
if ($text -match "^\s*To be added\.?\s*$") {
199199
if (-not $fields.ContainsKey("params")) { $fields["params"] = @{} }
200200
$fields["params"][$child.GetAttribute("name")] = $text
201201
}
202202
}
203203
"typeparam" {
204-
if ($text -match "To be added") {
204+
if ($text -match "^\s*To be added\.?\s*$") {
205205
if (-not $fields.ContainsKey("typeparams")) { $fields["typeparams"] = @{} }
206206
$fields["typeparams"][$child.GetAttribute("name")] = $text
207207
}
208208
}
209209
{ $_ -in "summary", "returns", "value", "remarks" } {
210-
if ($text -match "To be added") {
210+
if ($text -match "^\s*To be added\.?\s*$") {
211211
$fields[$child.LocalName] = $text
212212
}
213213
}
214214
}
215215
}
216+
217+
# Record which fields were extracted so the merge can reject agent-added fields
218+
if ($fields.Count -gt 0) {
219+
$allowedKeys = @($fields.Keys | Where-Object { $_ -ne "params" -and $_ -ne "typeparams" })
220+
if ($fields.ContainsKey("params")) {
221+
$allowedKeys += ($fields["params"].Keys | ForEach-Object { "params.$_" })
222+
}
223+
if ($fields.ContainsKey("typeparams")) {
224+
$allowedKeys += ($fields["typeparams"].Keys | ForEach-Object { "typeparams.$_" })
225+
}
226+
$fields["_extractedKeys"] = $allowedKeys
227+
}
228+
216229
return $fields
217230
}
218231

@@ -225,7 +238,7 @@ function Merge-Docs([string]$inputPath) {
225238
@(Get-Item $inputPath)
226239
}
227240
else {
228-
Get-ChildItem -Path $inputPath -Filter "*.json" | Sort-Object Name
241+
Get-ChildItem -Path $inputPath -Filter "*.json" | Where-Object { $_.Name -ne "manifest.json" } | Sort-Object Name
229242
}
230243

231244
$totalUpdates = 0
@@ -263,10 +276,26 @@ function Merge-Docs([string]$inputPath) {
263276

264277
$fields = $entry.fields
265278

279+
# Build allowed-keys set from extract metadata (guards against agent-added fields)
280+
$allowedKeys = $null
281+
$hasExtractMeta = $null -ne $fields.PSObject -and $null -ne $fields.PSObject.Properties['_extractedKeys']
282+
if ($hasExtractMeta) {
283+
$keyArray = @($fields._extractedKeys)
284+
$allowedKeys = [System.Collections.Generic.HashSet[string]]::new(
285+
[string[]]$keyArray,
286+
[System.StringComparer]::OrdinalIgnoreCase
287+
)
288+
}
289+
266290
# Update scalar fields
267291
foreach ($fieldName in @("summary", "returns", "value", "remarks")) {
268292
$content = $fields.$fieldName
269-
if ($null -ne $content -and $content -notmatch "To be added") {
293+
if ($null -ne $content -and $content -notmatch "^\s*To be added\.?\s*$") {
294+
# Reject fields not in original extract
295+
if ($allowedKeys -and -not $allowedKeys.Contains($fieldName)) {
296+
Write-Warning "Skipping $($docId ?? 'type').$fieldName — not in original extract (agent-added)"
297+
continue
298+
}
270299
$elem = $docs.SelectSingleNode($fieldName)
271300
if ($elem) {
272301
if ($DryRun) {
@@ -286,7 +315,12 @@ function Merge-Docs([string]$inputPath) {
286315
$h = @{}; $fields.params.PSObject.Properties | ForEach-Object { $h[$_.Name] = $_.Value }; $h
287316
}
288317
foreach ($kv in $paramMap.GetEnumerator()) {
289-
if ($null -ne $kv.Value -and $kv.Value -notmatch "To be added") {
318+
if ($null -ne $kv.Value -and $kv.Value -notmatch "^\s*To be added\.?\s*$") {
319+
# Reject params not in original extract
320+
if ($allowedKeys -and -not $allowedKeys.Contains("params.$($kv.Key)")) {
321+
Write-Warning "Skipping $($docId ?? 'type').params.$($kv.Key) — not in original extract (agent-added)"
322+
continue
323+
}
290324
$elem = $docs.SelectSingleNode("param[@name='$($kv.Key)']")
291325
if ($elem) {
292326
if (-not $DryRun) { Set-ElementContent $elem $kv.Value }
@@ -302,7 +336,12 @@ function Merge-Docs([string]$inputPath) {
302336
$h = @{}; $fields.typeparams.PSObject.Properties | ForEach-Object { $h[$_.Name] = $_.Value }; $h
303337
}
304338
foreach ($kv in $tpMap.GetEnumerator()) {
305-
if ($null -ne $kv.Value -and $kv.Value -notmatch "To be added") {
339+
if ($null -ne $kv.Value -and $kv.Value -notmatch "^\s*To be added\.?\s*$") {
340+
# Reject typeparams not in original extract
341+
if ($allowedKeys -and -not $allowedKeys.Contains("typeparams.$($kv.Key)")) {
342+
Write-Warning "Skipping $($docId ?? 'type').typeparams.$($kv.Key) — not in original extract (agent-added)"
343+
continue
344+
}
306345
$elem = $docs.SelectSingleNode("typeparam[@name='$($kv.Key)']")
307346
if ($elem) {
308347
if (-not $DryRun) { Set-ElementContent $elem $kv.Value }

0 commit comments

Comments
 (0)