feat: add diff-apidocs, an API surface diff tool for the object-reference archive - #6
Open
irondarrius wants to merge 8 commits into
Open
feat: add diff-apidocs, an API surface diff tool for the object-reference archive#6irondarrius wants to merge 8 commits into
irondarrius wants to merge 8 commits into
Conversation
…ference build_dotnet_apidoc computed the archive destination at runtime via os.path.dirname(os.path.abspath(__file__)). Because os.path.abspath resolves against the current working directory and a preceding build (e.g. the IronPDF-for-Java JavaDoc step) chdir's into scaffolds/ without restoring cwd, the destination resolved to scaffolds/object-reference/<code>/<version> instead of the repo-root ./object-reference/<code>/<version>. The content was assembled and copied correctly, just to the wrong tree, so it never reached the canonical cache. Use the frozen, import-time get_apidoc_path() helper (the same one the JavaDoc path and update-apidocs.mjs already use), which anchors to apidocs.py's APIDOCS_STORAGE_PATH and is independent of cwd. Also gitignore scaffolds/object-reference/** as a safety net so a stray wrong-location tree can never be committed. Verified: with cwd forced to scaffolds/ (the staging condition), get_apidoc_path returns <repo>/object-reference/ironzip/<version>, while the old inline computation returned <repo>/scaffolds/object-reference/... Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…X output The Iron.Pdf.Extensions namespace contains only obfuscator-generated types, and the obfuscator emits new random names on every build: 2025.12.2 auxkyk auxkyl 2026.1.3 kjmakb kjmakc 2026.6.1 bnubqp bnubqq They are never stable API surface, yet they are published to the docs site and show up as two removed plus two added types in every IronPDF release diff. Excluded by uidRegex alongside the existing IronPdfEngine.Proto entries. This only affects future builds; the 73 already-archived IronPDF versions still contain them, so diff-apidocs carries a matching BLOCK_NS backstop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMFBtpbUYSURA6Y911CvFJ
…ence archive
Answers "what changed in the public API between version X and Y?" for the 469
archived .NET version directories, classifying every change as breaking,
additive, or cosmetic. Ships as both ports per the dual-port rule.
Two committed sources are combined per version: xrefmap.yml supplies member
identity (uid, kind, parameter types) and the DocFX api/*.html pages supply the
signature detail xrefmap lacks (modifiers, return types, base types, default
parameter values, property accessors). The nupkg {PackageName}.xml files were
evaluated and rejected: their <member name="M:..."> keys are the same identity
as commentId, so they add prose rather than signatures, and would have made the
tool network-dependent. Nothing is downloaded and no build is triggered.
Declarations are located by the data-uid DocFX writes on each heading, which is
byte-identical to the xrefmap uid. This is a correctness requirement rather than
a style choice: Archetype-N injects runnable code samples into the same pages, so
a flat lang-csharp scan reports "using IronZip;" as a member.
Because a uid encodes parameter types, a parameter-type change or a new overload
appears as a removal plus an addition; the report notes the pairing. Two build
nondeterminism artifacts are normalised so they cannot produce permanent false
positives: DocFX <GUID> markers are stripped (745 uids in ironpdf, 663 in
ironword, 68 in ironocr, 2 in ironxl, which alone made IronXL report a phantom
2 breaking + 2 additive in every release), and obfuscated Iron.Pdf.Extensions
types are filtered as a backstop for versions built before the filterConfig fix.
Verified: 63/63 golden-corpus parity checks across 7 products, 3 version spans
and 3 flag combinations, comparing exit codes, stdout, and byte-identical JSON;
zero unexplained classifications across all 11 products diffed oldest to newest;
ironpdf at 341 pages per version diffs in 0.95s, so no cache layer is needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMFBtpbUYSURA6Y911CvFJ
74 diffs generated with diff-apidocs: one per consecutive 2026 release pair,
using the last pre-2026 release as the entry baseline, plus a cumulative
year diff per product. JSON for machine consumption, Markdown for reading.
Highlights from the year:
ironpdf 45 breaking / 106 additive cumulative, opening with an
IBoundedDocumentObject -> IBoundedPdfDocumentObject rename that
cascades through the object model
ironword 218 breaking in 2026.1.4 alone: 13 members bool -> Nullable<bool>,
10 lost their set accessor, 9 dropped IDocumentElement
ironocr 22 breaking / 29 additive across six releases
ironzip, ironprint, ironwebscraper, irondrawing, ironxl
no public API changes all year
ironqr nets to zero cumulatively despite non-zero steps: IQrInput and
QrImageInput lost IDisposable in 2026.1.1 and had it restored in 2026.1.2, an
accidental breaking change that shipped and was hotfixed a release later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMFBtpbUYSURA6Y911CvFJ
… line
Between the 2026.6 and 2026.7 builds DocFX stopped inlining interfaces in the
declaration line, while the Implements section stayed byte-identical:
2026.6.2 Implements: System.IDisposable
decl: public class IronZipArchive : IronBaseArchive, IDisposable
2026.7.2 Implements: System.IDisposable <- unchanged
decl: public class IronZipArchive : IronBaseArchive
Nothing about the API changed, but reading interfaces from the declaration
reported 183 breaking changes across 11 products. Reading them from Implements
reports 21 — IronWord's genuinely removed types, corroborated independently
against the raw archive. IronPDF 2026.7 goes from 53 breaking to 0.
parse_implements() reads the section, _implements_reasons() compares it, and
_base_reasons() now compares only the base class from the declaration. Interface
pages carry no Implements section (nor do a few classes), so for those an
interface-shaped base-list difference is reported as cosmetic with an explicit
"unverifiable" note rather than counted as breaking. That can under-report a real
interface removal on an interface type, which is the better trade against
re-reporting a rendering change as ~160 breaking changes.
Also closes the remaining obfuscation routes and one determinism bug:
- Obfuscated Iron.Pdf.Extensions names reach a diff three ways, not one: as a
type, as a fully-qualified parameter type inside a member uid, and as a bare
simple name in a base list. BLOCK_NS is now unanchored and applied to whole
member uids, and Surface.blocked_type_names carries the simple names.
- BLOCK_NS gains a word boundary on Interop; without it the alternative matched
System.Runtime.InteropServices and wrongly dropped 134 of ironocr's 1522
members. Blast radius is now 8 members across all products, all with genuinely
internal parameter types.
- _without_blocked removes names longest-first. Iterating the set unsorted let
IEnumerable strip out of IEnumerable<Cell> first, leaving a stray <Cell>, so
results depended on PYTHONHASHSEED and varied run to run.
Verified: 63/63 parity checks, both ports agree across all 11 products, and five
consecutive identical runs confirm determinism.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMFBtpbUYSURA6Y911CvFJ
85 diffs across 11 products: one per consecutive 2026 release pair, using the
last pre-2026 release as the entry baseline, plus a cumulative year-to-date diff
each. Replaces the earlier set, which predated the Implements-section fix and the
obfuscation filtering and so overstated breaking changes.
Year to date:
ironword 189 breaking / 100 additive — a large 2026.1.4 refactor, plus 21
types removed in 2026.7.1 (Text, Image, Shape, Style and 13 enums)
ironpdf 25 breaking / 127 additive — 2026.7.2 is purely additive: 27 new
members including ExtractTextFromLayer and the ContentOverlapBehavior
header/footer overloads
ironocr 21 breaking / 29 additive
ironbarcode, irondrawing, ironppt, ironprint additions only
ironqr, ironwebscraper, ironxl, ironzip no public API change all year
Cosmetic entries on the 2026.7 pairs are the DocFX declaration-rendering change
on pages with no Implements section to verify against, not API changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMFBtpbUYSURA6Y911CvFJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
diff-apidocs, a tool that answers "what changed in the public API between version X and Y?" for the 480+ archived .NET version directories, classifying every change as breaking, additive, or cosmetic. Ships as both ports per the dual-port rule. Also includes the 2026 release-chain diffs it produced, and the findings those surfaced.Exit codes:
0success,1tool error,2breaking changes found with--fail-on-breaking.How it works
Two committed sources are combined per version, so the tool is entirely offline — no registry calls, no build triggered:
xrefmap.ymlapi/*.htmldeclarationsapi/*.htmlImplements sectionDeclarations are located by the
data-uidDocFX writes on each heading, which is byte-identical to the xrefmap uid. That anchoring is a correctness requirement, not a style choice: Archetype-N injects runnable code samples into the same pages, so a flatlang-csharpscan — the approach inscaffolds/tools/archetype-n/facts.py— reportsusing IronZip;as a member.The
{PackageName}.xmlfiles inside each nupkg were evaluated and rejected: DocFX takes metadata from the assembly, and the.xmlsupplies only doc-comment prose with no representation of base types or interfaces. Using it would also have made the tool network-dependent.Because a uid encodes parameter types, a parameter-type change or a new overload surfaces as a removal plus an addition rather than a modification; the report notes the pairing.
Findings from the 2026 release diffs
Running this across the year turned up four things worth acting on.
1. IronWord 2026.7.1 dropped 21 documented types, including
Text,Image,Shape,Styleand 13 enums. Verified independently of the tool against the raw archive: 200 → 179 pages, 3865 → 3320 xrefmap entries.Two of them are a genuine documentation hole rather than a removal —
TextAlignmentandShapeTypeare still used in surviving public signatures (Paragraph.Alignment { get; set; }returnsTextAlignment;ShapeContent(ShapeType, RectangleF)takesShapeType) but no longer have type pages. A consumer cannot look up a type the API requires. Worth raising with the IronWord team.2. IronQR 2026.1.1 shipped an accidental breaking change.
IQrInputandQrImageInputlostIDisposable, restored in 2026.1.2. The cumulative year diff correctly nets to zero — a sum-of-steps tool would have reported 2 breaking for the year. This is the concrete argument for wiring--fail-on-breakinginto the release pipeline; it would have caught this pre-release.3.
Iron.Pdf.Extensionspublishes obfuscator output to the docs site. The namespace contains only obfuscator-generated types, renamed randomly every build:auxkykauxkylkjmakbkjmakcbnubqpbnubqqqdygytqdygyuscaffolds/filterConfig.ymlgains auidRegexexclude so future builds stop publishing them, alongside the existing^IronPdfEngine.Proto$entries. That only helps builds that pick the change up — the 2026.7 rebuild did not, and every already-archived version still contains them, so the tool carries a matching filter regardless.4. DocFX's declaration line is not a reliable source for interfaces. Between 2026.6 and 2026.7 it stopped inlining them, while the
Implementssection stayed byte-identical:Nothing about the API changed. Read from the declaration this reported 183 breaking changes across 11 products; read from
Implementsit reports 21, all IronWord's removed types. Interface pages carry noImplementssection, so for those an interface-shaped difference is reported as cosmetic with an explicit "unverifiable" note rather than counted as breaking — a deliberate trade against re-reporting a rendering change as ~160 false breakings.2026 year-to-date, as committed
85 diffs under
docs/api-diffs/<code>/<from>..<to>.{json,md}— one per consecutive 2026 pair plus a cumulative year diff each.docs/is already excluded from the Jekyll build, so these stay private to the repo.IronPDF 2026.7.2 is purely additive: 27 new members including
PdfDocument.ExtractTextFromLayer(int|string),AddHtmlHeaders/AddHtmlFootersoverloads takingContentOverlapBehavior, and theCssPageRulePolicy/ContentOverlapBehaviorenums.Verification
No test framework or CI exists in this repo, so this follows the documented golden-corpus procedure in
docs/api-surface-diff-plan.md. All of it was run and passed.PYTHONHASHSEED.irondrawing/2022.9.8843andironxl/2019.3.2.1.--namespace Gand--exclude Gsummaries sum exactly to the unfiltered summary.Notes for review
apidiff/sits at the repo root besidecheck-apidocs, with both ports in one directory asscaffolds/tools/archetype-n/does. Nothing was placed underscaffolds/tools/, where.gitattributeswould silently make it Git LFS._config.ymlexcludesapidiffoutright, so nothing under it can ever be published.apidocs.py/.mjsgainlist_archived_versions()andversion_sort_key(), shared rather than duplicated. The sorter handles both numbering eras (2019.3.2.1,2021.9.3650,2026.7.2) and skips the_archetype-n-samplessibling directory.coloramaviaStatusLogger; Node is dependency-free.statuslogger.pyandstatuslogger.mjsalready differ in spacing — a pre-existing divergence affecting every tool in this repo. It was left alone rather than changing output the devops pipeline may parse. JSON, which this tool fully controls, is byte-identical.__pycache__/*.pycfiles are tracked in the repo despite.gitignorecovering them. Worth a standalone cleanup.🤖 Generated with Claude Code
https://claude.ai/code/session_01EMFBtpbUYSURA6Y911CvFJ