Skip to content

feat: resolve <inheritdoc/> across assembly boundaries - #43

Merged
Malcolmnixon merged 17 commits into
mainfrom
feature/cross-assembly-inheritdoc
Sep 13, 2026
Merged

Malcolmnixon merged 17 commits into
mainfrom
feature/cross-assembly-inheritdoc

Conversation

@Malcolmnixon

Copy link
Copy Markdown
Member

Summary

Adds native cross-assembly <inheritdoc/> resolution to ApiMark.DotNet, so <inheritdoc/> correctly resolves documentation from base types/interfaces defined in external assemblies (NuGet packages, the .NET BCL, etc.) — not just within the same assembly as before.

This is conceptually inspired by SauceControl/InheritDoc (same core idea: resolve reference assemblies via ReferencePath-style items, find sibling XML doc files with ref/<->lib/ fallback), but implemented natively with no new external dependency.

What changed

  • ExternalXmlDocResolver (new) — locates and lazily parses external assemblies' sibling XML doc files, with ref/<->lib/ folder-swap fallback (some NuGet packages ship docs in only one of those folders) and per-member caching, including negative-cache (miss) results.
  • XmlDocReader — gained an optional external-member-lookup fallback, used only when a member isn't found in the local assembly's own XML docs. Fully backward compatible.
  • DotNetGenerator — seeds an assembly resolver from configured reference paths and wires the external resolver into XmlDocReader.
  • New ReferencePaths option, surfaced end-to-end:
    • ApiMark.Tool: --reference-paths <path> CLI flag (repeatable)
    • ApiMark.MSBuild: ApiMarkReferencePaths task property, auto-harvested from @(ReferencePath) when not explicitly set (never clobbers a user-supplied value)

Verification

  • Verified end-to-end against a real project referencing System.IDisposable from the .NET BCL reference-assembly XML docs shipped with the SDK, and against a real external NuGet-style package reference — confirmed <inheritdoc/> correctly pulls in the external documentation text.
  • Full test coverage added: ExternalXmlDocResolverTests, extended XmlDocReaderTests/DotNetGeneratorTests, new external fixture project, new MSBuild/CLI option tests, and new ApiMark.MSBuild.PackageTests integration tests proving the .targets auto-harvest behavior and its suppression when set explicitly.
  • All non-C++ suites pass clean: ApiMark.DotNet.Tests 312/312 (×3 TFMs), ApiMark.MSBuild.Tests 35/35, ApiMark.Tool.Tests 99/99, ApiMark.Core.Tests 118/118, ApiMark.MSBuild.PackageTests 7/7 (excluding the pre-existing, environment-only clang-toolchain C++ test).
  • dotnet reviewmark --lint / --plan --enforce, dotnet reqstream --lint / --enforce --tests, and pwsh ./lint.ps1 (cspell, markdownlint, yamllint, sysml2tools, dotnet format --verify-no-changes) all clean.

Review process

  • Ran the built-in code-review agent on the full diff — found 1 Medium issue (stale thread-safety claim in XmlDocReader's doc comment now that an external lookup delegate can be wired in) — fixed.
  • Ran formal reviews (using gpt-5.4-mini) across all 21 .reviewmark.yaml review-sets touched by this change. Initial sweep: 12 passed clean, 9 failed with Medium/High findings (mostly system/subsystem-level docs not yet updated to mention the new unit/option, plus a couple of missing edge-case requirements/tests). All feature-caused findings were fixed and independently re-verified; a few unrelated pre-existing gaps were confirmed as such and left untouched per scope discipline.

Documentation

Design, reqstream (requirements), verification, SysML2 model, and user-guide docs updated for all affected units in ApiMark.DotNet, ApiMark.MSBuild, and ApiMark.Tool.


Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Add native cross-assembly <inheritdoc/> resolution for ApiMark.DotNet,
inspired by SauceControl/InheritDoc but without adding that dependency.

- ExternalXmlDocResolver (new): locates and lazily parses external
  assemblies' sibling XML doc files, with ref/<->lib folder-swap
  fallback and per-member caching (including negative cache hits).
- XmlDocReader: optional external-member-lookup fallback used when a
  member isn't found in the local assembly's XML docs.
- DotNetGenerator: seeds an assembly resolver from configured reference
  paths and wires the external resolver into XmlDocReader.
- New ReferencePaths option surfaced end-to-end:
  - ApiMark.Tool: --reference-paths <path> CLI flag (repeatable)
  - ApiMark.MSBuild: ApiMarkReferencePaths task property, auto-harvested
    from @(ReferencePath) when unset

Verified end-to-end against real NuGet package references and the
.NET BCL (e.g. System.IDisposable) using reference-assembly XML docs
shipped with the SDK.

Includes full test coverage (DotNet/MSBuild/Tool/PackageTests), and
companion design/reqstream/verification/sysml2/user-guide updates.

Reviewed via the built-in code-review agent and formal reviews across
all 21 affected .reviewmark.yaml review-sets (gpt-5.4-mini); all
feature-caused findings addressed (thread-safety doc accuracy, stale
system-level design/verification docs, missing edge-case requirements/
tests, non-atomic requirement split, subsystem-level doc coverage).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 12, 2026 16:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in compatibility, reference resolution, resource cleanup, caching, and MSBuild override behavior.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds cross-assembly .NET <inheritdoc/> resolution through CLI and MSBuild reference paths.

Changes:

  • Adds external XML documentation lookup with caching and ref/lib fallback.
  • Wires repeatable reference-path options through CLI, MSBuild, and the generator.
  • Adds fixtures, tests, requirements, design, and verification documentation.
File summaries
File Reviewed changes Review notes
test/ApiMark.Tool.Tests/ProgramTests.cs CLI end-to-end coverage —
test/ApiMark.Tool.Tests/Cli/ContextTests.cs Reference-path parsing tests —
test/ApiMark.MSBuild.Tests/ApiMarkTaskTests.cs MSBuild argument tests —
test/ApiMark.MSBuild.PackageTests/PackageIntegrationTests.cs Package integration scenarios Nit (3 votes, lines 259 and 296): assertions do not prove reference harvesting or external inherited documentation.
test/ApiMark.MSBuild.PackageTests/Fixtures/DotNet/SampleLibWithReference/SampleLib.csproj Reference fixture project —
test/ApiMark.MSBuild.PackageTests/Fixtures/DotNet/SampleLibWithReference/SampleClass.cs Reference fixture source —
test/ApiMark.DotNet.Tests/XmlDocReaderTests.cs External inheritdoc tests —
test/ApiMark.DotNet.Tests/FixturePaths.cs External fixture paths —
test/ApiMark.DotNet.Tests/ExternalXmlDocResolverTests.cs Resolver unit tests Nit (1 vote, line 337): the negative-cache test should rewrite the XML before the second lookup.
test/ApiMark.DotNet.Tests/DotNetGeneratorTests.cs Generator integration tests —
test/ApiMark.DotNet.Tests/ApiMark.DotNet.Tests.csproj External fixture reference —
test/ApiMark.DotNet.Fixtures/ExternalInheritDocClass.cs Cross-assembly inheritdoc fixture —
test/ApiMark.DotNet.Fixtures/ApiMark.DotNet.Fixtures.csproj Fixture project reference —
test/ApiMark.DotNet.Fixtures.External/IExternalBaseInterface.cs External interface fixture —
test/ApiMark.DotNet.Fixtures.External/ExternalBaseClass.cs External base-class fixture —
test/ApiMark.DotNet.Fixtures.External/ApiMark.DotNet.Fixtures.External.csproj External fixture project —
src/ApiMark.Tool/Program.cs CLI option wiring —
src/ApiMark.Tool/Cli/Context.cs CLI option parsing —
src/ApiMark.MSBuild/build/DemaConsulting.ApiMark.MSBuild.targets MSBuild harvesting and forwarding Moderate (3 votes, line 70): explicit empty ApiMarkReferencePaths is repopulated; preserve an explicit-set marker or provide an opt-out.
src/ApiMark.MSBuild/ApiMarkTask.cs MSBuild property and argument support —
src/ApiMark.DotNet/XmlDocReader.cs External lookup fallback Critical (1 vote, line 63): preserve the original two-parameter constructor and add a separate three-parameter overload for binary compatibility.
src/ApiMark.DotNet/ExternalXmlDocResolver.cs External XML documentation resolver Moderate (1 vote, line 50): use a platform-aware path comparer to avoid conflating distinct Linux paths.
src/ApiMark.DotNet/DotNetGenerator.cs Cecil resolver and generator wiring Moderate (3 votes, line 110): normalize reference paths with Path.GetFullPath before extracting directories.
Moderate (1 vote, line 118): protect assembly loading with resolver cleanup on failure.
Moderate (1 vote, line 129): dispose the resolver on the successful path.
requirements.yaml Requirements inclusion —
docs/verification/api-mark-tool/program.md CLI verification documentation —
docs/verification/api-mark-tool/cli/context.md Context verification documentation —
docs/verification/api-mark-tool/cli.md CLI verification updates —
docs/verification/api-mark-msbuild/api-mark-task.md MSBuild task verification —
docs/verification/api-mark-msbuild.md MSBuild verification updates —
docs/verification/api-mark-dot-net/xml-doc-reader.md Reader verification documentation —
docs/verification/api-mark-dot-net/external-xml-doc-resolver.md Resolver verification documentation —
docs/verification/api-mark-dot-net/dot-net-generator.md Generator verification documentation —
docs/verification/api-mark-dot-net.md .NET verification updates —
docs/user_guide/dotnet.md User-facing option documentation —
docs/sysml2/model/api-mark-dot-net/external-xml-doc-resolver.sysml Resolver model unit —
docs/sysml2/model/api-mark-dot-net.sysml Model hierarchy update —
docs/reqstream/api-mark-tool/program.yaml CLI requirements —
docs/reqstream/api-mark-tool/cli/context.yaml Context requirements —
docs/reqstream/api-mark-tool/cli.yaml CLI requirement linkage —
docs/reqstream/api-mark-msbuild/api-mark-task.yaml MSBuild task requirements —
docs/reqstream/api-mark-msbuild.yaml MSBuild requirement linkage —
docs/reqstream/api-mark-dot-net/xml-doc-reader.yaml Reader requirements —
docs/reqstream/api-mark-dot-net/external-xml-doc-resolver.yaml Resolver requirements —
docs/reqstream/api-mark-dot-net/dot-net-generator.yaml Generator requirements —
docs/reqstream/api-mark-dot-net.yaml .NET requirement linkage —
docs/design/api-mark-tool/program.md CLI design update —
docs/design/api-mark-tool/cli/context.md Context design update —
docs/design/api-mark-tool/cli.md CLI design update —
docs/design/api-mark-tool.md Tool architecture update —
docs/design/api-mark-msbuild/api-mark-task.md Task design update —
docs/design/api-mark-msbuild.md MSBuild architecture update —
docs/design/api-mark-dot-net/xml-doc-reader.md Reader design update —
docs/design/api-mark-dot-net/external-xml-doc-resolver.md Resolver design Nit (1 vote, line 130): the design calls the public resolver internal; align the design contract or source visibility/tests.
docs/design/api-mark-dot-net/dot-net-generator.md Generator design update —
docs/design/api-mark-dot-net.md .NET architecture update —
ApiMark.slnx Adds external fixture project —
.reviewmark.yaml Adds review coverage —
.cspell.yaml Adds technical terms —
Review details

Suppressed comments (6)

docs/design/api-mark-dot-net/external-xml-doc-resolver.md:131

  • This design section calls the resolver an internal class, but ExternalXmlDocResolver is declared public and exposes public construction and lookup (src/ApiMark.DotNet/ExternalXmlDocResolver.cs:40,66,84). Update the design contract to describe the public helper and its lack of external service interfaces, or change the source visibility and tests consistently.
N/A — this is an internal class with no external interfaces exposed beyond
its assembly.

src/ApiMark.DotNet/DotNetGenerator.cs:120

  • assemblyResolver is allocated before AssemblyDefinition.ReadAssembly, but the cleanup try starts only after that call. If reading the primary assembly fails, the resolver is never disposed, so the failure-path cleanup documented above is skipped. Move the read into a protected block or dispose the resolver in a catch around this call.
    src/ApiMark.DotNet/DotNetGenerator.cs:133
  • The successful-parse path never disposes assemblyResolver; DotNetEmitter.Emit only disposes Model.Assembly, while the Cecil resolver can cache every external AssemblyDefinition loaded during inheritance analysis. Reusing this library in one process can therefore retain dependency file handles and metadata until process exit, despite the implementation being safe only because the CLI is short-lived; transfer resolver ownership to the emitter/model and dispose it with the assembly.
    src/ApiMark.DotNet/ExternalXmlDocResolver.cs:50
  • This cache uses a case-insensitive comparer for filesystem paths even on Linux, where /refs/Foo.dll and /refs/foo.dll can be distinct files. If both paths are configured, the second path reuses the first path's cached member index and is never loaded, so valid external documentation can be missed; use the platform-aware filesystem comparer already established by src/ApiMark.Cpp/CppEmitter.cs:95-107.
    test/ApiMark.DotNet.Tests/ExternalXmlDocResolverTests.cs:339
  • Deleting the XML file after the first miss does not distinguish a negative cache from a fresh lookup: an implementation that re-reads disk would also return null when the file is gone. Rewrite the file to contain T:Foo.Missing before the second call and assert that the second result is still null, so the test fails when the miss is not cached.
    test/ApiMark.MSBuild.PackageTests/PackageIntegrationTests.cs:298
  • This success/file-exists assertion cannot prove that an explicit empty property suppressed harvesting: the build succeeds either way, and no generated content depends on external documentation. Make the test observe the child arguments or an external-inheritdoc result that disappears when the empty override is honored; otherwise it will not catch the target's current overwrite behavior.
  • Files reviewed: 58/58 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/ApiMark.DotNet/XmlDocReader.cs Outdated
Comment thread src/ApiMark.DotNet/DotNetGenerator.cs Outdated
Comment thread src/ApiMark.MSBuild/build/DemaConsulting.ApiMark.MSBuild.targets Outdated
Comment thread test/ApiMark.MSBuild.PackageTests/PackageIntegrationTests.cs
… advisory

src/ApiMark.Tool/ApiMark.Tool.csproj and src/ApiMark.MSBuild/ApiMark.MSBuild.csproj
pinned Microsoft.SourceLink.GitHub 10.0.300, whose transitive
Microsoft.Build.Tasks.Git dependency now has a published moderate-severity
NuGet advisory (GHSA-23fw-v26w-5fgq). Restore treats this as a hard error
(NU1902) due to WarningsAsErrors, breaking dotnet restore/format for every
branch including main - unrelated to the inheritdoc feature in this PR, but
blocking CI, so bumping both to 10.0.401 (latest, matching the pinned SDK
version) to unblock the pipeline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 12, 2026 17:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Moderate findings remain in path handling, resolver lifecycle and caching, MSBuild opt-out semantics, and integration-test coverage.

Review details

Suppressed comments (8)

Previously missed (4) — in code that hasn't changed since the last review.

src/ApiMark.DotNet/DotNetGenerator.cs:113

  • This comparer is case-insensitive on every platform. On Linux, two distinct reference directories that differ only by case can both be valid; deduplicating them here can prevent Cecil from searching the directory containing the needed assembly. Use the platform-aware path comparer used by CppEmitter instead.
    src/ApiMark.DotNet/ExternalXmlDocResolver.cs:50
  • The per-path cache is also case-insensitive on Linux. If the first configured path (for example /tmp/Foo.dll) has no XML file and a second, case-distinct path (/tmp/foo.dll) does, the second lookup hits the first path's cached null and never loads its valid documentation. Match the repository's platform-aware file-system comparer here.
    src/ApiMark.DotNet/ExternalXmlDocResolver.cs:203
  • The fallback swaps the first ref/lib segment it encounters, so a parent directory named lib or ref can be changed instead of the package layout segment. For example, /var/lib/.nuget/packages/Pkg/ref/net8.0/Pkg.dll is probed under /var/ref/... rather than the corresponding .../lib/net8.0 directory. Select the layout segment nearest the DLL (or otherwise identify the package ref/lib segment) instead of stopping at the first match.
    src/ApiMark.MSBuild/build/DemaConsulting.ApiMark.MSBuild.targets:67
  • An empty value declared in the project (for example, <ApiMarkReferencePaths></ApiMarkReferencePaths>) still satisfies this condition and is overwritten by the harvested list; the command-line test does not expose this because global properties are protected from reassignment. If project-level empty is part of the “explicitly set” contract, add a definedness/opt-out mechanism and an integration test that declares it in the fixture.

src/ApiMark.DotNet/DotNetGenerator.cs:112

  • Path.GetDirectoryName("External.dll") returns an empty directory, so a valid relative --reference-paths External.dll is filtered out here. The XML resolver can still see the sibling file, but Mono.Cecil receives no search directory and bare cross-assembly <inheritdoc /> chain construction fails; normalize the path before extracting its directory.
    src/ApiMark.DotNet/DotNetGenerator.cs:113
  • On the success path the new DefaultAssemblyResolver is never disposed; only the exception path disposes it. Mono.Cecil's resolver caches external AssemblyDefinition instances, so in-process library callers can retain dependency memory/file handles after Emit and repeated generations can accumulate them. Tie the resolver to the emitter/model lifetime and dispose it after emission rather than relying on the CLI process exiting.
    src/ApiMark.DotNet/DotNetGenerator.cs:113
  • These path assumptions break valid configurations: a bare relative reference such as --reference-paths External.dll has no directory and is filtered out, so Cecil cannot resolve the external base even though the file is valid; additionally, OrdinalIgnoreCase collapses distinct reference directories on case-sensitive Linux. Normalize/fallback to the current directory and use a platform-aware filesystem comparer here.
    test/ApiMark.MSBuild.PackageTests/PackageIntegrationTests.cs:261
  • This assertion does not prove that auto-harvesting occurred: the fixture contains no <inheritdoc /> that needs the referenced package, and api.md would still be generated if the new @(ReferencePath) ItemGroup were removed. Add an external-inheritdoc fixture/assertion or otherwise capture the task arguments so this integration test fails when harvesting is broken.
  • Files reviewed: 66/67 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Fixes 7 issues found by GitHub Copilot code review on PR #43 for the
cross-assembly <inheritdoc/> feature:

1. XmlDocReader: restore the original 2-parameter constructor exactly as
   published (binary-compatible with pre-existing compiled callers) and add
   a separate 3-parameter overload for the external-lookup delegate, sharing
   implementation via constructor chaining. Updated two tests that relied on
   named-argument skipping of the removed merged-optional-parameter overload.

2. ExternalXmlDocResolver: use a platform-aware StringComparer (Ordinal on
   Linux, OrdinalIgnoreCase elsewhere) for the per-reference-assembly-path
   doc cache, matching the existing pattern in ApiMark.Cpp.CppEmitter, so
   case-differing paths are no longer incorrectly treated as duplicates on
   case-sensitive file systems.

3. DotNetGenerator.Parse:
   a. Normalize each reference path with Path.GetFullPath before computing
      its directory for the Mono.Cecil assembly resolver's search paths.
   b. Wrap assembly reading in a try/catch that disposes the assembly
      resolver on failure, so a failed AssemblyDefinition.ReadAssembly call
      no longer leaks the resolver.
   c. Transfer resolver ownership to the returned model (DotNetAstModel)
      instead of leaking it on the success path; DotNetEmitter.Emit now
      disposes both the assembly and its resolver together.

4. DemaConsulting.ApiMark.MSBuild.targets: add a dedicated
   ApiMarkDisableReferencePathsHarvest opt-out property (mirroring the
   existing DisableApiMark convention) because MSBuild cannot distinguish
   "never set" from "explicitly set to empty" for a plain property, so
   ApiMarkReferencePaths="" alone could not suppress auto-harvest. Documented
   the new property in the design doc and .NET user guide.

5. PackageIntegrationTests: strengthened the auto-harvest and suppression
   tests to query the effective ApiMarkReferencePaths value via
   dotnet build -getProperty after the build, proving harvesting actually
   occurred (and includes the expected Newtonsoft.Json reference) or was
   actually suppressed via the new opt-out property, rather than only
   checking that the build succeeded.

6. ExternalXmlDocResolverTests: the negative-cache test now rewrites the
   backing XML file (instead of deleting it) between the first and second
   lookup, proving the cached miss result is served from cache rather than
   from a (coincidentally also null) fresh disk read.

7. external-xml-doc-resolver.md: corrected the design doc to describe
   ExternalXmlDocResolver as the public class it actually is, documenting
   its real consumers, instead of incorrectly describing it as internal.

Verified: full solution build, ApiMark.DotNet.Tests (net8/9/10),
ApiMark.MSBuild.Tests, ApiMark.Tool.Tests, and ApiMark.MSBuild.PackageTests
all pass (one pre-existing, unrelated C++ vcxproj package test failure due to
local MSVC/toolchain environment, and pre-existing clang-dependent Cpp test
skips/failures, both out of scope). dotnet reviewmark --lint,
dotnet reqstream --lint, reviewmark --plan --enforce, and
reqstream --enforce --tests show only pre-existing, unrelated coverage gaps
(Cpp/Clang parsing and MSBuild C++ include-path harvesting, tied to the
missing clang/MSVC toolchain in this environment). fix.ps1 and lint.ps1
(cspell, markdownlint, yamllint, sysml2tools lint, dotnet format
--verify-no-changes) are clean.
Copilot AI review requested due to automatic review settings September 12, 2026 17:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Moderate findings remain in resolver fallback, external inheritance chaining, MSBuild option handling, and test coverage.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (6)

Previously missed (1) — in code that hasn't changed since the last review.

src/ApiMark.DotNet/ExternalXmlDocResolver.cs:220

  • The fallback swaps the first ref/lib segment, so a valid package path with an earlier segment of the same name (for example /home/lib/.nuget/.../ref/net8.0/Foo.dll) probes the wrong directory and misses the XML file. Search from the assembly path backwards so the package's ref/lib segment is selected.

docs/design/api-mark-dot-net/dot-net-generator.md:128

  • This design note repeats that the assembly resolver is not disposed on the success path, but the changed emitter now owns and disposes it after emission. Keeping this statement makes the documented ownership/lifetime contradict the implementation and can mislead API callers about using the resolver after Emit; update the design contract to describe disposal with the returned emitter.
  explicitly added this way. The resolver is intentionally not disposed on the
  success path (see the class-level remarks / Known limitation below); it is
  disposed on the failure path alongside the parsed assembly.

docs/verification/api-mark-dot-net/external-xml-doc-resolver.md:99

  • Deleting the XML file cannot prove the negative cache: a fresh lookup after deletion would also return null. The test actually rewrites the file with the previously missing member and asserts the second lookup remains null, which is the distinguishing evidence; update this verification text to match that behavior.
    src/ApiMark.DotNet/DotNetGenerator.cs:117
  • This deduplicates reference directories case-insensitively on every platform. On Linux, valid distinct paths such as /tmp/Lib and /tmp/lib can contain different referenced assemblies, so dropping one can make Mono.Cecil fail to resolve an external base/interface. The established filesystem convention uses StringComparer.Ordinal on Linux (src/ApiMark.Cpp/CppEmitter.cs:95-107); use that platform-aware comparer here as well.
    src/ApiMark.DotNet/XmlDocReader.cs:432
  • An externally resolved member is returned as a raw XElement, but if that member's own documentation contains a bare <inheritdoc />, ResolveInheritdocSource can only consult _inheritanceChain, which is built for the primary assembly and has no entry for the external member. A chain such as primary C.M → external I1.M → external I2.M therefore still loses its documentation; either provide inheritance metadata for referenced assemblies or explicitly constrain and test this behavior.
    test/ApiMark.DotNet.Tests/DotNetGeneratorTests.cs:2305
  • This end-to-end test is described as proving external base-type and interface resolution, but it only asserts ExternalInterfaceMethod. A regression in the external ExternalBaseClass.DescribeBase inheritance path or its XML lookup would still pass; add an assertion for the DescribeBase page containing Describes the base implementation..
  • Files reviewed: 68/69 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread src/ApiMark.DotNet/DotNetGenerator.cs Outdated
Comment thread src/ApiMark.MSBuild/build/DemaConsulting.ApiMark.MSBuild.targets
Comment thread test/ApiMark.MSBuild.PackageTests/PackageIntegrationTests.cs Outdated
Comment thread docs/verification/api-mark-msbuild/api-mark-task.md Outdated
Malcolm Nixon added 2 commits September 12, 2026 14:08
- Extract a shared platform-aware FileSystemPathComparer helper in
  ApiMark.DotNet and use it in both DotNetGenerator's reference-path
  directory Distinct() and ExternalXmlDocResolver's per-path doc cache,
  eliminating the last unconditional OrdinalIgnoreCase path comparison
  that could wrongly deduplicate case-distinct directories on Linux.
- Extract DotNetGenerator.ResolveReferenceSearchDirectory and add a unit
  test confirming a bare relative reference path (e.g. External.dll)
  resolves to the current working directory, verifying the existing
  GetFullPath-before-GetDirectoryName ordering already fixed this.
- Fix ExternalXmlDocResolver.SwapRefLibSegment to scan from the end of
  the path backwards and swap the ref/lib segment nearest the assembly
  file, instead of the first matching segment, so an unrelated earlier
  path segment (e.g. a decoy /home/lib/... folder) is never matched in
  preference to the real NuGet package-layout segment. Added a
  regression test with a decoy segment earlier in the path.
- Add a second assertion to
  DotNetGenerator_Parse_ExternalBaseWithReferencePaths_ResolvesInheritedDocumentation
  proving the external base-CLASS inheritdoc (DescribeBase) resolves,
  not just the external interface member.
- Document (XmlDocReader.cs remarks, design doc, user guide) and pin
  down with a regression test the known limitation that a bare
  (cref-less) <inheritdoc/> chain crossing two or more assembly
  boundaries does not resolve, because the inheritance chain is built
  only from the primary assembly's Cecil metadata. Explicit cref
  chains are unaffected and continue to resolve across any number of
  hops.
- Correct stale documentation: the assembly resolver is now disposed
  on the success path too (ownership transferred to DotNetAstModel/
  DotNetEmitter), and the negative-cache verification test rewrites
  (rather than deletes) the XML file between calls.
- Verified test/ApiMark.MSBuild.PackageTests/PackageIntegrationTests.cs
  auto-harvest test already asserts the effective ApiMarkReferencePaths
  property value contains the referenced Newtonsoft.Json path (approach
  b); no further change needed for that finding.
The requirements.yaml entry for ApiMarkMsbuild-ApiMarkTask-AutoPopulateReferencePaths referenced a test method name (ApiMarkMsbuild_NuGetPackage_DotNetProject_ExplicitReferencePaths_SuppressesAutoHarvest) that was renamed to ApiMarkMsbuild_NuGetPackage_DotNetProject_DisableReferencePathsHarvest_SuppressesAutoHarvest in the prior round-2 review-fix commit (14c7f55), without updating the requirements traceability link. This caused CI's aggregated reqstream --enforce (which runs against the full TRX set from all platforms/TFMs) to report both ApiMarkMsbuild-ApiMarkTask-AutoPopulateReferencePaths and its parent ApiMarkMsbuild-ApiMarkTask-IntegrateWithMsbuild (via rollup) as unsatisfied, dropping coverage from 380 to 378 of 380 requirements.
Copilot AI review requested due to automatic review settings September 12, 2026 18:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (7)

.reviewmark.yaml:316

  • The new shared src/ApiMark.DotNet/FileSystemPathComparer.cs is not included in any ReviewMark path set, while the other changed .NET source files are explicitly assigned to the generator or resolver sets. As a result, the review plan does not cover this new path-comparison implementation; add it to the appropriate set so ReviewMark enforces review coverage for the changed source.
      - src/ApiMark.DotNet/ExternalXmlDocResolver.cs
      - test/ApiMark.DotNet.Tests/ExternalXmlDocResolverTests.cs

docs/reqstream/api-mark-msbuild/api-mark-task.yaml:54

  • This requirement says the targets must not override any explicitly set ApiMarkReferencePaths value, but the implementation intentionally repopulates the property when the explicit value is empty because MSBuild cannot distinguish empty from unset. Narrow the requirement to non-empty explicit values or state the required ApiMarkDisableReferencePathsHarvest opt-out so the requirement matches the documented behavior and test.
              ApiMarkTask's .targets file shall automatically populate
              ApiMarkReferencePaths from the resolved @(ReferencePath) items when
              ApiMarkReferencePaths is not explicitly set, and shall not override
              an
              explicitly set value.

docs/verification/api-mark-msbuild/api-mark-task.md:174

  • This verification scenario claims that an explicitly empty ApiMarkReferencePaths suppresses harvesting and names a test that does not exist. The implementation and the adjacent integration test document the opposite: an empty value cannot be distinguished from unset, and ApiMarkDisableReferencePathsHarvest=true is the opt-out. Please align this scenario with the actual behavior and test name.
    src/ApiMark.DotNet/ExternalXmlDocResolver.cs:71
  • The constructor keeps the caller-owned IReadOnlyList by reference while this type caches misses for the lifetime of the instance. If a caller passes a mutable List<string> and adds a reference after a miss, the new path is never searched for that member because _memberCache still returns the old negative result. Snapshot the paths at construction so the cache and search set stay consistent.
    src/ApiMark.MSBuild/build/DemaConsulting.ApiMark.MSBuild.targets:75
  • The new .targets contract also promises that a user-supplied ApiMarkReferencePaths value is not overwritten, but the package integration tests only prove default harvesting and the separate disable switch. The direct task test bypasses this target logic, so add a real MSBuild integration case with a non-empty explicit path and assert that the effective property remains exactly that value.
    src/ApiMark.Tool/Cli/Context.cs:603
  • pattern is misleading here: this value is a single assembly file path, not a wildcard pattern, and the surrounding repeatable path option uses path-oriented names. Rename it to path so the parser's local variable matches the ReferencePaths contract.
    test/ApiMark.MSBuild.PackageTests/PackageIntegrationTests.cs:349
  • This checks whether all captured stdout is whitespace, but dotnet build -getProperty emits a property report (including an object even when the property's value is empty), and the helper also captures normal build output. Consequently the test will fail even when auto-harvest is correctly disabled. Assert the extracted ApiMarkReferencePaths value is empty instead of checking the entire stdout stream.
  • Files reviewed: 69/70 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread docs/design/api-mark-dot-net/external-xml-doc-resolver.md Outdated
- reviewmark: add FileSystemPathComparer.cs to the ExternalXmlDocResolver
  review set so it no longer escapes required review coverage.
- reqstream: correct the AutoPopulateReferencePaths requirement wording to
  say a non-empty explicit value is preserved (not any explicit value,
  since empty is indistinguishable from unset outside a target), and add
  a dedicated DisableReferencePathsHarvestOptOut requirement for the real
  ApiMarkDisableReferencePathsHarvest opt-out mechanism, wired into the
  ApiMarkMsbuild system requirement's children.
- verification docs: correct the api-mark-task.md scenario that wrongly
  claimed an explicit empty ApiMarkReferencePaths suppresses harvesting;
  split it into two accurate scenarios referencing the real tests.
- ExternalXmlDocResolver: snapshot the reference assembly path list at
  construction time (ToArray()) instead of holding a live reference to the
  caller's list, preventing stale negative-cache results if the caller
  mutates the original list after construction. Add a pinning unit test.
- Context.cs: rename the misleading pattern local to path in the
  --reference-paths case, since it is a single assembly path, not a
  wildcard pattern.
- PackageIntegrationTests.cs: add an ExtractGetPropertyValue helper that
  parses -getProperty output (plain or JSON) instead of asserting against
  raw captured stdout, use it in the existing harvest/disable tests, and
  add a new end-to-end test proving a non-empty explicit
  ApiMarkReferencePaths value survives the .targets auto-harvest logic
  unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 12, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical lookup issues and moderate type-support, command-line-size, test-isolation, and documentation findings remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

docs/design/api-mark-dot-net/external-xml-doc-resolver.md:88

  • The implementation scans backward and swaps the last ref/lib segment (lines 218-230), but this design description still says it swaps the first segment. Please update the description to say "last" so the documented fallback matches the behavior for paths containing an earlier segment with the same name.
**SwapRefLibSegment** (private static): Swaps the first path segment named
exactly `ref` or `lib` (case-insensitively) for the other, mimicking the
folder layout convention used by many NuGet packages where compile-time

src/ApiMark.DotNet/DotNetGenerator.cs:139

  • The new external resolver does not make bare type-level <inheritdoc/> work: BuildInheritanceChain/BuildTypeInheritanceEntries only add method, property, and event IDs, so there is no T:... chain entry for a derived class or interface declaration. A type documented only with bare <inheritdoc/> therefore remains unresolved even with ReferencePaths, despite the new contract claiming base types/interfaces are supported. Add type inheritance candidates (and tests), or narrow the documented/required scope to member implementations.
    src/ApiMark.MSBuild/build/DemaConsulting.ApiMark.MSBuild.targets:83
  • This default serializes every resolved @(ReferencePath) into ApiMarkReferencePaths, and ApiMarkTask forwards each one through ProcessStartInfo.ArgumentList. Large projects with many or long reference paths can exceed the host OS command-line limit (especially Windows' ~32K limit), causing the post-build task to fail before ApiMark runs. Avoid requiring the full reference set on the command line (for example, use a response/config file or resolve the references inside the task) or explicitly define a bounded strategy.
    test/ApiMark.MSBuild.PackageTests/Fixtures/DotNet/SampleLibWithReference/SampleLib.csproj:35
  • This new package reference makes the isolated integration tests depend on a network restore: RunInIsolation redirects NUGET_PACKAGES to a fresh temp directory, while its generated nuget.config only has the ApiMark package locally and still lists nuget.org (PackageIntegrationTests.cs:529-538). Consequently every run must download Newtonsoft.Json, so these tests fail in offline/restricted CI despite the package-test harness being designed to use local packages. Use a local fixture/package source (or an SDK/in-repository reference) instead of introducing an uncached nuget.org dependency.
  • Files reviewed: 69/70 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/ApiMark.DotNet/ExternalXmlDocResolver.cs
Comment thread src/ApiMark.DotNet/XmlDocReader.cs
- docs: correct external-xml-doc-resolver design doc to say SwapRefLibSegment
  swaps the LAST ref/lib segment (nearest the DLL), not the first, matching
  the round-2 decoy-segment fix already in the implementation
- docs: document bare <inheritdoc/> on a type declaration (class/interface/
  struct) as a known, pre-existing limitation of the inheritance-chain
  mechanism (not a cross-assembly regression) in the dotnet user guide and
  dot-net-generator design doc
- docs: document the theoretical OS command-line length risk of forwarding
  many ApiMarkReferencePaths entries as individual --reference-paths
  arguments, mirroring the same shared risk already documented for
  ApiMarkIncludePaths/--includes
- fix: replace the SampleLibWithReference fixture's Newtonsoft.Json
  PackageReference with a ProjectReference to a new offline-safe companion
  ReferencedLib fixture project, removing a hidden network dependency on
  nuget.org from the isolated/offline PackageIntegrationTests auto-harvest
  tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 12, 2026 19:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

src/ApiMark.DotNet/ExternalXmlDocResolver.cs:224

  • On case-sensitive systems this treats directory segments such as REF or Lib as the NuGet ref/lib layout even though they are different paths on Linux. If an unrelated lowercase counterpart exists, a reference under /pkg/REF/... can resolve documentation from it incorrectly. Use the shared platform-aware comparer for these segment checks (ordinal on Linux, ignore-case only on Windows/macOS).
    src/ApiMark.DotNet/ExternalXmlDocResolver.cs:77
  • The per-file cache is keyed by the raw path string, so aliases such as ./ref/Foo.dll and /cwd/ref/Foo.dll (or equivalent separator forms) are treated as different entries and cause the same XML file to be parsed twice. That violates the documented "each reference assembly's XML documentation file is parsed at most once" guarantee and is especially costly when MSBuild supplies many references; normalize and deduplicate paths when taking the snapshot.
    src/ApiMark.Tool/Cli/Context.cs:603
  • An empty value passed as --reference-paths is accepted and added to the configured list. That makes ReferencePaths.Count > 0 and later lets ExternalXmlDocResolver probe Path.ChangeExtension("", ".xml") (and the resolver search directory resolve from the current directory), so a working-directory .xml file could be treated as external documentation instead of the option being ignored or rejected. Reject whitespace-only paths before adding them.
  • Files reviewed: 71/72 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/ApiMark.MSBuild/build/DemaConsulting.ApiMark.MSBuild.targets
- SwapRefLibSegment now uses the platform-aware FileSystemPathComparer
  instead of always OrdinalIgnoreCase, so a ref/lib segment with
  different case on a case-sensitive file system is no longer
  incorrectly treated as a match.
- ExternalXmlDocResolver's constructor now normalizes each reference
  assembly path with Path.GetFullPath before caching, so two different
  string forms of the same underlying file collapse to a single
  _docsByReferencePath cache key, preserving the documented
  parsed-at-most-once guarantee.
- Context's --reference-paths CLI option now silently skips
  whitespace-only values instead of adding them to ReferencePaths,
  preventing MSBuild semicolon-splitting artifacts from producing a
  spurious empty reference path that could pick up an unrelated .xml
  file from the working directory.

Adds unit tests for all three fixes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 12, 2026 20:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Four unresolved review findings remain, including two moderate issues requiring code and test fixes.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

docs/design/api-mark-dot-net/external-xml-doc-resolver.md:87

  • This description says the ref/lib match is case-insensitive everywhere, but SwapRefLibSegment uses the platform-aware comparer, so ref and REF do not match on Linux. Please document the platform-specific behavior here so the design does not promise a fallback that the implementation intentionally does not provide.
exactly `ref` or `lib` (case-insensitively) for the other, mimicking the

src/ApiMark.DotNet/ExternalXmlDocResolver.cs:85

  • Blank entries are filtered only in the CLI/MSBuild argument path; direct callers can set DotNetGeneratorOptions.ReferencePaths (or construct this public resolver) with "". Path.GetFullPath then treats that as a real path, so the resolver can probe an unrelated XML file relative to the current working directory, while DotNetGenerator.Parse also derives an incorrect search directory. Normalize/filter empty entries at the shared API boundary before both resolver and Cecil setup.
  • Files reviewed: 71/72 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread test/ApiMark.DotNet.Tests/ExternalXmlDocResolverTests.cs Outdated
The new ExternalXmlDocResolver_Constructor_RelativeAndAbsoluteFormsOfSamePath_ShareSingleCacheKey
test (added in the round-6 review fixes) failed on macOS CI because the OS
temp directory lives under a /var symlink to /private/var. Directory.SetCurrentDirectory
resolves this symlink, but the originally-captured absolute dllPath string did not, so the
relative and absolute forms normalized to two different-looking paths instead of colliding on a
single cache key as the test intended. Fixed by deriving the absolute form from the
OS-resolved current directory (Directory.GetCurrentDirectory() after SetCurrentDirectory)
rather than the original, potentially-unresolved directory string. This is a test-only fix;
no production code changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

One or more issues must be addressed before approval.

Review details

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

src/ApiMark.MSBuild/ApiMarkTask.cs:667

  • A failure to delete the temporary response file escapes the finally block and can turn an otherwise successful tool run (or the intended graceful false result) into an unhandled MSBuild task exception. This can occur when the file is briefly locked or the temp directory denies deletion. Treat cleanup failures separately—log them and preserve the child-process result, while still attempting deletion.

src/ApiMark.DotNet/DotNetAstModel.cs:100

  • The new AssemblyResolver field changes the model's ownership and disposal contract, but the companion docs/design/api-mark-dot-net/dot-net-ast-model.md still documents only Assembly as the owned resource and omits this field from the data model. Please update that design artifact so the parse/emit lifetime and resolver dependency are traceable alongside the source change.
    src/ApiMark.DotNet/DotNetEmitter.cs:53
  • This makes the emitter single-use: after the first Emit returns, both the parsed Cecil assembly and resolver are disposed, so a second Emit cannot produce the alternate output format. That regresses the IApiEmitter contract, which explicitly says the same parsed data can drive different formats without reparsing (src/ApiMark.Core/IApiEmitter.cs:7-12). Please move ownership to an explicit emitter lifetime (or otherwise keep the model alive across emits), or update the public contract and callers if single-use is intentional.
    src/ApiMark.DotNet/FileSystemPathComparer.cs:165
  • On a case-sensitive file system, the case-insensitive fallback can canonicalize a path that does not exist to an arbitrary existing sibling. For example, if both /tmp/Foo.dll is valid and /tmp/foo.dll is absent, normalizing /tmp/FOO.dll selects Foo.dll; if both case variants exist, enumeration order is only avoided when the input exactly matches one. This can make ExternalXmlDocResolver read documentation for an assembly the caller did not actually reference. Only accept the case-insensitive match after confirming the original segment path exists (or otherwise preserve the supplied casing on case-sensitive misses).
  • Files reviewed: 72/73 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…t AssemblyResolver

Address triaged findings from PR review #pullrequestreview-5188632048:

- ApiMarkTask.RunToolProcessWithResponseFile: wrap the finally block's
  File.Delete(responseFilePath) in a try/catch for IOException and
  UnauthorizedAccessException, logging a warning instead of letting a
  transient cleanup failure (lock/permissions) escape as an unhandled
  exception and override the already-determined task result.

- FileSystemPathComparer.FindActualEntryName: when no exact match exists
  and two or more directory entries match the requested segment
  case-insensitively (only possible on case-sensitive file systems), stop
  guessing based on unspecified enumeration order. Only resolve the
  case-insensitive fallback when exactly one candidate exists; otherwise
  return null so the caller preserves the as-supplied casing. Adds a
  regression test for the ambiguous third-casing scenario.

- DotNetEmitter.Emit / dot-net-emitter.md: document the pre-existing
  single-use limitation (Model.Assembly and Model.AssemblyResolver are
  disposed once Emit returns) as a known, accepted tradeoff rather than
  changing the lifetime model - confirmed via git history that the
  Model.Assembly disposal predates this branch and is not a regression.

- dot-net-ast-model.md: document the AssemblyResolver property, its
  constructor parameter, and its Mono.Cecil dependency, closing a design
  doc gap left by the cross-assembly inheritdoc work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 13, 2026 00:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

One or more issues must be addressed before approval.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

src/ApiMark.Tool/Cli/Context.cs:557

  • Response-file lines bypass the @@ branch because they are appended after expansion. Therefore a documented response-file value such as @@mylib reaches parsing unchanged and becomes @@mylib, unlike the equivalent command-line token; the current escape test covers only direct arguments. Apply the same leading-@@ unescape to response-file lines without recursively expanding single-@ lines.

src/ApiMark.MSBuild/ApiMarkTask.cs:655

  • The new response-file parser treats every argument beginning with @ as a response-file token, but ApiMarkTask forwards all MSBuild property values verbatim. Consequently an existing value such as ApiMarkLibraryName=@mylib (and likewise any path/description beginning with @) now makes the tool try to read mylib and fail; the @@ escape only helps callers that construct the CLI arguments themselves. Escape literal-leading-@ values in the MSBuild task while preserving the generated response-file token, or constrain expansion to the intended response-file usage.
    src/ApiMark.Tool/Cli/Context.cs:538
  • Expanding every top-level token that starts with @ changes the meaning of existing literal values. ApiMarkTask forwards C++ and shared values such as --includes, --library-name, and --output without applying the @@ escape, so a valid MSBuild value like @include is treated as a response-file path and the build fails before parsing. Either make expansion context-aware or escape literal @ values in the MSBuild argument-forwarding path, and cover this regression.
  • Files reviewed: 74/75 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Address findings from PR review #pullrequestreview-5188706708:

- ApiMarkTask now escapes every forwarded MSBuild property/item value
  that legitimately begins with a literal '@' (assembly/xml-doc paths,
  --exclude/--reference-paths/--includes/--api-headers entries,
  --library-name/--library-description/--defines/--output/--visibility/
  --format) by prefixing an extra '@'. Without this, Context's response-
  file-token detection (added for --reference-paths support) would
  misinterpret an ordinary value starting with '@' as a response-file
  reference or escape sequence, breaking previously-valid MSBuild
  property values. Added AppendCommonArguments -> AppendOptionalArg
  reuse for --output/--visibility/--format so they get the same escaping
  as everything else instead of duplicating the raw args.Add pattern.

- Context.ExpandResponseFileArguments now applies the same "@@" literal-
  escape unescaping to lines read from an expanded response file that it
  already applied to top-level arguments. Previously a value written to
  a response file as "@@MyLib" (to preserve a literal leading '@') was
  passed through unchanged instead of being unescaped to "@MyLib",
  differing from the equivalent direct command-line token. The
  non-recursive, single-pass expansion semantics for lines starting
  with an unescaped single '@' are unchanged.

Both fixes close the loop on the same underlying issue: the '@' token
convention introduced for --reference-paths response-file support
needs to apply symmetrically regardless of which code path
(ApiMarkTask-generated args, ApiMarkTask-generated response file, or a
user-authored response file) a value travels through.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 13, 2026 00:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The review includes one critical and two moderate unresolved findings, including unsafe response-file creation.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

src/ApiMark.DotNet/FileSystemPathComparer.cs:89

  • The drive/UNC root is preserved exactly as supplied, so on Windows two spellings such as c:\... and C:\... (or differently cased UNC roots) normalize to different Ordinal keys even though they name the same location. That breaks the documented deduplication/at-most-once parsing guarantee and can make the resolver parse the same XML more than once; canonicalize the root prefix separately before combining it with the normalized segments.
    src/ApiMark.MSBuild/ApiMarkTask.cs:667
  • This handler covers both PrepareArgumentsForProcess and RunToolProcess, so an IOException or UnauthorizedAccessException raised while starting the child process or handling its redirected streams is reported as “unable to create the reference-paths response file.” That produces a misleading diagnostic (and also affects invocations with no response file); limit this catch to response-file creation or use a separate process-failure message/path.
  • Files reviewed: 74/75 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/ApiMark.MSBuild/ApiMarkTask.cs Outdated
Address findings from PR review #pullrequestreview-5188801331, confirmed
independently by a local code-review sub-agent pass over the full branch diff:

- FileSystemPathComparer.NormalizeCase: canonicalize the drive-letter or
  UNC root prefix (uppercase drive letters, lowercase UNC server/share)
  instead of preserving it verbatim. Unlike every subsequent segment, the
  root prefix cannot be resolved to a 'real' on-disk casing via a
  directory-entry lookup, so two differently-cased spellings of the same
  root (e.g. "c:\..." vs "C:\...") previously normalized to two distinct
  strings under Ordinal comparison, breaking the documented
  at-most-once/deduplication guarantee relied on by DotNetGenerator and
  ExternalXmlDocResolver. Added a regression test.

- ApiMarkTask.RunToolProcessWithResponseFile: narrowed the
  IOException/UnauthorizedAccessException catch to cover only
  PrepareArgumentsForProcess (response-file creation), no longer also
  wrapping the RunToolProcess call. Previously an I/O failure starting
  the child process or handling its redirected streams was misreported
  as "unable to create the reference-paths response file"; it now
  propagates unhandled, matching the pre-existing (pre-response-file)
  behavior. The response file is still deleted via the surrounding
  finally block regardless. Added a regression test.

- ApiMarkTask.PrepareArgumentsForProcess: replaced File.WriteAllLines
  (which opens with FileMode.Create, silently truncating or writing
  through any pre-existing file or symlink at the target path) with a
  new CreateResponseFile helper that opens with FileMode.CreateNew,
  closing the TOCTOU window between generating the random response-file
  name and writing to it. Retries (bounded) only on a detected name
  collision; any other failure propagates normally.

- .cspell.yaml: added "TOCTOU" (a genuine technical term used in the
  new code comments/doc).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 13, 2026 00:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (2)

src/ApiMark.DotNet/FileSystemPathComparer.cs:218

  • On a case-sensitive filesystem, this fallback treats a wrong-case path as if it named the existing file whenever there is exactly one case-insensitive match. For example, configuring /tmp/foo.dll when only /tmp/Foo.dll exists normalizes to Foo.dll and can load unrelated external XML documentation instead of leaving the configured reference unresolved. Gate the case-insensitive fallback on the candidate path itself existing with the filesystem's normal semantics (or use a filesystem-aware comparison policy) so this normalization does not conflate distinct Linux paths.
    src/ApiMark.MSBuild/ApiMarkTask.cs:731
  • The response-file format treats each line as a complete argument, but this writes each reference path verbatim. On Unix, a newline is valid in a file name, so a valid ApiMarkReferencePaths entry containing \n is split into multiple arguments (and could even become an injected flag), causing the child invocation to resolve the wrong path or fail. Encode line breaks in the response-file protocol or reject them before writing the file.
  • Files reviewed: 74/75 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/ApiMark.MSBuild/ApiMarkTask.cs
…eanup

The CreateResponseFile retry guard ('catch (IOException) when (attempt <
maxAttempts && File.Exists(path))') could not distinguish a name collision
at creation time (nothing written yet, safe to retry) from a failure while
writing/flushing content after the file was already successfully created
(not safe to retry, since doing so would leak the partially-written file).
Both cases satisfy 'File.Exists(path) == true' once the file exists, so a
write-time IOException (e.g. a full disk mid-write) would incorrectly
trigger a retry with a fresh name, masking the real failure and abandoning
the partial file.

Split the method into two scopes: the FileMode.CreateNew call alone is
retried on collision (nothing to clean up if it fails, since nothing was
created), while the subsequent write/flush is not retried — on failure the
partially-written file is deleted on a best-effort basis and the original
exception is rethrown unchanged.

Also made CreateResponseFile internal (previously private) so it can be
exercised directly by a new regression test that forces a write-time
failure via a throwing IEnumerable<string>, asserting no retry occurs, no
partial file is left behind, and the original exception propagates.

Found via an unresolved PR review thread on the prior CreateResponseFile
change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 13, 2026 01:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Four unresolved moderate findings affect resolver behavior/performance and MSBuild argument handling; a documentation nit also remains.

Review details

Suppressed comments (4)

src/ApiMark.DotNet/ExternalXmlDocResolver.cs:132

  • With the new MSBuild default, ReferencePaths can contain every resolved dependency. On any external member miss, this loop parses the XML index for every configured reference before caching null; a project with many references and distinct unresolved/unsupported <inheritdoc/> targets therefore incurs an O(reference-count) set of disk reads and XML parses. Carry the resolved declaring assembly/path with inheritance targets, or otherwise index candidates by assembly, so a miss does not probe the entire auto-harvested dependency set.
    src/ApiMark.DotNet/FileSystemPathComparer.cs:218
  • On a case-sensitive file system, a non-existent spelling can still have one case-insensitive match (for example, configured foo.dll when only Foo.dll exists). This branch currently treats that other entry as the requested path, so ExternalXmlDocResolver can silently load documentation for an assembly the supplied path does not identify (and the same issue applies to a wrongly-cased intermediate directory). Only accept the case-insensitive fallback after confirming that the original candidate path itself exists; otherwise leave the segment unchanged.
    src/ApiMark.DotNet/XmlDocReader.cs:543
  • This external fallback still cannot resolve a common transitive case: if the external member returned here has its own bare <inheritdoc/>, the recursive call reaches ResolveInheritdocSource with an ID absent from the primary assembly's _inheritanceChain and returns null. Consequently a local override silently loses documentation even though the external XML contains an inheritance path; build/cache inheritance metadata for resolved external assemblies (or otherwise recurse their bare targets) so cross-assembly resolution remains correct beyond one hop.
    src/ApiMark.MSBuild/ApiMarkTask.cs:732
  • PrepareArgumentsForProcess searches the entire logical argument list for the literal --reference-paths, even when that token is actually an unrelated option value. For example, an MSBuild ApiMarkLibraryDescription of --reference-paths makes this code consume the following flag as a reference-path value, write a malformed response file, and cause the tool invocation to fail. Restrict the substitution to the generated .NET reference-path pairs (or parse option/value pairs so values are skipped) instead of scanning arbitrary tokens.
  • Files reviewed: 74/75 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@Malcolmnixon
Malcolmnixon merged commit 6473537 into main Sep 13, 2026
16 checks passed
@Malcolmnixon
Malcolmnixon deleted the feature/cross-assembly-inheritdoc branch September 13, 2026 01:43
Malcolmnixon added a commit that referenced this pull request Sep 13, 2026
…h case-sensitivity helpers (#44)

* Move path case-normalization into ApiMark.Core.PathHelpers

FileSystemPathComparer.NormalizeCase/Comparer was duplicated verbatim as
its own internal class in ApiMark.DotNet, used independently by both
DotNetGenerator and ExternalXmlDocResolver via near-identical
'filter blank -> resolve -> NormalizeCase with a scoped directoryEntryCache
-> Distinct' boilerplate at each call site.

Path case-normalization is a general path concern, not a DotNet-specific
one, so move it into the existing ApiMark.Core.PathHelpers utility
(which already centralizes the other general path concern,
directory-traversal-safe combination) rather than duplicating it further.
PathHelpers is now public (SafePathCombine remains internal) so
NormalizeCase/Comparer can be called from ApiMark.DotNet.

- Added NormalizeCase/Comparer (plus the private CanonicalizeRoot/
  FindActualEntryName helpers) to src/ApiMark.Core/PathHelpers.cs.
- Deleted src/ApiMark.DotNet/FileSystemPathComparer.cs; updated
  DotNetGenerator.cs and ExternalXmlDocResolver.cs to call
  ApiMark.Core.PathHelpers instead.
- Moved the corresponding NormalizeCase regression tests from
  test/ApiMark.DotNet.Tests/FileSystemPathComparerTests.cs into
  test/ApiMark.Core.Tests/PathHelpersTests.cs.
- Updated the PathHelpers design/verification/reqstream/sysml2 companion
  docs, the ExternalXmlDocResolver/DotNetGenerator design docs' references,
  and .reviewmark.yaml's ExternalXmlDocResolver review-scope entry
  (FileSystemPathComparer.cs/Tests.cs removed, already covered by the
  existing PathHelpers review-scope entry).

No behavior change; this is a pure move/consolidation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: replace OS-guessed path case-sensitivity with on-disk normalization in ApiMark.Cpp

CppEmitter and ClangAstParser each independently guessed file-system case
sensitivity from the operating system (Linux => case-sensitive, else
case-insensitive), which is incorrect since case sensitivity is a
file-system property, not an OS property. Both now resolve actual on-disk
casing via the shared ApiMark.Core.PathHelpers.NormalizeCase, matching the
approach already used in ApiMark.DotNet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve cross-assembly inheritdoc across multiple external hops without full reference-path scan

Fixes two Copilot review findings on PR #43 that were missed pre-merge:

- ExternalXmlDocResolver previously scanned every configured reference path
  in order on any miss. With MSBuild's auto-harvested ReferencePaths default
  (which can include hundreds of transitive dependencies), an unresolved or
  unsupported inheritdoc target incurred an O(reference-count) set of disk
  reads and XML parses. DotNetGenerator.BuildInheritanceChain now also
  computes a declaring-assembly hint for each inheritance candidate, and
  ExternalXmlDocResolver gains a new TryGetMember(memberId, hint) overload
  that tries the hinted reference path first, only falling back to the full
  scan when the hint is absent, unmatched, or stale.

- Bare <inheritdoc/> resolution against an externally-resolved base member
  could not continue a second hop if that external member's own XML doc
  entry was itself a bare <inheritdoc/>, because the inheritance chain was
  built only from the primary assembly's Cecil metadata. BuildTypeInheritanceEntries
  is now recursive (guarded against revisiting a type) and walks into
  resolvable external base types/interfaces, adding their own members'
  inheritance entries to the chain so multi-hop external resolution works.

Both fixes are confined to DotNetGenerator's private inheritance-chain
construction and a purely additive ExternalXmlDocResolver overload;
XmlDocReader's public constructors and chain type are unchanged, preserving
binary compatibility.

Adds fixture types (ExternalGrandBaseClass/ExternalMidBaseClass/
ExternalTwoHopInheritDocClass) and regression tests covering both the
two-hop resolution and the hint-based fast path (including hint-miss
fallback).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: only accept case-insensitive path fallback when confirmed on disk

PathHelpers.FindActualEntryName previously accepted a single ambiguous
case-insensitive directory-entry match unconditionally. On a case-sensitive
file system, a non-existent spelling could still have one case-insensitive
match (e.g. configured foo.dll when only Foo.dll exists), and this branch
would treat that unrelated entry as the requested path -- for example
ExternalXmlDocResolver could silently load documentation for an assembly
the supplied path does not actually identify.

The fallback is now only accepted after confirming, via File.Exists/
Directory.Exists on the literal (un-normalized) candidate path, that the
current OS/file system itself resolves it -- which is only true on a
genuinely case-insensitive file system. On a case-sensitive file system the
check returns false and the segment is left unchanged, so a coincidentally
similarly-named entry can never be mistaken for the requested path.

Adds a regression test using a pre-populated fake directory-entry cache so
the test result does not depend on the host OS's actual case sensitivity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: parse MSBuild tool arguments positionally instead of scanning for literal flag text

PrepareArgumentsForProcess previously searched the entire logical argument
list for the literal string '--reference-paths', even when that token was
actually an unrelated option's VALUE. For example, an MSBuild
ApiMarkLibraryDescription value of exactly '--reference-paths' caused this
code to consume the following token as a reference-path value, writing a
malformed response file and failing the tool invocation.

BuildArguments always emits a well-known token sequence: a language
subcommand followed by flag/value pairs, with a fixed set of zero-arg
boolean flags (--include-obsolete). PrepareArgumentsForProcess now walks
that sequence positionally (flag, then its value, skipping zero-arg flags)
instead of scanning for flag text anywhere in the list, so a caller-supplied
value that happens to collide with a flag's literal text is never
misinterpreted as the flag itself.

Adds a regression test with an ApiMarkLibraryDescription of exactly
'--reference-paths' to prove it is treated as a plain value, not a flag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: update stale XmlDocReader inheritdoc limitation comments

The class-level remarks and ResolveInheritdocSource's remarks still
described the pre-fix behavior as a 'known limitation' -- that a bare
<inheritdoc/> could never continue a second hop after landing on an
externally-resolved member. That is no longer accurate: DotNetGenerator's
BuildTypeInheritanceEntries now recurses into resolvable external base
types/interfaces and populates the chain it passes to XmlDocReader with
those types' own members too, so a chain entry commonly exists for an
externally-resolved member and multi-hop resolution works when the further
base type is itself resolvable via Mono.Cecil.

Reworded both remarks (and the XmlDocReaderTests regression test that pins
down the isolated no-chain-entry case) to describe XmlDocReader's own
chain-lookup behavior accurately, without claiming a limitation that the
caller (DotNetGenerator) has already worked around for the common case.

Found by the code-review sub-agent while reviewing this branch's diff
before push.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* style: fix doc comment summary formatting in CppEmitter

Reformats _includeRootDirectoryEntryCache's <summary> tag onto its own
line, matching the codebase's established documentation convention.
Found by the formal-review agent pass on this branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert "fix: only accept case-insensitive path fallback when confirmed on disk"

This reverts commit 9716379.

* docs: link PathHelpers case-normalization requirements to a tagged parent

Three PathHelpers case-normalization requirements (NormalizeToActualOnDiskCasing,
NormalizeCasePreservesUnresolvableSegments, NormalizeCaseResolvesAmbiguityDeterministically)
existed in docs/reqstream/api-mark-core/path-helpers.yaml but were never referenced
as children of any requirement tagged 'system' or 'quality', leaving them orphaned
per reqstream's traceability check. This pre-existing gap (present since the
PathHelpers requirements were first added) only surfaced now because CI's
Build Documents job runs reqstream --enforce.

Add a new ApiMarkCore-PathHelpers-ProvideCaseNormalizationHelper parent requirement
(tags: [system]) in api-mark-core.yaml with the three orphaned requirements as
children, alongside the existing ProvideSafePathCombinationHelper parent which only
covers path combination (not case normalization).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address remaining PR review findings on PathHelpers and inheritdoc resolution

- ExternalXmlDocResolver: precompute an assembly-simple-name -> path(s)
  lookup in the constructor so the declaring-assembly hint fast path is
  O(1) instead of an O(n) FirstOrDefault scan over every configured
  reference path on each lookup.
- ExternalXmlDocResolver: key _memberCache by (hint, memberId) instead of
  memberId alone, so that in the rare case where two unrelated reference
  assemblies declare a member with an identical XML doc ID, a lookup for
  one hint can never be served the other hint's cached result.
- DotNetGenerator.TryAddAssemblyHint: correct the doc comment's overclaim
  that first-wins hint assignment is unconditionally safe; XML doc member
  IDs do not carry assembly identity, so document the narrow, tolerated
  collision case honestly instead of asserting a false guarantee.
- PathHelpers: correct the class-level remark claiming all members are
  thread-safe; NormalizeCase mutates a caller-supplied directoryEntryCache
  dictionary when one is passed, so callers sharing a cache across threads
  must synchronize their own access to it.
- docs/design: update path-helpers.md, clang-ast-parser.md, and
  cpp-emitter.md, which still described the deleted
  FileSystemPathComparer/FileSystemPathComparison members and an
  inaccurate 'no fields or properties' claim, to describe the current
  ApiMark.Core.PathHelpers-based implementation.
- docs/reqstream: correct the new PathHelpers case-normalization parent
  requirement's accessibility claim from 'internal' to 'public', matching
  PathHelpers/NormalizeCase/Comparer's actual public accessibility.
- test: remove a redundant ToList() call in
  ApiMarkTask_Execute_ColonSeparatedReferencePathsPseudoFlagValue_IsTreatedAsPlainTokenNotPathList.

Verified: full solution build clean; ApiMark.Core.Tests (125/125),
ApiMark.DotNet.Tests (324/324 across net8/9/10), and
ApiMark.MSBuild.Tests (42/42) all pass; fix.ps1/lint.ps1 clean;
dotnet reqstream --lint clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: document ExternalXmlDocResolver hint fast-path and composite cache key

The hint-based TryGetMember overload and its O(1) per-assembly lookup were
introduced without companion design/requirements/verification updates.
Add design doc coverage for the hinted overload and the composite
(Hint, MemberId) cache key, a new linked requirement for the hint
fast-path behavior, and matching verification test-scenario write-ups.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address second Copilot review batch (chain collisions, path ordering, perf caching, stale docs)

- DotNetGenerator: split BuildInheritanceChain into an authoritative first pass
  (primary-assembly members) and a non-authoritative second pass (base/interface
  recursion via TryAdd), so external base-type entries can no longer overwrite a
  primary-assembly member's inheritance target.
- DotNetGenerator: normalize reference directories via PathHelpers.NormalizeCase
  before the Directory.Exists filter, so a differently-cased configured directory
  can still be resolved on a case-sensitive file system.
- ClangAstParser: precompute normalized public include roots once in the
  constructor and memoize IsOwned results per as-supplied source file, removing
  an O(declarations x roots) hot path; drop the now-unused _options field.
- CppEmitter: precompute normalized public include roots once in the constructor
  and memoize GetIncludePath results per as-supplied source file.
- Add ExternalXmlDocResolverTests case for hint-matches-path-but-member-missing
  fallback (previously untested).
- Fix stale XmlDocReader remark referencing the renamed
  BuildTypeInheritanceEntries method.
- Update design/reqstream/verification companion docs for api-mark-core,
  api-mark-cpp, api-mark-dot-net, and api-mark-msbuild to match the above and
  trace two previously-untraced tests (external two-hop inheritdoc resolution,
  MSBuild --reference-paths literal-value collision).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address 6-item review batch and formal-review medium finding

- Fix stale wording overstating ExternalXmlDocResolver's hint-fast-path
  behavior in docs/comments (a hinted-path miss still falls back to a
  full scan of every configured reference path).
- Fix stale 'Assembly resolver seeding' paragraph in dot-net-generator.md
  that described the old filter-then-normalize order instead of the
  fixed normalize-then-filter order.
- Fix a real bug: CppGenerator.Parse now normalizes PublicIncludeRoots
  once upfront and threads a normalized-options clone through
  CollectHeaderFiles, ClangAstParser.Parse, and CppEmitter, so a
  differently-cased include root is resolved before Directory.Exists
  validation and before being passed to clang as a -I flag (previously
  only ClangAstParser's own constructor normalized roots, too late for
  header discovery and glob-pattern synthesis).
- Add regression tests for both the DotNetGenerator ReferencePaths and
  CppGenerator PublicIncludeRoots case-normalization fixes, and soften
  their claims (per formal-review feedback) to accurately describe that
  they only distinguish fixed/unfixed behavior on a case-sensitive file
  system such as the CI matrix's ubuntu-latest runner.
- Add verification/reqstream traceability for both new tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Consolidate C++ include-root normalization into PathHelpers.NormalizeCaseDirectory

Fixes a root-corruption bug: ClangAstParser, CppEmitter, and CppGenerator each
manually trimmed a path's trailing separator before normalizing and
re-appending one, an unsafe composition for a bare filesystem root (e.g.
'C:\' or '/'). The trimmed form ('C:' or '') is not equivalent to the root -
re-deriving a trailing separator by passing it through Path.GetFullPath or
Directory.Exists a second time silently corrupts it (CppGenerator's
CollectHeaderFiles resolved a trimmed drive root to the current working
directory, and treated a trimmed Unix root as nonexistent).

Adds PathHelpers.NormalizeCaseDirectory, which normalizes the untouched,
fully-qualified path directly and appends a trailing separator only when one
is not already present, avoiding the corruption entirely. All three Cpp
callers now use it instead of duplicating the fix.

Also addresses reqstream WHAT-vs-HOW wording findings in path-helpers.yaml,
cpp-generator.yaml, dot-net-generator.yaml, and external-xml-doc-resolver.yaml,
and adds a missing XML doc comment on PathHelpersTests.CreateTempDirectory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix ClangAstParser -I flags to use normalized include roots regardless of caller

ClangAstParser.Parse normalized PublicIncludeRoots only for its internal
ownership check, while BuildArguments still read the raw un-normalized
options. This only worked in production because CppGenerator.Parse already
normalizes options before calling ClangAstParser.Parse; any direct caller
with a differently-cased root on a case-sensitive file system got broken
-I arguments even though ownership checks used the corrected path.

Parse now normalizes PublicIncludeRoots once at the top of the method and
threads the same normalized options through both BuildArguments and the
constructor. Extracted the shallow-copy helper previously duplicated in
CppGenerator into a shared CppGeneratorOptions.WithPublicIncludeRoots so
both callers stay in sync.

Also corrects a stale design-doc paragraph describing the external XML doc
resolver wiring as a direct method-group reference rather than the actual
closure that also threads through AssemblyHints.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix stale ClangAstParser/CppEmitter design doc paragraphs

- clang-ast-parser.md: the directoryEntryCache passed into the constructor
  is shared with Parse's own cache, not a separate unshared instance; the
  design doc previously claimed the opposite.
- cpp-emitter.md: the CppEmitter instance fields inventory omitted
  _includeRootDirectoryEntryCache, _normalizedPublicIncludeRoots, and
  _includePathCache, which back GetIncludePath's case-normalization and
  memoization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add PathHelpers to Dependencies inventory in Cpp design docs

clang-ast-parser.md, cpp-emitter.md, and cpp-generator.md each gained a
direct ApiMark.Core.PathHelpers dependency in the case-normalization work
but the Dependencies sections were not updated to list it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Malcolm Nixon <Malcolm.Nixon@hiarc.inc>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants