diff --git a/.editorconfig b/.editorconfig
index e138b6593..0a38ce15a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -745,31 +745,6 @@ dotnet_diagnostic.VSTHRD111.severity = none
[tools/opencode-tool.cs]
dotnet_diagnostic.MA0048.severity = none
-# GraphKeyBuilder centralizes pointer and branch-identity policy shared by projection
-# collaborators. Winner: one sealed policy owner instead of duplicated static mechanics;
-# static-member rules remain enabled for every other file.
-[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs]
-dotnet_diagnostic.CA1822.severity = none
-dotnet_diagnostic.S2325.severity = none
-
-# ErrorStyleClassifier is a named policy collaborator constructed by object projection;
-# making its only operation static would erase that ownership boundary. Winner: the sealed
-# classifier contract; static-member rules remain enabled for every other file.
-[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs]
-dotnet_diagnostic.CA1822.severity = none
-dotnet_diagnostic.S2325.severity = none
-
-# EnumSchemaProjector and LiteralClassifier are named schema-policy collaborators composed by
-# SchemaProjector; static operations would erase that ownership. Winner: the sealed collaborator
-# contracts; static-member rules remain enabled for every other file.
-[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs]
-dotnet_diagnostic.CA1822.severity = none
-dotnet_diagnostic.S2325.severity = none
-
-[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs]
-dotnet_diagnostic.CA1822.severity = none
-dotnet_diagnostic.S2325.severity = none
-
# The abstract adapter base carries shared, non-virtual mapping behavior for the
# generated response adapters, and default interface members cannot carry shared
# behavior on the net472/netstandard2.0 targets (CS8701). Winner: the
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs
index 978e481dc..a3371dbd4 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs
@@ -32,9 +32,8 @@ public async Task ToPascalCase_Should_Keep_A_Brand_Spelling(string wireName, str
}
///
- /// A whole name that opens with a digit needs a letter prefix; a fragment appended to a
- /// stem does not, and taking it there would spell an interior underscore the repository's
- /// format pass rewrites out of the declaration without renaming the file it was written to.
+ /// A whole name that opens with a digit needs the Value prefix; a fragment appended to a
+ /// non-empty stem does not.
///
[Test]
[Arguments("0", "Value0", "0")]
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs
index 99586354a..e04e9ca5b 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs
@@ -157,11 +157,10 @@ private static async Task>> Co
var document = await BindingTestHost.IngestAsync(ReachScenario());
var operationsById = document.Operations.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal);
IReadOnlyList selected = [.. operationIds.Select(operationId => operationsById[operationId])];
- var reachableSchemas = new ReachableSchemaCollector();
- var reachable = reachableSchemas.Collect(document, selected, errors);
- var typeNames = new SchemaNameResolver().Resolve(document, reachable, selected, Curation(Groups()), errors);
+ var reachable = ReachableSchemaCollector.Collect(document, selected, errors);
+ var typeNames = SchemaNameResolver.Resolve(document, reachable, selected, Curation(Groups()), errors);
- var reach = ModelReachCollector.Collect(reachableSchemas, new ReachScope(document, selected, Clients(selected), typeNames),
+ var reach = ModelReachCollector.Collect(new ReachScope(document, selected, Clients(selected), typeNames),
candidates, errors);
errors.ThrowIfAny();
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs
index d6acbabe5..4ef0345d3 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs
@@ -106,7 +106,7 @@ private static IReadOnlyList NameCollisions(OwnershipCandidates ca
{
var inventory = GeneratedTypeNameInventory.Create(candidates, clients, [], schemaNames ?? new Dictionary(StringComparer.Ordinal));
var errors = new BindingErrorCollector();
- new OperationPlanBinder().CheckTypeNameCollisions(inventory, clients, errors);
+ OperationPlanBinder.CheckTypeNameCollisions(inventory, clients, errors);
return errors.Snapshot();
}
@@ -135,7 +135,7 @@ public async Task Bind_Should_Preserve_Type_Name_Claim_Order()
HoistedInterfaces = []
};
var errors = new BindingErrorCollector();
- new OperationPlanBinder().CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(candidates, clients, [], new Dictionary(StringComparer.Ordinal)), clients, errors);
+ OperationPlanBinder.CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(candidates, clients, [], new Dictionary(StringComparer.Ordinal)), clients, errors);
await Assert.That(errors.Snapshot()).IsEquivalentTo([
new BindingError(BindingErrorCategory.Naming, "WidgetsClient", "client type name 'WidgetsClient' collides with another generated type"),
new BindingError(BindingErrorCategory.Naming, "widget.list", "multiple operations map to response type 'WidgetListResponse'"),
@@ -447,17 +447,17 @@ public async Task Bind_Should_Refuse_Invalid_Envelope_Payload_Rows(string variat
{
if (variation == "invalid-binder")
{
- var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors);
- var schemas = new SchemaPlanBinder(new StructuralUnionPlanBinder(), new UnionMembershipValidator(),
- new UnionDiscriminatorSelector(), new UnionMemberHoistBinder()).Bind(document, reachable, curation,
- new Dictionary(StringComparer.Ordinal), errors);
- _ = new OperationPlanBinder().Bind(document, document.Operations, curation, schemas, errors);
+ var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors);
+ var names = new Dictionary(StringComparer.Ordinal);
+ var context = SchemaBindingContext.Create(document, reachable, names, errors);
+ var schemas = new SchemaPlanBinder(context).Bind(curation);
+ _ = OperationPlanBinder.Bind(document, document.Operations, curation, schemas, errors);
errors.ThrowIfAny();
}
else if (variation == "invalid")
{
- new CurationValidator().Validate(document, document.Operations,
- new ReachableSchemaCollector().Collect(document, document.Operations, errors), curation, errors);
+ CurationValidator.Validate(document, document.Operations,
+ ReachableSchemaCollector.Collect(document, document.Operations, errors), curation, errors);
errors.ThrowIfAny();
}
else
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs
index 8af284291..d9021785e 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs
@@ -69,7 +69,7 @@ public async Task Probe_Should_Mark_A_Walled_Operation_Refused_With_The_Walls_Ve
await Assert.That(marks.Count).IsEqualTo(1);
await Assert.That(marks[0].OperationId).IsEqualTo("widget.connect");
await Assert.That(marks[0].IsBindable).IsFalse();
- await Assert.That(marks[0].RefusalMessage).IsEqualTo("WebSocket operations are not supported in M1");
+ await Assert.That(marks[0].RefusalMessage).IsEqualTo("WebSocket operations are not supported");
}
[Test]
@@ -83,7 +83,7 @@ public async Task Probe_Should_List_Every_Independent_Wall_In_Binder_Order_When_
await Assert.That(marks.Count).IsEqualTo(1);
await Assert.That(marks[0].IsBindable).IsFalse();
await Assert.That(marks[0].RefusalMessage).IsEqualTo(
- "WebSocket operations are not supported in M1; PUT operations must carry a request body");
+ "WebSocket operations are not supported; PUT operations must carry a request body");
}
[Test]
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs
index 2183a46fb..d0536d802 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs
@@ -1,5 +1,6 @@
using OpenCode.Sdk.Tools.Generator.Binding;
using OpenCode.Sdk.Tools.Generator.Binding.Models;
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
using OpenCode.Sdk.Tools.Tests.Support;
using TUnit.Assertions.Enums;
using static OpenCode.Sdk.Tools.Tests.Support.BindingScenarioData;
@@ -38,10 +39,16 @@ public async Task Bind_Should_Refuse_The_Whole_Facet_When_One_Parameter_Is_Uncla
}
[Test]
- public async Task Bind_Should_Refuse_An_Enum_Without_A_Generated_Model()
+ [Arguments(false)]
+ [Arguments(true)]
+ public async Task Bind_Should_Refuse_An_Enum_Without_A_Generated_Model(bool referencedNullable)
{
- var context = await CreateContextAsync(static operation => operation.Parameter("state", "query",
- static schema => schema.Type("string").Enum("open", "closed")));
+ var context = referencedNullable
+ ? await CreateContextAsync(static operation => operation.Parameter("state", "query", static schema => schema.Ref("State")),
+ static spec => spec.WithSchema("State", static schema => schema.AnyOf(
+ static branch => branch.Type("string").Enum("open", "closed"), static branch => branch.Type("null"))))
+ : await CreateContextAsync(static operation => operation.Parameter("state", "query",
+ static schema => schema.Type("string").Enum("open", "closed")));
var result = new QueryRequestFacetBinder(context).Bind();
await Assert.That(result).IsTypeOf.Refused>();
await Assert.That(((FacetResult.Refused)result).Errors).IsEquivalentTo([
@@ -49,6 +56,30 @@ await Assert.That(((FacetResult.Refused)result).Errors).IsEqu
], CollectionOrdering.Matching);
}
+ [Test]
+ [Arguments(false)]
+ [Arguments(true)]
+ public async Task Bind_Should_Keep_A_Referenced_Nullable_Enum_Refused(bool fullPipeline)
+ {
+ var document = await BindingTestHost.IngestAsync(SpecScenario.Define(static spec => spec
+ .WithSchema("State", static schema => schema.AnyOf(
+ static branch => branch.Type("string").Enum("open", "closed"), static branch => branch.Type("null")))
+ .WithOperation("widget.list", configure: static operation => operation
+ .WithoutResponse(200).Response(204)
+ .Parameter("state", "query", static schema => schema.Ref("State")))));
+ if (!fullPipeline)
+ {
+ await Assert.That(QueryEnumShapePolicy.ResolveModelKey(new RefNode { Target = "State" }, document.Schemas)).IsNull();
+ return;
+ }
+
+ var exception = Assert.Throws(() => new BindingTestHost().Bind(document,
+ Selection("widget.list"), Curation(Groups("widget", RootGroup()))));
+ await Assert.That(exception.Errors).IsEquivalentTo([
+ new BindingError(BindingErrorCategory.Operation, "widget.list", "query parameter 'state' binds an enum that has no generated model"),
+ ], CollectionOrdering.Matching);
+ }
+
[Test]
public async Task Bind_Should_Distinguish_An_Absent_Query_From_A_Refused_Query()
{
@@ -63,9 +94,14 @@ await Assert.That(((FacetResult.Refused)refused).Errors).IsEq
], CollectionOrdering.Matching);
}
- private static async Task CreateContextAsync(Action configure)
+ private static async Task CreateContextAsync(Action configure,
+ Action? configureSchemas = null)
{
- var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec.WithOperation("widget.list", configure: configure)));
+ var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec =>
+ {
+ configureSchemas?.Invoke(spec);
+ spec.WithOperation("widget.list", configure: configure);
+ }));
var errors = new BindingErrorCollector();
var names = new Dictionary(StringComparer.Ordinal);
return new OperationFacetContext(document, document.Operations.Single(), Curation(Groups("widget", RootGroup())), names, errors,
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/RefChainTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/RefChainTests.cs
new file mode 100644
index 000000000..a58eaaae5
--- /dev/null
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/RefChainTests.cs
@@ -0,0 +1,49 @@
+using OpenCode.Sdk.Tools.Generator.Binding;
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
+
+namespace OpenCode.Sdk.Tools.Tests.Generator.Binding;
+
+public sealed class RefChainTests
+{
+ [Test]
+ [Arguments("terminal", "B")]
+ [Arguments("missing", "A")]
+ [Arguments("self-cycle", "A")]
+ [Arguments("two-node-cycle", "B")]
+ [Arguments("nullable", "B")]
+ public async Task Resolve_Should_Return_The_Last_Node_And_Last_Successful_Key(string shape, string lastKey)
+ {
+ var lookups = 0;
+ // A broken cycle guard fails deterministically, rather than hanging the test host.
+ var comparer = EqualityComparer.Create((left, right) => ++lookups > 32
+ ? throw new InvalidOperationException("Reference traversal revisited the cycle")
+ : StringComparer.Ordinal.Equals(left, right), StringComparer.Ordinal.GetHashCode);
+ var terminal = new EnumNode { Values = ["open", "closed"] };
+ SchemaNode expected = shape switch
+ {
+ "missing" => new RefNode { Target = "absent" },
+ "self-cycle" or "two-node-cycle" => new RefNode { Target = "A" },
+ "nullable" => new NullableNode { Inner = terminal },
+ _ => terminal,
+ };
+ var graph = new Dictionary(comparer)
+ {
+ ["A"] = shape is "missing" or "self-cycle" ? expected : new RefNode { Target = "B" },
+ ["B"] = expected,
+ };
+
+ var result = RefChain.Resolve(new RefNode { Target = "A" }, graph);
+
+ await Assert.That(result.Node).IsSameReferenceAs(expected);
+ await Assert.That(result.LastKey).IsEqualTo(lastKey);
+ }
+
+ [Test]
+ public async Task Resolve_Should_Not_Unwrap_A_Nullable_Node()
+ {
+ var node = new NullableNode { Inner = new RefNode { Target = "A" } };
+ var result = RefChain.Resolve(node, new Dictionary(StringComparer.Ordinal));
+ await Assert.That(result.Node).IsSameReferenceAs(node);
+ await Assert.That(result.LastKey).IsNull();
+ }
+}
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs
index f652854c2..8da9b9080 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs
@@ -11,6 +11,31 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Binding;
public sealed class SpecBinderTests
{
+ [Test]
+ [Arguments(false, "JSON-encoded strings are not supported by the emitter")]
+ [Arguments(true, "tuple schemas are not supported by the emitter")]
+ public async Task Bind_Should_Refuse_Unsupported_Type_Shapes_Without_Milestone_Text(bool tuple, string problem)
+ {
+ var document = await IngestAsync(SpecScenario.Define(spec => spec
+ .WithObject("ItemInfo", schema => schema.Property("value", value =>
+ {
+ if (tuple)
+ {
+ value.Type("array").PrefixItems(static item => item.Type("string"), static item => item.Type("integer"))
+ .MinItems(2).MaxItems(2);
+ }
+ else
+ {
+ value.Type("string").ContentSchema("application/json", static inner => inner.Type("string"));
+ }
+ }, required: true))
+ .WithJsonOperation("health.get", "/health", static schema => schema.Ref("ItemInfo"))));
+ var exception = RefuseHealth(document);
+ await Assert.That(exception.Errors).IsEquivalentTo([
+ new BindingError(BindingErrorCategory.Schema, "ItemInfo.value", problem),
+ ], CollectionOrdering.Matching);
+ }
+
[Test]
public async Task Bind_Should_Preserve_The_Marker_Refusal_For_An_Unnamed_Error_Schema()
{
@@ -180,11 +205,9 @@ public async Task Bind_Should_Retain_Per_Schema_Error_Variants_And_Refusals()
schemas["CBadMarker"] = (ObjectNode)schemas["CBadMarker"] with { LiteralMarkers = [] };
document = document with { Schemas = schemas };
var errors = new BindingErrorCollector();
- var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors);
+ var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors);
var names = new Dictionary(StringComparer.Ordinal) { ["AAdmitted"] = "AdmittedError", ["CBadMarker"] = "BadMarkerError" };
- var result = new ErrorUnionBinder(new SchemaBindingContext(document, reachable,
- reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal), reachable.StreamCauseKeys.ToHashSet(StringComparer.Ordinal), names,
- new TypePlanBinder(schemas, names, errors), new SchemaInhabitationPolicy(schemas), errors)).Bind();
+ var result = new ErrorUnionBinder(SchemaBindingContext.Create(document, reachable, names, errors)).Bind();
var expectedRefusals = new Dictionary(StringComparer.Ordinal)
{
["BUnnamed"] = new(BindingErrorCategory.Naming, "BUnnamed", "error schema has no unique C# type name"),
@@ -266,7 +289,7 @@ public async Task Bind_Should_Refuse_Invalid_Group_Configuration(string fault, o
experimentalDiagnostic: fault == "shared-diagnostic" ? "OPENCODE001" : null);
}
var errors = new BindingErrorCollector();
- new CurationValidator().Validate(document, selected, new ReachableSchemaCollector().Collect(document, selected, errors), Curation(groups), errors);
+ CurationValidator.Validate(document, selected, ReachableSchemaCollector.Collect(document, selected, errors), Curation(groups), errors);
var identity = expected[0];
await Assert.That(Assert.Throws(errors.ThrowIfAny).Errors
.Where(error => error.Category == identity.Category && error.Subject == identity.Subject))
@@ -302,7 +325,7 @@ public async Task Bind_Should_Refuse_Invalid_Transport_Owned_Rows(string fault,
var errors = new BindingErrorCollector();
var selected = document.Operations.Where(operation => operation.OperationId == "health.get" || fault == "selected").ToArray();
var declared = fault switch { "malformed" => "bad", "fingerprint" => new string('0', 64), _ => hash };
- new CurationValidator().Validate(document, selected, new ReachableSchemaCollector().Collect(document, selected, errors),
+ CurationValidator.Validate(document, selected, ReachableSchemaCollector.Collect(document, selected, errors),
Curation(Groups("health", RootGroup()), transportOwned: [TransportOwned(subject, declared)]), errors);
var expected = new BindingError(BindingErrorCategory.Curation, subject, problem);
var exception = Assert.Throws(errors.ThrowIfAny);
@@ -1847,8 +1870,6 @@ public async Task Bind_Should_Refuse_A_Tag_Owned_By_Two_Closure_Schemas(bool res
Curation(Groups("health", RootGroup()), schemaNames: responseCollision ? [SchemaName("ItemInfo", "HealthResponse")] : [])));
BindingError[] expected = [
new(BindingErrorCategory.Schema, "OpenCodeError", "multiple error schemas declare tag 'SharedError' (GoneError, LostError)"),
- new(BindingErrorCategory.Schema, "GoneError", "union membership references absent union 'IOpenCodeError'"),
- new(BindingErrorCategory.Schema, "LostError", "union membership references absent union 'IOpenCodeError'"),
];
if (responseCollision)
{
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs
index d36a258a7..492a24bf3 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs
@@ -36,8 +36,8 @@ public async Task Resolve_Should_Refuse_Invalid_Duplicate_Curation(bool identica
}
}));
var errors = new BindingErrorCollector();
- var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors);
- _ = new StabilizeDuplicatePolicy().Resolve(document, reachable, identical ? [SchemaName("Widget_1", "ReviewedWidget")] : [], errors);
+ var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors);
+ _ = StabilizeDuplicatePolicy.Resolve(document, reachable, identical ? [SchemaName("Widget_1", "ReviewedWidget")] : [], errors);
await Assert.That(Assert.Throws(errors.ThrowIfAny).Errors.Single()).IsEqualTo(new BindingError(category, "Widget_1", problem));
}
@@ -270,9 +270,9 @@ public async Task Resolve_Should_Fold_Equivalent_Pinned_Duplicates_And_Retain_Th
var operations = document
.Operations.Where(operation => selection.OperationIds.Contains(operation.OperationId, StringComparer.Ordinal))
.ToArray();
- var reachable = new ReachableSchemaCollector().Collect(document, operations, errors);
+ var reachable = ReachableSchemaCollector.Collect(document, operations, errors);
- var collapse = new StabilizeDuplicatePolicy().Resolve(document, reachable, curation.SchemaNames, errors);
+ var collapse = StabilizeDuplicatePolicy.Resolve(document, reachable, curation.SchemaNames, errors);
await Assert.That(errors.Count).IsEqualTo(0);
await Assert.That(collapse.Aliases.Count).IsGreaterThan(0);
@@ -320,8 +320,8 @@ private static IReadOnlyList ReachableStabilizeDuplicates(SpecDocument d
private static async Task ResolveAsync(SpecScenario scenario, BindingErrorCollector errors)
{
var document = await BindingTestHost.IngestAsync(scenario);
- var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors);
- return new StabilizeDuplicatePolicy().Resolve(document, reachable, [], errors);
+ var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors);
+ return StabilizeDuplicatePolicy.Resolve(document, reachable, [], errors);
}
private static IReadOnlyList Folds(StabilizeDuplicateCollapse collapse) =>
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs
index 4ee2dd444..c4bb41dac 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs
@@ -447,7 +447,7 @@ private static UnionHoistPlan Hoist(IReadOnlyList unions, IReadOnlyLi
GenerationCuration? curation = null)
{
var errors = new BindingErrorCollector();
- var plan = new UnionMemberHoistBinder().Bind(unions, models, curation ?? Curation(Groups()), errors);
+ var plan = UnionMemberHoistBinder.Bind(unions, models, curation ?? Curation(Groups()), errors);
CheckNames(plan, errors);
errors.ThrowIfAny();
return plan;
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs
index ef30e9cb2..e66301670 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs
@@ -19,7 +19,7 @@ public async Task Validate_Should_Refuse_The_Same_Marker_Name_With_Different_Kin
var unions = new[] { Union("IBooleanEvent", LiteralKind.Boolean), Union("IStringEvent", LiteralKind.String), };
var errors = new BindingErrorCollector();
- new UnionMembershipValidator().Validate([model], unions, errors);
+ UnionMembershipValidator.Validate([model], unions, errors);
var exception = Assert.Throws(errors.ThrowIfAny);
await Assert.That(exception.Errors.Single().Category).IsEqualTo(BindingErrorCategory.Schema);
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs
index 5c8de5aa3..1d757ec9c 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs
@@ -136,8 +136,8 @@ .. plan.Clients
/// A plain string or a number once references and a nullable wrapper are followed, as the binder classifies it.
private static bool IsScalarText(SpecDocument document, SchemaNode schema)
{
- var resolved = OperationFacetContext.Resolve(document, schema);
- var value = resolved is NullableNode { Format: null } nullable ? OperationFacetContext.Resolve(document, nullable.Inner) : resolved;
+ var resolved = RefChain.Resolve(schema, document.Schemas).Node;
+ var value = resolved is NullableNode { Format: null } nullable ? RefChain.Resolve(nullable.Inner, document.Schemas).Node : resolved;
return value is PrimitiveNode { Kind: PrimitiveKind.String, Format: null } or PrimitiveNode { Kind: PrimitiveKind.Integer or PrimitiveKind.Number };
}
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs
index 1c43b61c5..53196f1f7 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs
@@ -1038,7 +1038,7 @@ public static EmitPlan CreateHoistPlan()
};
var unions = new[] { CreateHoistEvent(), CreateHoistPhase(), };
var errors = new BindingErrorCollector();
- var hoisted = new UnionMemberHoistBinder().Bind(unions, models, HoistCuration(), errors);
+ var hoisted = UnionMemberHoistBinder.Bind(unions, models, HoistCuration(), errors);
errors.ThrowIfAny();
return new EmitPlan
{
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs
index 0b892d2fd..d11d74a78 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs
@@ -8,9 +8,7 @@ public sealed class GraphKeyBuilderTests
[Test]
public async Task Append_Should_Escape_Slash_In_Segment()
{
- var keys = new GraphKeyBuilder();
-
- var pointer = keys.Append(string.Empty, "a/b");
+ var pointer = GraphKeyBuilder.Append(string.Empty, "a/b");
await Assert.That(pointer).IsEqualTo("/a~1b");
}
@@ -18,9 +16,7 @@ public async Task Append_Should_Escape_Slash_In_Segment()
[Test]
public async Task Append_Should_Escape_Tilde_In_Segment()
{
- var keys = new GraphKeyBuilder();
-
- var pointer = keys.Append(string.Empty, "a~b");
+ var pointer = GraphKeyBuilder.Append(string.Empty, "a~b");
await Assert.That(pointer).IsEqualTo("/a~0b");
}
@@ -28,7 +24,6 @@ public async Task Append_Should_Escape_Tilde_In_Segment()
[Test]
public async Task UnionBranch_Should_Use_And_Escape_Literal_Marker_Identity()
{
- var keys = new GraphKeyBuilder();
var marker = new LiteralMarker
{
PropertyName = "type/name",
@@ -36,7 +31,7 @@ public async Task UnionBranch_Should_Use_And_Escape_Literal_Marker_Identity()
Value = "created~new",
};
- var pointer = keys.UnionBranch(string.Empty, "anyOf", 0, marker);
+ var pointer = GraphKeyBuilder.UnionBranch(string.Empty, "anyOf", 0, marker);
await Assert.That(pointer).IsEqualTo("/anyOf/type~1name=created~0new");
}
@@ -44,9 +39,7 @@ public async Task UnionBranch_Should_Use_And_Escape_Literal_Marker_Identity()
[Test]
public async Task UnionBranch_Should_Use_Ordinal_For_Unmarked_Branch()
{
- var keys = new GraphKeyBuilder();
-
- var pointer = keys.UnionBranch(string.Empty, "oneOf", 3, marker: null);
+ var pointer = GraphKeyBuilder.UnionBranch(string.Empty, "oneOf", 3, marker: null);
await Assert.That(pointer).IsEqualTo("/oneOf/3");
}
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs
index c9233e6f7..d4e6f499c 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs
@@ -118,7 +118,7 @@ public async Task FindFirstPrefixMarker_Should_Return_The_First_Required_Prefix_
.Property("later", property => property.Type("string").Pattern("^x\\.[\\s\\S]*?$"))));
var schema = await SchemaProjectionTestHost.LoadSchemaAsync(scenario, "Event");
- var marker = new LiteralClassifier().FindFirstPrefixMarker(schema);
+ var marker = LiteralClassifier.FindFirstPrefixMarker(schema);
await Assert.That(marker).IsNotNull();
await Assert.That(marker.PropertyName).IsEqualTo("type");
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs
index b2bbb6b6d..c6a43c9b7 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs
@@ -314,8 +314,7 @@ public async Task Project_Should_Refuse_When_Promoted_Graph_Key_Collides()
var context = scenario.Build();
var errors = new IngestionErrorCollector();
var loaded = await new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None);
- var keys = new GraphKeyBuilder();
- var projector = new SchemaProjector(keys);
+ var projector = new SchemaProjector();
var state = new ProjectionState(errors, loaded.Document);
_ = projector.Project(loaded.Document.Components!.Schemas!["First"], "Shared", "/value", state);
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs
index 59bb0f27b..5f4de63c3 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs
@@ -7,7 +7,7 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Ingestion.Support;
internal sealed class OperationProjectionTestHost
{
- private readonly GraphKeyBuilder _keys = new();
+ private readonly SchemaProjector _schemas = new();
public async Task ProjectAsync(SpecScenario scenario,
IReadOnlyDictionary? operationIdentities = null)
@@ -34,17 +34,16 @@ private async Task ProjectCoreAsync(SpecScenario scen
var errors = new IngestionErrorCollector();
try
{
- var loaded = await new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None);
- var schemaProjector = new SchemaProjector(_keys);
+ var loaded = await SchemaProjectionTestHost.LoadAsync(context, errors);
var state = new ProjectionState(errors, loaded.Document);
- var operations = new OperationProjector(schemaProjector, _keys)
+ var operations = new OperationProjector(_schemas)
.Project(loaded, state, operationIdentities ?? new Dictionary(StringComparer.Ordinal));
if (loaded.Document.Components?.Schemas is not null)
{
foreach (var (name, schema) in loaded.Document.Components.Schemas)
{
- _ = schemaProjector.Project(schema, _keys.Root(name), string.Empty, state);
+ _ = _schemas.Project(schema, GraphKeyBuilder.Root(name), string.Empty, state);
}
}
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs
index fef0edae5..e7875effb 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs
@@ -8,7 +8,7 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Ingestion.Support;
internal sealed class SchemaProjectionTestHost
{
- private readonly GraphKeyBuilder _keys = new();
+ private readonly SchemaProjector _projector = new();
public async Task ProjectAsync(SpecScenario scenario)
{
@@ -48,15 +48,14 @@ private async Task ProjectCoreAsync(SpecScenario scenari
var errors = new IngestionErrorCollector();
try
{
- var loaded = await new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None);
- var projector = new SchemaProjector(_keys);
+ var loaded = await LoadAsync(context, errors);
var state = new ProjectionState(errors, loaded.Document);
if (loaded.Document.Components?.Schemas is not null)
{
foreach (var (name, schema) in loaded.Document.Components.Schemas)
{
- _ = projector.Project(schema, _keys.Root(name), string.Empty, state);
+ _ = _projector.Project(schema, GraphKeyBuilder.Root(name), string.Empty, state);
}
}
@@ -68,4 +67,7 @@ private async Task ProjectCoreAsync(SpecScenario scenari
return new SchemaProjectionResult(new Dictionary(StringComparer.Ordinal), exception);
}
}
+
+ internal static Task LoadAsync(ScenarioContext context, IngestionErrorCollector errors) =>
+ new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None);
}
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt
index 48a78ce4c..b0aab1faf 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt
+++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt
@@ -2,7 +2,7 @@
A change that adds, removes or alters a suppression updates this file in the same commit.
build-switch: 12
-editorconfig: 158
+editorconfig: 150
globalconfig: 1
no-warn: 18
pragma: 26
@@ -170,14 +170,6 @@ editorconfig .editorconfig [tests/OpenCode.Sdk.Tests/Support/ServerDiagnosticSce
editorconfig .editorconfig [tests/OpenCode.Sdk.Tests/Support/SessionEventProbe.cs] dotnet_diagnostic.VSTHRD003.severity = none
editorconfig .editorconfig [tests/OpenCode.Sdk.Tests/Support/SessionEventProbeTests.cs] dotnet_diagnostic.VSTHRD003.severity = none
editorconfig .editorconfig [tests/Shared/ProcessMark.cs] dotnet_diagnostic.MA0045.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs] dotnet_diagnostic.CA1822.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs] dotnet_diagnostic.S2325.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs] dotnet_diagnostic.CA1822.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs] dotnet_diagnostic.S2325.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs] dotnet_diagnostic.CA1822.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs] dotnet_diagnostic.S2325.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs] dotnet_diagnostic.CA1822.severity = none
-editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs] dotnet_diagnostic.S2325.severity = none
editorconfig .editorconfig [tools/opencode-tool.cs] dotnet_diagnostic.MA0048.severity = none
globalconfig .globalconfig dotnet_diagnostic.VSTHRD010.severity = none
no-warn src/OpenCode.Sdk.Extensions/OpenCode.Sdk.Extensions.csproj OPENCODE001
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs
index f0efebf39..df2bf7f7c 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs
@@ -9,21 +9,7 @@ namespace OpenCode.Sdk.Tools.Tests.Support;
internal sealed class BindingTestHost
{
- private readonly SpecBinder _binder;
-
- public BindingTestHost()
- {
- _binder = new SpecBinder(
- new ReachableSchemaCollector(),
- new StabilizeDuplicatePolicy(),
- new CurationValidator(),
- new SchemaAliasValidator(),
- new SchemaAliasApplier(),
- new SchemaNameResolver(),
- new SchemaPlanBinder(new StructuralUnionPlanBinder(), new UnionMembershipValidator(), new UnionDiscriminatorSelector(),
- new UnionMemberHoistBinder()),
- new OperationPlanBinder());
- }
+ private readonly SpecBinder _binder = new();
/// The production binder pipeline, exposed for callers that drive it themselves (e.g. the pending-operation probe).
public ISpecBinder Binder => _binder;
diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs
index 51780a37b..a86603288 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs
@@ -15,7 +15,7 @@ public static UnionHoistPlan HoistDurableCarrier(GenerationCuration curation)
{
var errors = new BindingErrorCollector();
var arms = new[] { (Name: "CreatedEvent", Tag: "created", Version: "1"), (Name: "DeletedEvent", Tag: "deleted", Version: "2") };
- var result = new UnionMemberHoistBinder().Bind(
+ var result = UnionMemberHoistBinder.Bind(
[Union("IExampleEvent", [.. arms.Select(static arm => Arm(arm.Name))])],
[
.. arms.Select(static arm => Variant(arm.Name, arm.Tag, Property("durable", Named(arm.Name + "Durable"), isRequired: true))),
@@ -44,7 +44,7 @@ public static UnionPlan Union(string name, params UnionVariantPlan[] variants)
}
public static void CheckNames(UnionHoistPlan plan, BindingErrorCollector errors) =>
- new OperationPlanBinder().CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(new OwnershipCandidates
+ OperationPlanBinder.CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(new OwnershipCandidates
{
Models = plan.Models,
Unions = plan.Unions,
@@ -145,7 +145,7 @@ public static EmitPlan HoistedEmitPlan(IReadOnlyList unions, IReadOnl
ArgumentNullException.ThrowIfNull(curation);
var errors = new BindingErrorCollector();
- var hoisted = new UnionMemberHoistBinder().Bind(unions, models, curation, errors);
+ var hoisted = UnionMemberHoistBinder.Bind(unions, models, curation, errors);
CheckNames(hoisted, errors);
errors.ThrowIfAny();
return new EmitPlan
diff --git a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs
index 7c7f23dc0..242fcb4ae 100644
--- a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs
+++ b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs
@@ -213,7 +213,7 @@ public async Task RunAsync_Should_Mark_Pending_Operations_With_Their_Bindability
await Assert.That(marker).Contains("- plugin.list [bindable]");
await Assert.That(marker).Contains("- session.list [refused: the success response must carry a JSON schema]");
await Assert.That(marker).Contains(
- "- widget.tail [refused: WebSocket operations are not supported in M1; PUT operations must carry a request body]");
+ "- widget.tail [refused: WebSocket operations are not supported; PUT operations must carry a request body]");
}
[Test]
@@ -246,7 +246,7 @@ public async Task RunAsync_Should_List_Declined_Operations_With_Their_Reason_And
await Assert.That(marker).Contains(
"Declined:\n- widget.tail [declined: The operation is WebSocket-marked and a bodyless PUT, "
+ "so it does not bind; maintainer 2026-08-30.] "
- + "[refused: WebSocket operations are not supported in M1; PUT operations must carry a request body]\n");
+ + "[refused: WebSocket operations are not supported; PUT operations must carry a request body]\n");
await Assert.That(marker).DoesNotContain("Pending:\n- widget.tail");
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs
index 7ef2103c9..f2df40f73 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs
@@ -30,12 +30,8 @@ public static string ToPascalCase(string wireName)
///
/// Pascal-cases a fragment that is appended to a non-empty stem — a JSON pointer segment, and
- /// above all a promoted union branch's ordinal. A leading digit needs no identifier guard in
- /// that position, and applying 's guard would instead place an
- /// interior underscore inside the finished name. That spelling does not survive: the
- /// post-generation format pass rewrites it (IDE1006, and Sonar S101 reports it with no fix),
- /// which renames the declared type without renaming the file the emitter wrote it to, so the
- /// generated tree fails MA0048 rather than merely looking unusual.
+ /// above all a promoted union branch's ordinal. A leading digit in that position needs no
+ /// Value prefix, unlike the whole identifier that produces.
///
public static string ToPascalCaseFragment(string wireName) => PascalWords(wireName);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs
index 4d7c9d0c8..69a9e714f 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs
@@ -3,11 +3,9 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class CurationValidator
+internal static class CurationValidator
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public void Validate(SpecDocument document, IReadOnlyList selected, ReachableSchemaSet reachable,
+ public static void Validate(SpecDocument document, IReadOnlyList selected, ReachableSchemaSet reachable,
GenerationCuration curation, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(document);
@@ -16,10 +14,10 @@ public void Validate(SpecDocument document, IReadOnlyList selecte
ArgumentNullException.ThrowIfNull(curation);
ArgumentNullException.ThrowIfNull(errors);
- var selectedIds = selected.Select(static operation => operation.OperationId).ToHashSet(_comparer);
- var selectedGroups = selected.Select(GetGroup).ToHashSet(_comparer);
- var documentIds = document.Operations.Select(static operation => operation.OperationId).ToHashSet(_comparer);
- var documentGroups = document.Operations.Select(GetGroup).ToHashSet(_comparer);
+ var selectedIds = selected.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal);
+ var selectedGroups = selected.Select(GetGroup).ToHashSet(StringComparer.Ordinal);
+ var documentIds = document.Operations.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal);
+ var documentGroups = document.Operations.Select(GetGroup).ToHashSet(StringComparer.Ordinal);
ValidateGroups(selected, selectedGroups, documentGroups, curation, errors);
ValidateOperationNames(selected, documentIds, curation, errors);
ValidateParameterNames(selected, curation, errors);
@@ -169,10 +167,10 @@ private static void ValidateOperationNames(IReadOnlyList selected
/// working object, so every operation of a handle family that carries it must name it the same
/// way, across every group the family merges.
///
- private void ValidateParameterNames(IReadOnlyList selected, GenerationCuration curation,
+ private static void ValidateParameterNames(IReadOnlyList selected, GenerationCuration curation,
BindingErrorCollector errors)
{
- var operations = selected.ToDictionary(static operation => operation.OperationId, _comparer);
+ var operations = selected.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal);
foreach (var row in curation.ParameterNames)
{
var subject = row.OperationId + ":" + row.Parameter;
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/EffectStreamRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/EffectStreamRootNameResolver.cs
new file mode 100644
index 000000000..c5bb6595b
--- /dev/null
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/EffectStreamRootNameResolver.cs
@@ -0,0 +1,44 @@
+using System.Collections.ObjectModel;
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
+
+namespace OpenCode.Sdk.Tools.Generator.Binding;
+
+internal static class EffectStreamRootNameResolver
+{
+ ///
+ /// The extension gives its cause graph a shared semantic role independent of the operation
+ /// carrying it. Distinct schemas claiming the same name hit the ordinary schema-name wall
+ /// rather than silently merging.
+ ///
+ public static IReadOnlyDictionary Resolve(SpecDocument document, IReadOnlyList selected)
+ {
+ var names = new Dictionary(StringComparer.Ordinal);
+ foreach (var extension in selected
+ .SelectMany(static operation => operation.Responses)
+ .Select(static response => response.EffectStream)
+ .OfType())
+ {
+ if (extension.CauseSchema is not ArrayNode { Item: RefNode item }
+ || !document.Schemas.TryGetValue(item.Target, out var itemSchema)
+ || itemSchema is not UnionNode { Classification: UnionClassification.Marked } union)
+ {
+ continue;
+ }
+
+ names[item.Target] = EffectStreamTypeNamePolicy.CauseInterface;
+ foreach (var target in union.Branches.OfType().Select(static branch => branch.Target))
+ {
+ if (!document.Schemas.TryGetValue(target, out var branchSchema)
+ || branchSchema is not ObjectNode objectNode
+ || objectNode.LiteralMarkers.FirstOrDefault(static marker => marker.PropertyName is ErrorMarkerPolicy.WireName) is not { } marker)
+ {
+ continue;
+ }
+
+ names[target] = EffectStreamTypeNamePolicy.CauseVariant(marker.Value);
+ }
+ }
+
+ return new ReadOnlyDictionary(names);
+ }
+}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs
index 7e0094d0b..a7be5a0c7 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs
@@ -361,22 +361,9 @@ schema is RefNode reference
return ListOf(Named(itemName));
}
- // 'data' may also be a RefNode naming an ARRAY component directly — vcs.branches' exact
- // shape, Vcs.BranchList = {"type":"array","items":{"type":"string"}} — rather than
- // wrapping the array inline at this position. Resolve the reference through the ref
- // graph and, only when the resolved target is itself an array, apply the same item
- // logic as the arm above: a named item keeps its own identity, and every other item
- // shape (here, a primitive string) falls through to the type machinery, which walks the
- // very same reference (RefNode -> ArrayNode -> item) on its own. This does not widen the
- // guard below: the guard exists to stop a RefNode resolving to a NOMINAL
- // (object/enum/union) target with a failed name lookup from resurrecting a
- // mechanically-derived name, and that refusal is unchanged — only a ref resolving to an
- // ARRAY gains this path, keyed on shape (ref -> array), never on operation id. Resolve
- // only follows RefNode chains — it does not unwrap a NullableNode, and its own visited
- // set stops a ref alias cycle by handing back the cycling RefNode — so a nullable-wrapped
- // array or a cycling ref falls through this arm unmatched and reaches the same guard,
- // refusing exactly as before; intentional, since this dialect has no NullableNode-
- // unwrapping step of its own yet.
+ // A reference to an array component binds as an array: named items keep their identity,
+ // and other items bind through the type machinery. Ref walks do not unwrap nullable
+ // nodes; a nullable target or alias cycle reaches the nominal-name refusal below.
if (data.Schema is RefNode arrayReference && _context.Resolve(arrayReference) is ArrayNode resolvedArray)
{
if (resolvedArray.Item is RefNode resolvedItem
@@ -389,12 +376,8 @@ schema is RefNode reference
?? _context.RefuseNull("success payload does not bind to a supported type plan");
}
- // A RefNode or ArrayNode that reached here already failed the name lookups above (a
- // target the resolver left unnamed — a collapsed structural union, or a schema the
- // dialect excludes from naming — or a ref that does not resolve to an array); the
- // dialect keeps refusing those exactly as before instead of letting the type machinery
- // resurrect a mechanically-derived name. Every other shape (a dictionary, for one)
- // delegates to the type machinery.
+ // A failed nominal lookup must not resurrect a mechanically-derived name through the
+ // type machinery. Other shapes, such as dictionaries, delegate to that machinery.
if (data.Schema is RefNode or ArrayNode)
{
return _context.RefuseNull(
@@ -447,9 +430,7 @@ schema is RefNode reference
&& _context.Document.Schemas.TryGetValue(reference.Target, out var target)
&& target is ObjectNode wrapper
&& wrapper.Properties.FirstOrDefault(static property => property.Name is "data") is { } data
- // A literal inline array resolves to itself; a RefNode naming an array component (the
- // vcs.branches shape) resolves through the ref graph to the same ArrayNode — one check
- // classifies both.
+ // Inline arrays and references to array components share the same resolved shape.
&& _context.Resolve(data.Schema) is ArrayNode
? EnvelopeKind.DataLocationList
: EnvelopeKind.DataLocation;
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopePayloadRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopePayloadRootNameResolver.cs
new file mode 100644
index 000000000..24a3a180a
--- /dev/null
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopePayloadRootNameResolver.cs
@@ -0,0 +1,103 @@
+using System.Collections.ObjectModel;
+using OpenCode.Sdk.Tools.Generator.Binding.Models;
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
+
+namespace OpenCode.Sdk.Tools.Generator.Binding;
+
+internal static class EnvelopePayloadRootNameResolver
+{
+ /// Names success payloads promoted out of envelope spine by operation identity, not their projection keys.
+ ///
+ /// A success payload that ingestion promoted into the graph carries no schema identity of
+ /// its own: it was promoted out of envelope spine — the operation's own inline root, or a
+ /// wrapper the dialect never names — and neither spelling belongs on the public surface.
+ /// The rule covers bare roots, data members and location data (the list item or single object).
+ /// Component payloads keep their identity; schemaNames rows override. Cursor-list items stay
+ /// nominal (ADR-0017), so that position has no promoted key to claim.
+ ///
+ public static IReadOnlyDictionary Resolve(SpecDocument document,
+ IReadOnlyList selected, BindingErrorCollector errors)
+ {
+ var names = new Dictionary(StringComparer.Ordinal);
+ foreach (var operation in selected)
+ {
+ if (operation.Responses.FirstOrDefault(static response => response.StatusCode is 200) is not
+ { IsSse: false, Schema: RefNode reference, } success)
+ {
+ continue;
+ }
+
+ // Cursor-list items stay nominal, so that shape has no promoted key to claim; the
+ // remaining shapes never reach a payload at all.
+ var payloadKey = success.EnvelopeShape switch
+ {
+ SpecEnvelopeShape.Bare => reference.Target,
+ SpecEnvelopeShape.Data => ResolveDataMemberKey(document, reference.Target),
+ SpecEnvelopeShape.DataLocation => ResolveDataLocationPayloadKey(document, reference.Target),
+ // A single-key wrapper's value is nominal or a primitive shape the type
+ // machinery names on its own, so that shape never promotes a payload key here.
+ SpecEnvelopeShape.SingleKey or SpecEnvelopeShape.CursorData
+ or SpecEnvelopeShape.DataHasMore or SpecEnvelopeShape.None => null,
+ _ => null,
+ };
+ if (payloadKey is null || !payloadKey.Contains('#', StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ // A bare root embeds the operation id and cannot be shared, but a component data
+ // wrapper can be: two operations claiming one payload under different names refuse
+ // rather than letting iteration order pick a winner.
+ var payloadName = OperationNamePolicy.PayloadTypeName(operation);
+ if (names.TryGetValue(payloadKey, out var existing) && !StringComparer.Ordinal.Equals(existing, payloadName))
+ {
+ errors.Add(
+ BindingErrorCategory.Naming,
+ payloadKey,
+ $"envelope payload is claimed as both '{existing}' and '{payloadName}' by selected operations");
+ continue;
+ }
+
+ names[payloadKey] = payloadName;
+ }
+
+ return new ReadOnlyDictionary(names);
+ }
+
+ ///
+ /// Resolves the wrapper member the payload binder reads, matching the single-required-data
+ /// shape EnvelopeFacetBinder.BindDataPayload accepts; any other wrapper refuses there.
+ ///
+ private static string? ResolveDataMemberKey(SpecDocument document, string wrapperKey) =>
+ document.Schemas.TryGetValue(wrapperKey, out var wrapper)
+ && wrapper is ObjectNode { Properties: [{ Name: "data", IsRequired: true, Schema: RefNode payload, }] }
+ ? payload.Target
+ : null;
+
+ /// Resolves location data to the array's item key or the single value's key.
+ ///
+ /// The shape check is , the same call
+ /// EnvelopeFacetBinder.ResolveDataLocationWrapper makes, so what this claims and what
+ /// the binder reads cannot drift apart. The caller filters component
+ /// keys (no '#'), which also keeps references to named arrays out.
+ ///
+ private static string? ResolveDataLocationPayloadKey(SpecDocument document, string wrapperKey)
+ {
+ if (!document.Schemas.TryGetValue(wrapperKey, out var target) || target is not ObjectNode wrapper)
+ {
+ return null;
+ }
+
+ if (!EnvelopeWrapperShape.IsDataLocation(wrapper, out var data) || data is null)
+ {
+ return null;
+ }
+
+ return data.Schema switch
+ {
+ ArrayNode { Item: RefNode item } => item.Target,
+ RefNode payload => payload.Target,
+ _ => null,
+ };
+ }
+}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs
index c41e81b92..e088f22af 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs
@@ -71,7 +71,7 @@ public ErrorUnionBindingResult Bind()
var errors = _context.Errors.Snapshot();
_publishedErrors.Append(errors);
- return new ErrorUnionBindingResult(new UnionBindingResult(null, plan, edges.AsReadOnly()),
+ return new ErrorUnionBindingResult(new UnionBindingResult(null, plan, plan is null ? [] : edges.AsReadOnly()),
new ReadOnlyDictionary(admitted), new ReadOnlyDictionary(refused), errors);
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs
index 8324f5dd1..359db1571 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs
@@ -9,10 +9,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
///
internal static class ExperimentalModelPolicy
{
- public static IReadOnlyDictionary Collect(ReachableSchemaCollector reachableSchemas, ReachScope scope,
+ public static IReadOnlyDictionary Collect(ReachScope scope,
OwnershipCandidates candidates, GeneratedTypeNameInventory inventory, BindingErrorCollector errors)
{
- ArgumentNullException.ThrowIfNull(reachableSchemas);
ArgumentNullException.ThrowIfNull(scope);
ArgumentNullException.ThrowIfNull(candidates);
ArgumentNullException.ThrowIfNull(inventory);
@@ -20,14 +19,14 @@ public static IReadOnlyDictionary Collect(ReachableSchemaCollect
var diagnostics = scope.Clients.SelectMany(static client => client.Operations)
.ToDictionary(static operation => operation.OperationId, static operation => operation.ExperimentalDiagnostic, StringComparer.Ordinal);
- var stable = ModelReachCollector.CollectSubset(reachableSchemas,
+ var stable = ModelReachCollector.CollectSubset(
scope with { Selected = [.. scope.Selected.Where(static operation => !ExperimentalRoutePolicy.IsExperimental(operation.Path))] },
candidates, errors);
var reachedBy = new Dictionary>(StringComparer.Ordinal);
foreach (var subset in scope.Selected.Where(operation => diagnostics.GetValueOrDefault(operation.OperationId) is not null)
.GroupBy(operation => diagnostics[operation.OperationId]!, StringComparer.Ordinal))
{
- var reached = ModelReachCollector.CollectSubset(reachableSchemas, scope with { Selected = [.. subset] }, candidates, errors);
+ var reached = ModelReachCollector.CollectSubset(scope with { Selected = [.. subset] }, candidates, errors);
foreach (var name in reached.Where(name => inventory.Names.Contains(name) && !stable.Contains(name)))
{
if (!reachedBy.TryGetValue(name, out var ids))
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs
index 8f560432c..a541a25e8 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs
@@ -3,11 +3,10 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class MarkedUnionBinder(SchemaBindingContext context, UnionDiscriminatorSelector discriminators)
+internal sealed class MarkedUnionBinder(SchemaBindingContext context)
{
private static readonly IReadOnlyList NoPrefixMarkers = [];
private readonly SchemaBindingContext _context = context;
- private readonly UnionDiscriminatorSelector _discriminators = discriminators;
public UnionBindingResult Bind(string key, string name, UnionNode union)
{
@@ -24,7 +23,7 @@ public UnionBindingResult Bind(string key, string name, UnionNode union)
return null;
}
- var discriminator = _discriminators.Select(resolved);
+ var discriminator = UnionDiscriminatorSelector.Select(resolved);
if (discriminator is null)
{
_context.Errors.Add(BindingErrorCategory.Schema, key, "marked union branches share no discriminating marker property");
@@ -288,7 +287,7 @@ private static UnionLiteralVariants BindLiteralVariants(UnionBindingContext cont
IsNestedUnion: false, reference.Target, _context.Inhabitation.IsInhabited(leaf)));
}
- var arms = leaves.Count is 0 ? null : _discriminators.Select(leaves)?.PrefixBranches;
+ var arms = leaves.Count is 0 ? null : UnionDiscriminatorSelector.Select(leaves)?.PrefixBranches;
return arms is { Count: > 0 } ? arms[0].TypeName : null;
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs
index 858ed48ee..86f2511af 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs
@@ -11,10 +11,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
///
internal static class ModelReachCollector
{
- public static IReadOnlyDictionary> Collect(ReachableSchemaCollector reachableSchemas, ReachScope scope,
+ public static IReadOnlyDictionary> Collect(ReachScope scope,
OwnershipCandidates candidates, BindingErrorCollector errors)
{
- ArgumentNullException.ThrowIfNull(reachableSchemas);
ArgumentNullException.ThrowIfNull(scope);
ArgumentNullException.ThrowIfNull(candidates);
ArgumentNullException.ThrowIfNull(errors);
@@ -27,7 +26,7 @@ public static IReadOnlyDictionary> Collect(Reachab
.Where(operation => familyOf.ContainsKey(operation.OperationId))
.GroupBy(operation => familyOf[operation.OperationId], StringComparer.Ordinal))
{
- var reachable = reachableSchemas.Collect(scope.Document, [.. family], errors);
+ var reachable = ReachableSchemaCollector.Collect(scope.Document, [.. family], errors);
foreach (var typeName in reachable.GraphKeys.Where(scope.TypeNames.ContainsKey).Select(key => scope.TypeNames[key]))
{
_ = Reached(reach, typeName).Add(family.Key);
@@ -38,15 +37,14 @@ public static IReadOnlyDictionary> Collect(Reachab
return reach.ToDictionary(static entry => entry.Key, static entry => (IReadOnlyList)[.. entry.Value], StringComparer.Ordinal);
}
- public static IReadOnlySet CollectSubset(ReachableSchemaCollector reachableSchemas, ReachScope scope,
+ public static IReadOnlySet CollectSubset(ReachScope scope,
OwnershipCandidates candidates, BindingErrorCollector errors)
{
- ArgumentNullException.ThrowIfNull(reachableSchemas);
ArgumentNullException.ThrowIfNull(scope);
ArgumentNullException.ThrowIfNull(candidates);
ArgumentNullException.ThrowIfNull(errors);
- var reachable = reachableSchemas.Collect(scope.Document, scope.Selected, errors);
+ var reachable = ReachableSchemaCollector.Collect(scope.Document, scope.Selected, errors);
var reach = reachable.GraphKeys.Where(scope.TypeNames.ContainsKey).Select(key => scope.TypeNames[key])
.Distinct(StringComparer.Ordinal)
.ToDictionary(static name => name, static _ => new SortedSet(StringComparer.Ordinal) { "subset" }, StringComparer.Ordinal);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/RefChainResult.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/RefChainResult.cs
new file mode 100644
index 000000000..9585e3617
--- /dev/null
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/RefChainResult.cs
@@ -0,0 +1,5 @@
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
+
+namespace OpenCode.Sdk.Tools.Generator.Binding.Models;
+
+internal sealed record RefChainResult(SchemaNode Node, string? LastKey);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs
index 58bd8bc22..7d201f82c 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs
@@ -10,4 +10,10 @@ internal sealed record SchemaBindingContext(
IReadOnlyDictionary TypeNames,
TypePlanBinder Types,
SchemaInhabitationPolicy Inhabitation,
- BindingErrorCollector Errors);
+ BindingErrorCollector Errors)
+{
+ public static SchemaBindingContext Create(SpecDocument document, ReachableSchemaSet reachable,
+ IReadOnlyDictionary typeNames, BindingErrorCollector errors) =>
+ new(document, reachable, reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal), reachable.StreamCauseKeys.ToHashSet(StringComparer.Ordinal),
+ typeNames, new TypePlanBinder(document.Schemas, typeNames, errors), new SchemaInhabitationPolicy(document.Schemas), errors);
+}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs
index 24acc71e6..e3dab3787 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs
@@ -6,7 +6,7 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
///
/// Read-only per-operation state shared by the facet binders: the ingested document, the
/// operation under bind, its curation, the resolved type-name map, and the error batch.
-/// It also owns the one ref-graph and the refusal helpers, so every
+/// It also provides reference resolution and refusal helpers, so every
/// facet attributes its refusals to the same operation identically.
///
internal sealed class OperationFacetContext(
@@ -71,21 +71,5 @@ public FacetResult Publish(FacetResult result, IReadOnlyList Resolve(Document, schema);
-
- /// Follows a reference chain through the document's components to the schema it ends at; a cycle or a missing target stops it.
- public static SchemaNode Resolve(SpecDocument document, SchemaNode schema)
- {
- ArgumentNullException.ThrowIfNull(document);
-
- var visited = new HashSet(StringComparer.Ordinal);
- var current = schema;
- while (current is RefNode reference && visited.Add(reference.Target)
- && document.Schemas.TryGetValue(reference.Target, out var target))
- {
- current = target;
- }
-
- return current;
- }
+ public SchemaNode Resolve(SchemaNode schema) => RefChain.Resolve(schema, Document.Schemas).Node;
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs
index a029edfad..40879eab1 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs
@@ -4,13 +4,11 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class OperationPlanBinder
+internal static class OperationPlanBinder
{
private const string RootClientName = "OpenCodeClient";
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public IReadOnlyList Bind(SpecDocument document, IReadOnlyList selected,
+ public static IReadOnlyList Bind(SpecDocument document, IReadOnlyList selected,
GenerationCuration curation, SchemaBindingResult schemas, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(document);
@@ -22,7 +20,7 @@ public IReadOnlyList Bind(SpecDocument document, IReadOnlyList(selected.Count);
- foreach (var operation in selected.OrderBy(static operation => operation.OperationId, _comparer))
+ foreach (var operation in selected.OrderBy(static operation => operation.OperationId, StringComparer.Ordinal))
{
var operationPlan = new SingleOperationBinder(new OperationFacetContext(document, operation, curation, typeNames, errors, typeBinder), schemas.ErrorUnion).Bind();
if (operationPlan is not null)
@@ -41,12 +39,12 @@ public IReadOnlyList Bind(SpecDocument document, IReadOnlyList AssembleClients(List bound)
+ private static List AssembleClients(List bound)
{
var rootOperations = bound
.Where(static operation => operation.Row.Placement is GroupPlacement.Root)
.Select(static operation => operation.Plan)
- .OrderBy(static plan => plan.MethodName, _comparer)
+ .OrderBy(static plan => plan.MethodName, StringComparer.Ordinal)
.ToArray();
var subClients = new List();
var clients = new List();
@@ -55,8 +53,8 @@ private List AssembleClients(List bound)
// curation validator has already demanded identical handle declarations.
foreach (var group in bound
.Where(static operation => operation.Row.Placement is GroupPlacement.Client)
- .GroupBy(static operation => operation.Row.ClientName!, _comparer)
- .OrderBy(static group => group.Key, _comparer))
+ .GroupBy(static operation => operation.Row.ClientName!, StringComparer.Ordinal)
+ .OrderBy(static group => group.Key, StringComparer.Ordinal))
{
var members = group.ToArray();
var row = members[0].Row;
@@ -74,7 +72,7 @@ private List AssembleClients(List bound)
Name = RootClientName,
Namespace = GeneratedNamespace.Public,
Role = ClientRole.Root,
- SubClients = [.. subClients.OrderBy(static reference => reference.PropertyName, _comparer)],
+ SubClients = [.. subClients.OrderBy(static reference => reference.PropertyName, StringComparer.Ordinal)],
Operations = rootOperations,
});
@@ -95,16 +93,16 @@ private static string ClientNamespace(EmissionMode emission) =>
/// (ADR-0021): the public family name belongs to the hand-written door the root client
/// keeps pointing at, so nothing generated may claim it.
///
- private IEnumerable CreateGroupClients(IReadOnlyList group, GroupCuration row, string collectionName)
+ private static IEnumerable CreateGroupClients(IReadOnlyList group, GroupCuration row, string collectionName)
{
var handleOperations = group.Where(static operation => operation.IsHandleOperation).ToArray();
var collectionOperations = group
.Where(static operation => !operation.IsHandleOperation)
.Select(static operation => operation.Plan)
- .OrderBy(static plan => plan.MethodName, _comparer)
+ .OrderBy(static plan => plan.MethodName, StringComparer.Ordinal)
.ToArray();
var handleParameter = handleOperations
- .OrderBy(static operation => operation.Plan.MethodName, _comparer)
+ .OrderBy(static operation => operation.Plan.MethodName, StringComparer.Ordinal)
.SelectMany(static operation => operation.Plan.Parameters)
.FirstOrDefault(static parameter => parameter.IsHandleParameter);
var collectionTypeName = ClientTypeName(collectionName, row.Emission);
@@ -144,7 +142,7 @@ private IEnumerable CreateGroupClients(IReadOnlyList
[
.. handleOperations
.Select(static operation => operation.Plan)
- .OrderBy(static plan => plan.MethodName, _comparer),
+ .OrderBy(static plan => plan.MethodName, StringComparer.Ordinal),
],
};
}
@@ -153,7 +151,7 @@ .. handleOperations
private static string ClientTypeName(string familyName, EmissionMode emission) =>
emission is EmissionMode.InternalRaw ? CSharpNamePolicy.ToRawClientName(familyName) : familyName;
- private void CheckMemberCollisions(List clients, BindingErrorCollector errors)
+ private static void CheckMemberCollisions(List clients, BindingErrorCollector errors)
{
foreach (var client in clients)
{
@@ -169,22 +167,22 @@ private void CheckMemberCollisions(List clients, BindingErrorCollect
}
foreach (var member in members
- .GroupBy(static member => member, _comparer)
+ .GroupBy(static member => member, StringComparer.Ordinal)
.Where(static member => member.Skip(1).Any())
- .OrderBy(static member => member.Key, _comparer))
+ .OrderBy(static member => member.Key, StringComparer.Ordinal))
{
errors.Add(BindingErrorCategory.Naming, client.Name, $"multiple members map to C# name '{member.Key}'");
}
}
}
- public IReadOnlySet CheckTypeNameCollisions(GeneratedTypeNameInventory inventory, IReadOnlyList clients, BindingErrorCollector errors)
+ public static IReadOnlySet CheckTypeNameCollisions(GeneratedTypeNameInventory inventory, IReadOnlyList clients, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(inventory);
ArgumentNullException.ThrowIfNull(clients);
ArgumentNullException.ThrowIfNull(errors);
- var refusedHoists = new HashSet(_comparer);
- var hoistedOwners = new HashSet(_comparer);
+ var refusedHoists = new HashSet(StringComparer.Ordinal);
+ var hoistedOwners = new HashSet(StringComparer.Ordinal);
foreach (var claim in inventory.Claims.Where(claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Hoisted
&& (!hoistedOwners.Add(claim.Name) || inventory.OtherClaims(claim).Any(static other => other.Role is not GeneratedTypeNameInventory.ClaimRole.Hoisted))))
{
@@ -197,7 +195,7 @@ public IReadOnlySet CheckTypeNameCollisions(GeneratedTypeNameInventory i
}
// Spine names claim first, followed by models individually so a model/spine duplicate refuses.
- var owners = new HashSet(_comparer);
+ var owners = new HashSet(StringComparer.Ordinal);
owners.UnionWith(ReservedNamePolicy.SpineTypeNames);
// Derived kit/adapter names reserve their declarations; their owning declaration reports duplicates.
owners.UnionWith(inventory.Claims.Where(static claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Derived).Select(static claim => claim.Name));
@@ -227,9 +225,9 @@ GeneratedTypeNameInventory.ClaimRole.Model or GeneratedTypeNameInventory.ClaimRo
errors.Add(BindingErrorCategory.Naming, claim.Subject, problem);
}
- var derivedOwners = new HashSet(ReservedNamePolicy.SpineTypeNames, _comparer);
+ var derivedOwners = new HashSet(ReservedNamePolicy.SpineTypeNames, StringComparer.Ordinal);
var responseNames = inventory.Claims.Where(static claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Response)
- .ToDictionary(static claim => claim.Subject, static claim => claim.Name, _comparer);
+ .ToDictionary(static claim => claim.Subject, static claim => claim.Name, StringComparer.Ordinal);
foreach (var claim in inventory.Claims.Where(claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Derived && !derivedOwners.Add(claim.Name)))
{
// Equal response declarations already refuse with their response-name diagnostic.
@@ -242,8 +240,8 @@ GeneratedTypeNameInventory.ClaimRole.Model or GeneratedTypeNameInventory.ClaimRo
errors.Add(BindingErrorCategory.Naming, claim.Subject, $"type name '{claim.Name}' collides with another generated type");
}
- var routeMembers = new HashSet(_comparer);
- var operations = clients.SelectMany(static client => client.Operations).OrderBy(static operation => operation.OperationId, _comparer).ToArray();
+ var routeMembers = new HashSet(StringComparer.Ordinal);
+ var operations = clients.SelectMany(static client => client.Operations).OrderBy(static operation => operation.OperationId, StringComparer.Ordinal).ToArray();
foreach (var operation in operations.Where(operation => !routeMembers.Add($"{operation.RouteContainerName}.{operation.RouteMemberName}")))
{
errors.Add(BindingErrorCategory.Naming, operation.OperationId,
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs
index 6f9532795..7dc8b06ba 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs
@@ -30,7 +30,7 @@ public bool Check()
if (operation.IsWebSocket)
{
- _context.Refuse("WebSocket operations are not supported in M1");
+ _context.Refuse("WebSocket operations are not supported");
}
// A GET body has no defined semantics on any origin server and stays refused. DELETE is
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs
index d1bdaed7b..50a6c99d4 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs
@@ -7,8 +7,7 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
///
/// Marks each pending operation with the bindability the real selection-path binder finds, so
/// wall-free drift among pending operations surfaces as a committed diff instead of accumulating
-/// silently — the interim telltale bridging until the operation inventory/assurance-ledger lane
-/// standardizes pending-operation tracking. Declined operations are probed the same way and for
+/// silently. Declined operations are probed the same way and for
/// the same reason: a decline asserts a standing wall, and only this probe can confirm it still
/// stands.
///
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumRootNameResolver.cs
new file mode 100644
index 000000000..2ff42efe8
--- /dev/null
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumRootNameResolver.cs
@@ -0,0 +1,36 @@
+using System.Collections.ObjectModel;
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
+
+namespace OpenCode.Sdk.Tools.Generator.Binding;
+
+internal static class QueryEnumRootNameResolver
+{
+ /// Names promoted inline query enums by {RequestTypeName}{PropertyName}; component enums keep their identity.
+ ///
+ /// An inline query enum that ingestion promoted into the graph carries no schema identity
+ /// of its own — its key is the operation's parameter pointer — so it is named from the
+ /// request record it is a member of, mechanically {RequestTypeName}{PropertyName}.
+ /// Each promoted key embeds one operation id and parameter pointer, so two selected
+ /// operations cannot claim the same key. A reasoned schemaNames row still overrides.
+ ///
+ public static IReadOnlyDictionary Resolve(SpecDocument document, IReadOnlyList selected)
+ {
+ var names = new Dictionary(StringComparer.Ordinal);
+ foreach (var operation in selected)
+ {
+ foreach (var parameter in operation.Parameters.Where(static parameter =>
+ parameter.Location is SpecParameterLocation.Query))
+ {
+ var key = QueryEnumShapePolicy.ResolveModelKey(parameter.Schema, document.Schemas);
+ if (key is null || !key.Contains('#', StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ names[key] = $"{OperationNamePolicy.RequestTypeName(operation)}{CSharpNamePolicy.ToPascalCase(parameter.Name)}";
+ }
+ }
+
+ return new ReadOnlyDictionary(names);
+ }
+}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs
index f515c7dfe..1d83beaeb 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs
@@ -54,15 +54,7 @@ internal static class QueryEnumShapePolicy
// Ingestion promotes every inline enum into the graph, so the enum a parameter binds
// always arrives behind a reference; a key is what the model and its name hang on.
- string? key = null;
- var visited = new HashSet(StringComparer.Ordinal);
- while (candidate is RefNode reference && visited.Add(reference.Target)
- && graph.TryGetValue(reference.Target, out var target))
- {
- key = reference.Target;
- candidate = target;
- }
-
- return candidate is EnumNode { Format: null } node && ResolveSpineKind(node) is null ? key : null;
+ var resolved = candidate is null ? null : RefChain.Resolve(candidate, graph);
+ return resolved?.Node is EnumNode { Format: null } node && ResolveSpineKind(node) is null ? resolved.LastKey : null;
}
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs
index 329740bf6..8e38a6bbf 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs
@@ -20,15 +20,16 @@ public static void Validate(SpecDocument document, IReadOnlyList
ArgumentNullException.ThrowIfNull(curation);
ArgumentNullException.ThrowIfNull(errors);
- var documentIds = document.Operations.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal);
- var operations = selected.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal);
+ var context = new QueryParameterValidationContext(document,
+ document.Operations.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal),
+ selected.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal), errors);
var typed = new HashSet(StringComparer.Ordinal);
foreach (var row in curation.QueryParameterTypes)
{
var subject = row.OperationId + ":" + row.Parameter;
_ = typed.Add(subject);
- ValidateSubject(document, documentIds, operations, row.OperationId, row.Parameter, subject, errors);
+ ValidateSubject(context, row.OperationId, row.Parameter, subject);
ValidateTypedRow(row, subject, errors);
}
@@ -41,7 +42,7 @@ public static void Validate(SpecDocument document, IReadOnlyList
"the query parameter is classified twice: a queryParameterTypes row types it, so its stringQueryParameters row must go");
}
- ValidateSubject(document, documentIds, operations, row.OperationId, row.Parameter, subject, errors);
+ ValidateSubject(context, row.OperationId, row.Parameter, subject);
}
}
@@ -60,13 +61,12 @@ private static void ValidateTypedRow(QueryParameterTypeCuration row, string subj
}
}
- private static void ValidateSubject(SpecDocument document, HashSet documentIds,
- Dictionary operations, string operationId, string parameterName, string subject,
- BindingErrorCollector errors)
+ private static void ValidateSubject(QueryParameterValidationContext context, string operationId, string parameterName, string subject)
{
- if (!operations.TryGetValue(operationId, out var operation))
+ var errors = context.Errors;
+ if (!context.Operations.TryGetValue(operationId, out var operation))
{
- errors.Add(BindingErrorCategory.Curation, subject, documentIds.Contains(operationId)
+ errors.Add(BindingErrorCategory.Curation, subject, context.DocumentIds.Contains(operationId)
? "curated operation is not selected by the current profile"
: "the operation no longer exists in the document; remove this row");
return;
@@ -80,7 +80,7 @@ private static void ValidateSubject(SpecDocument document, HashSet docum
return;
}
- switch (ResolveValue(document, parameter.Schema))
+ switch (ResolveValue(context.Document, parameter.Schema))
{
case PrimitiveNode { Kind: PrimitiveKind.String, Format: null }:
return;
@@ -98,7 +98,10 @@ private static void ValidateSubject(SpecDocument document, HashSet docum
/// Follows references and one nullable wrapper to the value a query parameter carries, as the binder does.
private static SchemaNode ResolveValue(SpecDocument document, SchemaNode schema)
{
- var resolved = OperationFacetContext.Resolve(document, schema);
- return resolved is NullableNode { Format: null } nullable ? OperationFacetContext.Resolve(document, nullable.Inner) : resolved;
+ var resolved = RefChain.Resolve(schema, document.Schemas).Node;
+ return resolved is NullableNode { Format: null } nullable ? RefChain.Resolve(nullable.Inner, document.Schemas).Node : resolved;
}
+
+ private sealed record QueryParameterValidationContext(SpecDocument Document, IReadOnlySet DocumentIds,
+ IReadOnlyDictionary Operations, BindingErrorCollector Errors);
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs
index 138c3e060..6c09fe453 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs
@@ -3,17 +3,15 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class ReachableSchemaCollector
+internal static class ReachableSchemaCollector
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public ReachableSchemaSet Collect(SpecDocument document, IReadOnlyList operations, BindingErrorCollector errors)
+ public static ReachableSchemaSet Collect(SpecDocument document, IReadOnlyList operations, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(document);
ArgumentNullException.ThrowIfNull(operations);
ArgumentNullException.ThrowIfNull(errors);
- var traversal = new ReachabilityTraversal(document.Schemas, errors, _comparer);
+ var traversal = new ReachabilityTraversal(document.Schemas, errors);
foreach (var operation in operations)
{
foreach (var parameter in operation.Parameters)
@@ -52,14 +50,13 @@ public ReachableSchemaSet Collect(SpecDocument document, IReadOnlyList graph, BindingErrorCollector errors, StringComparer comparer)
+ private sealed class ReachabilityTraversal(IReadOnlyDictionary graph, BindingErrorCollector errors)
{
- private readonly StringComparer _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
private readonly BindingErrorCollector _errors = errors ?? throw new ArgumentNullException(nameof(errors));
private readonly IReadOnlyDictionary _graph = graph ?? throw new ArgumentNullException(nameof(graph));
- private readonly HashSet _keys = new(comparer);
- private readonly HashSet _responseRoots = new(comparer);
- private readonly HashSet _streamCauseKeys = new(comparer);
+ private readonly HashSet _keys = new(StringComparer.Ordinal);
+ private readonly HashSet _responseRoots = new(StringComparer.Ordinal);
+ private readonly HashSet _streamCauseKeys = new(StringComparer.Ordinal);
public void VisitResponse(string operationId, SpecResponse response)
{
@@ -192,9 +189,9 @@ private void Visit(SchemaNode schema, bool isStreamCause)
public ReachableSchemaSet Snapshot() =>
new()
{
- GraphKeys = [.. _keys.Order(_comparer)],
- ResponseRootKeys = [.. _responseRoots.Order(_comparer)],
- StreamCauseKeys = [.. _streamCauseKeys.Order(_comparer)],
+ GraphKeys = [.. _keys.Order(StringComparer.Ordinal)],
+ ResponseRootKeys = [.. _responseRoots.Order(StringComparer.Ordinal)],
+ StreamCauseKeys = [.. _streamCauseKeys.Order(StringComparer.Ordinal)],
};
private void VisitReference(string target, bool isStreamCause)
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/RefChain.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/RefChain.cs
new file mode 100644
index 000000000..8200bae19
--- /dev/null
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/RefChain.cs
@@ -0,0 +1,25 @@
+using OpenCode.Sdk.Tools.Generator.Binding.Models;
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
+
+namespace OpenCode.Sdk.Tools.Generator.Binding;
+
+/// Follows references only, retaining the last successfully followed key even when a chain cannot finish.
+internal static class RefChain
+{
+ public static RefChainResult Resolve(SchemaNode node, IReadOnlyDictionary graph)
+ {
+ ArgumentNullException.ThrowIfNull(node);
+ ArgumentNullException.ThrowIfNull(graph);
+
+ var visited = new HashSet(StringComparer.Ordinal);
+ string? lastKey = null;
+ while (node is RefNode reference && visited.Add(reference.Target)
+ && graph.TryGetValue(reference.Target, out var target))
+ {
+ lastKey = reference.Target;
+ node = target;
+ }
+
+ return new RefChainResult(node, lastKey);
+ }
+}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/RequestBodyRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/RequestBodyRootNameResolver.cs
new file mode 100644
index 000000000..cbf7ff01d
--- /dev/null
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/RequestBodyRootNameResolver.cs
@@ -0,0 +1,44 @@
+using System.Collections.ObjectModel;
+using OpenCode.Sdk.Tools.Generator.Binding.Models;
+using OpenCode.Sdk.Tools.Generator.Ingestion.Models;
+
+namespace OpenCode.Sdk.Tools.Generator.Binding;
+
+internal static class RequestBodyRootNameResolver
+{
+ ///
+ /// Every selected operation's nominal body root — inline or component-referenced — is
+ /// named from its operation identity, the mechanical {Subject}{Verb}Request rule
+ /// (public names never surface raw operation ids or dotted component spellings).
+ ///
+ ///
+ /// Pending operations cannot rename a component the selected closure shares. Two selected
+ /// operations claiming one root under different names refuse. Only the root reference is
+ /// claimed; nested component dependencies stay shared.
+ ///
+ public static IReadOnlyDictionary Resolve(IReadOnlyList selected, BindingErrorCollector errors)
+ {
+ var names = new Dictionary(StringComparer.Ordinal);
+ foreach (var operation in selected)
+ {
+ if (operation.RequestBody?.Schema is not RefNode reference)
+ {
+ continue;
+ }
+
+ var requestName = OperationNamePolicy.RequestTypeName(operation);
+ if (names.TryGetValue(reference.Target, out var existing) && !StringComparer.Ordinal.Equals(existing, requestName))
+ {
+ errors.Add(
+ BindingErrorCategory.Naming,
+ reference.Target,
+ $"request body root is claimed as both '{existing}' and '{requestName}' by selected operations");
+ continue;
+ }
+
+ names[reference.Target] = requestName;
+ }
+
+ return new ReadOnlyDictionary(names);
+ }
+}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs
index 3970b636b..f7eb984ad 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs
@@ -9,9 +9,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
/// duplicate. Container kinds without a rewrite rule pass through unchanged; a reference they
/// might carry then dangles and fails reachability loudly.
///
-internal sealed class SchemaAliasApplier
+internal static class SchemaAliasApplier
{
- public SpecDocument Apply(SpecDocument document, StabilizeDuplicateCollapse collapse, IReadOnlyList aliases)
+ public static SpecDocument Apply(SpecDocument document, StabilizeDuplicateCollapse collapse, IReadOnlyList aliases)
{
ArgumentNullException.ThrowIfNull(document);
@@ -58,7 +58,7 @@ public static IReadOnlyDictionary Compose(StabilizeDuplicateColl
return map;
}
- private SpecOperation RewriteOperation(SpecOperation operation, Fold map) =>
+ private static SpecOperation RewriteOperation(SpecOperation operation, Fold map) =>
operation with
{
Parameters =
@@ -84,7 +84,7 @@ .. operation.Responses.Select(response => response with
],
};
- private SpecEffectStreamContract? RewriteEffectStream(SpecEffectStreamContract? contract, Fold map)
+ private static SpecEffectStreamContract? RewriteEffectStream(SpecEffectStreamContract? contract, Fold map)
{
if (contract is null)
{
@@ -100,7 +100,7 @@ .. operation.Responses.Select(response => response with
};
}
- private SchemaNode Rewrite(SchemaNode node, Fold map) => node switch
+ private static SchemaNode Rewrite(SchemaNode node, Fold map) => node switch
{
RefNode reference when map.Targets.TryGetValue(reference.Target, out var target) => reference with
{
@@ -141,7 +141,7 @@ .. target.Properties.Select(property => property with
};
/// Collapsed branches can duplicate a reference; the duplicates fold away mechanically.
- private UnionNode RewriteUnion(UnionNode union, Fold map)
+ private static UnionNode RewriteUnion(UnionNode union, Fold map)
{
var branches = new List(union.Branches.Count);
var seen = new HashSet(StringComparer.Ordinal);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs
index 461100fdf..4e7897ce3 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs
@@ -12,11 +12,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
/// needs the collapse, which is why it is its own validator rather than a sixth check inside
/// .
///
-internal sealed class SchemaAliasValidator
+internal static class SchemaAliasValidator
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public void Validate(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation,
+ public static void Validate(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation,
StabilizeDuplicateCollapse collapse, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(document);
@@ -25,8 +23,8 @@ public void Validate(SpecDocument document, ReachableSchemaSet reachable, Genera
ArgumentNullException.ThrowIfNull(collapse);
ArgumentNullException.ThrowIfNull(errors);
- var reachableKeys = reachable.GraphKeys.ToHashSet(_comparer);
- var sources = curation.SchemaAliases.Select(static row => row.Schema).ToHashSet(_comparer);
+ var reachableKeys = reachable.GraphKeys.ToHashSet(StringComparer.Ordinal);
+ var sources = curation.SchemaAliases.Select(static row => row.Schema).ToHashSet(StringComparer.Ordinal);
// Identity is judged on the graph as it will be bound, so one alias can be what makes
// a second pair identical — the mechanical collapse included.
// A duplicated source is refused by the row contract; Compose tolerates it.
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs
index fbbe6e902..b4a6008b7 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs
@@ -4,11 +4,9 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class SchemaNameResolver
+internal static class SchemaNameResolver
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public IReadOnlyDictionary Resolve(SpecDocument document, ReachableSchemaSet reachable,
+ public static IReadOnlyDictionary Resolve(SpecDocument document, ReachableSchemaSet reachable,
IReadOnlyList selected, GenerationCuration curation, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(document);
@@ -17,18 +15,18 @@ public IReadOnlyDictionary Resolve(SpecDocument document, Reacha
ArgumentNullException.ThrowIfNull(curation);
ArgumentNullException.ThrowIfNull(errors);
- var responseRoots = reachable.ResponseRootKeys.ToHashSet(_comparer);
- var requestRoots = ResolveRequestBodyRootNames(selected, errors);
- var queryEnumRoots = ResolveQueryEnumRootNames(document, selected);
- var payloadRoots = ResolveEnvelopePayloadRootNames(document, selected, errors);
- var effectStreamTypes = ResolveEffectStreamTypeNames(document, selected);
+ var responseRoots = reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal);
+ var requestRoots = RequestBodyRootNameResolver.Resolve(selected, errors);
+ var queryEnumRoots = QueryEnumRootNameResolver.Resolve(document, selected);
+ var payloadRoots = EnvelopePayloadRootNameResolver.Resolve(document, selected, errors);
+ var effectStreamTypes = EffectStreamRootNameResolver.Resolve(document, selected);
var artifacts = new ProjectionArtifactNamePolicy(document.Schemas.Keys);
var curatedNames = curation
.SchemaNames
.DistinctBy(static row => row.Schema, StringComparer.Ordinal)
.ToDictionary(static row => row.Schema, static row => row.DotNetName, StringComparer.Ordinal);
- var result = new Dictionary(_comparer);
- var owners = new Dictionary(_comparer);
+ var result = new Dictionary(StringComparer.Ordinal);
+ var owners = new Dictionary(StringComparer.Ordinal);
foreach (var key in reachable.GraphKeys)
{
if (responseRoots.Contains(key) || !document.Schemas.TryGetValue(key, out var schema)
@@ -88,45 +86,6 @@ public IReadOnlyDictionary Resolve(SpecDocument document, Reacha
return new ReadOnlyDictionary(result);
}
- ///
- /// The extension gives its cause graph a shared semantic role independent of the operation
- /// carrying it. The pin currently exposes one such contract; a second selected contract
- /// claiming the same names would hit the ordinary owner collision below rather than silently
- /// merging distinct schemas.
- ///
- private static Dictionary ResolveEffectStreamTypeNames(SpecDocument document,
- IReadOnlyList selected)
- {
- var names = new Dictionary(StringComparer.Ordinal);
- foreach (var extension in selected
- .SelectMany(static operation => operation.Responses)
- .Select(static response => response.EffectStream)
- .OfType())
- {
- if (extension.CauseSchema is not ArrayNode { Item: RefNode item }
- || !document.Schemas.TryGetValue(item.Target, out var itemSchema)
- || itemSchema is not UnionNode { Classification: UnionClassification.Marked } union)
- {
- continue;
- }
-
- names[item.Target] = EffectStreamTypeNamePolicy.CauseInterface;
- foreach (var target in union.Branches.OfType().Select(static branch => branch.Target))
- {
- if (!document.Schemas.TryGetValue(target, out var branchSchema)
- || branchSchema is not ObjectNode objectNode
- || objectNode.LiteralMarkers.FirstOrDefault(static marker => marker.PropertyName is ErrorMarkerPolicy.WireName) is not { } marker)
- {
- continue;
- }
-
- names[target] = EffectStreamTypeNamePolicy.CauseVariant(marker.Value);
- }
- }
-
- return names;
- }
-
///
/// A union that carries no choice is not a type of its own — it binds to what its branches
/// already are (), so it never claims a C# name.
@@ -138,171 +97,7 @@ private static Dictionary ResolveEffectStreamTypeNames(SpecDocum
_ => false,
};
- ///
- /// Every selected operation's nominal body root — inline or component-referenced — is
- /// named from its operation identity, the mechanical {Subject}{Verb}Request rule
- /// (public names never surface raw operation ids or dotted component spellings).
- /// Ownership is scoped to the selection so a pending operation can never rename a
- /// component the selected closure shares; two selected operations claiming one root
- /// under different names refuse. Nested component dependencies stay shared: only the
- /// root reference is claimed.
- ///
- private Dictionary ResolveRequestBodyRootNames(IReadOnlyList selected,
- BindingErrorCollector errors)
- {
- var names = new Dictionary(_comparer);
- foreach (var operation in selected)
- {
- if (operation.RequestBody?.Schema is not RefNode reference)
- {
- continue;
- }
-
- var requestName = OperationNamePolicy.RequestTypeName(operation);
- if (names.TryGetValue(reference.Target, out var existing) && !_comparer.Equals(existing, requestName))
- {
- errors.Add(
- BindingErrorCategory.Naming,
- reference.Target,
- $"request body root is claimed as both '{existing}' and '{requestName}' by selected operations");
- continue;
- }
-
- names[reference.Target] = requestName;
- }
-
- return names;
- }
-
- ///
- /// An inline query enum that ingestion promoted into the graph carries no schema identity
- /// of its own — its key is the operation's parameter pointer — so it is named from the
- /// request record it is a member of, mechanically {RequestTypeName}{PropertyName}.
- /// A component-referenced enum keeps its component identity and is skipped here. Each
- /// promoted key embeds one operation id and one parameter pointer, so no two selected
- /// operations can claim the same key; a reasoned schemaNames row still overrides.
- ///
- private Dictionary ResolveQueryEnumRootNames(SpecDocument document, IReadOnlyList selected)
- {
- var names = new Dictionary(_comparer);
- foreach (var operation in selected)
- {
- foreach (var parameter in operation.Parameters.Where(static parameter =>
- parameter.Location is SpecParameterLocation.Query))
- {
- var key = QueryEnumShapePolicy.ResolveModelKey(parameter.Schema, document.Schemas);
- if (key is null || !key.Contains('#', StringComparison.Ordinal))
- {
- continue;
- }
-
- names[key] = $"{OperationNamePolicy.RequestTypeName(operation)}{CSharpNamePolicy.ToPascalCase(parameter.Name)}";
- }
- }
-
- return names;
- }
-
- ///
- /// A success payload that ingestion promoted into the graph carries no schema identity of
- /// its own: it was promoted out of envelope spine — the operation's own inline root, or a
- /// wrapper the dialect never names — and neither spelling belongs on the public surface.
- /// Such a payload is named from the operation instead, mechanically
- /// (), uniformly for the bare body root, for
- /// a data wrapper's data member, and for a location wrapper's data — the list's
- /// item, or the single object itself. A component-referenced payload keeps its component
- /// identity, and a reasoned schemaNames row still overrides. Cursor-list items stay
- /// nominal (ADR-0017), so that position never carries a promoted key for this to claim.
- ///
- private Dictionary ResolveEnvelopePayloadRootNames(SpecDocument document,
- IReadOnlyList selected, BindingErrorCollector errors)
- {
- var names = new Dictionary(_comparer);
- foreach (var operation in selected)
- {
- if (operation.Responses.FirstOrDefault(static response => response.StatusCode is 200) is not
- { IsSse: false, Schema: RefNode reference, } success)
- {
- continue;
- }
-
- // Cursor-list items stay nominal, so that shape has no promoted key to claim; the
- // remaining shapes never reach a payload at all.
- var payloadKey = success.EnvelopeShape switch
- {
- SpecEnvelopeShape.Bare => reference.Target,
- SpecEnvelopeShape.Data => ResolveDataMemberKey(document, reference.Target),
- SpecEnvelopeShape.DataLocation => ResolveDataLocationPayloadKey(document, reference.Target),
- // A single-key wrapper's value is nominal or a primitive shape the type
- // machinery names on its own, so that shape never promotes a payload key here.
- SpecEnvelopeShape.SingleKey or SpecEnvelopeShape.CursorData
- or SpecEnvelopeShape.DataHasMore or SpecEnvelopeShape.None => null,
- _ => null,
- };
- if (payloadKey is null || !payloadKey.Contains('#', StringComparison.Ordinal))
- {
- continue;
- }
-
- // A bare root embeds the operation id and cannot be shared, but a component data
- // wrapper can be: two operations claiming one payload under different names refuse
- // rather than letting iteration order pick a winner.
- var payloadName = OperationNamePolicy.PayloadTypeName(operation);
- if (names.TryGetValue(payloadKey, out var existing) && !_comparer.Equals(existing, payloadName))
- {
- errors.Add(
- BindingErrorCategory.Naming,
- payloadKey,
- $"envelope payload is claimed as both '{existing}' and '{payloadName}' by selected operations");
- continue;
- }
-
- names[payloadKey] = payloadName;
- }
-
- return names;
- }
-
- ///
- /// Resolves the wrapper member the payload binder reads, matching the single-required-data
- /// shape EnvelopeFacetBinder.BindDataPayload accepts; any other wrapper refuses there.
- ///
- private static string? ResolveDataMemberKey(SpecDocument document, string wrapperKey) =>
- document.Schemas.TryGetValue(wrapperKey, out var wrapper)
- && wrapper is ObjectNode { Properties: [{ Name: "data", IsRequired: true, Schema: RefNode payload, }] }
- ? payload.Target
- : null;
-
- ///
- /// Resolves the graph key of a location wrapper's data payload — the array's item for
- /// a list, the member itself for a single value. The shape check is
- /// , the same call
- /// EnvelopeFacetBinder.ResolveDataLocationWrapper makes, so what this claims and what
- /// the binder reads cannot drift apart. A real top-level component payload is filtered back
- /// out by this method's caller (its key carries no '#'), which is also what keeps the
- /// ref-to-named-array shape out.
- ///
- private static string? ResolveDataLocationPayloadKey(SpecDocument document, string wrapperKey)
- {
- if (!document.Schemas.TryGetValue(wrapperKey, out var target) || target is not ObjectNode wrapper)
- {
- return null;
- }
-
- if (!EnvelopeWrapperShape.IsDataLocation(wrapper, out var data) || data is null)
- {
- return null;
- }
-
- return data.Schema switch
- {
- ArrayNode { Item: RefNode item } => item.Target,
- RefNode payload => payload.Target,
- _ => null,
- };
- }
-
- private string ResolveUnionName(string key, UnionNode union, IReadOnlyDictionary graph,
+ private static string ResolveUnionName(string key, UnionNode union, IReadOnlyDictionary graph,
ProjectionArtifactNamePolicy artifacts)
{
if (!key.Contains('#', StringComparison.Ordinal))
@@ -337,7 +132,7 @@ private string ResolveUnionName(string key, UnionNode union, IReadOnlyDictionary
var owner = CSharpNamePolicy.ToPascalCase(GetRoot(key));
var common = string.Concat(commonWords.Select(CSharpNamePolicy.ToPascalCase));
- return common.Length > 0 && !_comparer.Equals(common, owner)
+ return common.Length > 0 && !StringComparer.Ordinal.Equals(common, owner)
? common
: ResolveDefault(key, artifacts);
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs
index 4dcfadf05..c048dce13 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs
@@ -3,41 +3,27 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class SchemaPlanBinder(
- StructuralUnionPlanBinder structuralUnions,
- UnionMembershipValidator unionMemberships,
- UnionDiscriminatorSelector discriminators,
- UnionMemberHoistBinder memberHoists)
+internal sealed class SchemaPlanBinder(SchemaBindingContext context)
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
- private readonly StructuralUnionPlanBinder _structuralUnions = structuralUnions
- ?? throw new ArgumentNullException(nameof(structuralUnions));
- private readonly UnionMembershipValidator _unionMemberships = unionMemberships
- ?? throw new ArgumentNullException(nameof(unionMemberships));
- private readonly UnionDiscriminatorSelector _discriminators = discriminators
- ?? throw new ArgumentNullException(nameof(discriminators));
- private readonly UnionMemberHoistBinder _memberHoists = memberHoists ?? throw new ArgumentNullException(nameof(memberHoists));
+ private readonly SchemaBindingContext _context = context;
- public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation,
- IReadOnlyDictionary typeNames, BindingErrorCollector errors)
+ public SchemaBindingResult Bind(GenerationCuration curation)
{
- ArgumentNullException.ThrowIfNull(document);
- ArgumentNullException.ThrowIfNull(reachable);
ArgumentNullException.ThrowIfNull(curation);
- ArgumentNullException.ThrowIfNull(typeNames);
- ArgumentNullException.ThrowIfNull(errors);
- var responseRoots = reachable.ResponseRootKeys.ToHashSet(_comparer);
- var requestKeys = reachable.RequestReachableKeys.ToHashSet(_comparer);
- var streamCauseKeys = reachable.StreamCauseKeys.ToHashSet(_comparer);
- var inhabitation = new SchemaInhabitationPolicy(document.Schemas);
- var typeBinder = new TypePlanBinder(document.Schemas, typeNames, errors);
- var context = new SchemaBindingContext(document, reachable, responseRoots, streamCauseKeys, typeNames, typeBinder, inhabitation, errors);
- var decisions = BindExplicitUnions(context, responseRoots);
- var errorUnion = new ErrorUnionBinder(context).Bind();
+ var document = _context.Document;
+ var reachable = _context.Reachable;
+ var typeNames = _context.TypeNames;
+ var errors = _context.Errors;
+ var responseRoots = _context.ResponseRootKeys;
+ var requestKeys = reachable.RequestReachableKeys.ToHashSet(StringComparer.Ordinal);
+ var streamCauseKeys = _context.StreamCauseKeys;
+ var inhabitation = _context.Inhabitation;
+ var decisions = BindExplicitUnions();
+ var errorUnion = new ErrorUnionBinder(_context).Bind();
decisions.Add(errorUnion.Union);
var graph = UnionInheritanceGraph.Create(decisions, streamCauseKeys, errors);
- var objects = new ObjectModelBinder(context);
+ var objects = new ObjectModelBinder(_context);
var models = new List();
foreach (var key in reachable.GraphKeys)
{
@@ -66,7 +52,7 @@ public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reacha
break;
case UnionNode { Classification: UnionClassification.Structural } structural
when UnstructuredUnionPolicy.Collapse(structural, document.Schemas) is null:
- var structuralPlan = _structuralUnions.Bind(key, name, structural, document.Schemas, typeBinder, errors);
+ var structuralPlan = StructuralUnionPlanBinder.Bind(_context, key, name, structural);
if (structuralPlan is not null)
{
models.Add(structuralPlan);
@@ -76,12 +62,12 @@ when UnstructuredUnionPolicy.Collapse(structural, document.Schemas) is null:
}
}
- var orderedModels = models.OrderBy(static model => model.Name, _comparer).ToArray();
- var orderedUnions = graph.Unions.OrderBy(static union => union.Name, _comparer).ToArray();
- _unionMemberships.Validate([.. orderedModels.OfType()], orderedUnions, errors);
+ var orderedModels = models.OrderBy(static model => model.Name, StringComparer.Ordinal).ToArray();
+ var orderedUnions = graph.Unions.OrderBy(static union => union.Name, StringComparer.Ordinal).ToArray();
+ UnionMembershipValidator.Validate([.. orderedModels.OfType()], orderedUnions, errors);
// Hoisting adds members after membership validation. Its interfaces never enter the
// registry because no converter or source-generated metadata materializes them.
- var hoisted = _memberHoists.Bind(orderedUnions, orderedModels, curation, errors);
+ var hoisted = UnionMemberHoistBinder.Bind(orderedUnions, orderedModels, curation, errors);
orderedModels = [.. hoisted.Models];
orderedUnions = [.. hoisted.Unions];
return new SchemaBindingResult
@@ -99,16 +85,16 @@ when UnstructuredUnionPolicy.Collapse(structural, document.Schemas) is null:
};
}
- private List BindExplicitUnions(SchemaBindingContext context, HashSet responseRoots)
+ private List BindExplicitUnions()
{
- var markedBinder = new MarkedUnionBinder(context, _discriminators);
+ var markedBinder = new MarkedUnionBinder(_context);
var decisions = new List();
- foreach (var key in context.Reachable.GraphKeys)
+ foreach (var key in _context.Reachable.GraphKeys)
{
- if (!responseRoots.Contains(key)
- && context.Document.Schemas.TryGetValue(key, out var schema)
+ if (!_context.ResponseRootKeys.Contains(key)
+ && _context.Document.Schemas.TryGetValue(key, out var schema)
&& schema is UnionNode { Classification: UnionClassification.Marked } union
- && context.TypeNames.TryGetValue(key, out var name))
+ && _context.TypeNames.TryGetValue(key, out var name))
{
decisions.Add(markedBinder.Bind(key, name, union));
}
@@ -118,7 +104,7 @@ private List BindExplicitUnions(SchemaBindingContext context
}
/// The materializable types the serializer registry must carry, never hoisted interfaces.
- private string[] ComposeRegistryNames(IReadOnlyList models, IReadOnlyList unions) =>
+ private static string[] ComposeRegistryNames(IReadOnlyList models, IReadOnlyList unions) =>
[
.. models
.Select(static model => model.Name)
@@ -128,8 +114,8 @@ .. models
.Where(static arm => arm.Type.IsCollection)
.Select(static arm => TypeReferenceNamePolicy.Format(arm.Type)))
.Concat(unions.SelectMany(static union => new[] { union.Name, union.UnknownTypeName, }))
- .Distinct(_comparer)
- .Order(_comparer),
+ .Distinct(StringComparer.Ordinal)
+ .Order(StringComparer.Ordinal),
];
/// Each value's member name comes from .
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs
index ea8756d11..2c53bbad2 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs
@@ -5,25 +5,8 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class SpecBinder(
- ReachableSchemaCollector reachableSchemas,
- StabilizeDuplicatePolicy stabilizeDuplicates,
- CurationValidator curationValidator,
- SchemaAliasValidator schemaAliasValidator,
- SchemaAliasApplier schemaAliases,
- SchemaNameResolver schemaNames,
- SchemaPlanBinder schemaPlans,
- OperationPlanBinder operationPlans) : ISpecBinder
+internal sealed class SpecBinder : ISpecBinder
{
- private readonly CurationValidator _curationValidator = curationValidator ?? throw new ArgumentNullException(nameof(curationValidator));
- private readonly OperationPlanBinder _operationPlans = operationPlans ?? throw new ArgumentNullException(nameof(operationPlans));
- private readonly ReachableSchemaCollector _reachableSchemas = reachableSchemas ?? throw new ArgumentNullException(nameof(reachableSchemas));
- private readonly SchemaAliasApplier _schemaAliases = schemaAliases ?? throw new ArgumentNullException(nameof(schemaAliases));
- private readonly SchemaAliasValidator _schemaAliasValidator = schemaAliasValidator ?? throw new ArgumentNullException(nameof(schemaAliasValidator));
- private readonly SchemaNameResolver _schemaNames = schemaNames ?? throw new ArgumentNullException(nameof(schemaNames));
- private readonly SchemaPlanBinder _schemaPlans = schemaPlans ?? throw new ArgumentNullException(nameof(schemaPlans));
- private readonly StabilizeDuplicatePolicy _stabilizeDuplicates = stabilizeDuplicates ?? throw new ArgumentNullException(nameof(stabilizeDuplicates));
-
public EmitPlan Bind(SpecDocument document, OperationSelection selection, GenerationCuration curation)
{
ArgumentNullException.ThrowIfNull(document);
@@ -33,35 +16,36 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera
var errors = new BindingErrorCollector();
var operationsById = document.Operations.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal);
var selected = SelectOperations(selection, operationsById, errors);
- var reachable = _reachableSchemas.Collect(document, selected, errors);
+ var reachable = ReachableSchemaCollector.Collect(document, selected, errors);
CurationRowContract.ValidateBeforeAliases(curation, errors);
// Upstream stabilize's per-site duplicates collapse mechanically, ahead of curation:
// curated rows are then judged against the graph as it will be bound, and a row that
// repeats a mechanical fold is refused as redundant instead of shadowing it.
- var collapse = _stabilizeDuplicates.Resolve(document, reachable, curation.SchemaNames, errors);
- _curationValidator.Validate(document, selected, reachable, curation, errors);
- _schemaAliasValidator.Validate(document, reachable, curation, collapse, errors);
+ var collapse = StabilizeDuplicatePolicy.Resolve(document, reachable, curation.SchemaNames, errors);
+ CurationValidator.Validate(document, selected, reachable, curation, errors);
+ SchemaAliasValidator.Validate(document, reachable, curation, collapse, errors);
// Aliases are validated against the raw document above; the collapse happens before
// names resolve so the rest of the pipeline never sees the duplicates. Reachability
// recollects on the transformed document because an alias target's promoted inline
// children only become reachable through the rewritten references.
- var aliased = _schemaAliases.Apply(document, collapse, curation.SchemaAliases);
+ var aliased = SchemaAliasApplier.Apply(document, collapse, curation.SchemaAliases);
if (!ReferenceEquals(aliased, document))
{
document = aliased;
var aliasedById = document.Operations.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal);
selected = Array.AsReadOnly([.. selected.Select(operation => aliasedById[operation.OperationId])]);
- reachable = _reachableSchemas.Collect(document, selected, errors);
+ reachable = ReachableSchemaCollector.Collect(document, selected, errors);
}
CurationValidator.ValidateCollapsedSchemas(document, reachable, curation, collapse, errors);
// Type names are resolved exactly once per bind; schema and operation binding
// consume the same map so neither can reinterpret a schema under another name.
- var typeNames = _schemaNames.Resolve(document, reachable, selected, curation, errors);
- var schemaResult = _schemaPlans.Bind(document, reachable, curation, typeNames, errors);
- var clients = _operationPlans.Bind(document, selected, curation, schemaResult, errors);
+ var typeNames = SchemaNameResolver.Resolve(document, reachable, selected, curation, errors);
+ var schemaContext = SchemaBindingContext.Create(document, reachable, typeNames, errors);
+ var schemaResult = new SchemaPlanBinder(schemaContext).Bind(curation);
+ var clients = OperationPlanBinder.Bind(document, selected, curation, schemaResult, errors);
var candidates = new OwnershipCandidates
{
Models = schemaResult.Models,
@@ -69,22 +53,18 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera
HoistedInterfaces = schemaResult.HoistedInterfaces,
};
var inventory = GeneratedTypeNameInventory.Create(candidates, clients, schemaResult.HoistedClaims, typeNames);
- var refusedHoists = _operationPlans.CheckTypeNameCollisions(inventory, clients, errors);
+ var refusedHoists = OperationPlanBinder.CheckTypeNameCollisions(inventory, clients, errors);
var pending = PendingOperations(document, selected, curation);
- var models = AttachRequestQueryProperties(schemaResult.Models, clients, errors);
- models = BindSessionScopedEvents(document, typeNames, schemaResult with { Models = models }, clients,
- curation, SchemaAliasApplier.Compose(collapse, curation.SchemaAliases), errors);
-
- var masked = SecretMemberPolicy.MaskUnknownPayloads(SecretMemberPolicy.Apply(models, curation, errors), schemaResult.Unions);
+ var masked = BindModelFacets(schemaContext, schemaResult, clients, curation, collapse);
CheckDtoNameCollisions(inventory, errors);
candidates = candidates with { Models = masked.Models, Unions = masked.Unions, HoistedInterfaces = [.. candidates.HoistedInterfaces.Where(plan => !refusedHoists.Contains(plan.Name))] };
InternalNameCollisionCheck.Check(inventory, errors);
// An unplaced model never refuses a bind: it is the generate command's wall, so the
// pending-operation probe still judges bindability under its synthetic curation.
var placement = ModelOwnershipPolicy.Place(candidates, clients, curation, errors);
- var reach = ModelReachCollector.Collect(_reachableSchemas, new ReachScope(document, selected, clients, typeNames), candidates, errors);
- var experimental = ExperimentalModelPolicy.Collect(_reachableSchemas, new ReachScope(document, selected, clients, typeNames), candidates, inventory, errors);
+ var reach = ModelReachCollector.Collect(new ReachScope(document, selected, clients, typeNames), candidates, errors);
+ var experimental = ExperimentalModelPolicy.Collect(new ReachScope(document, selected, clients, typeNames), candidates, inventory, errors);
errors.ThrowIfAny();
var plan = new EmitPlan
{
@@ -106,6 +86,32 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera
return plan;
}
+ /// Attaches operation-owned model properties and session-event roles before masking secret payloads.
+ private static UnknownPayloadMaskResult BindModelFacets(SchemaBindingContext context, SchemaBindingResult schemas,
+ IReadOnlyList clients, GenerationCuration curation, StabilizeDuplicateCollapse collapse)
+ {
+ var models = AttachRequestQueryProperties(schemas.Models, clients, context.Errors);
+ if (curation.SessionScopedEvent is { } row)
+ {
+ // The row's identifier is read through the same alias map the schemas were bound with,
+ // so a stabilized duplicate of the identifier schema is the identifier too.
+ var aliases = SchemaAliasApplier.Compose(collapse, curation.SchemaAliases);
+ var identifier = !string.IsNullOrWhiteSpace(row.Identifier) && aliases.TryGetValue(row.Identifier, out var target)
+ ? target
+ : row.Identifier;
+ models = SessionScopedEventBinder.Bind(new SessionScopedEventBindingContext
+ {
+ Document = context.Document,
+ TypeNames = context.TypeNames,
+ Schemas = schemas with { Models = models },
+ Clients = clients,
+ Row = row with { Identifier = identifier },
+ }, context.Errors);
+ }
+
+ return SecretMemberPolicy.MaskUnknownPayloads(SecretMemberPolicy.Apply(models, curation, context.Errors), schemas.Unions);
+ }
+
/// Declined operations retain their reviewed reasons in ordinal identity order.
private static IReadOnlyList DeclinedOperations(GenerationCuration curation) =>
[
@@ -148,33 +154,6 @@ .. document
/// A merged operation's query properties ride its body model; the model plan gains
/// them here because schema binding cannot see operation placement.
///
- ///
- /// Marks the session-scoped event records when the curation names the rule. The row's identifier
- /// is read through the same alias map the schemas were bound with, so a stabilized duplicate of
- /// the identifier schema is the identifier too.
- ///
- private static IReadOnlyList BindSessionScopedEvents(SpecDocument document,
- IReadOnlyDictionary typeNames, SchemaBindingResult schemas, IReadOnlyList clients,
- GenerationCuration curation, IReadOnlyDictionary aliases, BindingErrorCollector errors)
- {
- if (curation.SessionScopedEvent is not { } row)
- {
- return schemas.Models;
- }
-
- var identifier = !string.IsNullOrWhiteSpace(row.Identifier) && aliases.TryGetValue(row.Identifier, out var target)
- ? target
- : row.Identifier;
- return SessionScopedEventBinder.Bind(new SessionScopedEventBindingContext
- {
- Document = document,
- TypeNames = typeNames,
- Schemas = schemas,
- Clients = clients,
- Row = row with { Identifier = identifier },
- }, errors);
- }
-
private static IReadOnlyList AttachRequestQueryProperties(IReadOnlyList models,
IReadOnlyList clients, BindingErrorCollector errors)
{
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs
index 1f30c0753..9321d81bb 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs
@@ -25,12 +25,10 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
/// ordinary curation and naming checks validate those rows after this policy resolves aliases. A
/// row naming a candidate that folds is refused as redundant.
///
-internal sealed class StabilizeDuplicatePolicy
+internal static class StabilizeDuplicatePolicy
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
/// Resolves aliases and reports non-equivalent candidates without explicit names.
- public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchemaSet reachable,
+ public static StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchemaSet reachable,
IReadOnlyList schemaNames, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(document);
@@ -44,8 +42,8 @@ public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchema
return StabilizeDuplicateCollapse.Empty;
}
- var explicitlyNamed = schemaNames.Select(static row => row.Schema).ToHashSet(_comparer);
- var responseRoots = reachable.ResponseRootKeys.ToHashSet(_comparer);
+ var explicitlyNamed = schemaNames.Select(static row => row.Schema).ToHashSet(StringComparer.Ordinal);
+ var responseRoots = reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal);
foreach (var (schema, baseKey) in ReduceToFixpoint(document, candidates))
{
// A reason-bearing name can preserve a distinct shape. The ordinary curation
@@ -67,7 +65,7 @@ public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchema
// refresh that made the pair equivalent is the moment to retire it, so it is refused
// rather than carried along silently (the schemaAliases validator treats a redundant
// alias row the same way).
- foreach (var (schema, baseKey) in candidates.OrderBy(static pair => pair.Key, _comparer))
+ foreach (var (schema, baseKey) in candidates.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
{
if (explicitlyNamed.Contains(schema))
{
@@ -82,9 +80,9 @@ public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchema
}
/// Every reachable component spelled as a stabilize duplicate of a base the graph declares.
- private Dictionary CollectCandidates(SpecDocument document, ReachableSchemaSet reachable)
+ private static Dictionary CollectCandidates(SpecDocument document, ReachableSchemaSet reachable)
{
- var candidates = new Dictionary(_comparer);
+ var candidates = new Dictionary(StringComparer.Ordinal);
foreach (var key in reachable.GraphKeys)
{
if (TryGetStabilizeBase(key, out var baseKey)
@@ -103,7 +101,7 @@ private Dictionary CollectCandidates(SpecDocument document, Reac
/// Drops every candidate that is not deeply equal to its base under the surviving map and
/// repeats until nothing drops, so a refusal cascades into the pairs that leaned on it.
///
- private IReadOnlyList> ReduceToFixpoint(SpecDocument document,
+ private static IReadOnlyList> ReduceToFixpoint(SpecDocument document,
Dictionary candidates)
{
var refused = new List>();
@@ -111,7 +109,7 @@ private IReadOnlyList> ReduceToFixpoint(SpecDocumen
do
{
dropped = false;
- foreach (var candidate in candidates.OrderBy(static pair => pair.Key, _comparer).ToArray())
+ foreach (var candidate in candidates.OrderBy(static pair => pair.Key, StringComparer.Ordinal).ToArray())
{
if (IsSpellingOfBase(document, candidate, candidates))
{
@@ -125,7 +123,7 @@ private IReadOnlyList> ReduceToFixpoint(SpecDocumen
}
while (dropped);
- return [.. refused.OrderBy(static pair => pair.Key, _comparer)];
+ return [.. refused.OrderBy(static pair => pair.Key, StringComparer.Ordinal)];
}
private static bool IsSpellingOfBase(SpecDocument document, KeyValuePair candidate,
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs
index 3ad16d9cc..84e19c5f0 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs
@@ -4,35 +4,30 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class StructuralUnionPlanBinder
+internal static class StructuralUnionPlanBinder
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public StructuralUnionModelPlan? Bind(string key, string name, UnionNode union,
- IReadOnlyDictionary graph, TypePlanBinder typeBinder, BindingErrorCollector errors)
+ public static StructuralUnionModelPlan? Bind(SchemaBindingContext context, string key, string name, UnionNode union)
{
+ ArgumentNullException.ThrowIfNull(context);
ArgumentException.ThrowIfNullOrWhiteSpace(key);
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(union);
- ArgumentNullException.ThrowIfNull(graph);
- ArgumentNullException.ThrowIfNull(typeBinder);
- ArgumentNullException.ThrowIfNull(errors);
-
- var scope = new ArmScope(key, typeBinder, errors);
- var arms = new List(union.Branches.Count);
- var claimedTokens = new HashSet();
+ var graph = context.Document.Schemas;
+ var errors = context.Errors;
+ var scope = new ArmScope(key, context.Types, errors);
+ var arms = scope.Arms;
var objectArms = new List();
var inhabitableBranchCount = 0;
foreach (var (branch, index) in union.Branches.Select(static (branch, index) => (branch, index)))
{
- var resolved = Resolve(branch, graph, []);
+ var resolved = RefChain.Resolve(branch, graph).Node;
if (resolved is NeverNode)
{
continue;
}
inhabitableBranchCount++;
- var arm = BindArm(scope, branch, resolved, index, arms, claimedTokens, sharesObjectToken: objectArms.Count > 0);
+ var arm = BindArm(scope, branch, resolved, index, sharesObjectToken: objectArms.Count > 0);
if (arm is null)
{
continue;
@@ -44,7 +39,7 @@ internal sealed class StructuralUnionPlanBinder
}
arms.Add(arm);
- claimedTokens.UnionWith(arm.Tokens);
+ scope.ClaimedTokens.UnionWith(arm.Tokens);
}
if (arms.Count < 2 || arms.Count != inhabitableBranchCount)
@@ -73,8 +68,8 @@ internal sealed class StructuralUnionPlanBinder
/// a dictionary, a free-form object, a marked union - takes every object, so a neighbour
/// there is ambiguous and stays refused.
///
- private StructuralUnionArmPlan? BindArm(ArmScope scope, SchemaNode branch, SchemaNode resolved, int index,
- IReadOnlyList arms, HashSet claimedTokens, bool sharesObjectToken)
+ private static StructuralUnionArmPlan? BindArm(ArmScope scope, SchemaNode branch, SchemaNode resolved, int index,
+ bool sharesObjectToken)
{
var armNameSubject = index.ToString(System.Globalization.CultureInfo.InvariantCulture);
if (!TryGetTokens(resolved, out var tokens))
@@ -87,13 +82,13 @@ internal sealed class StructuralUnionPlanBinder
var firstMatch = resolved is ObjectNode && sharesObjectToken;
var effective = firstMatch
? branch
- : ResolveEffectiveBranch(branch, resolved, tokens, claimedTokens, scope.Key, index, scope.Errors);
+ : ResolveEffectiveBranch(scope, branch, resolved, tokens, index);
if (effective is null)
{
return null;
}
- JsonTokenType[] effectiveTokens = firstMatch ? [.. tokens] : [.. tokens.Where(token => !claimedTokens.Contains(token))];
+ JsonTokenType[] effectiveTokens = firstMatch ? [.. tokens] : [.. tokens.Where(token => !scope.ClaimedTokens.Contains(token))];
if (!ValidateSpecialNumberArm(resolved, effectiveTokens, scope.Key, scope.Errors))
{
return null;
@@ -106,7 +101,7 @@ internal sealed class StructuralUnionPlanBinder
}
var armName = ArmName(type);
- return ValidateArmName(armName, arms, scope.Key, scope.Errors)
+ return ValidateArmName(armName, scope.Arms, scope.Key, scope.Errors)
? new StructuralUnionArmPlan { Name = armName, Type = type, Tokens = effectiveTokens }
: null;
}
@@ -176,7 +171,7 @@ private static bool TryClaimObjectArms(List arms, IReadO
var valid = true;
foreach (var property in objectArm.Node.Properties)
{
- switch (Resolve(property.Schema, graph, []))
+ switch (RefChain.Resolve(property.Schema, graph).Node)
{
case LiteralNode { Kind: LiteralKind.Number }:
errors.Add(BindingErrorCategory.Schema, key, string.Create(
@@ -216,7 +211,11 @@ private static bool Subsumes(StructuralObjectClaimPlan earlier, StructuralObject
private sealed record ObjectArm(int ArmIndex, int BranchIndex, ObjectNode Node);
- private sealed record ArmScope(string Key, TypePlanBinder TypeBinder, BindingErrorCollector Errors);
+ private sealed record ArmScope(string Key, TypePlanBinder TypeBinder, BindingErrorCollector Errors)
+ {
+ public List Arms { get; } = [];
+ public HashSet ClaimedTokens { get; } = [];
+ }
private static StructuralUnionModelPlan CreatePlan(string name, UnionNode union,
IReadOnlyList arms) =>
@@ -258,7 +257,7 @@ private static bool ValidateNotBinaryArm(TypeReferencePlan type, string armNameS
return false;
}
- private bool ValidateArmName(string armName, IReadOnlyList arms, string key,
+ private static bool ValidateArmName(string armName, IReadOnlyList arms, string key,
BindingErrorCollector errors)
{
if (armName is "Kind" or "Unknown")
@@ -268,7 +267,7 @@ private bool ValidateArmName(string armName, IReadOnlyList _comparer.Equals(arm.Name, armName)))
+ if (!arms.Any(arm => StringComparer.Ordinal.Equals(arm.Name, armName)))
{
return true;
}
@@ -278,17 +277,16 @@ private bool ValidateArmName(string armName, IReadOnlyList tokens, HashSet claimedTokens, string key, int index,
- BindingErrorCollector errors)
+ private static SchemaNode? ResolveEffectiveBranch(ArmScope scope, SchemaNode original, SchemaNode resolved,
+ IReadOnlyList tokens, int index)
{
- var overlap = tokens.Where(claimedTokens.Contains).ToArray();
+ var overlap = tokens.Where(scope.ClaimedTokens.Contains).ToArray();
if (overlap.Length is 0)
{
return original;
}
- var remaining = tokens.Where(token => !claimedTokens.Contains(token)).ToArray();
+ var remaining = tokens.Where(token => !scope.ClaimedTokens.Contains(token)).ToArray();
if (resolved is SpecialNumberNode
&& overlap is [JsonTokenType.String]
&& remaining is [JsonTokenType.Number])
@@ -302,22 +300,11 @@ private bool ValidateArmName(string armName, IReadOnlyList token.ToString()));
- errors.Add(BindingErrorCategory.Schema, key,
+ scope.Errors.Add(BindingErrorCategory.Schema, scope.Key,
$"structural union branch {index.ToString(System.Globalization.CultureInfo.InvariantCulture)} overlaps earlier branch token kind(s): {tokenNames}");
return null;
}
- private static SchemaNode Resolve(SchemaNode node, IReadOnlyDictionary graph, HashSet visited)
- {
- if (node is not RefNode reference || !visited.Add(reference.Target)
- || !graph.TryGetValue(reference.Target, out var target))
- {
- return node;
- }
-
- return Resolve(target, graph, visited);
- }
-
private static bool TryGetTokens(SchemaNode branch, out IReadOnlyList tokens)
{
tokens = branch switch
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs
index a85f17b25..8222391b4 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs
@@ -45,16 +45,16 @@ internal sealed class TypePlanBinder(
NeverNode => Refuse(subject, "never schemas cannot materialize a .NET value"),
SpecialNumberNode => SpecialNumber(),
NullableNode nullable => BindNullable(nullable, subject, aliases),
- JsonStringNode => Refuse(subject, "JSON-encoded strings are not supported by the M1 emitter"),
+ JsonStringNode => Refuse(subject, "JSON-encoded strings are not supported by the emitter"),
EncodedStringNode { ContentEncoding: "base64" } => Binary(),
EncodedStringNode encoded => Refuse(
subject,
$"content encoding '{encoded.ContentEncoding}' is not supported by the emitter; only base64 materializes as bytes"),
- TupleNode => Refuse(subject, "tuple schemas are not supported by the M1 emitter"),
+ TupleNode => Refuse(subject, "tuple schemas are not supported by the emitter"),
UnionNode { Classification: UnionClassification.Structural } union
when UnstructuredUnionPolicy.Collapse(union, _graph) is { } collapsed => BindCore(collapsed, subject, aliases),
ObjectNode or EnumNode or UnionNode => Refuse(subject, "inline nominal schema was not promoted into the graph"),
- _ => Refuse(subject, $"schema node '{schema.GetType().Name}' is not supported by the M1 emitter"),
+ _ => Refuse(subject, $"schema node '{schema.GetType().Name}' is not supported by the emitter"),
};
private TypeReferencePlan? BindReference(RefNode reference, string subject, HashSet aliases)
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs
index a79061ff3..7f77a25b0 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs
@@ -14,12 +14,10 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
/// branch that also carries an unrelated prefix marker — a templated identifier, say — stays
/// an ordinary variant, because that marker discriminates nothing.
///
-internal sealed class UnionDiscriminatorSelector
+internal static class UnionDiscriminatorSelector
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
/// Returns the discriminator every branch answers, or null when no candidate qualifies.
- public UnionDiscriminator? Select(IReadOnlyList branches)
+ public static UnionDiscriminator? Select(IReadOnlyList branches)
{
ArgumentNullException.ThrowIfNull(branches);
@@ -49,15 +47,15 @@ internal sealed class UnionDiscriminatorSelector
/// tags the property neither way disqualifies the candidate outright. Several prefix carriers
/// still qualify, so the arity refusal can name them.
///
- private UnionDiscriminator? Qualify(IReadOnlyList branches, string property)
+ private static UnionDiscriminator? Qualify(IReadOnlyList branches, string property)
{
var literalBranches = new List(branches.Count);
var prefixBranches = new List();
- var values = new HashSet(_comparer);
+ var values = new HashSet(StringComparer.Ordinal);
LiteralMarker? marker = null;
foreach (var branch in branches)
{
- var literal = branch.Markers.FirstOrDefault(candidate => _comparer.Equals(candidate.PropertyName, property));
+ var literal = branch.Markers.FirstOrDefault(candidate => StringComparer.Ordinal.Equals(candidate.PropertyName, property));
if (literal is null)
{
if (branch.PrefixMarkers.Count is 0)
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs
index b79ad570b..c4cdce279 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs
@@ -14,11 +14,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding;
/// outer promise instead of hiding it. A union with fewer than two members shares nothing across
/// arms and is left alone.
///
-internal sealed class UnionMemberHoistBinder
+internal static class UnionMemberHoistBinder
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList models,
+ public static UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList models,
GenerationCuration curation, BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(unions);
@@ -27,7 +25,7 @@ public UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList>(_comparer);
+ var hoisted = new Dictionary>(StringComparer.Ordinal);
foreach (var union in OrderByChainDepth(unions, context.Unions))
{
hoisted[union.Name] = HoistUnion(union, context, hoisted);
@@ -43,17 +41,17 @@ public UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList unions, IReadOnlyList models,
+ private static HoistContext CreateContext(IReadOnlyList unions, IReadOnlyList models,
GenerationCuration curation, BindingErrorCollector errors)
{
- var objectModels = models.OfType().ToDictionary(static model => model.Name, _comparer);
+ var objectModels = models.OfType().ToDictionary(static model => model.Name, StringComparer.Ordinal);
var implementations = new HoistedImplementationAccumulator();
// One policy instance answers both the union interface and every carrier interface, so the
// two can never disagree about which record property satisfies a declared member.
var satisfaction = new HoistedMemberSatisfactionPolicy(
- models.OfType().Select(static model => model.Name).ToHashSet(_comparer));
+ models.OfType().Select(static model => model.Name).ToHashSet(StringComparer.Ordinal));
return new HoistContext(
- unions.ToDictionary(static union => union.Name, _comparer),
+ unions.ToDictionary(static union => union.Name, StringComparer.Ordinal),
objectModels,
new HoistedMemberIdentityPolicy(objectModels),
satisfaction,
@@ -62,7 +60,7 @@ private HoistContext CreateContext(IReadOnlyList unions, IReadOnlyLis
errors);
}
- private ReadOnlyCollection HoistUnion(UnionPlan union, HoistContext context,
+ private static ReadOnlyCollection HoistUnion(UnionPlan union, HoistContext context,
IReadOnlyDictionary> hoisted)
{
var result = new List();
@@ -188,10 +186,10 @@ private static void RecordImplementations(UnionPlan union, List
}
}
- private HashSet InheritedNames(UnionPlan union, IReadOnlyDictionary unions,
+ private static HashSet InheritedNames(UnionPlan union, IReadOnlyDictionary unions,
IReadOnlyDictionary> hoisted)
{
- var result = new HashSet(_comparer) { union.MarkerName };
+ var result = new HashSet(StringComparer.Ordinal) { union.MarkerName };
for (var current = union.BaseTypeName; current is not null && unions.TryGetValue(current, out var outer);
current = outer.BaseTypeName)
{
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs
index 76b8af22c..ba26576a8 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs
@@ -3,18 +3,16 @@
namespace OpenCode.Sdk.Tools.Generator.Binding;
-internal sealed class UnionMembershipValidator
+internal static class UnionMembershipValidator
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public void Validate(IReadOnlyList models, IReadOnlyList unions,
+ public static void Validate(IReadOnlyList models, IReadOnlyList unions,
BindingErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(models);
ArgumentNullException.ThrowIfNull(unions);
ArgumentNullException.ThrowIfNull(errors);
- var unionsByName = unions.ToDictionary(static union => union.Name, _comparer);
+ var unionsByName = unions.ToDictionary(static union => union.Name, StringComparer.Ordinal);
foreach (var model in models)
{
ValidateModel(model, unionsByName, errors);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs
index c5ec18601..c21665083 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs
@@ -15,7 +15,7 @@ internal static class UnstructuredUnionPolicy
{
ArgumentNullException.ThrowIfNull(union);
- var kinds = union.Branches.Select(branch => PrimitiveKindOf(branch, graph, [])).ToArray();
+ var kinds = union.Branches.Select(branch => PrimitiveKindOf(branch, graph)).ToArray();
if (kinds.Length > 1 && kinds[0] is { } first && Array.TrueForAll(kinds, kind => kind == first))
{
return new PrimitiveNode
@@ -31,12 +31,11 @@ internal static class UnstructuredUnionPolicy
: null;
}
- private static PrimitiveKind? PrimitiveKindOf(SchemaNode branch, IReadOnlyDictionary? graph, HashSet visited)
+ private static PrimitiveKind? PrimitiveKindOf(SchemaNode branch, IReadOnlyDictionary? graph)
{
- if (branch is RefNode reference && graph is not null && visited.Add(reference.Target)
- && graph.TryGetValue(reference.Target, out var target))
+ if (graph is not null)
{
- return PrimitiveKindOf(target, graph, visited);
+ branch = RefChain.Resolve(branch, graph).Node;
}
return branch switch
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs
index de6411789..2812edca1 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs
@@ -31,7 +31,7 @@ public static EmitPlan Apply(EmitPlan plan)
continue;
}
- // An event record used anywhere besides its unions has a second role and keeps today's text.
+ // An event record used besides its unions keeps the fallback summary because its wire role is not unique.
if (uses.GetValueOrDefault(model.Name) is 1)
{
Add(candidates, model.Name, $"The '{tag}' event.");
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs
index 55692d644..3c4470a20 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs
@@ -8,13 +8,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
/// Projects the typed semantics carried by an x-effect-stream extension.
internal sealed class EffectStreamProjector
{
- private readonly GraphKeyBuilder _keys;
private readonly SchemaProjector _schemas;
- public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys)
+ public EffectStreamProjector(SchemaProjector schemas)
{
_schemas = schemas ?? throw new ArgumentNullException(nameof(schemas));
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
}
public SpecEffectStreamContract? Project(JsonNode? source, string root, string mediaPointer, ProjectionState state)
@@ -28,7 +26,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys)
return null;
}
- var pointer = _keys.Append(mediaPointer, "x-effect-stream");
+ var pointer = GraphKeyBuilder.Append(mediaPointer, "x-effect-stream");
var location = string.Concat(root, pointer);
if (source is not JsonObject extension)
{
@@ -52,7 +50,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys)
return null;
}
- var schemaPointer = _keys.Append(pointer, name);
+ var schemaPointer = GraphKeyBuilder.Append(pointer, name);
var location = string.Concat(root, schemaPointer);
if (raw is null)
{
@@ -85,7 +83,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys)
return _schemas.Project(parsed, root, schemaPointer, state);
}
- private string? ReadString(JsonObject extension, string name, string root, string pointer, IngestionErrorCollector errors)
+ private static string? ReadString(JsonObject extension, string name, string root, string pointer, IngestionErrorCollector errors)
{
if (!extension.TryGetPropertyValue(name, out var raw))
{
@@ -97,7 +95,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys)
return text;
}
- errors.Add(string.Concat(root, _keys.Append(pointer, name)), $"'{name}' must contain a string");
+ errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, name)), $"'{name}' must contain a string");
return null;
}
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs
index 15bf310e7..2274b2ab7 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs
@@ -4,9 +4,9 @@
namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
-internal sealed class EnumSchemaProjector
+internal static class EnumSchemaProjector
{
- public EnumNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors)
+ public static EnumNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(schema);
ArgumentException.ThrowIfNullOrWhiteSpace(location);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs
index 00eaea0a0..7ba8b40c0 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs
@@ -7,11 +7,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
/// Any other shape classifies as no error, and the binder refuses it wherever an error response
/// references it.
///
-internal sealed class ErrorStyleClassifier
+internal static class ErrorStyleClassifier
{
public const string WireName = "_tag";
- public ErrorStyle Classify(IReadOnlyList properties)
+ public static ErrorStyle Classify(IReadOnlyList properties)
{
ArgumentNullException.ThrowIfNull(properties);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs
index 1a50b67d7..8a71151f4 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs
@@ -3,16 +3,16 @@
namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
-internal sealed class GraphKeyBuilder
+internal static class GraphKeyBuilder
{
- public string Root(string wireNameOrOpId)
+ public static string Root(string wireNameOrOpId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(wireNameOrOpId);
return wireNameOrOpId;
}
- public string Append(string parentPointer, string segment)
+ public static string Append(string parentPointer, string segment)
{
ArgumentNullException.ThrowIfNull(parentPointer);
ArgumentNullException.ThrowIfNull(segment);
@@ -24,7 +24,7 @@ public string Append(string parentPointer, string segment)
return $"{parentPointer}/{escaped}";
}
- public string UnionBranch(string parentPointer, string keyword, int index, LiteralMarker? marker)
+ public static string UnionBranch(string parentPointer, string keyword, int index, LiteralMarker? marker)
{
ArgumentNullException.ThrowIfNull(parentPointer);
ArgumentException.ThrowIfNullOrWhiteSpace(keyword);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs
index cc420aa67..3ccec4865 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs
@@ -5,13 +5,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
internal sealed class JsonStringSchemaProjector
{
- private readonly GraphKeyBuilder _keys;
private readonly SchemaProjector _schemaProjector;
- public JsonStringSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys)
+ public JsonStringSchemaProjector(SchemaProjector schemaProjector)
{
_schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector));
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
}
public JsonStringNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state)
@@ -24,7 +22,7 @@ public JsonStringSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilde
if (!string.Equals(schema.ContentMediaType, "application/json", StringComparison.Ordinal))
{
- state.Errors.Add(string.Concat(root, _keys.Append(pointer, "contentMediaType")), "contentMediaType must be 'application/json'");
+ state.Errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, "contentMediaType")), "contentMediaType must be 'application/json'");
return null;
}
@@ -34,7 +32,7 @@ public JsonStringSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilde
return null;
}
- var inner = _schemaProjector.Project(schema.ContentSchema, root, _keys.Append(pointer, "contentSchema"), state);
+ var inner = _schemaProjector.Project(schema.ContentSchema, root, GraphKeyBuilder.Append(pointer, "contentSchema"), state);
return inner is null
? null
: new JsonStringNode
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs
index 9f8f3da34..a11c43cd6 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs
@@ -4,21 +4,15 @@
namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
-internal sealed class LiteralClassifier
+internal static class LiteralClassifier
{
- public LiteralMarker? FindFirstMarker(IOpenApiSchema schema)
+ public static LiteralMarker? FindFirstMarker(IOpenApiSchema schema)
{
ArgumentNullException.ThrowIfNull(schema);
- var candidate = schema is OpenApiSchemaReference reference ? reference.Target : schema;
- if (candidate is not OpenApiSchema { Properties: not null, Required: not null } objectSchema)
+ foreach (var property in RequiredProperties(schema))
{
- return null;
- }
-
- foreach (var property in objectSchema.Properties)
- {
- if (!objectSchema.Required.Contains(property.Key) || property.Value is not OpenApiSchema propertySchema)
+ if (property.Value is not OpenApiSchema propertySchema)
{
continue;
}
@@ -33,20 +27,13 @@ internal sealed class LiteralClassifier
return null;
}
- public PrefixMarker? FindFirstPrefixMarker(IOpenApiSchema schema)
+ public static PrefixMarker? FindFirstPrefixMarker(IOpenApiSchema schema)
{
ArgumentNullException.ThrowIfNull(schema);
- var candidate = schema is OpenApiSchemaReference reference ? reference.Target : schema;
- if (candidate is not OpenApiSchema { Properties: not null, Required: not null } objectSchema)
- {
- return null;
- }
-
- foreach (var property in objectSchema.Properties)
+ foreach (var property in RequiredProperties(schema))
{
- if (!objectSchema.Required.Contains(property.Key)
- || property.Value is not OpenApiSchema { Type: JsonSchemaType.String, Enum: null, Const: null, Format: null, Pattern: { } pattern }
+ if (property.Value is not OpenApiSchema { Type: JsonSchemaType.String, Enum: null, Const: null, Format: null, Pattern: { } pattern }
|| TemplateLiteralPrefixPolicy.TryDecodePrefix(pattern) is not { } prefix)
{
continue;
@@ -58,7 +45,15 @@ internal sealed class LiteralClassifier
return null;
}
- public LiteralNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors)
+ private static IEnumerable> RequiredProperties(IOpenApiSchema schema)
+ {
+ var candidate = schema is OpenApiSchemaReference reference ? reference.Target : schema;
+ return candidate is OpenApiSchema { Properties: not null, Required: not null } objectSchema
+ ? objectSchema.Properties.Where(property => objectSchema.Required.Contains(property.Key))
+ : [];
+ }
+
+ public static LiteralNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors)
{
ArgumentNullException.ThrowIfNull(schema);
ArgumentException.ThrowIfNullOrWhiteSpace(location);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs
index 2c3bdac03..48b8770a2 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs
@@ -5,6 +5,14 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
internal static class MediaTypeProjector
{
+ internal static MediaTypeProjection? ProjectMediaEntry(KeyValuePair mediaEntry, string root, string pointer,
+ ProjectionState state, out string mediaPointer)
+ {
+ mediaPointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "content"), mediaEntry.Key);
+ var mediaLocation = string.Concat(root, mediaPointer);
+ return Project(mediaEntry.Key, mediaEntry.Value, mediaLocation, state.Errors);
+ }
+
public static MediaTypeProjection? Project(string raw, IOpenApiMediaType media, string location, IngestionErrorCollector errors)
{
ArgumentException.ThrowIfNullOrWhiteSpace(raw);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs
index 4dfc91d91..300031f4b 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs
@@ -5,15 +5,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
internal sealed class ObjectSchemaProjector
{
- private readonly ErrorStyleClassifier _errorStyleClassifier;
- private readonly GraphKeyBuilder _keys;
private readonly SchemaProjector _schemaProjector;
- public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys, ErrorStyleClassifier errorStyleClassifier)
+ public ObjectSchemaProjector(SchemaProjector schemaProjector)
{
_schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector));
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
- _errorStyleClassifier = errorStyleClassifier ?? throw new ArgumentNullException(nameof(errorStyleClassifier));
}
public SchemaNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state)
@@ -36,7 +32,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke
if (schema.AdditionalProperties is not null)
{
- return ProjectDictionary(schema, schema.AdditionalProperties, root, _keys.Append(pointer, "additionalProperties"), state);
+ return ProjectDictionary(schema, schema.AdditionalProperties, root, GraphKeyBuilder.Append(pointer, "additionalProperties"), state);
}
return new FreeFormObjectNode
@@ -69,7 +65,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke
}
var pattern = patterns.Single();
- var valuePointer = _keys.Append(_keys.Append(pointer, "patternProperties"), pattern.Key);
+ var valuePointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "patternProperties"), pattern.Key);
return ProjectDictionary(schema, pattern.Value, root, valuePointer, state);
}
@@ -93,7 +89,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke
var missingRequired = schema.Required?.Where(name => !properties.ContainsKey(name)).ToArray() ?? [];
if (missingRequired.Length > 0)
{
- var requiredLocation = string.Concat(root, _keys.Append(pointer, "required"));
+ var requiredLocation = string.Concat(root, GraphKeyBuilder.Append(pointer, "required"));
foreach (var missing in missingRequired)
{
state.Errors.Add(requiredLocation, $"required property '{missing}' is not declared in properties");
@@ -105,7 +101,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke
var projectedProperties = new List(properties.Count);
foreach (var property in properties)
{
- var projected = _schemaProjector.Project(property.Value, root, _keys.Append(_keys.Append(pointer, "properties"), property.Key), state);
+ var projected = _schemaProjector.Project(property.Value, root, GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "properties"), property.Key), state);
if (projected is null)
{
continue;
@@ -157,7 +153,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke
AdditionalPropertiesSchema = additionalPropertiesSchema,
LiteralMarkers = markers,
PrefixMarkers = prefixMarkers,
- ErrorStyle = _errorStyleClassifier.Classify(projectedProperties),
+ ErrorStyle = ErrorStyleClassifier.Classify(projectedProperties),
};
}
@@ -166,16 +162,16 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke
/// StructWithRest encoding (); the pointer names where the
/// document really says it, so a promoted value keeps an honest graph key.
///
- private (IOpenApiSchema? Schema, string? Pointer) ResolveAdditionalProperties(OpenApiSchema schema, string pointer)
+ private static (IOpenApiSchema? Schema, string? Pointer) ResolveAdditionalProperties(OpenApiSchema schema, string pointer)
{
if (schema.AdditionalProperties is { } own)
{
- return (own, _keys.Append(pointer, "additionalProperties"));
+ return (own, GraphKeyBuilder.Append(pointer, "additionalProperties"));
}
if (RestWrapperPolicy.TryGetRest(schema) is { } rest)
{
- return (rest, _keys.Append(_keys.Append(_keys.Append(pointer, "allOf"), "0"), "additionalProperties"));
+ return (rest, GraphKeyBuilder.Append(GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "allOf"), "0"), "additionalProperties"));
}
return (null, null);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs
index c76b012fa..d4a546ae1 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs
@@ -7,20 +7,17 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
internal sealed class OperationProjector
{
- private readonly GraphKeyBuilder _keys;
private readonly ParameterProjector _parameters;
- private readonly PathTemplateValidator _pathTemplates = new();
private readonly RequestBodyProjector _requestBodies;
private readonly ResponseProjector _responses;
- public OperationProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys)
+ public OperationProjector(SchemaProjector schemaProjector)
{
ArgumentNullException.ThrowIfNull(schemaProjector);
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
- _parameters = new ParameterProjector(schemaProjector, keys);
- _requestBodies = new RequestBodyProjector(schemaProjector, keys);
- _responses = new ResponseProjector(schemaProjector, keys, new EnvelopeClassifier());
+ _parameters = new ParameterProjector(schemaProjector);
+ _requestBodies = new RequestBodyProjector(schemaProjector);
+ _responses = new ResponseProjector(schemaProjector, new EnvelopeClassifier());
}
public IReadOnlyList Project(LoadedSpec loaded, ProjectionState state,
@@ -122,9 +119,9 @@ private void ProjectOperation(string path, KeyValuePair Project(OpenApiOperation operation, JsonObject rawOperation, string root, ProjectionState state)
@@ -38,7 +36,7 @@ public IReadOnlyList Project(OpenApiOperation operation, JsonObje
var identities = new HashSet<(string Name, ParameterLocation Location)>();
for (var index = 0; index < parameters.Count; index++)
{
- var pointer = _keys.Append(_keys.Append(string.Empty, "parameters"), index.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ var pointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(string.Empty, "parameters"), index.ToString(System.Globalization.CultureInfo.InvariantCulture));
var location = string.Concat(root, pointer);
if (rawParameters[index] is not JsonObject rawParameter)
{
@@ -70,7 +68,7 @@ public IReadOnlyList Project(OpenApiOperation operation, JsonObje
continue;
}
- var schema = _schemaProjector.Project(parameter.Schema, root, _keys.Append(pointer, "schema"), state);
+ var schema = _schemaProjector.Project(parameter.Schema, root, GraphKeyBuilder.Append(pointer, "schema"), state);
if (schema is null || state.Errors.Count != errorCount)
{
continue;
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs
index a0ab5a711..c2186dd7b 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs
@@ -2,11 +2,9 @@
namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
-internal sealed class PathTemplateValidator
+internal static class PathTemplateValidator
{
- private readonly StringComparer _comparer = StringComparer.Ordinal;
-
- public void Check(string path, IReadOnlyList parameters, string location, IngestionErrorCollector errors)
+ public static void Check(string path, IReadOnlyList parameters, string location, IngestionErrorCollector errors)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
ArgumentNullException.ThrowIfNull(parameters);
@@ -17,14 +15,14 @@ public void Check(string path, IReadOnlyList parameters, string l
var declared = parameters
.Where(static parameter => parameter.Location is SpecParameterLocation.Path)
.Select(static parameter => parameter.Name)
- .ToHashSet(_comparer);
+ .ToHashSet(StringComparer.Ordinal);
- foreach (var token in tokens.Where(token => !declared.Contains(token)).Order(_comparer))
+ foreach (var token in tokens.Where(token => !declared.Contains(token)).Order(StringComparer.Ordinal))
{
errors.Add(location, $"path token '{token}' is not declared as a path parameter");
}
- foreach (var name in declared.Where(name => !tokens.Contains(name)).Order(_comparer))
+ foreach (var name in declared.Where(name => !tokens.Contains(name)).Order(StringComparer.Ordinal))
{
errors.Add(location, $"path parameter '{name}' is absent from the path template");
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs
index 94165285c..33496c04e 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs
@@ -7,13 +7,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
internal sealed class PrefixItemsAdapter
{
- private readonly GraphKeyBuilder _keys;
private readonly SchemaProjector _schemaProjector;
- public PrefixItemsAdapter(SchemaProjector schemaProjector, GraphKeyBuilder keys)
+ public PrefixItemsAdapter(SchemaProjector schemaProjector)
{
_schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector));
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
}
public SchemaNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state)
@@ -45,7 +43,7 @@ public PrefixItemsAdapter(SchemaProjector schemaProjector, GraphKeyBuilder keys)
var items = new List(prefixItems.Count);
for (var index = 0; index < prefixItems.Count; index++)
{
- var itemPointer = _keys.Append(_keys.Append(pointer, "prefixItems"), index.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ var itemPointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "prefixItems"), index.ToString(System.Globalization.CultureInfo.InvariantCulture));
var itemLocation = string.Concat(root, itemPointer);
if (prefixItems[index] is not { } rawItem)
{
@@ -115,24 +113,16 @@ public PrefixItemsAdapter(SchemaProjector schemaProjector, GraphKeyBuilder keys)
return null;
}
- var item = _schemaProjector.Project(schema.Items, root, _keys.Append(pointer, "items"), state);
- return item is null
- ? null
- : new ArrayNode
- {
- Description = schema.Description,
- Format = schema.Format,
- Item = item,
- };
+ return _schemaProjector.ProjectArrayItems(schema, itemsReference, root, pointer, state);
}
- private bool ValidateArity(OpenApiSchema schema, int arity, string root, string pointer, IngestionErrorCollector errors)
+ private static bool ValidateArity(OpenApiSchema schema, int arity, string root, string pointer, IngestionErrorCollector errors)
{
var isValid = true;
var arityText = arity.ToString(System.Globalization.CultureInfo.InvariantCulture);
if (schema.MinItems != arity)
{
- errors.Add(string.Concat(root, _keys.Append(pointer, "minItems")), $"minItems must equal tuple arity {arityText}");
+ errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, "minItems")), $"minItems must equal tuple arity {arityText}");
isValid = false;
}
@@ -141,7 +131,7 @@ private bool ValidateArity(OpenApiSchema schema, int arity, string root, string
return isValid;
}
- errors.Add(string.Concat(root, _keys.Append(pointer, "maxItems")), $"maxItems must equal tuple arity {arityText}");
+ errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, "maxItems")), $"maxItems must equal tuple arity {arityText}");
return false;
}
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs
index 58649020d..832cad5dc 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs
@@ -5,13 +5,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
internal sealed class RequestBodyProjector
{
- private readonly GraphKeyBuilder _keys;
private readonly SchemaProjector _schemaProjector;
- public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys)
+ public RequestBodyProjector(SchemaProjector schemaProjector)
{
_schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector));
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
}
public SpecRequestBody? Project(IOpenApiRequestBody? requestBody, string root, ProjectionState state)
@@ -24,7 +22,7 @@ public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder key
return null;
}
- var pointer = _keys.Append(string.Empty, "requestBody");
+ var pointer = GraphKeyBuilder.Append(string.Empty, "requestBody");
var location = string.Concat(root, pointer);
var errorCount = state.Errors.Count;
if (requestBody is not OpenApiRequestBody)
@@ -41,9 +39,7 @@ public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder key
}
var mediaEntry = content.Single();
- var mediaPointer = _keys.Append(_keys.Append(pointer, "content"), mediaEntry.Key);
- var mediaLocation = string.Concat(root, mediaPointer);
- var media = MediaTypeProjector.Project(mediaEntry.Key, mediaEntry.Value, mediaLocation, state.Errors);
+ var media = MediaTypeProjector.ProjectMediaEntry(mediaEntry, root, pointer, state, out var mediaPointer);
if (media is null)
{
return null;
@@ -51,11 +47,11 @@ public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder key
if (mediaEntry.Value.Schema is null)
{
- state.Errors.Add(mediaLocation, "request body media schema is required");
+ state.Errors.Add(string.Concat(root, mediaPointer), "request body media schema is required");
return null;
}
- var schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, _keys.Append(mediaPointer, "schema"), state);
+ var schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, GraphKeyBuilder.Append(mediaPointer, "schema"), state);
return schema is null || state.Errors.Count != errorCount
? null
: new SpecRequestBody
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs
index 6dc507c09..ad74bd705 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs
@@ -8,15 +8,13 @@ internal sealed class ResponseProjector
{
private readonly EnvelopeClassifier _envelopes;
private readonly EffectStreamProjector _effectStreams;
- private readonly GraphKeyBuilder _keys;
private readonly SchemaProjector _schemaProjector;
- public ResponseProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys, EnvelopeClassifier envelopes)
+ public ResponseProjector(SchemaProjector schemaProjector, EnvelopeClassifier envelopes)
{
_schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector));
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
_envelopes = envelopes ?? throw new ArgumentNullException(nameof(envelopes));
- _effectStreams = new EffectStreamProjector(_schemaProjector, _keys);
+ _effectStreams = new EffectStreamProjector(_schemaProjector);
}
public IReadOnlyList Project(OpenApiResponses? responses, string root, ProjectionState state)
@@ -33,7 +31,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r
var statusCodes = new HashSet();
foreach (var (key, response) in responses)
{
- var keyLocation = string.Concat(root, _keys.Append(_keys.Append(string.Empty, "responses"), key));
+ var keyLocation = string.Concat(root, GraphKeyBuilder.Append(GraphKeyBuilder.Append(string.Empty, "responses"), key));
if (!int.TryParse(key, NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode))
{
state.Errors.Add(keyLocation, $"response key '{key}' must be an integer status code");
@@ -65,7 +63,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r
private SpecResponse? ProjectResponse(int statusCode, IOpenApiResponse response, string root, ProjectionState state)
{
var status = statusCode.ToString(CultureInfo.InvariantCulture);
- var pointer = _keys.Append(_keys.Append(string.Empty, "responses"), status);
+ var pointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(string.Empty, "responses"), status);
var location = string.Concat(root, pointer);
var errorCount = state.Errors.Count;
if (response is not OpenApiResponse)
@@ -98,9 +96,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r
}
var mediaEntry = content.Single();
- var mediaPointer = _keys.Append(_keys.Append(pointer, "content"), mediaEntry.Key);
- var mediaLocation = string.Concat(root, mediaPointer);
- var media = MediaTypeProjector.Project(mediaEntry.Key, mediaEntry.Value, mediaLocation, state.Errors);
+ var media = MediaTypeProjector.ProjectMediaEntry(mediaEntry, root, pointer, state, out var mediaPointer);
if (media is null)
{
return null;
@@ -109,7 +105,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r
SchemaNode? schema = null;
if (mediaEntry.Value.Schema is not null)
{
- schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, _keys.Append(mediaPointer, "schema"), state);
+ schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, GraphKeyBuilder.Append(mediaPointer, "schema"), state);
}
var effectStream = _effectStreams.Project(media.EffectStream, root, mediaPointer, state);
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs
index 07176e67b..fcd4a8762 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs
@@ -6,25 +6,19 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection;
internal sealed class SchemaProjector
{
- private readonly GraphKeyBuilder _keys;
- private readonly EnumSchemaProjector _enumProjector;
private readonly JsonStringSchemaProjector _jsonStringProjector;
- private readonly LiteralClassifier _literalClassifier;
private readonly ObjectSchemaProjector _objectProjector;
private readonly PrefixItemsAdapter _prefixItemsAdapter;
private readonly UnionNormalizer _unionNormalizer;
private readonly SchemaShapeClassifier _shapeClassifier;
- public SchemaProjector(GraphKeyBuilder keys)
+ public SchemaProjector()
{
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
_shapeClassifier = new SchemaShapeClassifier();
- _enumProjector = new EnumSchemaProjector();
- _literalClassifier = new LiteralClassifier();
- _jsonStringProjector = new JsonStringSchemaProjector(this, _keys);
- _objectProjector = new ObjectSchemaProjector(this, _keys, new ErrorStyleClassifier());
- _unionNormalizer = new UnionNormalizer(this, _keys, _shapeClassifier, _literalClassifier);
- _prefixItemsAdapter = new PrefixItemsAdapter(this, _keys);
+ _jsonStringProjector = new JsonStringSchemaProjector(this);
+ _objectProjector = new ObjectSchemaProjector(this);
+ _unionNormalizer = new UnionNormalizer(this, _shapeClassifier);
+ _prefixItemsAdapter = new PrefixItemsAdapter(this);
}
public SchemaNode? Project(IOpenApiSchema schema, string root, string pointer, ProjectionState state)
@@ -78,8 +72,8 @@ public SchemaProjector(GraphKeyBuilder keys)
(CoreSchemaShape.Primitive, JsonSchemaType.Boolean) => CreatePrimitive(schema, PrimitiveKind.Boolean),
(CoreSchemaShape.Array, _) => ProjectArray(schema, root, pointer, state),
(CoreSchemaShape.Object, _) => _objectProjector.Project(schema, root, pointer, location, state),
- (CoreSchemaShape.Enum, _) => _enumProjector.Project(schema, location, state.Errors),
- (CoreSchemaShape.Literal, _) => _literalClassifier.Project(schema, location, state.Errors),
+ (CoreSchemaShape.Enum, _) => EnumSchemaProjector.Project(schema, location, state.Errors),
+ (CoreSchemaShape.Literal, _) => LiteralClassifier.Project(schema, location, state.Errors),
(CoreSchemaShape.Union, _) => _unionNormalizer.Project(schema, root, pointer, location, state),
(CoreSchemaShape.Null, _) => RefuseStandaloneNull(location, state),
(CoreSchemaShape.Tuple, _) => _prefixItemsAdapter.Project(schema, root, pointer, location, state),
@@ -120,7 +114,12 @@ private static SchemaNode ProjectNegation(OpenApiSchema schema) =>
};
}
- var item = Project(schema.Items, root, _keys.Append(pointer, "items"), state);
+ return ProjectArrayItems(schema, schema.Items, root, pointer, state);
+ }
+
+ internal ArrayNode? ProjectArrayItems(OpenApiSchema schema, IOpenApiSchema items, string root, string pointer, ProjectionState state)
+ {
+ var item = Project(items, root, GraphKeyBuilder.Append(pointer, "items"), state);
return item is null
? null
: new ArrayNode
diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs
index 53118720c..16c0bb352 100644
--- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs
+++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs
@@ -14,18 +14,13 @@ internal sealed class UnionNormalizer
"-Infinity",
};
- private readonly GraphKeyBuilder _keys;
- private readonly LiteralClassifier _literalClassifier;
private readonly SchemaProjector _schemaProjector;
private readonly SchemaShapeClassifier _shapeClassifier;
- public UnionNormalizer(SchemaProjector schemaProjector, GraphKeyBuilder keys, SchemaShapeClassifier shapeClassifier,
- LiteralClassifier literalClassifier)
+ public UnionNormalizer(SchemaProjector schemaProjector, SchemaShapeClassifier shapeClassifier)
{
_schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector));
- _keys = keys ?? throw new ArgumentNullException(nameof(keys));
_shapeClassifier = shapeClassifier ?? throw new ArgumentNullException(nameof(shapeClassifier));
- _literalClassifier = literalClassifier ?? throw new ArgumentNullException(nameof(literalClassifier));
}
public SchemaNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state)
@@ -107,7 +102,7 @@ public UnionNormalizer(SchemaProjector schemaProjector, GraphKeyBuilder keys, Sc
};
}
- private bool ValidateSpecialNumberBranches(IList branches, string root, string pointer, string keyword, ProjectionState state)
+ private static bool ValidateSpecialNumberBranches(IList branches, string root, string pointer, string keyword, ProjectionState state)
{
var errorCount = state.Errors.Count;
for (var index = 0; index < branches.Count; index++)
@@ -117,7 +112,7 @@ private bool ValidateSpecialNumberBranches(IList branches, strin
continue;
}
- var branchPointer = _keys.UnionBranch(pointer, keyword, index, marker: null);
+ var branchPointer = GraphKeyBuilder.UnionBranch(pointer, keyword, index, marker: null);
SchemaWallPolicy.Check(branch, string.Concat(root, branchPointer), state.Errors);
if (branch.AnyOf is not { Count: > 0 } nested)
{
@@ -131,7 +126,7 @@ private bool ValidateSpecialNumberBranches(IList branches, strin
continue;
}
- var innerPointer = _keys.UnionBranch(branchPointer, "anyOf", innerIndex, marker: null);
+ var innerPointer = GraphKeyBuilder.UnionBranch(branchPointer, "anyOf", innerIndex, marker: null);
SchemaWallPolicy.Check(inner, string.Concat(root, innerPointer), state.Errors);
}
}
@@ -147,10 +142,10 @@ private bool ValidateSpecialNumberBranches(IList branches, strin
for (var index = 0; index < sourceBranches.Count; index++)
{
var source = sourceBranches[index];
- var marker = _literalClassifier.FindFirstMarker(source);
- var prefixMarker = marker is null ? _literalClassifier.FindFirstPrefixMarker(source) : null;
- var isMarkedUnionReference = marker is null && IsMarkedUnionReference(source);
- var branchPointer = _keys.UnionBranch(pointer, keyword, index, marker);
+ var marker = LiteralClassifier.FindFirstMarker(source);
+ var prefixMarker = marker is null ? LiteralClassifier.FindFirstPrefixMarker(source) : null;
+ var isMarkedUnionReference = marker is null && ReferencesMarkedUnion(source);
+ var branchPointer = GraphKeyBuilder.UnionBranch(pointer, keyword, index, marker);
var branchLocation = string.Concat(root, branchPointer);
if (source is OpenApiSchema { Type: JsonSchemaType.Null } concrete)
{
@@ -187,11 +182,11 @@ private bool ValidateSpecialNumberBranches(IList branches, strin
/// of the referenced union is itself a marked object. The Binder validates that the
/// nested variants contribute one uniform outer tag; this records only markedness.
///
- private bool IsMarkedUnionReference(IOpenApiSchema source) =>
+ private static bool ReferencesMarkedUnion(IOpenApiSchema source) =>
source is OpenApiSchemaReference { Target: OpenApiSchema target }
&& (IsPureAnyOfBranch(target) || IsPureOneOfBranch(target))
&& (target.AnyOf is { Count: > 1 } ? target.AnyOf : target.OneOf) is { Count: > 1 } nested
- && nested.All(branch => _literalClassifier.FindFirstMarker(branch) is not null);
+ && nested.All(static branch => LiteralClassifier.FindFirstMarker(branch) is not null);
/// The oneOf twin of