diff --git a/CLAUDE.md b/CLAUDE.md index 2c3e05935..975947c9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,7 @@ All test projects exercise real Visio COM calls. There is no mock/fake layer (in ## Per-commit conventions - **Changelogs:** when a change is consumer-visible (public API, behavior, supported runtime, dependencies) add an entry to the matching `[Unreleased]` section of [`NuGet/CHANGELOG.md`](NuGet/CHANGELOG.md) or [`VisioAutomation_2010/VisioPowerShell/CHANGELOG.md`](VisioAutomation_2010/VisioPowerShell/CHANGELOG.md) in the **same commit**. Pure internal / build / docs changes don't need entries. +- **`VisioScripting` public-API contract:** the `Client` facade and the public method signatures on each `*Commands` class are public-stable per [`docs/decisions/visioscripting-public-api.md`](docs/decisions/visioscripting-public-api.md). **Renaming or removing** a public method on `Client` or any `*Commands` class is a **breaking change** — treat it accordingly. Adding methods is non-breaking. The plumbing (`Helpers/`, `Loaders/`, `CommandTarget`, `CommandTargetFlags`, `Get_*Dimensions` static factories) is `internal` and free to change without notice. When the line is unclear, the ADR has the authoritative table. - **PowerShell loader scripts** in `VisioAutomation_2010/VisioPowerShell/`: `Load*` does in-session imports; `Install*` does persistent installs to the user's PS modules folder; `*.ISE.ps1` is the ISE-launched variant. See the folder's [README.md](VisioAutomation_2010/VisioPowerShell/README.md). ## Tooling notes diff --git a/NuGet/CHANGELOG.md b/NuGet/CHANGELOG.md index 517e11bfe..dce9010e5 100644 --- a/NuGet/CHANGELOG.md +++ b/NuGet/CHANGELOG.md @@ -12,6 +12,21 @@ The format follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/ ## [Unreleased] +### Added +- New facade methods on `VisioScripting.Client` for operations that previously required reaching past the facade. These are the canonical entry points going forward and align with the hybrid public-API contract decided in [#156](https://github.com/saveenr/VisioAutomation/issues/156): + - `Client.Model.LoadDirectedGraphFromXml(XDocument)` returns a `DirectedGraphDocument` (replaces `DirectedGraphDocumentLoader.LoadFromXml`). + - `Client.Model.LoadOrgChartFromXml(XDocument)` returns an `OrgChartDocument` (replaces `OrgChartDocumentLoader.LoadFromXml`). + - `Client.Page.GetPageDimensions(TargetPages)` returns `List` (replaces the static `PageDimensions.Get_PageDimensions`). + - `Client.Selection.GetShapeDimensions(TargetShapes)` returns `List` (replaces the static `ShapeDimensions.Get_ShapeDimensions`). + + Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). + +### Removed +- `VisioScripting.Loaders.DirectedGraphDocumentLoader` and `VisioScripting.Loaders.OrgChartDocumentLoader` are now `internal`. Direct consumers should switch to `Client.Model.LoadDirectedGraphFromXml` / `Client.Model.LoadOrgChartFromXml`. Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). +- Static `Get_PageDimensions` / `Get_ShapeDimensions` factory methods on `VisioScripting.Models.PageDimensions` / `Models.ShapeDimensions` are now `internal`. Use `Client.Page.GetPageDimensions` / `Client.Selection.GetShapeDimensions` instead. The data-carrier types themselves remain `public` (they are return values). Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). +- Unused `LoadFromXml(Client, string filename)` overloads on both `DirectedGraphDocumentLoader` and `OrgChartDocumentLoader`. Each was an internal convenience that loaded the XML file and deferred to the `XDocument` overload; no external caller ever invoked them. Part of [#182](https://github.com/saveenr/VisioAutomation/issues/182). +- The plumbing types `VisioScripting.CommandTarget`, `VisioScripting.CommandTargetFlags`, and `Client.GetCommandTarget(CommandTargetFlags)` are now `internal`. The remaining `public` helpers in `VisioScripting.Helpers` (`WildcardHelper`, `InteropHelper`, `SelectionHelper`) are also now `internal`. None of these appear in any documented `*Commands` method signature; they are implementation detail of the `Client` facade. The new public-API contract is captured in [`docs/decisions/visioscripting-public-api.md`](https://github.com/saveenr/VisioAutomation/blob/master/docs/decisions/visioscripting-public-api.md). Phase C of [#182](https://github.com/saveenr/VisioAutomation/issues/182). + ### Fixed - `MsaglRenderer` now honors `Node.Size` when `Node.Cells` is also set on a directed-graph node. Previously the `Cells` assignment overwrote the entire `shape_node.Cells` object, silently dropping the `XFormWidth` / `XFormHeight` populated from `Size`, so the rendered shape came out at the master's default size instead of the requested `Size`. Now the user's `Cells` is merged onto the existing cells via `ApplyFormulasTo`, which preserves Size-derived width/height. Closes [#82](https://github.com/saveenr/VisioAutomation/issues/82). diff --git a/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs b/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs index c973ebaae..9c8b7ddc2 100644 --- a/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs +++ b/VisioAutomation_2010/VTest.Models/DirectedGraphDrawModelTests.cs @@ -300,7 +300,7 @@ public void Loader_RootElement_WrongNameThrows() var dg_xml = SXL.XDocument.Parse(xml); var client = this.GetScriptingClient(); MUT.Assert.ThrowsExactly( - () => VisioScripting.Loaders.DirectedGraphDocumentLoader.LoadFromXml(client, dg_xml)); + () => client.Model.LoadDirectedGraphFromXml(dg_xml)); } [MUT.TestMethod] @@ -364,13 +364,13 @@ private VA.Models.Layouts.DirectedGraph.DirectedGraphDocument load_two_node_grap extra_attrs ?? ""); var dg_xml = SXL.XDocument.Parse(xml); var client = this.GetScriptingClient(); - return VisioScripting.Loaders.DirectedGraphDocumentLoader.LoadFromXml(client, dg_xml); + return client.Model.LoadDirectedGraphFromXml(dg_xml); } private void draw_directed_graph(VisioScripting.Client client, string dg_text) { var dg_xml = SXL.XDocument.Parse(dg_text); - var dgdoc = VisioScripting.Loaders.DirectedGraphDocumentLoader.LoadFromXml(client, dg_xml); + var dgdoc = client.Model.LoadDirectedGraphFromXml(dg_xml); // TODO: Investigate if this this special case for Visio 2013 can be removed // this is a temporary fix to handle the fact that server_u.vss in Visio 2013 doesn't result in server_u.vssx diff --git a/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs b/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs index 162fe0613..8becbd126 100644 --- a/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs +++ b/VisioAutomation_2010/VTest.Models/OrgChartDrawModelTests.cs @@ -29,7 +29,7 @@ public void RenderOrgChart_FromSampleData_DoesNotThrow() private void draw_org_chart(VisioScripting.Client client, string text) { var xmldoc = SXL.XDocument.Parse(text); - var orgchart = VisioScripting.Loaders.OrgChartDocumentLoader.LoadFromXml(client, xmldoc); + var orgchart = client.Model.LoadOrgChartFromXml(xmldoc); client.Model.DrawOrgChart(VisioScripting.TargetPage.Auto, orgchart); } diff --git a/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs b/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs index b672f7829..73dbfffe4 100644 --- a/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs +++ b/VisioAutomation_2010/VisioPowerShell/Commands/VisioModel/ImportVisioModel.cs @@ -1,4 +1,3 @@ -using VisioScripting.Loaders; using SXL = System.Xml.Linq; using SMA = System.Management.Automation; @@ -26,13 +25,13 @@ protected override void ProcessRecord() if (root.Name == "directedgraph") { this.WriteVerbose("Loading Directed Graph"); - var dgdoc = DirectedGraphDocumentLoader.LoadFromXml(this.Client, xmldoc); - this.WriteObject(dgdoc); + var dgdoc = this.Client.Model.LoadDirectedGraphFromXml(xmldoc); + this.WriteObject(dgdoc); } else if (root.Name == "orgchart") { this.WriteVerbose("Loading as Org Chart"); - var orgchartdoc = OrgChartDocumentLoader.LoadFromXml(this.Client, xmldoc); + var orgchartdoc = this.Client.Model.LoadOrgChartFromXml(xmldoc); this.WriteObject(orgchartdoc); } else diff --git a/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs b/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs index 56fcdb011..46bd807e5 100644 --- a/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs +++ b/VisioAutomation_2010/VisioPowerShell/Commands/VisioPage/MeasureVisioPage.cs @@ -17,18 +17,9 @@ public class MeasureVisioPage : VisioCmdlet protected override void ProcessRecord() { - - var targetpages = new VisioScripting.TargetPages(this.Page).ResolveToPages(this.Client); - - if (targetpages.Pages.Count < 1) - { - return; - } - - var list_pagedim = VisioScripting.Models.PageDimensions.Get_PageDimensions(targetpages.Pages); - + var targetpages = new VisioScripting.TargetPages(this.Page); + var list_pagedim = this.Client.Page.GetPageDimensions(targetpages); this.WriteObject(list_pagedim,true); - } } } diff --git a/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs b/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs index d5d214494..f9ec23785 100644 --- a/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs +++ b/VisioAutomation_2010/VisioPowerShell/Commands/VisioShape/MeasureVisioShape.cs @@ -1,4 +1,3 @@ -using System.Linq; using SMA = System.Management.Automation; using IVisio = Microsoft.Office.Interop.Visio; @@ -7,29 +6,15 @@ namespace VisioPowerShell.Commands.VisioShape [SMA.Cmdlet(SMA.VerbsDiagnostic.Measure, Nouns.VisioShape)] public class MeasureVisioShape: VisioCmdlet { - // CONTEXT:SHAPES + // CONTEXT:SHAPES [SMA.Parameter(Mandatory = false)] public IVisio.Shape[] Shape; protected override void ProcessRecord() { - - var targetshapes = new VisioScripting.TargetShapes(this.Shape).ResolveToShapes(this.Client); - - if (targetshapes.Shapes.Count < 1) - { - return; - } - - - var shapeids = VisioAutomation.Core.ShapeIDPairs.FromShapes(targetshapes.Shapes).Select(i => i.ShapeID).ToList(); - var page = targetshapes.Shapes[0].ContainingPage; - var list_shapedim = VisioScripting.Models.ShapeDimensions.Get_ShapeDimensions(page, shapeids); - + var targetshapes = new VisioScripting.TargetShapes(this.Shape); + var list_shapedim = this.Client.Selection.GetShapeDimensions(targetshapes); this.WriteObject(list_shapedim,true); - } - - } } \ No newline at end of file diff --git a/VisioAutomation_2010/VisioScripting/Client.cs b/VisioAutomation_2010/VisioScripting/Client.cs index c7e1d19ea..5a1525aa6 100644 --- a/VisioAutomation_2010/VisioScripting/Client.cs +++ b/VisioAutomation_2010/VisioScripting/Client.cs @@ -74,7 +74,7 @@ public ClientContext ClientContext get { return this._client_context; } } - public CommandTarget GetCommandTarget(CommandTargetFlags flags) + internal CommandTarget GetCommandTarget(CommandTargetFlags flags) { var command_target = new CommandTarget(this, flags); return command_target; diff --git a/VisioAutomation_2010/VisioScripting/CommandTarget.cs b/VisioAutomation_2010/VisioScripting/CommandTarget.cs index 9ea320aa0..47d2fe4a1 100644 --- a/VisioAutomation_2010/VisioScripting/CommandTarget.cs +++ b/VisioAutomation_2010/VisioScripting/CommandTarget.cs @@ -3,7 +3,7 @@ namespace VisioScripting { - public class CommandTarget + internal class CommandTarget { private readonly Client _client; public IVisio.Application Application { get; private set; } diff --git a/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs b/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs index dd6f2fde5..423621d10 100644 --- a/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs +++ b/VisioAutomation_2010/VisioScripting/CommandTargetFlags.cs @@ -1,7 +1,7 @@ namespace VisioScripting { [System.Flags] - public enum CommandTargetFlags + internal enum CommandTargetFlags { RequireApplication, RequireDocument, diff --git a/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs b/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs index 32f674d7d..82e0c0d29 100644 --- a/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs +++ b/VisioAutomation_2010/VisioScripting/Commands/ModelCommands.cs @@ -4,6 +4,7 @@ using ORG = VisioAutomation.Models.Documents.OrgCharts; using GRAPH = VisioAutomation.Models.Layouts.DirectedGraph; using GRID = VisioAutomation.Models.Layouts.Grid; +using SXL = System.Xml.Linq; namespace VisioScripting.Commands { @@ -192,5 +193,15 @@ public void DrawDirectedGraphDocument(GRAPH.DirectedGraphDocument dgdoc, GRAPH.D this._client.Output.WriteVerbose("Finished rendering all pages"); this._client.Output.WriteVerbose("Finished rendering directed graph."); } + + public GRAPH.DirectedGraphDocument LoadDirectedGraphFromXml(SXL.XDocument xmldoc) + { + return Loaders.DirectedGraphDocumentLoader.LoadFromXml(this._client, xmldoc); + } + + public ORG.OrgChartDocument LoadOrgChartFromXml(SXL.XDocument xmldoc) + { + return Loaders.OrgChartDocumentLoader.LoadFromXml(this._client, xmldoc); + } } } \ No newline at end of file diff --git a/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs b/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs index 1544bdc11..48d3f00b2 100644 --- a/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs +++ b/VisioAutomation_2010/VisioScripting/Commands/PageCommands.cs @@ -60,6 +60,18 @@ public void DeletePages(TargetPages targetpages, bool renumber) return sizes; } + public List GetPageDimensions(TargetPages targetpages) + { + targetpages = targetpages.ResolveToPages(this._client); + + if (targetpages.Pages.Count < 1) + { + return new List(0); + } + + return Models.PageDimensions.Get_PageDimensions(targetpages.Pages); + } + public IVisio.Page NewPage(VisioScripting.TargetDocument targetdoc, VisioAutomation.Core.Size? size, bool isbackgroundpage) { targetdoc = targetdoc.ResolveToDocument(this._client); diff --git a/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs b/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs index d7f6d11a5..3ddf696eb 100644 --- a/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs +++ b/VisioAutomation_2010/VisioScripting/Commands/SelectionCommands.cs @@ -267,5 +267,19 @@ public bool ContainsShapes(TargetSelection targetselection, int min_items) bool v = num_selected >= min_items; return v; } + + public List GetShapeDimensions(TargetShapes targetshapes) + { + targetshapes = targetshapes.ResolveToShapes(this._client); + + if (targetshapes.Shapes.Count < 1) + { + return new List(0); + } + + var shapeids = VisioAutomation.Core.ShapeIDPairs.FromShapes(targetshapes.Shapes).Select(i => i.ShapeID).ToList(); + var page = targetshapes.Shapes[0].ContainingPage; + return Models.ShapeDimensions.Get_ShapeDimensions(page, shapeids); + } } } \ No newline at end of file diff --git a/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs b/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs index cb3f87b9a..524265baf 100644 --- a/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs +++ b/VisioAutomation_2010/VisioScripting/Helpers/InteropHelper.cs @@ -4,7 +4,7 @@ namespace VisioScripting.Helpers { - public static class InteropHelper + internal static class InteropHelper { private static bool _static_initialized = false; private static Dictionary _static_g_name_to_enum; diff --git a/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs b/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs index c39ea70e0..da80b048d 100644 --- a/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs +++ b/VisioAutomation_2010/VisioScripting/Helpers/SelectionHelper.cs @@ -5,7 +5,7 @@ namespace VisioScripting.Helpers { - public static class SelectionHelper + internal static class SelectionHelper { public static List GetSelectedShapes(IVisio.Selection selection) { diff --git a/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs b/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs index c89db3c5f..13f3227bd 100644 --- a/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs +++ b/VisioAutomation_2010/VisioScripting/Helpers/WildcardHelper.cs @@ -3,7 +3,7 @@ namespace VisioScripting.Helpers { - public static class WildcardHelper + internal static class WildcardHelper { public static bool NullOrStar(string s) { diff --git a/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs b/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs index 89744d433..62ae54bf5 100644 --- a/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs +++ b/VisioAutomation_2010/VisioScripting/Loaders/DirectedGraphDocumentLoader.cs @@ -8,7 +8,7 @@ namespace VisioScripting.Loaders { - public class DirectedGraphDocumentLoader + internal class DirectedGraphDocumentLoader { private class BuilderError { @@ -46,12 +46,6 @@ public static BuilderError InvalidToNode(int pagenum, string conid, string toid) } } - public static DirectedGraphDocument LoadFromXml(Client client, string filename) - { - var xmldoc = SXL.XDocument.Load(filename); - return DirectedGraphDocumentLoader.LoadFromXml(client, xmldoc); - } - private class PageData { public VisioAutomation.Models.ConnectorType ConnectorType; diff --git a/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs b/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs index 8ddbe54c2..84d311567 100644 --- a/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs +++ b/VisioAutomation_2010/VisioScripting/Loaders/OrgChartDocumentLoader.cs @@ -5,14 +5,8 @@ namespace VisioScripting.Loaders { - public class OrgChartDocumentLoader + internal class OrgChartDocumentLoader { - public static VAORGCHART.OrgChartDocument LoadFromXml(Client client, string filename) - { - var xdoc = SXL.XDocument.Load(filename); - return OrgChartDocumentLoader.LoadFromXml(client, xdoc); - } - public static VAORGCHART.OrgChartDocument LoadFromXml(Client client, SXL.XDocument xdoc) { var root = xdoc.Root; diff --git a/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs b/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs index c280ae42d..9e0683298 100644 --- a/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs +++ b/VisioAutomation_2010/VisioScripting/Models/PageDimensions.cs @@ -17,7 +17,7 @@ public class PageDimensions public double PrintBottomMargin; - public static List Get_PageDimensions(IList pages) + internal static List Get_PageDimensions(IList pages) { var list_pagedim = new List(pages.Count); diff --git a/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs b/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs index 3f4b9ca1c..477f2292f 100644 --- a/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs +++ b/VisioAutomation_2010/VisioScripting/Models/ShapeDimensions.cs @@ -22,7 +22,7 @@ public class ShapeDimensions public double OneDEndY; - public static List Get_ShapeDimensions(IVisio.Page page, List shapeids) + internal static List Get_ShapeDimensions(IVisio.Page page, List shapeids) { var query = new VASS.Query.CellQuery(); diff --git a/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs b/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs index 27b5a32e7..98adf08dc 100644 --- a/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs +++ b/VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs @@ -1,7 +1,13 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// VTest exercises a couple of VisioScripting helpers (e.g. WildcardHelper) +// directly. The helpers are not part of the hybrid public-API contract +// (see docs/decisions/visioscripting-public-api.md) and stay `internal`. +[assembly: InternalsVisibleTo("VTest")] + +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("VisioScripting")] diff --git a/docs/decisions/visioscripting-public-api.md b/docs/decisions/visioscripting-public-api.md new file mode 100644 index 000000000..fa919cd10 --- /dev/null +++ b/docs/decisions/visioscripting-public-api.md @@ -0,0 +1,82 @@ +# `VisioScripting.Client` is a hybrid public API + +**Status:** Accepted (codified 2026-05-09; see [#156](https://github.com/saveenr/VisioAutomation/issues/156) for the decision thread) + +## Context + +`VisioScripting` is the higher-level facade between the low-level `VisioAutomation` library and the `VisioPowerShell` cmdlets. The entry point is `VisioScripting.Client`, which exposes 25 group properties (`Application`, `Arrange`, `Connection`, `ConnectionPoint`, `Container`, `Control`, `CustomProperty`, `Developer`, `Document`, `Draw`, `Export`, `Grouping`, `Hyperlink`, `Layer`, `Lock`, `Master`, `Model`, `Output`, `Page`, `Selection`, `ShapeSheet`, `Text`, `Undo`, `UserDefinedCell`, `View`). Each property returns a `*Commands` instance whose public methods do the actual work. + +The repo's [`readme.md`](../../readme.md) opens its C# quick-start with `new VisioScripting.Client(app)`, so direct .NET consumers are not hypothetical — but the type was previously undocumented on the gitbook and treated as project-internal in practice. We had to choose: + +1. **Fully public.** Document the entire surface (every method on every `*Commands`, plus `Helpers/`, `Loaders/`, `CommandTarget`, etc.) and treat all of it as a public contract under SemVer. +2. **Fully internal.** Mark the namespace as not-for-direct-use, fix the readme quick-start to use `VisioAutomation` directly, close out [#131](https://github.com/saveenr/VisioAutomation/issues/131) won't-fix. +3. **Hybrid.** Public-stable for the `Client` facade itself; implementation detail for the plumbing. + +The pre-decision usage audit found that `Client..` accounts for ~76% of method usage across cmdlets and tests. Two cmdlets bypassed the facade (the `Loaders` reach-in and the `*Dimensions.Get_*` reach-in), but both were small and migrating them was straightforward. + +## Decision + +Hybrid (option 3). + +## The contract + +### Public-stable (documented; breaking changes treated as breaking) + +- `VisioScripting.Client` — the type, both constructors, and the 25 group properties. +- The public method signatures on each `*Commands` class. Adding methods is non-breaking; renaming or removing methods is breaking. +- `Target*` types at the namespace root (`TargetDocument`, `TargetDocuments`, `TargetPage`, `TargetPages`, `TargetSelection`, `TargetShapes`, `TargetWindow`). Forced — they appear in `*Commands` method signatures. +- `VisioScripting.Models.*` types referenced from public signatures (enums like `PageOrientation`, `ZoomToObject`, `ShapeSelectionOperation`; data carriers like `PageDimensions`, `ShapeDimensions`, `ShapeSheetReader`, `ShapeSheetWriter`). +- `ClientContext` and `DefaultClientContext`. The `Client(app, ClientContext)` overload exists so an embedding host (today `VisioPowerShell`'s [`VisioPsClientContext`](../../VisioAutomation_2010/VisioPowerShell/VisioPsClientContext.cs)) can plug in its own `Output` plumbing; subclassing has to remain a supported pattern. + +### Internal-mutable (not part of contract; free to change) + +- The `*Commands` classes as **constructible types**. The classes themselves are necessarily `public` (they appear as the return type of `Client.Page`, `Client.ShapeSheet`, etc.), but their constructors are `internal`. Consumers obtain instances via `Client.`, never `new`. +- `VisioScripting.Helpers/*` — `WildcardHelper`, `InteropHelper`, `SelectionHelper`, `ArrangeHelper`, `ReflectionHelper`, `TextHelper`. Pure utilities. +- `VisioScripting.Loaders/*` — `DirectedGraphDocumentLoader`, `OrgChartDocumentLoader`. Reached only through `Client.Model.LoadDirectedGraphFromXml(...)` / `Client.Model.LoadOrgChartFromXml(...)`. +- `CommandTarget`, `CommandTargetFlags`. Used inside `*Commands` method bodies to validate preconditions; never appear in a public signature. +- `Client.GetCommandTarget(flags)` — helper for `*Commands` implementations. +- `Models.*` types not appearing in any public signature (today: `DgShapeInfo`, `DgConnectorInfo`, both already `internal`). +- The static factory methods on data-carrier types: `Models.PageDimensions.Get_PageDimensions(...)`, `Models.ShapeDimensions.Get_ShapeDimensions(...)`. The data classes themselves stay `public` (return values); the factories are `internal`. + +## Enforcement + +Layered, weakest to strongest: + +1. **C# `internal` keyword.** The default mechanism. Every type that can be `internal` is `internal`. Same-assembly callers (the `*Commands` classes calling `Helpers/`, `*Dimensions.Get_*`, `CommandTarget`) keep working. +2. **`[InternalsVisibleTo("VTest")]`** on the `VisioScripting` assembly ([`Properties/AssemblyInfo.cs`](../../VisioAutomation_2010/VisioScripting/Properties/AssemblyInfo.cs)). `VTest` exercises `WildcardHelper.GetRegexForWildcardPattern` directly — the test is testing the helper itself, not its consumers. None of `VisioPowerShell`, `VTest.Models`, `VTest.Scripting`, or `VTest.PowerShell` need internal access today; they consume `VisioScripting` exclusively through public surface. If a future test or cmdlet needs to reach in, add an `InternalsVisibleTo` line for the specific project rather than blanket-granting access. +3. **`[EditorBrowsable(EditorBrowsableState.Never)]` + XML doc comments.** Held in reserve for any type that has to stay `public` for type-system reasons but isn't part of the contract. None today after the Phase B + C work, but the mechanism is documented here so future code knows to use it. + +The `Microsoft.CodeAnalysis.PublicApiAnalyzers` package (`PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`) was deferred. It's the strongest enforcement (a build error when the shipped surface drifts unintentionally), but it's another moving part. Revisit if drift becomes a real problem. + +## Consequences + +### Documentation + +- The doc-write under [#131](https://github.com/saveenr/VisioAutomation/issues/131) covers the public surface listed above and only that surface. The pre-decision estimate was ~152 method signatures across the 25 `*Commands` classes; after the Phase B facade additions in [#182](https://github.com/saveenr/VisioAutomation/issues/182) it's ~156. Plus `Client` itself, the 7 `Target*` types, `ClientContext`/`DefaultClientContext`, and the `Models.*` types referenced from public signatures. +- The ~36 dead methods identified in the audit (zero external callers as of 2026-05-09) are part of the locked surface; their removal is deferred to CY27 in [#183](https://github.com/saveenr/VisioAutomation/issues/183) per the project's "improve before audience-reducing changes" guiding principle. Per [#184](https://github.com/saveenr/VisioAutomation/issues/184), each dead method's gitbook page carries a "Remarks: candidate for CY27 removal (see [#183](https://github.com/saveenr/VisioAutomation/issues/183))" note. When CY27 deletion is approached, the codebase first gets `[Obsolete("Will be removed in version X")]` markers on those methods (one minor release before deletion); the gitbook page strengthens its remark accordingly. Final removal in CY27 takes both the method and its gitbook page out together. + +### Code review + +- Removing or renaming a public method on a `*Commands` class is a breaking change. Reviewers and PR authors need to recognize this. +- Adding methods is not breaking. Same for adding properties to `Client`. +- Touching anything in `Helpers/`, `Loaders/`, `CommandTarget`, `CommandTargetFlags`, or the `Get_*Dimensions` static methods is not a public-API change. + +### Embedding hosts + +- A future host besides `VisioPowerShell` (a VS extension, a different shell, an in-process consumer) can subclass `ClientContext` to plug in its own `Output` routing. The subclass-friendly contract on `ClientContext` is part of the public commitment. + +## Cross-references + +- [#156](https://github.com/saveenr/VisioAutomation/issues/156) — the decision thread, including the four-question Q1–Q4 walkthrough that produced this contract. +- [#182](https://github.com/saveenr/VisioAutomation/issues/182) — pre-lock cleanup (Phases B + C). Phase B added the four facade methods; Phase C applied the enforcement layer that this ADR records. +- [#183](https://github.com/saveenr/VisioAutomation/issues/183) — CY27 dead-method removal (Phase A). Cites this ADR. +- [#184](https://github.com/saveenr/VisioAutomation/issues/184) — the open dead-method-stance sub-question. +- [#131](https://github.com/saveenr/VisioAutomation/issues/131) — the doc-write for the public surface. +- [`CLAUDE.md`](../../CLAUDE.md) — per-commit conventions section carries the reviewer-facing summary of "what's a breaking change in `VisioScripting`." +- [`VisioAutomation_2010/VisioScripting/README.md`](../../VisioAutomation_2010/VisioScripting/README.md) — in-source overview of the facade structure. + +## Reconsider when + +- **A second embedding host appears that needs a different boundary.** Today's only host is `VisioPowerShell`, which lives in this repo. A VS extension or out-of-repo consumer could surface needs that the current line doesn't serve. +- **The dead-surface backlog ([#183](https://github.com/saveenr/VisioAutomation/issues/183)) ships and the line shifts.** Phase A in CY27 will likely shrink the public surface enough to revisit whether the hybrid mechanism is still the right one, or whether a tighter "fully public for what's left" stance becomes affordable. +- **Drift becomes a real problem.** If the contract erodes via inadvertent breaking changes that slip past code review, escalate to the `PublicAPI.Shipped.txt` analyzer (deferred above) or move the off-contract types to a `VisioScripting.Internal.*` sub-namespace as a more visible signal.