From fd72353aacab1f2263d87e63a87d5e03adb5896e Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 00:43:18 +0200 Subject: [PATCH 1/7] Add granular artifact type-use contracts Establish independent artifact, member, type-use, binding, and member-role facts so adapters can contribute exact use-site semantics without replacing complete artifact definitions. Admission freezes and rejects malformed inputs atomically while preserving existing contracts. --- .../AdapterContributionAdmissionContracts.cs | 12 +- .../AdapterContributionAdmissionValidator.cs | 5 + .../AdapterContributionFreezer.cs | 40 +++ .../AdapterDescriptors.cs | 27 +- .../AdapterFactAdmissionValidator.cs | 15 + .../ArtifactDeclarations.cs | 84 ++++++ .../ArtifactMemberRoles.cs | 52 ++++ .../GranularFactAdmissionValidator.cs | 158 ++++++++++ .../GranularFactFreezer.cs | 284 ++++++++++++++++++ .../DotNET/Generation.Contracts/TypeUses.cs | 108 +++++++ .../given/a_contribution.cs | 74 ++++- .../when_admitting_a_valid_contribution.cs | 5 + ...itting_cross_adapter_subject_references.cs | 2 +- .../with_an_undeclared_fact_capability.cs | 2 +- .../with_inconsistent_ownership.cs | 74 ++++- .../with_invalid_granular_declarations.cs | 71 +++++ .../with_missing_kind_operands.cs | 12 +- .../with_null_granular_contracts.cs | 37 +++ .../when_mutating_admitted_input.cs | 12 +- ...reading_adapter_contract_discriminators.cs | 12 + .../when_validating_source_authority.cs | 6 +- 21 files changed, 1065 insertions(+), 27 deletions(-) create mode 100644 Source/DotNET/Generation.Contracts/ArtifactDeclarations.cs create mode 100644 Source/DotNET/Generation.Contracts/ArtifactMemberRoles.cs create mode 100644 Source/DotNET/Generation.Contracts/GranularFactAdmissionValidator.cs create mode 100644 Source/DotNET/Generation.Contracts/GranularFactFreezer.cs create mode 100644 Source/DotNET/Generation.Contracts/TypeUses.cs create mode 100644 Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_invalid_granular_declarations.cs create mode 100644 Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_null_granular_contracts.cs diff --git a/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionContracts.cs b/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionContracts.cs index 427237f..b5c547f 100644 --- a/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionContracts.cs +++ b/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionContracts.cs @@ -123,7 +123,17 @@ public enum AdapterContributionAdmissionDiagnosticCode /// /// A required API capability occurs more than once. /// - DuplicateApiCapability = 21 + DuplicateApiCapability = 21, + + /// + /// A type-use shape is empty or does not terminate in exactly one named type. + /// + InvalidTypeUseShape = 22, + + /// + /// An artifact member declaration has a negative declaration order. + /// + InvalidDeclarationOrder = 23 } /// diff --git a/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs b/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs index 62c1816..cc09f17 100644 --- a/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs +++ b/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs @@ -487,6 +487,11 @@ static bool IsDotSegment(string segment) SpecificationScenarioFact => GenerationFactCapability.SpecificationScenario, SpecificationStepFact => GenerationFactCapability.SpecificationStep, SpecificationValueFact => GenerationFactCapability.SpecificationValue, + ArtifactDeclarationFact => GenerationFactCapability.ArtifactDeclaration, + ArtifactMemberDeclarationFact => GenerationFactCapability.ArtifactMemberDeclaration, + ArtifactMemberTypeUseFact => GenerationFactCapability.ArtifactMemberTypeUse, + TypeUseBindingFact => GenerationFactCapability.TypeUseBinding, + ArtifactMemberRoleFact => GenerationFactCapability.ArtifactMemberRole, _ => GenerationFactCapability.Unknown }; } diff --git a/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs b/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs index 92ef1ef..20c5a11 100644 --- a/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs +++ b/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs @@ -195,6 +195,41 @@ .. frozen Artifact = FreezeArtifactKey(placement.Artifact, $"{path}.Artifact", context), Placement = FreezePlacement(placement.Placement, $"{path}.Placement", context) }, + ArtifactDeclarationFact declaration => GranularFactFreezer.Freeze( + declaration, + id, + subject, + evidence, + path, + context), + ArtifactMemberDeclarationFact member => GranularFactFreezer.Freeze( + member, + id, + subject, + evidence, + path, + context), + ArtifactMemberTypeUseFact typeUse => GranularFactFreezer.Freeze( + typeUse, + id, + subject, + evidence, + path, + context), + TypeUseBindingFact binding => GranularFactFreezer.Freeze( + binding, + id, + subject, + evidence, + path, + context), + ArtifactMemberRoleFact role => GranularFactFreezer.Freeze( + role, + id, + subject, + evidence, + path, + context), RelationshipFact relationship => new RelationshipFact { Id = id, @@ -852,6 +887,11 @@ static string StablePathComponent(string? value, string fallback) => SpecificationScenarioFact => (int)GenerationFactCapability.SpecificationScenario, SpecificationStepFact => (int)GenerationFactCapability.SpecificationStep, SpecificationValueFact => (int)GenerationFactCapability.SpecificationValue, + ArtifactDeclarationFact => (int)GenerationFactCapability.ArtifactDeclaration, + ArtifactMemberDeclarationFact => (int)GenerationFactCapability.ArtifactMemberDeclaration, + ArtifactMemberTypeUseFact => (int)GenerationFactCapability.ArtifactMemberTypeUse, + TypeUseBindingFact => (int)GenerationFactCapability.TypeUseBinding, + ArtifactMemberRoleFact => (int)GenerationFactCapability.ArtifactMemberRole, _ => int.MaxValue }; } diff --git a/Source/DotNET/Generation.Contracts/AdapterDescriptors.cs b/Source/DotNET/Generation.Contracts/AdapterDescriptors.cs index c8aad06..beb6e3d 100644 --- a/Source/DotNET/Generation.Contracts/AdapterDescriptors.cs +++ b/Source/DotNET/Generation.Contracts/AdapterDescriptors.cs @@ -161,7 +161,32 @@ public enum GenerationFactCapability /// /// facts. /// - SpecificationValue = 8 + SpecificationValue = 8, + + /// + /// facts. + /// + ArtifactDeclaration = 9, + + /// + /// facts. + /// + ArtifactMemberDeclaration = 10, + + /// + /// facts. + /// + ArtifactMemberTypeUse = 11, + + /// + /// facts. + /// + TypeUseBinding = 12, + + /// + /// facts. + /// + ArtifactMemberRole = 13 } /// diff --git a/Source/DotNET/Generation.Contracts/AdapterFactAdmissionValidator.cs b/Source/DotNET/Generation.Contracts/AdapterFactAdmissionValidator.cs index ac5a0da..eb5e244 100644 --- a/Source/DotNET/Generation.Contracts/AdapterFactAdmissionValidator.cs +++ b/Source/DotNET/Generation.Contracts/AdapterFactAdmissionValidator.cs @@ -18,6 +18,21 @@ public static void Validate( case ArtifactPlacementFact placement: ArtifactFactAdmissionValidator.Validate(placement, path, context); break; + case ArtifactDeclarationFact declaration: + GranularFactAdmissionValidator.Validate(declaration, path, context); + break; + case ArtifactMemberDeclarationFact member: + GranularFactAdmissionValidator.Validate(member, path, context); + break; + case ArtifactMemberTypeUseFact typeUse: + GranularFactAdmissionValidator.Validate(typeUse, path, context); + break; + case TypeUseBindingFact binding: + GranularFactAdmissionValidator.Validate(binding, path, context); + break; + case ArtifactMemberRoleFact role: + GranularFactAdmissionValidator.Validate(role, path, context); + break; case RelationshipFact relationship: RelationshipFactAdmissionValidator.Validate(relationship, path, context); break; diff --git a/Source/DotNET/Generation.Contracts/ArtifactDeclarations.cs b/Source/DotNET/Generation.Contracts/ArtifactDeclarations.cs new file mode 100644 index 0000000..e1435c0 --- /dev/null +++ b/Source/DotNET/Generation.Contracts/ArtifactDeclarations.cs @@ -0,0 +1,84 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation; + +/// +/// Identifies one declared member of an exact artifact role. +/// +public sealed record ArtifactMemberKey +{ + /// + /// Gets the artifact role that owns the member. + /// + public required ArtifactKey Artifact { get; init; } + + /// + /// Gets the member name within the artifact declaration. + /// + public required string Name { get; init; } +} + +/// +/// Describes an artifact declaration without repeating its member declarations. +/// +public sealed record ArtifactDeclarationDefinition +{ + /// + /// Gets the artifact identity and role. + /// + public required ArtifactKey Artifact { get; init; } + + /// + /// Gets the display and Screenplay declaration name. + /// + public required string Name { get; init; } + + /// + /// Gets the optional human-readable description. + /// + public string? Description { get; init; } + + /// + /// Gets the repository-relative source file realizing the artifact. + /// + public string? File { get; init; } +} + +/// +/// Asserts one artifact declaration independently from its members. +/// +public sealed record ArtifactDeclarationFact : GenerationFact +{ + /// + /// Gets the asserted artifact declaration. + /// + public required ArtifactDeclarationDefinition Definition { get; init; } +} + +/// +/// Describes one declared artifact member and its position in the authored declaration. +/// +public sealed record ArtifactMemberDeclarationDefinition +{ + /// + /// Gets the exact artifact member. + /// + public required ArtifactMemberKey Member { get; init; } + + /// + /// Gets the zero-based member position in the authored declaration. + /// + public required int DeclarationOrder { get; init; } +} + +/// +/// Asserts one artifact member declaration without repeating the complete artifact. +/// +public sealed record ArtifactMemberDeclarationFact : GenerationFact +{ + /// + /// Gets the asserted member declaration. + /// + public required ArtifactMemberDeclarationDefinition Definition { get; init; } +} diff --git a/Source/DotNET/Generation.Contracts/ArtifactMemberRoles.cs b/Source/DotNET/Generation.Contracts/ArtifactMemberRoles.cs new file mode 100644 index 0000000..97fae32 --- /dev/null +++ b/Source/DotNET/Generation.Contracts/ArtifactMemberRoles.cs @@ -0,0 +1,52 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation; + +/// +/// Defines a semantic role established for one artifact member. +/// +public enum ArtifactMemberRoleKind +{ + /// + /// The adapter could not determine a supported member role. + /// + Unknown = -1, + + /// + /// A typed identifier for the owning artifact. + /// + Identifier = 0, + + /// + /// The identifier of the event source targeted by the owning behavior. + /// + EventSourceIdentifier = 1 +} + +/// +/// Describes one semantic role established for an exact artifact member. +/// +public sealed record ArtifactMemberRoleDefinition +{ + /// + /// Gets the exact artifact member. + /// + public required ArtifactMemberKey Member { get; init; } + + /// + /// Gets the semantic member role. + /// + public required ArtifactMemberRoleKind Role { get; init; } +} + +/// +/// Asserts one semantic role for an exact artifact member. +/// +public sealed record ArtifactMemberRoleFact : GenerationFact +{ + /// + /// Gets the asserted member role. + /// + public required ArtifactMemberRoleDefinition Definition { get; init; } +} diff --git a/Source/DotNET/Generation.Contracts/GranularFactAdmissionValidator.cs b/Source/DotNET/Generation.Contracts/GranularFactAdmissionValidator.cs new file mode 100644 index 0000000..96217fa --- /dev/null +++ b/Source/DotNET/Generation.Contracts/GranularFactAdmissionValidator.cs @@ -0,0 +1,158 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation; + +static class GranularFactAdmissionValidator +{ + public static void Validate( + ArtifactDeclarationFact fact, + string path, + AdapterContributionAdmissionContext context) + { + AdapterContributionAdmissionValidator.ValidateArtifactKey( + fact.Definition.Artifact, + $"{path}.Definition.Artifact", + fact.Id, + context); + ValidateOwner(fact, fact.Definition.Artifact.Subject, $"{path}.Definition.Artifact.Subject", context); + AdapterContributionAdmissionValidator.ValidateRequiredText( + fact.Definition.Name, + $"{path}.Definition.Name", + fact.Id, + fact.Subject, + context); + } + + public static void Validate( + ArtifactMemberDeclarationFact fact, + string path, + AdapterContributionAdmissionContext context) + { + ValidateMember(fact, fact.Definition.Member, $"{path}.Definition.Member", context); + if (fact.Definition.DeclarationOrder < 0) + { + context.Add( + AdapterContributionAdmissionDiagnosticCode.InvalidDeclarationOrder, + $"{path}.Definition.DeclarationOrder", + $"Artifact member declaration order '{fact.Definition.DeclarationOrder}' must not be negative", + fact.Id, + fact.Subject); + } + } + + public static void Validate( + ArtifactMemberTypeUseFact fact, + string path, + AdapterContributionAdmissionContext context) + { + ValidateMember(fact, fact.Definition.Member, $"{path}.Definition.Member", context); + var typePath = $"{path}.Definition.Type"; + AdapterContributionAdmissionValidator.ValidateRequiredText( + fact.Definition.Type.Name, + $"{typePath}.Name", + fact.Id, + fact.Subject, + context); + if (fact.Definition.Type.ObservedTypeSubject is not null) + { + AdapterContributionAdmissionValidator.ValidateSubject( + fact.Definition.Type.ObservedTypeSubject, + $"{typePath}.ObservedTypeSubject", + fact.Id, + context); + } + + ValidateShape(fact, fact.Definition.Type.Shape, $"{typePath}.Shape", context); + } + + public static void Validate( + TypeUseBindingFact fact, + string path, + AdapterContributionAdmissionContext context) + { + ValidateMember(fact, fact.Definition.Member, $"{path}.Definition.Member", context); + AdapterContributionAdmissionValidator.ValidateArtifactKey( + fact.Definition.Target, + $"{path}.Definition.Target", + fact.Id, + context); + } + + public static void Validate( + ArtifactMemberRoleFact fact, + string path, + AdapterContributionAdmissionContext context) + { + ValidateMember(fact, fact.Definition.Member, $"{path}.Definition.Member", context); + context.Enum( + fact.Definition.Role, + ArtifactMemberRoleKind.Unknown, + $"{path}.Definition.Role", + fact.Id, + fact.Subject); + } + + static void ValidateMember( + GenerationFact fact, + ArtifactMemberKey member, + string path, + AdapterContributionAdmissionContext context) + { + AdapterContributionAdmissionValidator.ValidateArtifactKey( + member.Artifact, + $"{path}.Artifact", + fact.Id, + context); + ValidateOwner(fact, member.Artifact.Subject, $"{path}.Artifact.Subject", context); + AdapterContributionAdmissionValidator.ValidateRequiredText( + member.Name, + $"{path}.Name", + fact.Id, + fact.Subject, + context); + } + + static void ValidateShape( + GenerationFact fact, + IReadOnlyList shape, + string path, + AdapterContributionAdmissionContext context) + { + for (var index = 0; index < shape.Count; index++) + { + context.Enum(shape[index], TypeUseShapeKind.Unknown, $"{path}[{index}]", fact.Id, fact.Subject); + } + + var allNodesAreDefined = shape.All(node => node != TypeUseShapeKind.Unknown && Enum.IsDefined(node)); + var hasExactNamedTerminal = shape.Count > 0 && + shape[^1] == TypeUseShapeKind.Named && + shape.Take(shape.Count - 1).All(node => node != TypeUseShapeKind.Named); + if (allNodesAreDefined && !hasExactNamedTerminal) + { + context.Add( + AdapterContributionAdmissionDiagnosticCode.InvalidTypeUseShape, + path, + "A type-use shape must contain wrappers followed by exactly one terminal named type", + fact.Id, + fact.Subject); + } + } + + static void ValidateOwner( + GenerationFact fact, + SubjectId owner, + string path, + AdapterContributionAdmissionContext context) + { + if (owner != fact.Subject) + { + context.Add( + AdapterContributionAdmissionDiagnosticCode.OwnershipMismatch, + path, + $"Nested artifact owner '{owner.Value}' does not equal fact subject '{fact.Subject.Value}'", + fact.Id, + fact.Subject); + } + } +} diff --git a/Source/DotNET/Generation.Contracts/GranularFactFreezer.cs b/Source/DotNET/Generation.Contracts/GranularFactFreezer.cs new file mode 100644 index 0000000..65be400 --- /dev/null +++ b/Source/DotNET/Generation.Contracts/GranularFactFreezer.cs @@ -0,0 +1,284 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +namespace Cratis.Screenplay.Generation; + +static class GranularFactFreezer +{ + public static ArtifactDeclarationFact Freeze( + ArtifactDeclarationFact fact, + FactId id, + SubjectId subject, + Evidence evidence, + string path, + AdapterContributionAdmissionContext context) => new() + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = FreezeArtifactDeclaration(fact.Definition, $"{path}.Definition", context) + }; + + public static ArtifactMemberDeclarationFact Freeze( + ArtifactMemberDeclarationFact fact, + FactId id, + SubjectId subject, + Evidence evidence, + string path, + AdapterContributionAdmissionContext context) => new() + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = FreezeMemberDeclaration(fact.Definition, $"{path}.Definition", context) + }; + + public static ArtifactMemberTypeUseFact Freeze( + ArtifactMemberTypeUseFact fact, + FactId id, + SubjectId subject, + Evidence evidence, + string path, + AdapterContributionAdmissionContext context) => new() + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = FreezeMemberTypeUse(fact.Definition, $"{path}.Definition", context) + }; + + public static TypeUseBindingFact Freeze( + TypeUseBindingFact fact, + FactId id, + SubjectId subject, + Evidence evidence, + string path, + AdapterContributionAdmissionContext context) => new() + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = FreezeBinding(fact.Definition, $"{path}.Definition", context) + }; + + public static ArtifactMemberRoleFact Freeze( + ArtifactMemberRoleFact fact, + FactId id, + SubjectId subject, + Evidence evidence, + string path, + AdapterContributionAdmissionContext context) => new() + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = FreezeMemberRole(fact.Definition, $"{path}.Definition", context) + }; + + static ArtifactDeclarationDefinition FreezeArtifactDeclaration( + ArtifactDeclarationDefinition? definition, + string path, + AdapterContributionAdmissionContext context) + { + if (definition is null) + { + context.Missing(path); + return new ArtifactDeclarationDefinition + { + Artifact = FreezeArtifactKey(null, $"{path}.Artifact", context), + Name = string.Empty + }; + } + + return new ArtifactDeclarationDefinition + { + Artifact = FreezeArtifactKey(definition.Artifact, $"{path}.Artifact", context), + Name = definition.Name ?? string.Empty, + Description = definition.Description, + File = definition.File + }; + } + + static ArtifactMemberDeclarationDefinition FreezeMemberDeclaration( + ArtifactMemberDeclarationDefinition? definition, + string path, + AdapterContributionAdmissionContext context) + { + if (definition is null) + { + context.Missing(path); + return new ArtifactMemberDeclarationDefinition + { + Member = FreezeMemberKey(null, $"{path}.Member", context), + DeclarationOrder = -1 + }; + } + + return new ArtifactMemberDeclarationDefinition + { + Member = FreezeMemberKey(definition.Member, $"{path}.Member", context), + DeclarationOrder = definition.DeclarationOrder + }; + } + + static ArtifactMemberTypeUseDefinition FreezeMemberTypeUse( + ArtifactMemberTypeUseDefinition? definition, + string path, + AdapterContributionAdmissionContext context) + { + if (definition is null) + { + context.Missing(path); + return new ArtifactMemberTypeUseDefinition + { + Member = FreezeMemberKey(null, $"{path}.Member", context), + Type = FreezeTypeUse(null, $"{path}.Type", context) + }; + } + + return new ArtifactMemberTypeUseDefinition + { + Member = FreezeMemberKey(definition.Member, $"{path}.Member", context), + Type = FreezeTypeUse(definition.Type, $"{path}.Type", context) + }; + } + + static TypeUseBindingDefinition FreezeBinding( + TypeUseBindingDefinition? definition, + string path, + AdapterContributionAdmissionContext context) + { + if (definition is null) + { + context.Missing(path); + return new TypeUseBindingDefinition + { + Member = FreezeMemberKey(null, $"{path}.Member", context), + Target = FreezeArtifactKey(null, $"{path}.Target", context) + }; + } + + return new TypeUseBindingDefinition + { + Member = FreezeMemberKey(definition.Member, $"{path}.Member", context), + Target = FreezeArtifactKey(definition.Target, $"{path}.Target", context) + }; + } + + static ArtifactMemberRoleDefinition FreezeMemberRole( + ArtifactMemberRoleDefinition? definition, + string path, + AdapterContributionAdmissionContext context) + { + if (definition is null) + { + context.Missing(path); + return new ArtifactMemberRoleDefinition + { + Member = FreezeMemberKey(null, $"{path}.Member", context), + Role = ArtifactMemberRoleKind.Unknown + }; + } + + return new ArtifactMemberRoleDefinition + { + Member = FreezeMemberKey(definition.Member, $"{path}.Member", context), + Role = definition.Role + }; + } + + static ArtifactMemberKey FreezeMemberKey( + ArtifactMemberKey? member, + string path, + AdapterContributionAdmissionContext context) + { + if (member is null) + { + context.Missing(path); + return new ArtifactMemberKey + { + Artifact = FreezeArtifactKey(null, $"{path}.Artifact", context), + Name = string.Empty + }; + } + + return new ArtifactMemberKey + { + Artifact = FreezeArtifactKey(member.Artifact, $"{path}.Artifact", context), + Name = member.Name ?? string.Empty + }; + } + + static TypeUseDefinition FreezeTypeUse( + TypeUseDefinition? type, + string path, + AdapterContributionAdmissionContext context) + { + if (type is null) + { + context.Missing(path); + return new TypeUseDefinition { Name = string.Empty, Shape = [] }; + } + + return new TypeUseDefinition + { + Name = type.Name ?? string.Empty, + ObservedTypeSubject = type.ObservedTypeSubject is null + ? null + : FreezeSubject(type.ObservedTypeSubject, $"{path}.ObservedTypeSubject", context), + Shape = FreezeShape(type.Shape, $"{path}.Shape", context) + }; + } + + static ArtifactKey FreezeArtifactKey( + ArtifactKey? key, + string path, + AdapterContributionAdmissionContext context) + { + if (key is null) + { + context.Missing(path); + return new ArtifactKey + { + Subject = new SubjectId { Value = string.Empty }, + Kind = ArtifactKind.Unknown + }; + } + + return new ArtifactKey + { + Subject = FreezeSubject(key.Subject, $"{path}.Subject", context), + Kind = key.Kind + }; + } + + static SubjectId FreezeSubject( + SubjectId? subject, + string path, + AdapterContributionAdmissionContext context) + { + if (subject is null) + { + context.Missing(path); + return new SubjectId { Value = string.Empty }; + } + + return new SubjectId { Value = subject.Value ?? string.Empty }; + } + + static ImmutableArray FreezeShape( + IReadOnlyList? shape, + string path, + AdapterContributionAdmissionContext context) + { + if (shape is null) + { + context.NullCollection(path); + return []; + } + + return [.. shape]; + } +} diff --git a/Source/DotNET/Generation.Contracts/TypeUses.cs b/Source/DotNET/Generation.Contracts/TypeUses.cs new file mode 100644 index 0000000..3f02702 --- /dev/null +++ b/Source/DotNET/Generation.Contracts/TypeUses.cs @@ -0,0 +1,108 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation; + +/// +/// Defines one node in the exact optionality and collection shape of a type use. +/// +public enum TypeUseShapeKind +{ + /// + /// The adapter could not determine a supported type-use shape. + /// + Unknown = -1, + + /// + /// The terminal named type. + /// + Named = 0, + + /// + /// An optional wrapper around the following shape node. + /// + Optional = 1, + + /// + /// A collection wrapper around the following shape node. + /// + Collection = 2 +} + +/// +/// Describes one exact source type use independently from any artifact binding. +/// +/// +/// Shape nodes are ordered from the outermost wrapper to the terminal node. +/// +public sealed record TypeUseDefinition +{ + /// + /// Gets the source type name observed at the use site. + /// + public required string Name { get; init; } + + /// + /// Gets the exact source-level type subject observed at the use site, when available. + /// + public SubjectId? ObservedTypeSubject { get; init; } + + /// + /// Gets the exact optionality and collection shape from outermost wrapper to named type. + /// + public IReadOnlyList Shape { get; init; } = [TypeUseShapeKind.Named]; +} + +/// +/// Describes the exact type use of one artifact member. +/// +public sealed record ArtifactMemberTypeUseDefinition +{ + /// + /// Gets the exact artifact member using the type. + /// + public required ArtifactMemberKey Member { get; init; } + + /// + /// Gets the observed source type use. + /// + public required TypeUseDefinition Type { get; init; } +} + +/// +/// Asserts the exact source type use of one artifact member. +/// +public sealed record ArtifactMemberTypeUseFact : GenerationFact +{ + /// + /// Gets the asserted member type use. + /// + public required ArtifactMemberTypeUseDefinition Definition { get; init; } +} + +/// +/// Describes an exact binding from one member type use to a declared artifact role. +/// +public sealed record TypeUseBindingDefinition +{ + /// + /// Gets the exact artifact member whose type use is bound. + /// + public required ArtifactMemberKey Member { get; init; } + + /// + /// Gets the exact artifact role targeted by the type use. + /// + public required ArtifactKey Target { get; init; } +} + +/// +/// Asserts an exact binding from one member type use to a declared artifact role. +/// +public sealed record TypeUseBindingFact : GenerationFact +{ + /// + /// Gets the asserted type-use binding. + /// + public required TypeUseBindingDefinition Definition { get; init; } +} diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs index 3715649..a021930 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs @@ -46,13 +46,19 @@ protected static ImmutableArray AllCapabilities() => GenerationFactCapability.ConceptAttribute, GenerationFactCapability.SpecificationStep, GenerationFactCapability.ConceptRepresentation, + GenerationFactCapability.ArtifactDeclaration, + GenerationFactCapability.ArtifactMemberDeclaration, + GenerationFactCapability.ArtifactMemberTypeUse, + GenerationFactCapability.TypeUseBinding, + GenerationFactCapability.ArtifactMemberRole, GenerationFactCapability.Artifact ]; protected static List EveryFact( IReadOnlyList? properties = null, IReadOnlyList? scenarioSteps = null, - IReadOnlyList? valuePath = null) + IReadOnlyList? valuePath = null, + IReadOnlyList? typeUseShape = null) { var scenarioKey = ScenarioKey(); var stepKey = StepKey(); @@ -97,6 +103,66 @@ protected static List EveryFact( SliceKind = GenerationSliceKind.StateChange } }, + new ArtifactDeclarationFact + { + Id = Id("artifact-declaration"), + Subject = ArtifactSubject, + Evidence = Evidence(), + Definition = new ArtifactDeclarationDefinition + { + Artifact = ArtifactKey(ArtifactSubject, ArtifactKind.Command), + Name = "Register" + } + }, + new ArtifactMemberDeclarationFact + { + Id = Id("artifact-member"), + Subject = ArtifactSubject, + Evidence = Evidence(), + Definition = new ArtifactMemberDeclarationDefinition + { + Member = MemberKey("second"), + DeclarationOrder = 0 + } + }, + new ArtifactMemberTypeUseFact + { + Id = Id("artifact-member-type-use"), + Subject = ArtifactSubject, + Evidence = Evidence(), + Definition = new ArtifactMemberTypeUseDefinition + { + Member = MemberKey("second"), + Type = new TypeUseDefinition + { + Name = "External", + ObservedTypeSubject = ExternalSubject, + Shape = typeUseShape ?? [TypeUseShapeKind.Optional, TypeUseShapeKind.Collection, TypeUseShapeKind.Named] + } + } + }, + new TypeUseBindingFact + { + Id = Id("type-use-binding"), + Subject = ArtifactSubject, + Evidence = Evidence(), + Definition = new TypeUseBindingDefinition + { + Member = MemberKey("second"), + Target = ArtifactKey(ExternalSubject, ArtifactKind.Concept) + } + }, + new ArtifactMemberRoleFact + { + Id = Id("artifact-member-role"), + Subject = ArtifactSubject, + Evidence = Evidence(), + Definition = new ArtifactMemberRoleDefinition + { + Member = MemberKey("second"), + Role = ArtifactMemberRoleKind.Identifier + } + }, new RelationshipFact { Id = Id("relationship"), @@ -247,6 +313,12 @@ sealed class AcceptingSourceAuthorityValidator : ISourceAuthorityValidator Kind = kind }; + protected static ArtifactMemberKey MemberKey(string name) => new() + { + Artifact = ArtifactKey(ArtifactSubject, ArtifactKind.Command), + Name = name + }; + protected static SpecificationScenarioKey ScenarioKey() => new() { Scenario = ScenarioSubject }; protected static SpecificationStepKey StepKey() => new() diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_a_valid_contribution.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_a_valid_contribution.cs index df5d2c0..0b1cbfb 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_a_valid_contribution.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_a_valid_contribution.cs @@ -14,6 +14,11 @@ public class when_admitting_a_valid_contribution : given.a_contribution [Fact] void should_freeze_every_current_fact_family() => _result.Snapshot!.Facts.Select(fact => fact.GetType()).ShouldContainOnly( typeof(ArtifactFact), typeof(ArtifactPlacementFact), + typeof(ArtifactDeclarationFact), + typeof(ArtifactMemberDeclarationFact), + typeof(ArtifactMemberTypeUseFact), + typeof(TypeUseBindingFact), + typeof(ArtifactMemberRoleFact), typeof(RelationshipFact), typeof(ConceptRepresentationFact), typeof(ConceptAttributeFact), diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_cross_adapter_subject_references.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_cross_adapter_subject_references.cs index 6a47167..df61ad8 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_cross_adapter_subject_references.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_cross_adapter_subject_references.cs @@ -12,7 +12,7 @@ void Because() var facts = EveryFact(); _result = Admit( Descriptor(GenerationFactCapability.Artifact, GenerationFactCapability.Relationship), - Contribution([facts[0], facts[2]])); + Contribution([facts[0], facts[7]])); } [Fact] void should_admit_structurally_valid_references_not_declared_by_this_adapter() => _result.IsAdmitted.ShouldBeTrue(); diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_an_undeclared_fact_capability.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_an_undeclared_fact_capability.cs index 4fcec5e..9e43c9f 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_an_undeclared_fact_capability.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_an_undeclared_fact_capability.cs @@ -10,5 +10,5 @@ public class with_an_undeclared_fact_capability : given.a_contribution void Because() => _result = Admit(Descriptor(GenerationFactCapability.Artifact)); [Fact] void should_reject_the_whole_contribution() => _result.Snapshot.ShouldBeNull(); - [Fact] void should_report_each_undeclared_runtime_fact_family() => _result.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.UndeclaredFactCapability).ShouldEqual(8); + [Fact] void should_report_each_undeclared_runtime_fact_family() => _result.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.UndeclaredFactCapability).ShouldEqual(13); } diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_inconsistent_ownership.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_inconsistent_ownership.cs index e56cf9b..f70d7f5 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_inconsistent_ownership.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_inconsistent_ownership.cs @@ -20,29 +20,81 @@ void Because() }; var placement = (ArtifactPlacementFact)facts[1]; facts[1] = placement with { Artifact = placement.Artifact with { Subject = ExternalSubject } }; - var relationship = (RelationshipFact)facts[2]; - facts[2] = relationship with + var declaration = (ArtifactDeclarationFact)facts[2]; + facts[2] = declaration with + { + Definition = declaration.Definition with + { + Artifact = declaration.Definition.Artifact with { Subject = ExternalSubject } + } + }; + var member = (ArtifactMemberDeclarationFact)facts[3]; + facts[3] = member with + { + Definition = member.Definition with + { + Member = member.Definition.Member with + { + Artifact = member.Definition.Member.Artifact with { Subject = ExternalSubject } + } + } + }; + var typeUse = (ArtifactMemberTypeUseFact)facts[4]; + facts[4] = typeUse with + { + Definition = typeUse.Definition with + { + Member = typeUse.Definition.Member with + { + Artifact = typeUse.Definition.Member.Artifact with { Subject = ExternalSubject } + } + } + }; + var binding = (TypeUseBindingFact)facts[5]; + facts[5] = binding with + { + Definition = binding.Definition with + { + Member = binding.Definition.Member with + { + Artifact = binding.Definition.Member.Artifact with { Subject = ExternalSubject } + } + } + }; + var role = (ArtifactMemberRoleFact)facts[6]; + facts[6] = role with + { + Definition = role.Definition with + { + Member = role.Definition.Member with + { + Artifact = role.Definition.Member.Artifact with { Subject = ExternalSubject } + } + } + }; + var relationship = (RelationshipFact)facts[7]; + facts[7] = relationship with { Definition = relationship.Definition with { Key = relationship.Definition.Key with { Source = ExternalSubject } } }; - var concept = (ConceptRepresentationFact)facts[3]; - facts[3] = concept with + var concept = (ConceptRepresentationFact)facts[8]; + facts[8] = concept with { Definition = concept.Definition with { Concept = ExternalSubject } }; - var scenario = (SpecificationScenarioFact)facts[6]; - facts[6] = scenario with + var scenario = (SpecificationScenarioFact)facts[11]; + facts[11] = scenario with { Definition = scenario.Definition with { Key = new SpecificationScenarioKey { Scenario = ExternalSubject } } }; - var step = (SpecificationStepFact)facts[7]; - facts[7] = step with + var step = (SpecificationStepFact)facts[12]; + facts[12] = step with { Definition = step.Definition with { @@ -56,8 +108,8 @@ void Because() ] } }; - var value = (SpecificationValueFact)facts[8]; - facts[8] = value with + var value = (SpecificationValueFact)facts[13]; + facts[13] = value with { Definition = value.Definition with { @@ -68,5 +120,5 @@ void Because() } [Fact] void should_reject_the_whole_contribution() => _result.Snapshot.ShouldBeNull(); - [Fact] void should_report_every_inconsistent_ownership_chain() => _result.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.OwnershipMismatch).ShouldBeGreaterThan(6); + [Fact] void should_report_every_inconsistent_ownership_chain() => _result.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.OwnershipMismatch).ShouldBeGreaterThan(11); } diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_invalid_granular_declarations.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_invalid_granular_declarations.cs new file mode 100644 index 0000000..3efab8a --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_invalid_granular_declarations.cs @@ -0,0 +1,71 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_AdapterContributionAdmission.when_admitting_malformed_contributions; + +public class with_invalid_granular_declarations : given.a_contribution +{ + AdapterContributionAdmissionResult _invalid = null!; + AdapterContributionAdmissionResult _undefinedShape = null!; + + void Because() + { + var facts = EveryFact(); + var declaration = (ArtifactDeclarationFact)facts[2]; + facts[2] = declaration with + { + Definition = declaration.Definition with { Name = " " } + }; + var member = (ArtifactMemberDeclarationFact)facts[3]; + facts[3] = member with + { + Definition = member.Definition with { DeclarationOrder = -1 } + }; + var typeUse = (ArtifactMemberTypeUseFact)facts[4]; + facts[4] = typeUse with + { + Definition = typeUse.Definition with + { + Type = typeUse.Definition.Type with + { + Name = " ", + ObservedTypeSubject = new SubjectId { Value = "CustomerCode" }, + Shape = [TypeUseShapeKind.Named, TypeUseShapeKind.Optional] + } + } + }; + var binding = (TypeUseBindingFact)facts[5]; + facts[5] = binding with + { + Definition = binding.Definition with + { + Target = binding.Definition.Target with { Kind = ArtifactKind.Unknown } + } + }; + var role = (ArtifactMemberRoleFact)facts[6]; + facts[6] = role with + { + Definition = role.Definition with { Role = ArtifactMemberRoleKind.Unknown } + }; + _invalid = Admit(contribution: Contribution(facts)); + + var undefinedFacts = EveryFact(); + var undefinedTypeUse = (ArtifactMemberTypeUseFact)undefinedFacts[4]; + undefinedFacts[4] = undefinedTypeUse with + { + Definition = undefinedTypeUse.Definition with + { + Type = undefinedTypeUse.Definition.Type with { Shape = [(TypeUseShapeKind)731] } + } + }; + _undefinedShape = Admit(contribution: Contribution(undefinedFacts)); + } + + [Fact] void should_reject_the_complete_invalid_contribution() => _invalid.Snapshot.ShouldBeNull(); + [Fact] void should_reject_blank_declaration_and_type_names() => _invalid.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(AdapterContributionAdmissionDiagnosticCode.MissingRequiredValue); + [Fact] void should_reject_negative_member_order() => _invalid.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(AdapterContributionAdmissionDiagnosticCode.InvalidDeclarationOrder); + [Fact] void should_reject_malformed_observed_type_subjects() => _invalid.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(AdapterContributionAdmissionDiagnosticCode.InvalidSubject); + [Fact] void should_reject_nonterminal_named_type_shapes() => _invalid.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(AdapterContributionAdmissionDiagnosticCode.InvalidTypeUseShape); + [Fact] void should_reject_unknown_target_and_member_roles() => _invalid.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(AdapterContributionAdmissionDiagnosticCode.UnknownEnumValue); + [Fact] void should_reject_undefined_type_shape_nodes() => _undefinedShape.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(AdapterContributionAdmissionDiagnosticCode.UndefinedEnumValue); +} diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs index b07a6ae..699f7c7 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs @@ -10,18 +10,18 @@ public class with_missing_kind_operands : given.a_contribution void Because() { var facts = EveryFact(); - var representation = (ConceptRepresentationFact)facts[3]; - facts[3] = representation with + var representation = (ConceptRepresentationFact)facts[8]; + facts[8] = representation with { Definition = representation.Definition with { Primitive = null } }; - var step = (SpecificationStepFact)facts[7]; - facts[7] = step with + var step = (SpecificationStepFact)facts[12]; + facts[12] = step with { Definition = step.Definition with { Artifact = null } }; - var value = (SpecificationValueFact)facts[8]; - facts[8] = value with + var value = (SpecificationValueFact)facts[13]; + facts[13] = value with { Definition = value.Definition with { diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_null_granular_contracts.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_null_granular_contracts.cs new file mode 100644 index 0000000..37f58e1 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_null_granular_contracts.cs @@ -0,0 +1,37 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_AdapterContributionAdmission.when_admitting_malformed_contributions; + +public class with_null_granular_contracts : given.a_contribution +{ + AdapterContributionAdmissionResult _nullDefinitions = null!; + AdapterContributionAdmissionResult _nullShape = null!; + + void Because() + { + var facts = EveryFact(); + facts[2] = ((ArtifactDeclarationFact)facts[2]) with { Definition = null! }; + facts[3] = ((ArtifactMemberDeclarationFact)facts[3]) with { Definition = null! }; + facts[4] = ((ArtifactMemberTypeUseFact)facts[4]) with { Definition = null! }; + facts[5] = ((TypeUseBindingFact)facts[5]) with { Definition = null! }; + facts[6] = ((ArtifactMemberRoleFact)facts[6]) with { Definition = null! }; + _nullDefinitions = Admit(contribution: Contribution(facts)); + + var nullShapeFacts = EveryFact(); + var typeUse = (ArtifactMemberTypeUseFact)nullShapeFacts[4]; + nullShapeFacts[4] = typeUse with + { + Definition = typeUse.Definition with + { + Type = typeUse.Definition.Type with { Shape = null! } + } + }; + _nullShape = Admit(contribution: Contribution(nullShapeFacts)); + } + + [Fact] void should_reject_all_null_granular_definitions_atomically() => _nullDefinitions.Snapshot.ShouldBeNull(); + [Fact] void should_report_each_missing_granular_definition() => _nullDefinitions.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.MissingRequiredValue).ShouldBeGreaterThan(4); + [Fact] void should_reject_a_null_type_use_shape_atomically() => _nullShape.Snapshot.ShouldBeNull(); + [Fact] void should_report_the_null_type_use_shape_collection() => _nullShape.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(AdapterContributionAdmissionDiagnosticCode.NullRequiredCollection); +} diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs index 5682f1b..b7fa497 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs @@ -19,13 +19,19 @@ public class when_mutating_admitted_input : given.a_contribution } ]; readonly List _path = ["arguments", "name"]; + readonly List _shape = + [ + TypeUseShapeKind.Optional, + TypeUseShapeKind.Collection, + TypeUseShapeKind.Named + ]; List _facts = null!; GenerationFact _originalFact = null!; AdapterContributionAdmissionResult _result = null!; void Establish() { - _facts = EveryFact(_properties, valuePath: _path); + _facts = EveryFact(_properties, valuePath: _path, typeUseShape: _shape); _originalFact = _facts[0]; } @@ -36,10 +42,12 @@ void Because() _properties.Clear(); _path.Reverse(); _path.Clear(); + _shape.Clear(); } - [Fact] void should_keep_the_frozen_fact_list() => _result.Snapshot!.Facts.Length.ShouldEqual(9); + [Fact] void should_keep_the_frozen_fact_list() => _result.Snapshot!.Facts.Length.ShouldEqual(14); [Fact] void should_deep_copy_fact_records() => ReferenceEquals(_originalFact, _result.Snapshot!.Facts.Single(fact => fact.Id.Value == "atomic:artifact")).ShouldBeFalse(); [Fact] void should_keep_nested_properties_in_authored_order() => _result.Snapshot!.Facts.OfType().Single().Definition.Properties.Select(property => property.Name).ShouldEqual(["second", "first"]); [Fact] void should_keep_nested_value_paths_in_authored_order() => string.Join('|', _result.Snapshot!.Facts.OfType().Single().Definition.Key.Path).ShouldEqual("arguments|name"); + [Fact] void should_keep_nested_type_use_shapes_in_authored_order() => string.Join('|', _result.Snapshot!.Facts.OfType().Single().Definition.Type.Shape).ShouldEqual("Optional|Collection|Named"); } diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_reading_adapter_contract_discriminators.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_reading_adapter_contract_discriminators.cs index f723c94..f97c13b 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_reading_adapter_contract_discriminators.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_reading_adapter_contract_discriminators.cs @@ -11,12 +11,24 @@ [Fact] void should_define_every_new_unknown_discriminator_as_minus_one() ((int)AdapterCategory.Unknown).ShouldEqual(-1); ((int)AdapterHostCapability.Unknown).ShouldEqual(-1); ((int)GenerationFactCapability.Unknown).ShouldEqual(-1); + ((int)TypeUseShapeKind.Unknown).ShouldEqual(-1); + ((int)ArtifactMemberRoleKind.Unknown).ShouldEqual(-1); ((int)AdapterRunDisposition.Unknown).ShouldEqual(-1); ((int)GenerationFactDisposition.Unknown).ShouldEqual(-1); ((int)AdapterContributionAdmissionDiagnosticCode.Unknown).ShouldEqual(-1); ((int)GenerationDiagnosticSeverity.Unknown).ShouldEqual(-1); } + [Fact] void should_append_granular_fact_capabilities_without_renumbering_existing_values() + { + ((int)GenerationFactCapability.SpecificationValue).ShouldEqual(8); + ((int)GenerationFactCapability.ArtifactDeclaration).ShouldEqual(9); + ((int)GenerationFactCapability.ArtifactMemberDeclaration).ShouldEqual(10); + ((int)GenerationFactCapability.ArtifactMemberTypeUse).ShouldEqual(11); + ((int)GenerationFactCapability.TypeUseBinding).ShouldEqual(12); + ((int)GenerationFactCapability.ArtifactMemberRole).ShouldEqual(13); + } + [Fact] void should_preserve_existing_diagnostic_severity_values() { ((int)GenerationDiagnosticSeverity.Information).ShouldEqual(0); diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_validating_source_authority.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_validating_source_authority.cs index f071a9a..39c5030 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_validating_source_authority.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_validating_source_authority.cs @@ -38,11 +38,11 @@ void Because() } [Fact] void should_admit_authoritative_fact_and_diagnostic_ranges() => _accepted.IsAdmitted.ShouldBeTrue(); - [Fact] void should_validate_every_fact_and_contribution_diagnostic_range() => _acceptingValidator.Validated.Count.ShouldEqual(10); + [Fact] void should_validate_every_fact_and_contribution_diagnostic_range() => _acceptingValidator.Validated.Count.ShouldEqual(15); [Fact] void should_reject_nonauthoritative_fact_and_diagnostic_ranges_atomically() => _rejected.Snapshot.ShouldBeNull(); - [Fact] void should_report_every_nonauthoritative_range() => _rejected.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.SourceNotAuthoritative).ShouldEqual(10); + [Fact] void should_report_every_nonauthoritative_range() => _rejected.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.SourceNotAuthoritative).ShouldEqual(15); [Fact] void should_reject_source_evidence_when_no_authority_validator_is_supplied() => _withoutValidator.Snapshot.ShouldBeNull(); - [Fact] void should_require_authority_for_every_unvalidated_range() => _withoutValidator.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.SourceAuthorityRequired).ShouldEqual(10); + [Fact] void should_require_authority_for_every_unvalidated_range() => _withoutValidator.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.SourceAuthorityRequired).ShouldEqual(15); [Fact] void should_order_rejected_source_diagnostics_independently_of_fact_order() => Projection(_rejectedReversed).ShouldEqual(Projection(_rejected)); static string[] Projection(AdapterContributionAdmissionResult result) => From 779131488d5c1a9384022a984c395b6b1656b705 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 01:13:51 +0200 Subject: [PATCH 2/7] Add fixed-snapshot type-use derivation Derive exact member bindings once from a canonical admitted base snapshot and retain the rule, input facts, and complete evidence lineage. Conflicting or incomplete subjects fail closed without consulting adapter identities, Roslyn, or prior derived outputs. --- Documentation/guides/build-source-adapter.md | 12 +- .../Generation.Contracts/AdapterRuns.cs | 10 + .../DotNET/Generation.Contracts/Derivation.cs | 90 +++++ .../given/a_derivation.cs | 148 ++++++++ ...mitive_type_use_has_no_observed_subject.cs | 17 + ...ed_subject_has_conflicting_declarations.cs | 44 +++ ...eriving_an_exact_cross_adapter_type_use.cs | 29 ++ .../when_inputs_are_permuted.cs | 25 ++ ...names_contain_unpaired_utf16_code_units.cs | 20 ++ ...inding_inputs_are_missing_or_conflicted.cs | 37 ++ ..._concepts_have_different_exact_subjects.cs | 33 ++ ...hen_attaching_fixed_snapshot_derivation.cs | 192 ++++++++++ .../Generation/AdapterRunCanonicalizer.cs | 118 ++++++ .../Generation/GenerationDiagnosticCodes.cs | 40 +++ .../Generation/GenerationFactDerivation.cs | 45 +++ .../ScreenplayDefinitionGenerator.cs | 28 +- Source/DotNET/Generation/Structural.cs | 38 ++ .../Generation/TypeUseBindingDerivation.cs | 336 ++++++++++++++++++ 18 files changed, 1250 insertions(+), 12 deletions(-) create mode 100644 Source/DotNET/Generation.Contracts/Derivation.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/given/a_derivation.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_primitive_type_use_has_no_observed_subject.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_an_observed_subject_has_conflicting_declarations.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_deriving_an_exact_cross_adapter_type_use.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_inputs_are_permuted.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_member_names_contain_unpaired_utf16_code_units.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_required_binding_inputs_are_missing_or_conflicted.cs create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_same_named_concepts_have_different_exact_subjects.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_attaching_fixed_snapshot_derivation.cs create mode 100644 Source/DotNET/Generation/GenerationFactDerivation.cs create mode 100644 Source/DotNET/Generation/TypeUseBindingDerivation.cs diff --git a/Documentation/guides/build-source-adapter.md b/Documentation/guides/build-source-adapter.md index 6f03917..84e2487 100644 --- a/Documentation/guides/build-source-adapter.md +++ b/Documentation/guides/build-source-adapter.md @@ -524,7 +524,17 @@ var legacy = DotNetAdapterRunner.Run( The modern descriptor has category `Concepts`, source language `CSharp`, requires authored source, stable source locations, semantic analysis, and exact Vogen declaration API evidence, and declares its concept fact families. Its probe distinguishes no declarations, safely applicable declarations, and unsafe mappings. Run the modern and legacy registrations separately: both use the `vogen` identity, so placing both in one roster is a deliberate duplicate rejection. When each path is safely applicable, their contribution facts and diagnostics are identical. -This execution snapshot is not a history model. It does not implement issue #19 adapter or fact lineage. It also has no issue #24 serializer or stable fingerprints; keep snapshots in process and compare canonical generated bytes when determinism matters. +### Derive facts from one fixed admitted snapshot + +`GenerationFactDerivation.Derive(...)` runs the closed built-in rule set once over `AdapterRunSnapshot.Facts`. Every rule sees the same deeply frozen base array. A rule never consumes another rule's output, inspects adapter registrations or instances, or reopens source-language state. + +The type-use binding rule joins an `ArtifactMemberTypeUseFact` to an exact declared artifact subject. It does not join by display name and does not replace the owning artifact's complete property list. Its derived `TypeUseBindingFact` remains separate from the admitted base facts under `AdapterRunSnapshot.Derivation`. + +Each derived `GenerationFactRecord` carries `GenerationFactLineage`: the stable derivation rule identity and version, canonical input `FactId` references, and complete input evidence. `GenerationDerivationRuleRecord` records the fixed inputs, outputs, and diagnostics for that rule execution. Directly invoking derivation leaves fact dispositions unknown because disposition is a later generation decision. `Generate(snapshot, options)` currently attaches the derivation result and propagates its diagnostics; resolution and lowering still consume only the admitted base contributions until the granular overlay stage applies the derived binding. + +Exact subjects can come from any source frontend. A C# member type use can bind a declaration contributed by another adapter, while a source-independent or non-.NET adapter can contribute the same neutral contracts without Roslyn or Screenplay-layout dependencies. Missing, ambiguous, or conflicting inputs produce stable diagnostics without selecting a winner. + +The execution and derivation snapshots are not a history model. They have no issue #24 serializer or stable fingerprints; keep them in process and compare canonical generated bytes when determinism matters. Adapters never call the runner, resolver, lowerer, printer, or compiler themselves. Adopt a newly required API in this order: diff --git a/Source/DotNET/Generation.Contracts/AdapterRuns.cs b/Source/DotNET/Generation.Contracts/AdapterRuns.cs index c29484a..4d9a397 100644 --- a/Source/DotNET/Generation.Contracts/AdapterRuns.cs +++ b/Source/DotNET/Generation.Contracts/AdapterRuns.cs @@ -156,6 +156,11 @@ public sealed record GenerationFactRecord /// public required GenerationFact Fact { get; init; } + /// + /// Gets derivation producer and input lineage when the fact was produced from admitted base facts. + /// + public GenerationFactLineage? Lineage { get; init; } + /// /// Gets the disposition calculated by later generation stages. /// @@ -223,6 +228,11 @@ public sealed record AdapterRunSnapshot /// public ImmutableArray Facts { get; init; } = []; + /// + /// Gets the fixed-snapshot derivation result after generation has run, or before derivation. + /// + public GenerationDerivationSnapshot? Derivation { get; init; } + /// /// Gets run-level diagnostics in canonical order. /// diff --git a/Source/DotNET/Generation.Contracts/Derivation.cs b/Source/DotNET/Generation.Contracts/Derivation.cs new file mode 100644 index 0000000..4e245c8 --- /dev/null +++ b/Source/DotNET/Generation.Contracts/Derivation.cs @@ -0,0 +1,90 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +namespace Cratis.Screenplay.Generation; + +/// +/// Identifies one deterministic derivation rule and its semantic version. +/// +public sealed record GenerationDerivationRuleIdentity +{ + /// + /// Gets the stable source-neutral rule identifier. + /// + public required string Id { get; init; } + + /// + /// Gets the semantic rule version. + /// + public required string Version { get; init; } +} + +/// +/// Describes the producer, canonical inputs, and complete evidence lineage of one derived fact. +/// +public sealed record GenerationFactLineage +{ + /// + /// Gets the rule that produced the fact. + /// + public required GenerationDerivationRuleIdentity Producer { get; init; } + + /// + /// Gets the canonical identities of every admitted base fact used to derive the fact. + /// + public ImmutableArray Inputs { get; init; } = []; + + /// + /// Gets the canonical evidence from every admitted base fact used to derive the fact. + /// + public ImmutableArray Evidence { get; init; } = []; +} + +/// +/// Records one rule execution over a fixed admitted base snapshot. +/// +public sealed record GenerationDerivationRuleRecord +{ + /// + /// Gets the executed rule identity and version. + /// + public required GenerationDerivationRuleIdentity Rule { get; init; } + + /// + /// Gets every admitted base fact considered by the rule in canonical order. + /// + public ImmutableArray Inputs { get; init; } = []; + + /// + /// Gets every derived output fact identity in canonical order. + /// + public ImmutableArray Outputs { get; init; } = []; + + /// + /// Gets deterministic diagnostics produced by the rule. + /// + public ImmutableArray Diagnostics { get; init; } = []; +} + +/// +/// Represents one immutable derivation pass over a fixed admitted base snapshot. +/// +public sealed record GenerationDerivationSnapshot +{ + /// + /// Gets rule execution records in canonical rule order. + /// + public ImmutableArray Rules { get; init; } = []; + + /// + /// Gets derived fact records in canonical fact order. + /// + public ImmutableArray Facts { get; init; } = []; + + /// + /// Gets all derivation diagnostics in canonical order. + /// + public ImmutableArray Diagnostics { get; init; } = []; +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/given/a_derivation.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/given/a_derivation.cs new file mode 100644 index 0000000..38cce6a --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/given/a_derivation.cs @@ -0,0 +1,148 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Globalization; +using System.Reflection; + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation.given; + +public class a_derivation : Specification +{ + protected static readonly AdapterIdentity ApplicationAdapter = new() { Id = "application", Version = "1.0.0" }; + protected static readonly AdapterIdentity ConceptAdapter = new() { Id = "concepts", Version = "2.0.0" }; + protected static readonly SubjectId CommandSubject = new() { Value = "dotnet://Ordering/Commands.RegisterCustomer" }; + protected static readonly SubjectId ConceptSubject = new() { Value = "dotnet://Ordering/Concepts.CustomerCode" }; + protected static readonly ArtifactKey Command = new() { Subject = CommandSubject, Kind = ArtifactKind.Command }; + + protected static ArtifactDeclarationFact CommandDeclaration(string suffix = "command") => new() + { + Id = Id(ApplicationAdapter, suffix), + Subject = CommandSubject, + Evidence = Evidence(ApplicationAdapter, "Commands/RegisterCustomer.cs", 1), + Definition = new ArtifactDeclarationDefinition + { + Artifact = Command, + Name = "RegisterCustomer", + File = "Commands/RegisterCustomer.cs" + } + }; + + protected static ArtifactMemberDeclarationFact MemberDeclaration( + string name, + int order, + string suffix) => new() + { + Id = Id(ApplicationAdapter, suffix), + Subject = CommandSubject, + Evidence = Evidence(ApplicationAdapter, "Commands/RegisterCustomer.cs", order + 2), + Definition = new ArtifactMemberDeclarationDefinition + { + Member = Member(name), + DeclarationOrder = order + } + }; + + protected static ArtifactMemberTypeUseFact TypeUse( + string name, + SubjectId? observedType, + string suffix, + params TypeUseShapeKind[] shape) => new() + { + Id = Id(ApplicationAdapter, suffix), + Subject = CommandSubject, + Evidence = Evidence(ApplicationAdapter, "Commands/RegisterCustomer.cs", 10), + Definition = new ArtifactMemberTypeUseDefinition + { + Member = Member(name), + Type = new TypeUseDefinition + { + Name = "CustomerCode", + ObservedTypeSubject = observedType, + Shape = shape.Length == 0 ? [TypeUseShapeKind.Named] : shape + } + } + }; + + protected static ArtifactFact Concept( + SubjectId subject, + string suffix, + string name = "CustomerCode") => new() + { + Id = Id(ConceptAdapter, suffix), + Subject = subject, + Evidence = Evidence(ConceptAdapter, $"Concepts/{suffix}.cs", 1), + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = subject, Kind = ArtifactKind.Concept }, + Name = name, + File = $"Concepts/{suffix}.cs" + } + }; + + protected static ArtifactMemberKey Member(string name) => new() + { + Artifact = Command, + Name = name + }; + + protected static GenerationDerivationSnapshot Derive(params GenerationFact[] facts) => + GenerationFactDerivation.Derive(new AdapterRunSnapshot + { + Facts = [.. facts.Select(fact => new GenerationFactRecord { Fact = fact })] + }); + + protected static string Projection(object? value) + { + if (value is null) + { + return Node([null]); + } + + if (value is string text) + { + return Node([typeof(string).FullName, text]); + } + + var type = value.GetType(); + if (type.IsEnum || type.IsPrimitive || value is decimal) + { + return Node([type.FullName, Convert.ToString(value, CultureInfo.InvariantCulture)]); + } + + if (value is IEnumerable enumerable) + { + return Node([type.FullName, .. enumerable.Cast().Select(Projection)]); + } + + var properties = type + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(property => property.GetIndexParameters().Length == 0) + .OrderBy(property => property.Name, StringComparer.Ordinal); + return Node( + [ + type.FullName, + .. properties.Select(property => Node([property.Name, Projection(property.GetValue(value))])) + ]); + } + + static FactId Id(AdapterIdentity adapter, string suffix) => new() { Value = $"{adapter.Id}:{suffix}" }; + + static Evidence Evidence(AdapterIdentity adapter, string path, int line) => new() + { + Adapter = adapter, + Strength = EvidenceStrength.Exact, + Source = new SourceRange + { + Path = path, + StartLine = line, + StartColumn = 1, + EndLine = line, + EndColumn = 20 + } + }; + + static string Node(IEnumerable values) => string.Concat(values.Select(value => value is null + ? "-1:" + : $"{value.Length.ToString(CultureInfo.InvariantCulture)}:{value}")); +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_primitive_type_use_has_no_observed_subject.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_primitive_type_use_has_no_observed_subject.cs new file mode 100644 index 0000000..648b1a8 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_primitive_type_use_has_no_observed_subject.cs @@ -0,0 +1,17 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_a_primitive_type_use_has_no_observed_subject : given.a_derivation +{ + GenerationDerivationSnapshot _result = null!; + + void Because() => _result = Derive( + CommandDeclaration(), + MemberDeclaration("customerCode", 0, "member"), + TypeUse("customerCode", null, "primitive-use")); + + [Fact] void should_not_invent_an_artifact_binding() => _result.Facts.ShouldBeEmpty(); + [Fact] void should_not_report_false_derivation_loss() => _result.Diagnostics.ShouldBeEmpty(); +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_an_observed_subject_has_conflicting_declarations.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_an_observed_subject_has_conflicting_declarations.cs new file mode 100644 index 0000000..35a0a91 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_an_observed_subject_has_conflicting_declarations.cs @@ -0,0 +1,44 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_an_observed_subject_has_conflicting_declarations : given.a_derivation +{ + GenerationDerivationSnapshot _roles = null!; + GenerationDerivationSnapshot _variants = null!; + + void Because() + { + var concept = Concept(ConceptSubject, "concept-target"); + var composite = concept with + { + Id = new FactId { Value = "concepts:composite-target" }, + Definition = concept.Definition with + { + Key = concept.Definition.Key with { Kind = ArtifactKind.CompositeType } + } + }; + var alternate = concept with + { + Id = new FactId { Value = "concepts:alternate-target" }, + Definition = concept.Definition with { File = "Concepts/AlternateCustomerCode.cs" } + }; + var common = new GenerationFact[] + { + CommandDeclaration(), + MemberDeclaration("customerCode", 0, "member"), + TypeUse("customerCode", ConceptSubject, "type-use") + }; + _roles = Derive([.. common, concept, composite]); + _variants = Derive([.. common, concept, alternate]); + } + + [Fact] void should_not_choose_one_artifact_role() => _roles.Facts.ShouldBeEmpty(); + [Fact] void should_report_incompatible_target_roles() => Codes(_roles).ShouldContain(GenerationDiagnosticCodes.ConflictingTypeUseTarget); + [Fact] void should_not_choose_one_declaration_variant() => _variants.Facts.ShouldBeEmpty(); + [Fact] void should_report_incompatible_target_declarations() => Codes(_variants).ShouldContain(GenerationDiagnosticCodes.ConflictingTypeUseDeclaration); + + static IEnumerable Codes(GenerationDerivationSnapshot snapshot) => + snapshot.Diagnostics.Select(diagnostic => diagnostic.Code); +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_deriving_an_exact_cross_adapter_type_use.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_deriving_an_exact_cross_adapter_type_use.cs new file mode 100644 index 0000000..fe260ed --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_deriving_an_exact_cross_adapter_type_use.cs @@ -0,0 +1,29 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_deriving_an_exact_cross_adapter_type_use : given.a_derivation +{ + GenerationDerivationSnapshot _result = null!; + + void Because() => _result = Derive( + TypeUse("customerCode", ConceptSubject, "type-use", TypeUseShapeKind.Optional, TypeUseShapeKind.Named), + Concept(ConceptSubject, "customer-code"), + MemberDeclaration("customerCode", 0, "member"), + CommandDeclaration()); + + [Fact] void should_execute_the_stable_rule_version() => _result.Rules.Single().Rule.ShouldEqual(new GenerationDerivationRuleIdentity { Id = "cratis.screenplay.type-use-binding", Version = "1.0.0" }); + [Fact] void should_derive_one_granular_binding_fact() => _result.Facts.Select(record => record.Fact.GetType()).ShouldContainOnly(typeof(TypeUseBindingFact)); + [Fact] void should_not_repeat_a_complete_artifact_fact() => _result.Facts.Any(record => record.Fact is ArtifactFact).ShouldBeFalse(); + [Fact] void should_bind_the_exact_member_to_the_exact_concept_role() => Binding().Definition.ShouldEqual(new TypeUseBindingDefinition { Member = Member("customerCode"), Target = new ArtifactKey { Subject = ConceptSubject, Kind = ArtifactKind.Concept } }); + [Fact] void should_identify_the_derivation_producer() => Record().Lineage!.Producer.ShouldEqual(_result.Rules.Single().Rule); + [Fact] void should_reference_every_canonical_input_fact() => Record().Lineage!.Inputs.Select(id => id.Value).ShouldEqual("application:command", "application:member", "application:type-use", "concepts:customer-code"); + [Fact] void should_retain_every_input_evidence() => Record().Lineage!.Evidence.Length.ShouldEqual(4); + [Fact] void should_leave_the_derived_disposition_for_generation() => Record().Disposition.ShouldEqual(GenerationFactDisposition.Unknown); + [Fact] void should_not_report_derivation_loss() => _result.Diagnostics.ShouldBeEmpty(); + + GenerationFactRecord Record() => _result.Facts.Single(); + + TypeUseBindingFact Binding() => (TypeUseBindingFact)Record().Fact; +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_inputs_are_permuted.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_inputs_are_permuted.cs new file mode 100644 index 0000000..7502d44 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_inputs_are_permuted.cs @@ -0,0 +1,25 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_inputs_are_permuted : given.a_derivation +{ + GenerationDerivationSnapshot _forward = null!; + GenerationDerivationSnapshot _reverse = null!; + + void Because() + { + var facts = new GenerationFact[] + { + TypeUse("customerCode", ConceptSubject, "type-use"), + Concept(ConceptSubject, "customer-code"), + MemberDeclaration("customerCode", 0, "member"), + CommandDeclaration() + }; + _forward = Derive(facts); + _reverse = Derive([.. facts.AsEnumerable().Reverse()]); + } + + [Fact] void should_produce_recursively_identical_facts_lineage_and_diagnostics() => Projection(_reverse).ShouldEqual(Projection(_forward)); +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_member_names_contain_unpaired_utf16_code_units.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_member_names_contain_unpaired_utf16_code_units.cs new file mode 100644 index 0000000..49e1287 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_member_names_contain_unpaired_utf16_code_units.cs @@ -0,0 +1,20 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_member_names_contain_unpaired_utf16_code_units : given.a_derivation +{ + const string MemberName = "customer\ud800Code"; + GenerationDerivationSnapshot _result = null!; + + void Because() => _result = Derive( + CommandDeclaration(), + MemberDeclaration(MemberName, 0, "member"), + TypeUse(MemberName, ConceptSubject, "type-use"), + Concept(ConceptSubject, "customer-code")); + + [Fact] void should_derive_without_throwing() => _result.Facts.Length.ShouldEqual(1); + [Fact] void should_reversibly_encode_every_utf16_code_unit() => _result.Facts.Single().Fact.Id.Value.ShouldContain("D800"); + [Fact] void should_retain_the_exact_member_name() => ((TypeUseBindingFact)_result.Facts.Single().Fact).Definition.Member.Name.ShouldEqual(MemberName); +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_required_binding_inputs_are_missing_or_conflicted.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_required_binding_inputs_are_missing_or_conflicted.cs new file mode 100644 index 0000000..a00f23d --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_required_binding_inputs_are_missing_or_conflicted.cs @@ -0,0 +1,37 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_required_binding_inputs_are_missing_or_conflicted : given.a_derivation +{ + GenerationDerivationSnapshot _conflicted = null!; + GenerationDerivationSnapshot _missingMember = null!; + GenerationDerivationSnapshot _missingOwner = null!; + GenerationDerivationSnapshot _missingTarget = null!; + + void Because() + { + var otherTarget = new SubjectId { Value = "dotnet://Ordering/Concepts.OtherCustomerCode" }; + _conflicted = Derive( + CommandDeclaration(), + MemberDeclaration("customerCode", 0, "member"), + Concept(ConceptSubject, "customer-code"), + Concept(otherTarget, "other-customer-code"), + TypeUse("customerCode", ConceptSubject, "first-use"), + TypeUse("customerCode", otherTarget, "second-use")); + _missingOwner = Derive(TypeUse("customerCode", ConceptSubject, "missing-owner"), Concept(ConceptSubject, "target")); + _missingMember = Derive(CommandDeclaration(), TypeUse("customerCode", ConceptSubject, "missing-member"), Concept(ConceptSubject, "target")); + _missingTarget = Derive(CommandDeclaration(), MemberDeclaration("customerCode", 0, "member"), TypeUse("customerCode", ConceptSubject, "missing-target")); + } + + [Fact] void should_not_choose_one_conflicting_type_use() => _conflicted.Facts.ShouldBeEmpty(); + [Fact] void should_report_the_conflicting_type_uses() => Codes(_conflicted).ShouldContain(GenerationDiagnosticCodes.ConflictingMemberTypeUse); + [Fact] void should_fail_closed_without_an_owner() => Codes(_missingOwner).ShouldContain(GenerationDiagnosticCodes.MissingTypeUseOwner); + [Fact] void should_fail_closed_without_a_member() => Codes(_missingMember).ShouldContain(GenerationDiagnosticCodes.MissingTypeUseMember); + [Fact] void should_fail_closed_without_a_target() => Codes(_missingTarget).ShouldContain(GenerationDiagnosticCodes.MissingTypeUseTarget); + [Fact] void should_identify_every_affected_input_fact_canonically() => _conflicted.Diagnostics.Single().Message.ShouldContain("'application:first-use', 'application:second-use'"); + + static IEnumerable Codes(GenerationDerivationSnapshot snapshot) => + snapshot.Diagnostics.Select(diagnostic => diagnostic.Code); +} diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_same_named_concepts_have_different_exact_subjects.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_same_named_concepts_have_different_exact_subjects.cs new file mode 100644 index 0000000..a0d14d7 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_same_named_concepts_have_different_exact_subjects.cs @@ -0,0 +1,33 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_same_named_concepts_have_different_exact_subjects : given.a_derivation +{ + readonly SubjectId _otherConcept = new() { Value = "dotnet://Shipping/Concepts.CustomerCode" }; + readonly SubjectId _nonDotNetConcept = new() { Value = "typescript://catalog/CustomerCode" }; + GenerationDerivationSnapshot _dotNet = null!; + GenerationDerivationSnapshot _nonDotNet = null!; + + void Because() + { + var declarations = new GenerationFact[] + { + CommandDeclaration(), + MemberDeclaration("customerCode", 0, "member"), + Concept(_otherConcept, "shipping-customer-code"), + Concept(ConceptSubject, "ordering-customer-code"), + Concept(_nonDotNetConcept, "catalog-customer-code") + }; + _dotNet = Derive([.. declarations, TypeUse("customerCode", ConceptSubject, "dotnet-type-use")]); + _nonDotNet = Derive([.. declarations, TypeUse("customerCode", _nonDotNetConcept, "typescript-type-use")]); + } + + [Fact] void should_bind_only_the_exact_dotnet_subject() => Binding(_dotNet).Definition.Target.Subject.ShouldEqual(ConceptSubject); + [Fact] void should_allow_a_non_dotnet_frontend_subject() => Binding(_nonDotNet).Definition.Target.Subject.ShouldEqual(_nonDotNetConcept); + [Fact] void should_not_conflict_on_equal_display_names() => _dotNet.Diagnostics.Concat(_nonDotNet.Diagnostics).ShouldBeEmpty(); + + static TypeUseBindingFact Binding(GenerationDerivationSnapshot snapshot) => + (TypeUseBindingFact)snapshot.Facts.Single().Fact; +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_attaching_fixed_snapshot_derivation.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_attaching_fixed_snapshot_derivation.cs new file mode 100644 index 0000000..883830f --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_attaching_fixed_snapshot_derivation.cs @@ -0,0 +1,192 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_attaching_fixed_snapshot_derivation : given.a_generator +{ + const string ExpectedBindingId = "generation:derive:type-use-binding:1:" + + "0064006F0074006E00650074003A002F002F004F00720064006500720069006E0067002F0043006F006D006D0061006E00640073002E005200650067006900730074006500720043007500730074006F006D00650072" + + ":3:" + + "0063007500730074006F006D006500720043006F00640065" + + ":" + + "0064006F0074006E00650074003A002F002F004F00720064006500720069006E0067002F0043006F006E00630065007000740073002E0043007500730074006F006D006500720043006F00640065" + + ":1"; + readonly AdapterIdentity _application = new() { Id = "application", Version = "1.0.0" }; + readonly AdapterIdentity _concepts = new() { Id = "concepts", Version = "2.0.0" }; + readonly SubjectId _commandSubject = new() { Value = "dotnet://Ordering/Commands.RegisterCustomer" }; + readonly SubjectId _conceptSubject = new() { Value = "dotnet://Ordering/Concepts.CustomerCode" }; + readonly List _shape = [TypeUseShapeKind.Optional, TypeUseShapeKind.Named]; + Evidence _admittedTypeUseEvidence = null!; + GeneratedScreenplayDefinition _forward = null!; + GeneratedScreenplayDefinition _reverse = null!; + string _beforeMutation = string.Empty; + + void Because() + { + var command = new ArtifactKey { Subject = _commandSubject, Kind = ArtifactKind.Command }; + var missingSubject = new SubjectId { Value = "dotnet://Ordering/Concepts.MissingCode" }; + var applicationFacts = new GenerationFact[] + { + new ArtifactDeclarationFact + { + Id = Id(_application, "command"), + Subject = _commandSubject, + Evidence = Evidence(_application), + Definition = new ArtifactDeclarationDefinition + { + Artifact = command, + Name = "RegisterCustomer" + } + }, + Member(_application, command, _commandSubject, "customerCode", 0), + TypeUse(_application, command, _commandSubject, "customerCode", _conceptSubject, "customer-code", _shape), + Member(_application, command, _commandSubject, "missingCode", 1), + TypeUse(_application, command, _commandSubject, "missingCode", missingSubject, "missing-code", [TypeUseShapeKind.Named]) + }; + var conceptFacts = new GenerationFact[] + { + new ArtifactFact + { + Id = Id(_concepts, "customer-code"), + Subject = _conceptSubject, + Evidence = Evidence(_concepts), + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = _conceptSubject, Kind = ArtifactKind.Concept }, + Name = "CustomerCode" + } + }, + new ConceptRepresentationFact + { + Id = Id(_concepts, "customer-code-representation"), + Subject = _conceptSubject, + Evidence = Evidence(_concepts), + Definition = new ConceptRepresentationDefinition + { + Concept = _conceptSubject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + }; + var applicationForward = Admit(_application, applicationFacts); + var applicationReverse = Admit(_application, [.. applicationFacts.AsEnumerable().Reverse()]); + var conceptsForward = Admit(_concepts, conceptFacts); + var conceptsReverse = Admit(_concepts, [.. conceptFacts.AsEnumerable().Reverse()]); + _admittedTypeUseEvidence = applicationForward.Facts + .OfType() + .Single(fact => fact.Definition.Member.Name == "customerCode") + .Evidence; + var options = new ScreenplayGenerationOptions { Domain = "Ordering" }; + _forward = Generator.Generate( + Snapshot(Completed(applicationForward), Completed(conceptsForward)), + options); + _reverse = Generator.Generate( + Snapshot(Completed(conceptsReverse), Completed(applicationReverse)), + options); + _beforeMutation = AdapterRunProjection(_forward.AdapterRun); + + _shape.Clear(); + } + + [Fact] void should_attach_the_closed_derivation_rule() => Derivation().Rules.Single().Rule.ShouldEqual(new GenerationDerivationRuleIdentity { Id = "cratis.screenplay.type-use-binding", Version = "1.0.0" }); + [Fact] void should_attach_the_exact_derived_binding() => Binding().Definition.ShouldEqual(new TypeUseBindingDefinition { Member = new ArtifactMemberKey { Artifact = new ArtifactKey { Subject = _commandSubject, Kind = ArtifactKind.Command }, Name = "customerCode" }, Target = new ArtifactKey { Subject = _conceptSubject, Kind = ArtifactKind.Concept } }); + [Fact] void should_attach_the_exact_stable_binding_identity() => Binding().Id.Value.ShouldEqual(ExpectedBindingId); + [Fact] void should_attach_the_exact_lineage_producer() => Record().Lineage!.Producer.ShouldEqual(new GenerationDerivationRuleIdentity { Id = "cratis.screenplay.type-use-binding", Version = "1.0.0" }); + [Fact] void should_attach_the_exact_canonical_lineage_inputs() => Record().Lineage!.Inputs.Select(input => input.Value).ShouldEqual("application:command", "application:member:customerCode", "application:type-use:customer-code", "concepts:customer-code"); + [Fact] void should_attach_evidence_corresponding_to_every_lineage_input() => Record().Lineage!.Evidence.ShouldEqual(Evidence(_application), Evidence(_application), Evidence(_application), Evidence(_concepts)); + [Fact] void should_propagate_derivation_diagnostics_to_the_generated_result() => _forward.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.MissingTypeUseTarget); + [Fact] void should_retain_derivation_diagnostics_on_the_adapter_run() => Derivation().Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.MissingTypeUseTarget); + [Fact] void should_canonicalize_adapter_and_fact_permutations() => AdapterRunProjection(_reverse.AdapterRun).ShouldEqual(AdapterRunProjection(_forward.AdapterRun)); + [Fact] void should_generate_identical_source_for_adapter_and_fact_permutations() => _reverse.Source.ShouldEqual(_forward.Source); + [Fact] void should_remain_deeply_immutable_after_input_mutation() => AdapterRunProjection(_forward.AdapterRun).ShouldEqual(_beforeMutation); + [Fact] void should_deep_copy_admitted_input_evidence_into_lineage() => ReferenceEquals(Record().Lineage!.Evidence[2], _admittedTypeUseEvidence).ShouldBeFalse(); + + GenerationDerivationSnapshot Derivation() => _forward.AdapterRun!.Derivation!; + + GenerationFactRecord Record() => Derivation().Facts.Single(); + + TypeUseBindingFact Binding() => (TypeUseBindingFact)Record().Fact; + + static AdapterContributionSnapshot Admit(AdapterIdentity adapter, IReadOnlyList facts) + { + var descriptor = new AdapterDescriptor + { + Identity = adapter, + SourceLanguage = AdapterSourceLanguage.SourceIndependent, + Category = AdapterCategory.ApplicationFramework, + EmittedFactCapabilities = + [ + GenerationFactCapability.Artifact, + GenerationFactCapability.ConceptRepresentation, + GenerationFactCapability.ArtifactDeclaration, + GenerationFactCapability.ArtifactMemberDeclaration, + GenerationFactCapability.ArtifactMemberTypeUse + ] + }; + return AdapterContributionAdmission.Admit( + descriptor, + new AdapterContribution { Adapter = adapter, Facts = facts }).Snapshot!; + } + + static AdapterRunRecord Completed(AdapterContributionSnapshot contribution) => new() + { + Considered = true, + Probed = true, + Executed = true, + Descriptor = contribution.Descriptor, + Probe = new AdapterProbeApplicable(), + Execution = new AdapterExecutionCompleted { Contribution = contribution }, + Disposition = AdapterRunDisposition.Admitted + }; + + static ArtifactMemberDeclarationFact Member( + AdapterIdentity adapter, + ArtifactKey artifact, + SubjectId subject, + string name, + int order) => new() + { + Id = Id(adapter, $"member:{name}"), + Subject = subject, + Evidence = Evidence(adapter), + Definition = new ArtifactMemberDeclarationDefinition + { + Member = new ArtifactMemberKey { Artifact = artifact, Name = name }, + DeclarationOrder = order + } + }; + + static ArtifactMemberTypeUseFact TypeUse( + AdapterIdentity adapter, + ArtifactKey artifact, + SubjectId subject, + string name, + SubjectId observedType, + string suffix, + IReadOnlyList shape) => new() + { + Id = Id(adapter, $"type-use:{suffix}"), + Subject = subject, + Evidence = Evidence(adapter), + Definition = new ArtifactMemberTypeUseDefinition + { + Member = new ArtifactMemberKey { Artifact = artifact, Name = name }, + Type = new TypeUseDefinition + { + Name = observedType.Value.Split('.')[^1], + ObservedTypeSubject = observedType, + Shape = shape + } + } + }; + + static FactId Id(AdapterIdentity adapter, string suffix) => new() { Value = $"{adapter.Id}:{suffix}" }; + + static Evidence Evidence(AdapterIdentity adapter) => new() + { + Adapter = adapter, + Strength = EvidenceStrength.Exact + }; +} diff --git a/Source/DotNET/Generation/AdapterRunCanonicalizer.cs b/Source/DotNET/Generation/AdapterRunCanonicalizer.cs index e0cbbab..1fcfaa5 100644 --- a/Source/DotNET/Generation/AdapterRunCanonicalizer.cs +++ b/Source/DotNET/Generation/AdapterRunCanonicalizer.cs @@ -12,11 +12,38 @@ public static ImmutableArray FactRecords(IEnumerable new GenerationFactRecord { Fact = Fact(record.Fact), + Lineage = record.Lineage is null ? null : Lineage(record.Lineage), Disposition = record.Disposition, Diagnostics = Diagnostics(record.Diagnostics) }) ]; + public static GenerationDerivationSnapshot Derivation(GenerationDerivationSnapshot snapshot) => new() + { + Rules = + [ + .. snapshot.Rules + .Select(rule => new GenerationDerivationRuleRecord + { + Rule = Rule(rule.Rule), + Inputs = [.. rule.Inputs.Select(id => new FactId { Value = id.Value }).Distinct().OrderBy(id => id.Value, StringComparer.Ordinal)], + Outputs = [.. rule.Outputs.Select(id => new FactId { Value = id.Value }).Distinct().OrderBy(id => id.Value, StringComparer.Ordinal)], + Diagnostics = Diagnostics(rule.Diagnostics) + }) + .OrderBy(rule => rule.Rule.Id, StringComparer.Ordinal) + .ThenBy(rule => rule.Rule.Version, StringComparer.Ordinal) + ], + Facts = + [ + .. FactRecords(snapshot.Facts) + .OrderBy(record => record.Fact.Id.Value, StringComparer.Ordinal) + .ThenBy(record => record.Fact.Subject.Value, StringComparer.Ordinal) + .ThenBy(record => Structural.FactFamily(record.Fact)) + .ThenBy(record => Structural.FactDefinition(record.Fact), StringComparer.Ordinal) + ], + Diagnostics = Diagnostics(snapshot.Diagnostics) + }; + public static ImmutableArray Diagnostics(IEnumerable diagnostics) => [ .. diagnostics @@ -49,6 +76,41 @@ public static GenerationFact Fact(GenerationFact fact) Artifact = ArtifactKey(placement.Artifact), Placement = Placement(placement.Placement) }, + ArtifactDeclarationFact declaration => new ArtifactDeclarationFact + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = ArtifactDeclaration(declaration.Definition) + }, + ArtifactMemberDeclarationFact member => new ArtifactMemberDeclarationFact + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = ArtifactMemberDeclaration(member.Definition) + }, + ArtifactMemberTypeUseFact typeUse => new ArtifactMemberTypeUseFact + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = ArtifactMemberTypeUse(typeUse.Definition) + }, + TypeUseBindingFact binding => new TypeUseBindingFact + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = TypeUseBinding(binding.Definition) + }, + ArtifactMemberRoleFact role => new ArtifactMemberRoleFact + { + Id = id, + Subject = subject, + Evidence = evidence, + Definition = ArtifactMemberRole(role.Definition) + }, RelationshipFact relationship => new RelationshipFact { Id = id, @@ -117,6 +179,19 @@ public static AdapterRunRecord Adapter(AdapterRunRecord record) }; } + static GenerationFactLineage Lineage(GenerationFactLineage lineage) => new() + { + Producer = Rule(lineage.Producer), + Inputs = [.. lineage.Inputs.Select(id => new FactId { Value = id.Value }).Distinct().OrderBy(id => id.Value, StringComparer.Ordinal)], + Evidence = [.. lineage.Evidence.Select(Evidence)] + }; + + static GenerationDerivationRuleIdentity Rule(GenerationDerivationRuleIdentity rule) => new() + { + Id = rule.Id, + Version = rule.Version + }; + static AdapterDescriptor Descriptor(AdapterDescriptor descriptor) => AdapterDescriptorAdmission.Admit(descriptor).Descriptor; @@ -302,6 +377,49 @@ .. diagnostics ] }; + static ArtifactDeclarationDefinition ArtifactDeclaration(ArtifactDeclarationDefinition definition) => new() + { + Artifact = ArtifactKey(definition.Artifact), + Name = definition.Name, + Description = definition.Description, + File = definition.File + }; + + static ArtifactMemberKey ArtifactMemberKey(ArtifactMemberKey member) => new() + { + Artifact = ArtifactKey(member.Artifact), + Name = member.Name + }; + + static ArtifactMemberDeclarationDefinition ArtifactMemberDeclaration(ArtifactMemberDeclarationDefinition definition) => new() + { + Member = ArtifactMemberKey(definition.Member), + DeclarationOrder = definition.DeclarationOrder + }; + + static ArtifactMemberTypeUseDefinition ArtifactMemberTypeUse(ArtifactMemberTypeUseDefinition definition) => new() + { + Member = ArtifactMemberKey(definition.Member), + Type = new TypeUseDefinition + { + Name = definition.Type.Name, + ObservedTypeSubject = definition.Type.ObservedTypeSubject is null ? null : Subject(definition.Type.ObservedTypeSubject), + Shape = [.. definition.Type.Shape] + } + }; + + static TypeUseBindingDefinition TypeUseBinding(TypeUseBindingDefinition definition) => new() + { + Member = ArtifactMemberKey(definition.Member), + Target = ArtifactKey(definition.Target) + }; + + static ArtifactMemberRoleDefinition ArtifactMemberRole(ArtifactMemberRoleDefinition definition) => new() + { + Member = ArtifactMemberKey(definition.Member), + Role = definition.Role + }; + static ArtifactPlacement Placement(ArtifactPlacement placement) => new() { Module = placement.Module, diff --git a/Source/DotNET/Generation/GenerationDiagnosticCodes.cs b/Source/DotNET/Generation/GenerationDiagnosticCodes.cs index 29f589b..d0b9002 100644 --- a/Source/DotNET/Generation/GenerationDiagnosticCodes.cs +++ b/Source/DotNET/Generation/GenerationDiagnosticCodes.cs @@ -207,4 +207,44 @@ public static class GenerationDiagnosticCodes /// An admitted fact participated in a conflict without a more specific pipeline diagnostic. /// public const string ConflictingGenerationFact = "GEN0042"; + + /// + /// A member type use names an artifact owner that was not declared in the fixed base snapshot. + /// + public const string MissingTypeUseOwner = "GEN0043"; + + /// + /// A member type use names a member that was not declared in the fixed base snapshot. + /// + public const string MissingTypeUseMember = "GEN0044"; + + /// + /// An observed exact type subject has no declared artifact target in the fixed base snapshot. + /// + public const string MissingTypeUseTarget = "GEN0045"; + + /// + /// Incompatible exact type uses were asserted for one artifact member. + /// + public const string ConflictingMemberTypeUse = "GEN0046"; + + /// + /// An exact observed type subject resolves to incompatible artifact targets. + /// + public const string ConflictingTypeUseTarget = "GEN0047"; + + /// + /// Incompatible artifact declarations prevent an exact type-use binding. + /// + public const string ConflictingTypeUseDeclaration = "GEN0048"; + + /// + /// An exact type-use shape cannot be represented without semantic loss. + /// + public const string UnsupportedTypeUseShape = "GEN0049"; + + /// + /// Incompatible declarations or roles were asserted for one artifact member. + /// + public const string ConflictingArtifactMember = "GEN0050"; } diff --git a/Source/DotNET/Generation/GenerationFactDerivation.cs b/Source/DotNET/Generation/GenerationFactDerivation.cs new file mode 100644 index 0000000..3f58444 --- /dev/null +++ b/Source/DotNET/Generation/GenerationFactDerivation.cs @@ -0,0 +1,45 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +namespace Cratis.Screenplay.Generation; + +/// +/// Derives source-neutral facts once from one fixed admitted base snapshot. +/// +public static class GenerationFactDerivation +{ + /// + /// Runs the closed built-in derivation rule set over the admitted facts in . + /// + /// The immutable adapter run containing the admitted base facts. + /// An immutable derivation snapshot whose rules consumed only the fixed base fact array. + public static GenerationDerivationSnapshot Derive(AdapterRunSnapshot snapshot) + { + var baseFacts = AdapterRunCanonicalizer.FactRecords(snapshot.Facts) + .OrderBy(record => record.Fact.Id.Value, StringComparer.Ordinal) + .ThenBy(record => record.Fact.Subject.Value, StringComparer.Ordinal) + .ThenBy(record => Structural.FactFamily(record.Fact)) + .ThenBy(record => Structural.FactDefinition(record.Fact), StringComparer.Ordinal) + .ToImmutableArray(); + var typeUseBindings = TypeUseBindingDerivation.Derive(baseFacts); + var derivation = new GenerationDerivationSnapshot + { + Rules = + [ + new GenerationDerivationRuleRecord + { + Rule = TypeUseBindingDerivation.Rule, + Inputs = typeUseBindings.Inputs, + Outputs = [.. typeUseBindings.Facts.Select(record => record.Fact.Id)], + Diagnostics = typeUseBindings.Diagnostics + } + ], + Facts = typeUseBindings.Facts, + Diagnostics = typeUseBindings.Diagnostics + }; + + return AdapterRunCanonicalizer.Derivation(derivation); + } +} diff --git a/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs b/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs index 3eae2ea..f78acd5 100644 --- a/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs +++ b/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs @@ -135,6 +135,21 @@ public GeneratedScreenplayDefinition Generate( .OrderBy(contribution => contribution.Descriptor.Identity.Id, StringComparer.Ordinal) .ThenBy(contribution => contribution.Descriptor.Identity.Version, StringComparer.Ordinal) .ToArray(); + var facts = completed + .SelectMany(contribution => contribution.Facts.Select(fact => new ProducedFact(contribution.Descriptor.Identity, fact))) + .OrderBy(item => item.Producer.Id, StringComparer.Ordinal) + .ThenBy(item => item.Producer.Version, StringComparer.Ordinal) + .ThenBy(item => item.Fact.Id.Value, StringComparer.Ordinal) + .ThenBy(item => item.Fact.Subject.Value, StringComparer.Ordinal) + .ThenBy(item => Structural.FactFamily(item.Fact)) + .ThenBy(item => Structural.FactDefinition(item.Fact), StringComparer.Ordinal) + .ThenBy(item => Structural.Evidence(item.Fact.Evidence), StringComparer.Ordinal) + .Select(item => item.Fact) + .ToArray(); + var derivation = GenerationFactDerivation.Derive(new AdapterRunSnapshot + { + Facts = [.. facts.Select(fact => new GenerationFactRecord { Fact = fact })] + }); var contributions = completed.Select(contribution => new AdapterContribution { Adapter = contribution.Descriptor.Identity, @@ -158,21 +173,11 @@ public GeneratedScreenplayDefinition Generate( } var pipelineDiagnostics = graph.Diagnostics + .Concat(derivation.Diagnostics) .Concat(lowering.Diagnostics) .Concat(verificationDiagnostics) .OrderBy(Canonical.Diagnostic, StringComparer.Ordinal) .ToArray(); - var facts = completed - .SelectMany(contribution => contribution.Facts.Select(fact => new ProducedFact(contribution.Descriptor.Identity, fact))) - .OrderBy(item => item.Producer.Id, StringComparer.Ordinal) - .ThenBy(item => item.Producer.Version, StringComparer.Ordinal) - .ThenBy(item => item.Fact.Id.Value, StringComparer.Ordinal) - .ThenBy(item => item.Fact.Subject.Value, StringComparer.Ordinal) - .ThenBy(item => Structural.FactFamily(item.Fact)) - .ThenBy(item => Structural.FactDefinition(item.Fact), StringComparer.Ordinal) - .ThenBy(item => Structural.Evidence(item.Fact.Evidence), StringComparer.Ordinal) - .Select(item => item.Fact) - .ToArray(); var factRecords = GenerationFactDispositionCalculator.Calculate( facts, graph, @@ -185,6 +190,7 @@ public GeneratedScreenplayDefinition Generate( { Adapters = canonicalAdapters, Facts = canonicalFactRecords, + Derivation = derivation, Diagnostics = CanonicalDiagnostics(runnerDiagnostics.Concat(dispositionDiagnostics)) }; var diagnostics = CanonicalDiagnostics( diff --git a/Source/DotNET/Generation/Structural.cs b/Source/DotNET/Generation/Structural.cs index e54bb70..2d0355e 100644 --- a/Source/DotNET/Generation/Structural.cs +++ b/Source/DotNET/Generation/Structural.cs @@ -20,6 +20,34 @@ public static string Artifact(ArtifactDefinition definition) => definition.File, Sequence(definition.Properties, Property)); + public static string ArtifactDeclaration(ArtifactDeclarationDefinition definition) => + Node( + ArtifactKey(definition.Artifact), + definition.Name, + definition.Description, + definition.File); + + public static string ArtifactMemberKey(ArtifactMemberKey member) => + Node(ArtifactKey(member.Artifact), member.Name); + + public static string ArtifactMemberDeclaration(ArtifactMemberDeclarationDefinition definition) => + Node(ArtifactMemberKey(definition.Member), Integer(definition.DeclarationOrder)); + + public static string ArtifactMemberTypeUse(ArtifactMemberTypeUseDefinition definition) => + Node(ArtifactMemberKey(definition.Member), TypeUse(definition.Type)); + + public static string TypeUse(TypeUseDefinition definition) => + Node( + definition.Name, + definition.ObservedTypeSubject?.Value, + Sequence(definition.Shape, node => Integer((int)node))); + + public static string TypeUseBinding(TypeUseBindingDefinition definition) => + Node(ArtifactMemberKey(definition.Member), ArtifactKey(definition.Target)); + + public static string ArtifactMemberRole(ArtifactMemberRoleDefinition definition) => + Node(ArtifactMemberKey(definition.Member), Integer((int)definition.Role)); + public static string Placement(ArtifactPlacement placement) => Node( placement.Module, @@ -100,6 +128,11 @@ public static string SpecificationValue(SpecificationValueDefinition definition) { ArtifactFact artifact => Node("artifact", Artifact(artifact.Definition)), ArtifactPlacementFact placement => Node("placement", ArtifactKey(placement.Artifact), Placement(placement.Placement)), + ArtifactDeclarationFact declaration => Node("artifact-declaration", ArtifactDeclaration(declaration.Definition)), + ArtifactMemberDeclarationFact member => Node("artifact-member-declaration", ArtifactMemberDeclaration(member.Definition)), + ArtifactMemberTypeUseFact typeUse => Node("artifact-member-type-use", ArtifactMemberTypeUse(typeUse.Definition)), + TypeUseBindingFact binding => Node("type-use-binding", TypeUseBinding(binding.Definition)), + ArtifactMemberRoleFact role => Node("artifact-member-role", ArtifactMemberRole(role.Definition)), RelationshipFact relationship => Node("relationship", Relationship(relationship.Definition)), ConceptRepresentationFact representation => Node("concept-representation", ConceptRepresentation(representation.Definition)), ConceptAttributeFact attribute => Node("concept-attribute", ConceptAttribute(attribute.Definition)), @@ -181,6 +214,11 @@ public static string AdapterRecord(AdapterRunRecord record) => SpecificationScenarioFact => (int)GenerationFactCapability.SpecificationScenario, SpecificationStepFact => (int)GenerationFactCapability.SpecificationStep, SpecificationValueFact => (int)GenerationFactCapability.SpecificationValue, + ArtifactDeclarationFact => (int)GenerationFactCapability.ArtifactDeclaration, + ArtifactMemberDeclarationFact => (int)GenerationFactCapability.ArtifactMemberDeclaration, + ArtifactMemberTypeUseFact => (int)GenerationFactCapability.ArtifactMemberTypeUse, + TypeUseBindingFact => (int)GenerationFactCapability.TypeUseBinding, + ArtifactMemberRoleFact => (int)GenerationFactCapability.ArtifactMemberRole, _ => int.MaxValue }; diff --git a/Source/DotNET/Generation/TypeUseBindingDerivation.cs b/Source/DotNET/Generation/TypeUseBindingDerivation.cs new file mode 100644 index 0000000..1e6267b --- /dev/null +++ b/Source/DotNET/Generation/TypeUseBindingDerivation.cs @@ -0,0 +1,336 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +namespace Cratis.Screenplay.Generation; + +static class TypeUseBindingDerivation +{ + public static GenerationDerivationRuleIdentity Rule { get; } = new() + { + Id = "cratis.screenplay.type-use-binding", + Version = "1.0.0" + }; + + public static TypeUseBindingDerivationResult Derive(ImmutableArray baseFacts) + { + var facts = baseFacts.Select(record => record.Fact).ToArray(); + var declarations = ArtifactDeclarations(facts); + var members = MemberDeclarations(facts); + var consideredInputs = facts + .Where(fact => fact is ArtifactFact or + ArtifactDeclarationFact or + ArtifactMemberDeclarationFact or + ArtifactMemberTypeUseFact) + .Select(fact => fact.Id) + .OrderBy(id => id.Value, StringComparer.Ordinal) + .ToImmutableArray(); + var derived = new List(); + var diagnostics = new List(); + var typeUseGroups = facts + .OfType() + .GroupBy(fact => Structural.ArtifactMemberKey(fact.Definition.Member), StringComparer.Ordinal) + .OrderBy(group => group.Key, StringComparer.Ordinal); + + foreach (var group in typeUseGroups) + { + DeriveBinding([.. group], declarations, members, derived, diagnostics); + } + + return new TypeUseBindingDerivationResult( + consideredInputs, + [.. derived], + [.. diagnostics.OrderBy(Canonical.Diagnostic, StringComparer.Ordinal)]); + } + + static void DeriveBinding( + ArtifactMemberTypeUseFact[] typeUses, + IReadOnlyList declarations, + IReadOnlyList members, + List derived, + List diagnostics) + { + var member = typeUses[0].Definition.Member; + var typeVariants = typeUses + .GroupBy(fact => Structural.TypeUse(fact.Definition.Type), StringComparer.Ordinal) + .OrderBy(group => group.Key, StringComparer.Ordinal) + .ToArray(); + if (typeVariants.Length > 1) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.ConflictingMemberTypeUse, + GenerationDiagnosticOutcome.Conflict, + member, + typeUses, + $"Incompatible exact type uses were asserted for member '{member.Name}'")); + return; + } + + var typeUse = typeVariants[0] + .OrderBy(fact => fact.Id.Value, StringComparer.Ordinal) + .ThenBy(DerivationInputKey, StringComparer.Ordinal) + .First(); + if (typeUse.Definition.Type.ObservedTypeSubject is not { } observedType) + { + return; + } + + var ownerDeclarations = declarations + .Where(declaration => declaration.Key == member.Artifact) + .ToArray(); + if (ownerDeclarations.Length == 0) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.MissingTypeUseOwner, + GenerationDiagnosticOutcome.Unknown, + member, + typeUses, + $"Artifact owner '{member.Artifact.Subject.Value}' was not declared for member '{member.Name}'")); + return; + } + + if (HasConflictingDeclarations(ownerDeclarations)) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.ConflictingTypeUseDeclaration, + GenerationDiagnosticOutcome.Conflict, + member, + ownerDeclarations.Select(declaration => declaration.Fact).Concat(typeUses), + $"Incompatible declarations were asserted for artifact owner '{member.Artifact.Subject.Value}'")); + return; + } + + var memberDeclarations = members + .Where(declaration => declaration.Member == member) + .ToArray(); + if (memberDeclarations.Length == 0) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.MissingTypeUseMember, + GenerationDiagnosticOutcome.Unknown, + member, + ownerDeclarations.Select(declaration => declaration.Fact).Concat(typeUses), + $"Member '{member.Name}' was not declared on artifact owner '{member.Artifact.Subject.Value}'")); + return; + } + + if (memberDeclarations.Select(declaration => declaration.Order).Distinct().Count() > 1) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.ConflictingTypeUseDeclaration, + GenerationDiagnosticOutcome.Conflict, + member, + memberDeclarations.Select(declaration => declaration.Fact).Concat(typeUses), + $"Incompatible declaration orders were asserted for member '{member.Name}'")); + return; + } + + var targetDeclarations = declarations + .Where(declaration => declaration.Key.Subject == observedType) + .ToArray(); + if (targetDeclarations.Length == 0) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.MissingTypeUseTarget, + GenerationDiagnosticOutcome.Unknown, + member, + typeUses, + $"Observed type subject '{observedType.Value}' has no declared artifact target")); + return; + } + + var targetKeys = targetDeclarations + .GroupBy(declaration => Structural.ArtifactKey(declaration.Key), StringComparer.Ordinal) + .OrderBy(group => group.Key, StringComparer.Ordinal) + .ToArray(); + if (targetKeys.Length > 1) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.ConflictingTypeUseTarget, + GenerationDiagnosticOutcome.Conflict, + member, + targetDeclarations.Select(declaration => declaration.Fact).Concat(typeUses), + $"Observed type subject '{observedType.Value}' declares incompatible artifact targets")); + return; + } + + var exactTargetDeclarations = targetKeys[0].ToArray(); + if (HasConflictingDeclarations(exactTargetDeclarations)) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.ConflictingTypeUseDeclaration, + GenerationDiagnosticOutcome.Conflict, + member, + exactTargetDeclarations.Select(declaration => declaration.Fact).Concat(typeUses), + $"Observed type subject '{observedType.Value}' has incompatible declarations")); + return; + } + + var inputs = CanonicalInputs( + typeUses + .Cast() + .Concat(ownerDeclarations.Select(declaration => declaration.Fact)) + .Concat(memberDeclarations.Select(declaration => declaration.Fact)) + .Concat(exactTargetDeclarations.Select(declaration => declaration.Fact))); + var target = exactTargetDeclarations[0].Key; + var fact = new TypeUseBindingFact + { + Id = DerivedId(member, target), + Subject = member.Artifact.Subject, + Evidence = typeUse.Evidence, + Definition = new TypeUseBindingDefinition + { + Member = member, + Target = target + } + }; + derived.Add(new GenerationFactRecord + { + Fact = fact, + Lineage = new GenerationFactLineage + { + Producer = Rule, + Inputs = [.. inputs.Select(input => input.Id)], + Evidence = [.. inputs.Select(input => input.Evidence)] + } + }); + } + + static IReadOnlyList ArtifactDeclarations(IEnumerable facts) => + [ + .. facts.SelectMany(fact => fact switch + { + ArtifactFact artifact => + [ + new DeclaredArtifact( + artifact.Definition.Key, + DeclarationKey( + artifact.Definition.Key, + artifact.Definition.Name, + artifact.Definition.Description, + artifact.Definition.File), + artifact) + ], + ArtifactDeclarationFact declaration => + [ + new DeclaredArtifact( + declaration.Definition.Artifact, + DeclarationKey( + declaration.Definition.Artifact, + declaration.Definition.Name, + declaration.Definition.Description, + declaration.Definition.File), + declaration) + ], + _ => [] + }) + ]; + + static IReadOnlyList MemberDeclarations(IEnumerable facts) => + [ + .. facts.SelectMany(fact => fact switch + { + ArtifactFact artifact => artifact.Definition.Properties.Select((property, order) => + new DeclaredMember( + new ArtifactMemberKey { Artifact = artifact.Definition.Key, Name = property.Name }, + order, + artifact)), + ArtifactMemberDeclarationFact member => + [new DeclaredMember(member.Definition.Member, member.Definition.DeclarationOrder, member)], + _ => [] + }) + ]; + + static bool HasConflictingDeclarations(IEnumerable declarations) => + declarations.Select(declaration => declaration.Declaration).Distinct(StringComparer.Ordinal).Count() > 1; + + static string DeclarationKey( + ArtifactKey artifact, + string name, + string? description, + string? file) => + Structural.ArtifactDeclaration(new ArtifactDeclarationDefinition + { + Artifact = artifact, + Name = name, + Description = description, + File = file + }); + + static GenerationFact[] CanonicalInputs(IEnumerable inputs) => + [ + .. inputs + .GroupBy(input => input.Id.Value, StringComparer.Ordinal) + .OrderBy(group => group.Key, StringComparer.Ordinal) + .Select(group => group + .OrderBy(DerivationInputKey, StringComparer.Ordinal) + .First()) + ]; + + static string DerivationInputKey(GenerationFact fact) + { + var source = fact.Evidence.Source; + return Structural.SemanticKey( + "derivation-input", + fact.Subject.Value, + Structural.FactFamily(fact).ToString(System.Globalization.CultureInfo.InvariantCulture), + Structural.FactDefinition(fact), + ((int)fact.Evidence.Strength).ToString(System.Globalization.CultureInfo.InvariantCulture), + source?.FileIdentity?.Project, + source?.FileIdentity?.Path, + source?.Path, + source?.StartLine.ToString(System.Globalization.CultureInfo.InvariantCulture), + source?.StartColumn.ToString(System.Globalization.CultureInfo.InvariantCulture), + source?.EndLine.ToString(System.Globalization.CultureInfo.InvariantCulture), + source?.EndColumn.ToString(System.Globalization.CultureInfo.InvariantCulture), + fact.Evidence.Explanation); + } + + static GenerationDiagnostic Diagnostic( + string code, + GenerationDiagnosticOutcome outcome, + ArtifactMemberKey member, + IEnumerable inputs, + string message) + { + var canonicalInputs = CanonicalInputs(inputs); + var identities = string.Join(", ", canonicalInputs.Select(input => $"'{input.Id.Value}'")); + return new GenerationDiagnostic + { + Code = code, + Severity = GenerationDiagnosticSeverity.Error, + Outcome = outcome, + Message = $"{message}. Input facts: {identities}", + Source = canonicalInputs.FirstOrDefault()?.Evidence.Source, + Subject = member.Artifact.Subject + }; + } + + static FactId DerivedId(ArtifactMemberKey member, ArtifactKey target) => new() + { + Value = string.Join( + ':', + "generation", + "derive", + "type-use-binding", + "1", + Escape(member.Artifact.Subject.Value), + ((int)member.Artifact.Kind).ToString(System.Globalization.CultureInfo.InvariantCulture), + Escape(member.Name), + Escape(target.Subject.Value), + ((int)target.Kind).ToString(System.Globalization.CultureInfo.InvariantCulture)) + }; + + static string Escape(string value) => string.Concat( + value.Select(character => ((int)character).ToString("X4", System.Globalization.CultureInfo.InvariantCulture))); + + sealed record DeclaredArtifact(ArtifactKey Key, string Declaration, GenerationFact Fact); + + sealed record DeclaredMember(ArtifactMemberKey Member, int Order, GenerationFact Fact); +} + +sealed record TypeUseBindingDerivationResult( + ImmutableArray Inputs, + ImmutableArray Facts, + ImmutableArray Diagnostics); From 676bd7f85d7fec586cbbe8f3847f999373cf0609 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 09:00:48 +0200 Subject: [PATCH 3/7] Apply derived bindings as granular overlays Resolve admitted and derived member facets at their smallest semantic key, materializing compatibility artifact definitions only in the resolved graph. Conflicts and unsupported shapes now fail closed with exact dispositions while legacy aggregate paths remain supported. --- ..._a_type_use_asserts_a_different_subject.cs | 26 + ...anular_fact_asserts_a_different_subject.cs | 119 +++++ ..._exact_type_use_shape_cannot_be_lowered.cs | 87 ++++ .../when_identifier_roles_conflict.cs | 140 ++++++ ...owering_an_event_source_identifier_role.cs | 106 ++++ ...when_lowering_granular_type_use_binding.cs | 160 ++++++ ...when_member_declaration_orders_conflict.cs | 99 ++++ ...layed_legacy_artifact_variants_conflict.cs | 125 +++++ ...ying_one_legacy_artifact_member_binding.cs | 121 +++++ ...urce_members_normalize_to_the_same_name.cs | 58 +++ .../when_type_use_bindings_conflict.cs | 132 +++++ .../Generation/GenerationDiagnosticCodes.cs | 20 + .../Generation/GenerationFactDerivation.cs | 16 +- .../GenerationFactDiscriminatorValidator.cs | 53 ++ .../GenerationFactDispositionCalculator.cs | 157 +++++- .../DotNET/Generation/GenerationResolver.cs | 20 +- .../Generation/GranularArtifactResolver.cs | 473 ++++++++++++++++++ .../Generation/ResolvedApplicationGraph.cs | 5 + .../ScreenplayDefinitionGenerator.cs | 49 +- .../Generation/ScreenplayLoweringCoverage.cs | 20 + 20 files changed, 1957 insertions(+), 29 deletions(-) create mode 100644 Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_type_use_asserts_a_different_subject.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_granular_fact_asserts_a_different_subject.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_granular_type_use_binding.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_member_declaration_orders_conflict.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlayed_legacy_artifact_variants_conflict.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlaying_one_legacy_artifact_member_binding.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_source_members_normalize_to_the_same_name.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs create mode 100644 Source/DotNET/Generation/GranularArtifactResolver.cs diff --git a/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_type_use_asserts_a_different_subject.cs b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_type_use_asserts_a_different_subject.cs new file mode 100644 index 0000000..eff31bc --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_GenerationFactDerivation/when_a_type_use_asserts_a_different_subject.cs @@ -0,0 +1,26 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_GenerationFactDerivation; + +public class when_a_type_use_asserts_a_different_subject : given.a_derivation +{ + GenerationDerivationSnapshot _result = null!; + + void Because() + { + var typeUse = TypeUse("customerCode", ConceptSubject, "type-use") with + { + Subject = new SubjectId { Value = "dotnet://Foreign/Commands.RegisterCustomer" } + }; + _result = Derive( + CommandDeclaration(), + MemberDeclaration("customerCode", 0, "member"), + typeUse, + Concept(ConceptSubject, "customer-code")); + } + + [Fact] void should_not_derive_from_the_foreign_type_use() => _result.Facts.ShouldBeEmpty(); + [Fact] void should_report_invalid_granular_ownership() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.InvalidGranularFactOwnership); + [Fact] void should_exclude_the_foreign_fact_from_rule_inputs() => _result.Rules.Single().Inputs.Select(input => input.Value).ShouldNotContain("application:type-use"); +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_granular_fact_asserts_a_different_subject.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_granular_fact_asserts_a_different_subject.cs new file mode 100644 index 0000000..f7cdcb4 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_granular_fact_asserts_a_different_subject.cs @@ -0,0 +1,119 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_a_granular_fact_asserts_a_different_subject : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var artifactSubject = new SubjectId { Value = "dotnet://Ordering/Commands.SubmitOrder" }; + var foreignSubject = new SubjectId { Value = "dotnet://Foreign/Commands.SubmitOrder" }; + var artifact = new ArtifactKey { Subject = artifactSubject, Kind = ArtifactKind.Command }; + var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.OrderSubmitted" }; + var hiddenSubject = new SubjectId { Value = "dotnet://Ordering/Events.HiddenEvent" }; + var @event = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var placement = new ArtifactPlacement + { + Module = "Orders", + Slice = "Submit", + SliceKind = GenerationSliceKind.StateChange + }; + var facts = new GenerationFact[] + { + new ArtifactFact + { + Id = new FactId { Value = "command:submit" }, + Subject = artifactSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = artifact, + Name = "SubmitOrder", + Properties = + [ + new PropertyDefinition + { + Name = "orderId", + Type = new TypeReferenceDefinition { Name = "Uuid" } + } + ] + } + }, + new ArtifactDeclarationFact + { + Id = new FactId { Value = "foreign:declaration" }, + Subject = foreignSubject, + Evidence = evidence, + Definition = new ArtifactDeclarationDefinition + { + Artifact = new ArtifactKey { Subject = hiddenSubject, Kind = ArtifactKind.Event }, + Name = "HiddenEvent" + } + }, + new ArtifactMemberRoleFact + { + Id = new FactId { Value = "foreign:role" }, + Subject = foreignSubject, + Evidence = evidence, + Definition = new ArtifactMemberRoleDefinition + { + Member = new ArtifactMemberKey { Artifact = artifact, Name = "orderId" }, + Role = ArtifactMemberRoleKind.EventSourceIdentifier + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "command:placement" }, + Subject = artifactSubject, + Evidence = evidence, + Artifact = artifact, + Placement = placement + }, + new ArtifactFact + { + Id = new FactId { Value = "event:submitted" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactDefinition { Key = @event, Name = "OrderSubmitted" } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "event:placement" }, + Subject = eventSubject, + Evidence = evidence, + Artifact = @event, + Placement = placement + }, + new RelationshipFact + { + Id = new FactId { Value = "command:produces" }, + Subject = artifactSubject, + Evidence = evidence, + Definition = new RelationshipDefinition + { + Key = new RelationshipKey + { + Kind = RelationshipKind.Produces, + Source = artifactSubject, + Target = eventSubject + } + } + } + }; + + _result = Generator.Generate( + [new AdapterContribution { Adapter = Adapter, Facts = facts }], + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_report_invalid_granular_ownership() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.InvalidGranularFactOwnership); + [Fact] void should_not_create_an_artifact_from_the_foreign_declaration() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Subject.Value == "dotnet://Ordering/Events.HiddenEvent").ShouldBeFalse(); + [Fact] void should_not_apply_the_foreign_role() => Command().Definition.Properties.Single().IsIdentifier.ShouldBeFalse(); + [Fact] void should_not_emit_identifier_semantics() => _result.Source.ShouldNotContain("orderId Uuid identifier"); + + ResolvedArtifactVariant Command() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Command).Variants.Single(); +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs new file mode 100644 index 0000000..b2d5c9d --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs @@ -0,0 +1,87 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_an_exact_type_use_shape_cannot_be_lowered : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var subject = new SubjectId { Value = "dotnet://Ordering/Events.CustomerRegistered" }; + var artifact = new ArtifactKey { Subject = subject, Kind = ArtifactKind.Event }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var facts = new GenerationFact[] + { + new ArtifactDeclarationFact + { + Id = new FactId { Value = "critter-stack:declaration" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactDeclarationDefinition + { + Artifact = artifact, + Name = "CustomerRegistered" + } + }, + new ArtifactMemberDeclarationFact + { + Id = new FactId { Value = "critter-stack:member" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactMemberDeclarationDefinition + { + Member = new ArtifactMemberKey { Artifact = artifact, Name = "codes" }, + DeclarationOrder = 0 + } + }, + new ArtifactMemberTypeUseFact + { + Id = new FactId { Value = "critter-stack:type-use" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = new ArtifactMemberKey { Artifact = artifact, Name = "codes" }, + Type = new TypeUseDefinition + { + Name = "String", + Shape = + [ + TypeUseShapeKind.Collection, + TypeUseShapeKind.Optional, + TypeUseShapeKind.Named + ] + } + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "critter-stack:placement" }, + Subject = subject, + Evidence = evidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + } + }; + + _result = Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_fail_closed() => _result.IsSuccess.ShouldBeFalse(); + [Fact] void should_report_the_exact_shape_as_unsupported() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.UnsupportedTypeUseShape); + [Fact] void should_omit_the_incomplete_granular_only_artifact_atomically() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Kind == ArtifactKind.Event).ShouldBeFalse(); + [Fact] void should_not_emit_the_unsupported_member() => _result.Source.ShouldNotContain("codes String"); + [Fact] void should_omit_the_type_use_with_its_diagnostic() => TypeUseRecord().Disposition.ShouldEqual(GenerationFactDisposition.OmittedWithDiagnostic); + [Fact] void should_associate_the_shape_diagnostic_with_the_type_use() => TypeUseRecord().Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.UnsupportedTypeUseShape); + + GenerationFactRecord TypeUseRecord() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "critter-stack:type-use"); +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs new file mode 100644 index 0000000..6227833 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs @@ -0,0 +1,140 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_identifier_roles_conflict : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var commandSubject = new SubjectId { Value = "dotnet://Ordering/Commands.SubmitOrder" }; + var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.OrderSubmitted" }; + var unrelatedSubject = new SubjectId { Value = "dotnet://Ordering/Events.AuditRecorded" }; + var command = new ArtifactKey { Subject = commandSubject, Kind = ArtifactKind.Command }; + var @event = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var member = new ArtifactMemberKey { Artifact = command, Name = "orderId" }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var placement = new ArtifactPlacement + { + Module = "Orders", + Slice = "Submit", + SliceKind = GenerationSliceKind.StateChange + }; + var facts = new GenerationFact[] + { + new ArtifactFact + { + Id = new FactId { Value = "command:submit" }, + Subject = commandSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = command, + Name = "SubmitOrder", + Properties = + [ + new PropertyDefinition + { + Name = "orderId", + Type = new TypeReferenceDefinition { Name = "Uuid" } + } + ] + } + }, + Role("role:identifier", ArtifactMemberRoleKind.Identifier, commandSubject, member, evidence), + Role("role:event-source-identifier", ArtifactMemberRoleKind.EventSourceIdentifier, commandSubject, member, evidence), + new ArtifactPlacementFact + { + Id = new FactId { Value = "command:placement" }, + Subject = commandSubject, + Evidence = evidence, + Artifact = command, + Placement = placement + }, + new ArtifactFact + { + Id = new FactId { Value = "event:submitted" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactDefinition { Key = @event, Name = "OrderSubmitted" } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "event:placement" }, + Subject = eventSubject, + Evidence = evidence, + Artifact = @event, + Placement = placement + }, + new ArtifactFact + { + Id = new FactId { Value = "orderId" }, + Subject = unrelatedSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = unrelatedSubject, Kind = ArtifactKind.Event }, + Name = "AuditRecorded" + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "audit:placement" }, + Subject = unrelatedSubject, + Evidence = evidence, + Artifact = new ArtifactKey { Subject = unrelatedSubject, Kind = ArtifactKind.Event }, + Placement = placement + }, + new RelationshipFact + { + Id = new FactId { Value = "command:produces" }, + Subject = commandSubject, + Evidence = evidence, + Definition = new RelationshipDefinition + { + Key = new RelationshipKey + { + Kind = RelationshipKind.Produces, + Source = commandSubject, + Target = eventSubject + } + } + } + }; + + _result = Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_fail_closed() => _result.IsSuccess.ShouldBeFalse(); + [Fact] void should_report_the_role_conflict() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.ConflictingArtifactMember); + [Fact] void should_not_choose_either_role() => Command().Definition.Properties.Single().IsIdentifier.ShouldBeFalse(); + [Fact] void should_conflict_both_role_facts() => Dispositions().ShouldContainOnly(GenerationFactDisposition.Conflicted, GenerationFactDisposition.Conflicted); + [Fact] void should_not_emit_identifier_semantics() => _result.Source.ShouldNotContain("orderId Uuid identifier"); + [Fact] void should_not_associate_a_quoted_member_name_with_an_unrelated_legacy_fact_id() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "orderId").Disposition.ShouldEqual(GenerationFactDisposition.Lowered); + + ResolvedArtifactVariant Command() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Command).Variants.Single(); + + GenerationFactDisposition[] Dispositions() => + [ + .. _result.AdapterRun!.Facts + .Where(record => record.Fact is ArtifactMemberRoleFact) + .Select(record => record.Disposition) + ]; + + static ArtifactMemberRoleFact Role( + string id, + ArtifactMemberRoleKind role, + SubjectId subject, + ArtifactMemberKey member, + Evidence evidence) => new() + { + Id = new FactId { Value = id }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactMemberRoleDefinition { Member = member, Role = role } + }; +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs new file mode 100644 index 0000000..0f7c06c --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs @@ -0,0 +1,106 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_lowering_an_event_source_identifier_role : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var commandSubject = new SubjectId { Value = "dotnet://Ordering/Commands.SubmitOrder" }; + var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.OrderSubmitted" }; + var command = new ArtifactKey { Subject = commandSubject, Kind = ArtifactKind.Command }; + var @event = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var placement = new ArtifactPlacement + { + Module = "Orders", + Slice = "Submit", + SliceKind = GenerationSliceKind.StateChange + }; + var facts = new GenerationFact[] + { + new ArtifactFact + { + Id = new FactId { Value = "command:submit" }, + Subject = commandSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = command, + Name = "SubmitOrder", + Properties = + [ + new PropertyDefinition + { + Name = "orderId", + Type = new TypeReferenceDefinition { Name = "Uuid" } + } + ] + } + }, + new ArtifactMemberRoleFact + { + Id = new FactId { Value = "command:submit:event-source-identifier" }, + Subject = commandSubject, + Evidence = evidence, + Definition = new ArtifactMemberRoleDefinition + { + Member = new ArtifactMemberKey { Artifact = command, Name = "orderId" }, + Role = ArtifactMemberRoleKind.EventSourceIdentifier + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "command:submit:placement" }, + Subject = commandSubject, + Evidence = evidence, + Artifact = command, + Placement = placement + }, + new ArtifactFact + { + Id = new FactId { Value = "event:submitted" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactDefinition { Key = @event, Name = "OrderSubmitted" } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "event:submitted:placement" }, + Subject = eventSubject, + Evidence = evidence, + Artifact = @event, + Placement = placement + }, + new RelationshipFact + { + Id = new FactId { Value = "command:submit:produces" }, + Subject = commandSubject, + Evidence = evidence, + Definition = new RelationshipDefinition + { + Key = new RelationshipKey + { + Kind = RelationshipKind.Produces, + Source = commandSubject, + Target = eventSubject + } + } + } + }; + + _result = Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_generate_successfully() => _result.IsSuccess.ShouldBeTrue(); + [Fact] void should_mark_the_exact_command_member_as_identifying() => Command().Definition.Properties.Single().IsIdentifier.ShouldBeTrue(); + [Fact] void should_lower_the_event_source_identifier_role() => _result.Source.ShouldContain("orderId Uuid identifier"); + [Fact] void should_classify_the_role_as_lowered() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "command:submit:event-source-identifier").Disposition.ShouldEqual(GenerationFactDisposition.Lowered); + + ResolvedArtifactVariant Command() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Command).Variants.Single(); +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_granular_type_use_binding.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_granular_type_use_binding.cs new file mode 100644 index 0000000..da68206 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_granular_type_use_binding.cs @@ -0,0 +1,160 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_lowering_granular_type_use_binding : given.a_generator +{ + readonly AdapterIdentity _application = new() { Id = "application", Version = "1.0.0" }; + readonly AdapterIdentity _concepts = new() { Id = "concepts", Version = "2.0.0" }; + GeneratedScreenplayDefinition _contribution = null!; + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.CustomerRegistered" }; + var conceptSubject = new SubjectId { Value = "dotnet://Ordering/Concepts.CustomerCode" }; + var artifact = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var member = new ArtifactMemberKey { Artifact = artifact, Name = "customerCode" }; + var applicationEvidence = Evidence(_application, "Customers/Register/CustomerRegistered.cs"); + var conceptEvidence = Evidence(_concepts, "Concepts/CustomerCode.cs"); + var applicationFacts = new GenerationFact[] + { + new ArtifactDeclarationFact + { + Id = Id(_application, "event"), + Subject = eventSubject, + Evidence = applicationEvidence, + Definition = new ArtifactDeclarationDefinition + { + Artifact = artifact, + Name = "CustomerRegistered", + File = "Customers/Register/CustomerRegistered.cs" + } + }, + new ArtifactMemberDeclarationFact + { + Id = Id(_application, "member"), + Subject = eventSubject, + Evidence = applicationEvidence, + Definition = new ArtifactMemberDeclarationDefinition + { + Member = member, + DeclarationOrder = 0 + } + }, + new ArtifactMemberTypeUseFact + { + Id = Id(_application, "type-use"), + Subject = eventSubject, + Evidence = applicationEvidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = new TypeUseDefinition + { + Name = "CustomerCode", + ObservedTypeSubject = conceptSubject, + Shape = [TypeUseShapeKind.Optional, TypeUseShapeKind.Named] + } + } + }, + new ArtifactPlacementFact + { + Id = Id(_application, "placement"), + Subject = eventSubject, + Evidence = applicationEvidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Features = ["Registration"], + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + } + }; + var conceptFacts = new GenerationFact[] + { + new ArtifactFact + { + Id = Id(_concepts, "concept"), + Subject = conceptSubject, + Evidence = conceptEvidence, + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = conceptSubject, Kind = ArtifactKind.Concept }, + Name = "CustomerCode", + File = "Concepts/CustomerCode.cs" + } + }, + new ConceptRepresentationFact + { + Id = Id(_concepts, "representation"), + Subject = conceptSubject, + Evidence = conceptEvidence, + Definition = new ConceptRepresentationDefinition + { + Concept = conceptSubject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + }; + + var options = new ScreenplayGenerationOptions { Domain = "Ordering" }; + _result = Generator.Generate( + Snapshot( + Completed(_application, applicationFacts), + Completed(_concepts, conceptFacts)), + options); + _contribution = Generator.Generate( + [ + new AdapterContribution { Adapter = _concepts, Facts = conceptFacts }, + new AdapterContribution { Adapter = _application, Facts = applicationFacts } + ], + options); + } + + [Fact] void should_generate_successfully() => _result.IsSuccess.ShouldBeTrue(); + [Fact] void should_preserve_contribution_overload_compatibility() => _contribution.Source.ShouldEqual(_result.Source); + [Fact] void should_resolve_the_same_contribution_overload_binding() => ContributionEvent().Definition.Properties.Single().Type.Subject.ShouldEqual(Event().Definition.Properties.Single().Type.Subject); + [Fact] void should_resolve_the_same_contribution_overload_diagnostics() => _contribution.Diagnostics.ShouldContainOnly(_result.Diagnostics); + [Fact] void should_leave_the_contribution_overload_without_an_adapter_run() => _contribution.AdapterRun.ShouldBeNull(); + [Fact] void should_materialize_one_effective_event_property() => Event().Definition.Properties.Select(property => property.Name).ShouldEqual(["customerCode"]); + [Fact] void should_bind_the_effective_property_to_the_exact_concept_subject() => Event().Definition.Properties.Single().Type.Subject!.Value.ShouldEqual("dotnet://Ordering/Concepts.CustomerCode"); + [Fact] void should_preserve_exact_optionality_in_screenplay() => _result.Source.ShouldContain("customerCode CustomerCode?"); + [Fact] void should_retain_the_declaration_as_provenance() => Disposition("application:event").ShouldEqual(GenerationFactDisposition.ProvenanceOnly); + [Fact] void should_retain_the_member_declaration_as_provenance() => Disposition("application:member").ShouldEqual(GenerationFactDisposition.ProvenanceOnly); + [Fact] void should_retain_the_type_use_as_provenance() => Disposition("application:type-use").ShouldEqual(GenerationFactDisposition.ProvenanceOnly); + [Fact] void should_lower_the_derived_binding() => _result.AdapterRun!.Derivation!.Facts.Single().Disposition.ShouldEqual(GenerationFactDisposition.Lowered); + [Fact] void should_not_leave_any_direct_or_derived_disposition_unknown() => _result.AdapterRun!.Facts.Concat(_result.AdapterRun.Derivation!.Facts).Any(record => record.Disposition == GenerationFactDisposition.Unknown).ShouldBeFalse(); + + ResolvedArtifactVariant Event() => _result.Graph.Artifacts + .Single(artifact => artifact.Key.Kind == ArtifactKind.Event) + .Variants.Single(); + + ResolvedArtifactVariant ContributionEvent() => _contribution.Graph.Artifacts + .Single(artifact => artifact.Key.Kind == ArtifactKind.Event) + .Variants.Single(); + + GenerationFactDisposition Disposition(string id) => _result.AdapterRun!.Facts + .Single(record => record.Fact.Id.Value == id) + .Disposition; + + static FactId Id(AdapterIdentity adapter, string suffix) => new() { Value = $"{adapter.Id}:{suffix}" }; + + static Evidence Evidence(AdapterIdentity adapter, string path) => new() + { + Adapter = adapter, + Strength = EvidenceStrength.Exact, + Source = new SourceRange + { + Path = path, + StartLine = 1, + StartColumn = 1, + EndLine = 1, + EndColumn = 20 + } + }; +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_member_declaration_orders_conflict.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_member_declaration_orders_conflict.cs new file mode 100644 index 0000000..ad41ae9 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_member_declaration_orders_conflict.cs @@ -0,0 +1,99 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_member_declaration_orders_conflict : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var subject = new SubjectId { Value = "dotnet://Ordering/Events.CustomerRegistered" }; + var artifact = new ArtifactKey { Subject = subject, Kind = ArtifactKind.Event }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var facts = new List + { + new ArtifactDeclarationFact + { + Id = new FactId { Value = "event:declaration" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactDeclarationDefinition + { + Artifact = artifact, + Name = "CustomerRegistered" + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "event:placement" }, + Subject = subject, + Evidence = evidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + } + }; + facts.AddRange(Member("first", 0, artifact, subject, evidence)); + facts.AddRange(Member("second", 0, artifact, subject, evidence)); + facts.AddRange(Member("unrelated", 1, artifact, subject, evidence)); + facts.AddRange(Member("fourth", 2, artifact, subject, evidence)); + facts.AddRange(Member("fifth", 2, artifact, subject, evidence)); + + _result = Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_report_the_duplicate_order_conflict() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.ConflictingArtifactMember); + [Fact] void should_omit_the_incomplete_granular_artifact() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Kind == ArtifactKind.Event).ShouldBeFalse(); + [Fact] void should_conflict_members_occupying_the_first_duplicate_order() => Records("first", "second").All(record => record.Disposition == GenerationFactDisposition.Conflicted).ShouldBeTrue(); + [Fact] void should_conflict_members_occupying_the_later_duplicate_order() => Records("fourth", "fifth").All(record => record.Disposition == GenerationFactDisposition.Conflicted).ShouldBeTrue(); + [Fact] void should_not_conflict_the_unrelated_member() => Records("unrelated").Any(record => record.Disposition == GenerationFactDisposition.Conflicted).ShouldBeFalse(); + [Fact] void should_not_associate_the_duplicate_order_diagnostic_with_the_unrelated_member() => Records("unrelated").SelectMany(record => record.Diagnostics).Select(diagnostic => diagnostic.Code).ShouldNotContain(GenerationDiagnosticCodes.ConflictingArtifactMember); + + GenerationFactRecord[] Records(params string[] members) => + [ + .. _result.AdapterRun!.Facts.Where(record => members.Any(member => record.Fact.Id.Value.EndsWith(member, StringComparison.Ordinal))) + ]; + + static GenerationFact[] Member( + string name, + int order, + ArtifactKey artifact, + SubjectId subject, + Evidence evidence) + { + var member = new ArtifactMemberKey { Artifact = artifact, Name = name }; + return + [ + new ArtifactMemberDeclarationFact + { + Id = new FactId { Value = $"member:declaration:{name}" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactMemberDeclarationDefinition + { + Member = member, + DeclarationOrder = order + } + }, + new ArtifactMemberTypeUseFact + { + Id = new FactId { Value = $"member:type-use:{name}" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = new TypeUseDefinition { Name = "String" } + } + } + ]; + } +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlayed_legacy_artifact_variants_conflict.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlayed_legacy_artifact_variants_conflict.cs new file mode 100644 index 0000000..8e27ea3 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlayed_legacy_artifact_variants_conflict.cs @@ -0,0 +1,125 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_overlayed_legacy_artifact_variants_conflict : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.CustomerRegistered" }; + var conceptSubject = new SubjectId { Value = "dotnet://Ordering/Concepts.CustomerCode" }; + var artifact = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var member = new ArtifactMemberKey { Artifact = artifact, Name = "customerCode" }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var first = Legacy("event:first", "Events/First.cs", artifact, eventSubject, evidence); + var second = Legacy("event:second", "Events/Second.cs", artifact, eventSubject, evidence); + var facts = new GenerationFact[] + { + first, + second, + new ArtifactMemberTypeUseFact + { + Id = new FactId { Value = "event:type-use" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = new TypeUseDefinition { Name = "RawCustomerCode" } + } + }, + new TypeUseBindingFact + { + Id = new FactId { Value = "event:binding" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new TypeUseBindingDefinition + { + Member = member, + Target = new ArtifactKey { Subject = conceptSubject, Kind = ArtifactKind.Concept } + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "event:placement" }, + Subject = eventSubject, + Evidence = evidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + }, + new ArtifactFact + { + Id = new FactId { Value = "concept:customer-code" }, + Subject = conceptSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = conceptSubject, Kind = ArtifactKind.Concept }, + Name = "CustomerCode" + } + }, + new ConceptRepresentationFact + { + Id = new FactId { Value = "concept:customer-code:representation" }, + Subject = conceptSubject, + Evidence = evidence, + Definition = new ConceptRepresentationDefinition + { + Concept = conceptSubject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + }; + + _result = Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_retain_both_effective_overlay_variants_as_a_conflict() => Event().IsConflicted.ShouldBeTrue(); + [Fact] void should_bind_both_effective_variants_without_selecting_one() => Event().Variants.All(variant => variant.Definition.Properties.Single().Type.Subject!.Value == "dotnet://Ordering/Concepts.CustomerCode").ShouldBeTrue(); + [Fact] void should_conflict_both_complete_legacy_support_facts() => Dispositions("event:first", "event:second").ShouldContainOnly(GenerationFactDisposition.Conflicted, GenerationFactDisposition.Conflicted); + [Fact] void should_conflict_the_binding_supporting_both_variants() => Dispositions("event:binding").ShouldContainOnly(GenerationFactDisposition.Conflicted); + + ResolvedArtifact Event() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Event); + + GenerationFactDisposition[] Dispositions(params string[] ids) => + [ + .. ids.Select(id => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == id).Disposition) + ]; + + static ArtifactFact Legacy( + string id, + string file, + ArtifactKey artifact, + SubjectId subject, + Evidence evidence) => new() + { + Id = new FactId { Value = id }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = artifact, + Name = "CustomerRegistered", + File = file, + Properties = + [ + new PropertyDefinition + { + Name = "customerCode", + Type = new TypeReferenceDefinition { Name = "RawCustomerCode" } + } + ] + } + }; +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlaying_one_legacy_artifact_member_binding.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlaying_one_legacy_artifact_member_binding.cs new file mode 100644 index 0000000..233d834 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_overlaying_one_legacy_artifact_member_binding.cs @@ -0,0 +1,121 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_overlaying_one_legacy_artifact_member_binding : given.a_generator +{ + readonly AdapterIdentity _application = new() { Id = "legacy-application", Version = "1.0.0" }; + readonly AdapterIdentity _concepts = new() { Id = "concepts", Version = "2.0.0" }; + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.CustomerRegistered" }; + var conceptSubject = new SubjectId { Value = "dotnet://Ordering/Concepts.CustomerCode" }; + var artifact = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var member = new ArtifactMemberKey { Artifact = artifact, Name = "customerCode" }; + var applicationEvidence = Evidence(_application); + var applicationFacts = new GenerationFact[] + { + new ArtifactFact + { + Id = Id(_application, "event"), + Subject = eventSubject, + Evidence = applicationEvidence, + Definition = new ArtifactDefinition + { + Key = artifact, + Name = "CustomerRegistered", + Properties = + [ + new PropertyDefinition + { + Name = "customerCode", + Type = new TypeReferenceDefinition { Name = "UnresolvedCustomerCode" } + } + ] + } + }, + new ArtifactMemberTypeUseFact + { + Id = Id(_application, "type-use"), + Subject = eventSubject, + Evidence = applicationEvidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = new TypeUseDefinition + { + Name = "UnresolvedCustomerCode", + ObservedTypeSubject = conceptSubject + } + } + }, + new ArtifactPlacementFact + { + Id = Id(_application, "placement"), + Subject = eventSubject, + Evidence = applicationEvidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + } + }; + var conceptEvidence = Evidence(_concepts); + var conceptFacts = new GenerationFact[] + { + new ArtifactFact + { + Id = Id(_concepts, "concept"), + Subject = conceptSubject, + Evidence = conceptEvidence, + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = conceptSubject, Kind = ArtifactKind.Concept }, + Name = "CustomerCode" + } + }, + new ConceptRepresentationFact + { + Id = Id(_concepts, "representation"), + Subject = conceptSubject, + Evidence = conceptEvidence, + Definition = new ConceptRepresentationDefinition + { + Concept = conceptSubject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + }; + + _result = Generator.Generate( + Snapshot(Completed(_application, applicationFacts), Completed(_concepts, conceptFacts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_generate_successfully() => _result.IsSuccess.ShouldBeTrue(); + [Fact] void should_bind_only_the_matching_legacy_property() => Event().Definition.Properties.Single().Type.Subject!.Value.ShouldEqual("dotnet://Ordering/Concepts.CustomerCode"); + [Fact] void should_lower_the_concept_name_instead_of_the_unresolved_source_name() => _result.Source.ShouldContain("customerCode CustomerCode"); + [Fact] void should_not_emit_the_unresolved_source_name() => _result.Source.ShouldNotContain("UnresolvedCustomerCode"); + [Fact] void should_retain_the_legacy_aggregate_as_overlay_provenance() => DirectDisposition("legacy-application:event").ShouldEqual(GenerationFactDisposition.ProvenanceOnly); + [Fact] void should_retain_the_granular_type_use_as_provenance() => DirectDisposition("legacy-application:type-use").ShouldEqual(GenerationFactDisposition.ProvenanceOnly); + [Fact] void should_lower_the_granular_derived_binding() => _result.AdapterRun!.Derivation!.Facts.Single().Disposition.ShouldEqual(GenerationFactDisposition.Lowered); + + ResolvedArtifactVariant Event() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Event).Variants.Single(); + + GenerationFactDisposition DirectDisposition(string id) => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == id).Disposition; + + static FactId Id(AdapterIdentity adapter, string suffix) => new() { Value = $"{adapter.Id}:{suffix}" }; + + static Evidence Evidence(AdapterIdentity adapter) => new() + { + Adapter = adapter, + Strength = EvidenceStrength.Exact + }; +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_source_members_normalize_to_the_same_name.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_source_members_normalize_to_the_same_name.cs new file mode 100644 index 0000000..06f84c5 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_source_members_normalize_to_the_same_name.cs @@ -0,0 +1,58 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_source_members_normalize_to_the_same_name : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var subject = new SubjectId { Value = "dotnet://Ordering/Types.CaseCollision" }; + var artifact = new ArtifactKey { Subject = subject, Kind = ArtifactKind.CompositeType }; + var member = new ArtifactMemberKey { Artifact = artifact, Name = "uRL" }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var facts = new GenerationFact[] + { + new ArtifactDeclarationFact + { + Id = new FactId { Value = "type:declaration" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactDeclarationDefinition + { + Artifact = artifact, + Name = "CaseCollision" + } + }, + Member("member:URL", 0, subject, member, evidence), + Member("member:uRL", 1, subject, member, evidence) + }; + + _result = Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_report_the_normalized_member_conflict() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.ConflictingArtifactMember); + [Fact] void should_omit_the_conflicted_granular_artifact() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Kind == ArtifactKind.CompositeType).ShouldBeFalse(); + [Fact] void should_conflict_both_exact_source_member_assertions() => _result.AdapterRun!.Facts.Where(record => record.Fact is ArtifactMemberDeclarationFact).All(record => record.Disposition == GenerationFactDisposition.Conflicted).ShouldBeTrue(); + + static ArtifactMemberDeclarationFact Member( + string id, + int order, + SubjectId subject, + ArtifactMemberKey member, + Evidence evidence) => new() + { + Id = new FactId { Value = id }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactMemberDeclarationDefinition + { + Member = member, + DeclarationOrder = order + } + }; +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs new file mode 100644 index 0000000..94ad5cf --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs @@ -0,0 +1,132 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_type_use_bindings_conflict : given.a_generator +{ + readonly AdapterIdentity _application = new() { Id = "application", Version = "1.0.0" }; + readonly AdapterIdentity _concepts = new() { Id = "concepts", Version = "2.0.0" }; + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.CustomerRegistered" }; + var firstConcept = new SubjectId { Value = "dotnet://Ordering/Concepts.CustomerCode" }; + var secondConcept = new SubjectId { Value = "dotnet://Ordering/Concepts.LegacyCustomerCode" }; + var artifact = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var member = new ArtifactMemberKey { Artifact = artifact, Name = "customerCode" }; + var evidence = new Evidence { Adapter = _application, Strength = EvidenceStrength.Exact }; + var applicationFacts = new GenerationFact[] + { + new ArtifactFact + { + Id = Id(_application, "event"), + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = artifact, + Name = "CustomerRegistered", + Properties = + [ + new PropertyDefinition + { + Name = "customerCode", + Type = new TypeReferenceDefinition { Name = "RawCustomerCode" } + } + ] + } + }, + new ArtifactMemberTypeUseFact + { + Id = Id(_application, "type-use"), + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = new TypeUseDefinition + { + Name = "RawCustomerCode", + ObservedTypeSubject = firstConcept + } + } + }, + new TypeUseBindingFact + { + Id = Id(_application, "legacy-binding"), + Subject = eventSubject, + Evidence = evidence, + Definition = new TypeUseBindingDefinition + { + Member = member, + Target = new ArtifactKey { Subject = secondConcept, Kind = ArtifactKind.Concept } + } + }, + new ArtifactPlacementFact + { + Id = Id(_application, "placement"), + Subject = eventSubject, + Evidence = evidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + } + }; + var conceptEvidence = new Evidence { Adapter = _concepts, Strength = EvidenceStrength.Exact }; + var conceptFacts = ConceptFacts(firstConcept, "CustomerCode", "customer-code", conceptEvidence) + .Concat(ConceptFacts(secondConcept, "LegacyCustomerCode", "legacy-customer-code", conceptEvidence)) + .ToArray(); + + _result = Generator.Generate( + Snapshot(Completed(_application, applicationFacts), Completed(_concepts, conceptFacts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_fail_closed() => _result.IsSuccess.ShouldBeFalse(); + [Fact] void should_report_the_binding_conflict() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.ConflictingArtifactMember); + [Fact] void should_not_choose_either_target_subject() => Event().Definition.Properties.Single().Type.Subject.ShouldBeNull(); + [Fact] void should_conflict_the_direct_binding() => DirectBinding().Disposition.ShouldEqual(GenerationFactDisposition.Conflicted); + [Fact] void should_conflict_the_derived_binding() => DerivedBinding().Disposition.ShouldEqual(GenerationFactDisposition.Conflicted); + [Fact] void should_associate_the_same_conflict_with_both_bindings() => DirectBinding().Diagnostics.Select(diagnostic => diagnostic.Code).Concat(DerivedBinding().Diagnostics.Select(diagnostic => diagnostic.Code)).ShouldContainOnly(GenerationDiagnosticCodes.ConflictingArtifactMember, GenerationDiagnosticCodes.ConflictingArtifactMember); + + ResolvedArtifactVariant Event() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Event).Variants.Single(); + + GenerationFactRecord DirectBinding() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "application:legacy-binding"); + + GenerationFactRecord DerivedBinding() => _result.AdapterRun!.Derivation!.Facts.Single(); + + static GenerationFact[] ConceptFacts(SubjectId subject, string name, string suffix, Evidence evidence) => + [ + new ArtifactFact + { + Id = Id(evidence.Adapter, suffix), + Subject = subject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = subject, Kind = ArtifactKind.Concept }, + Name = name + } + }, + new ConceptRepresentationFact + { + Id = Id(evidence.Adapter, $"{suffix}:representation"), + Subject = subject, + Evidence = evidence, + Definition = new ConceptRepresentationDefinition + { + Concept = subject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + ]; + + static FactId Id(AdapterIdentity adapter, string suffix) => new() { Value = $"{adapter.Id}:{suffix}" }; +} diff --git a/Source/DotNET/Generation/GenerationDiagnosticCodes.cs b/Source/DotNET/Generation/GenerationDiagnosticCodes.cs index d0b9002..49b4966 100644 --- a/Source/DotNET/Generation/GenerationDiagnosticCodes.cs +++ b/Source/DotNET/Generation/GenerationDiagnosticCodes.cs @@ -247,4 +247,24 @@ public static class GenerationDiagnosticCodes /// Incompatible declarations or roles were asserted for one artifact member. /// public const string ConflictingArtifactMember = "GEN0050"; + + /// + /// An artifact member lacks the declaration or exact type use required for safe lowering. + /// + public const string IncompleteArtifactMember = "GEN0051"; + + /// + /// A member type use contains an unknown or undefined shape node. + /// + public const string UnsupportedTypeUseShapeKind = "GEN0052"; + + /// + /// A member role fact contains an unknown or undefined role. + /// + public const string UnsupportedArtifactMemberRoleKind = "GEN0053"; + + /// + /// A granular fact's asserted subject does not equal its nested artifact owner. + /// + public const string InvalidGranularFactOwnership = "GEN0054"; } diff --git a/Source/DotNET/Generation/GenerationFactDerivation.cs b/Source/DotNET/Generation/GenerationFactDerivation.cs index 3f58444..f711741 100644 --- a/Source/DotNET/Generation/GenerationFactDerivation.cs +++ b/Source/DotNET/Generation/GenerationFactDerivation.cs @@ -17,13 +17,25 @@ public static class GenerationFactDerivation /// An immutable derivation snapshot whose rules consumed only the fixed base fact array. public static GenerationDerivationSnapshot Derive(AdapterRunSnapshot snapshot) { - var baseFacts = AdapterRunCanonicalizer.FactRecords(snapshot.Facts) + var canonicalInput = AdapterRunCanonicalizer.FactRecords(snapshot.Facts); + var discriminatorValidation = GenerationFactDiscriminatorValidator.Validate( + canonicalInput.Select(record => record.Fact)); + var validationDiagnostics = discriminatorValidation.Diagnostics.ToList(); + var validFacts = GranularArtifactResolver.ValidFactsForDerivation( + discriminatorValidation.Facts, + validationDiagnostics); + var baseFacts = validFacts + .Select(fact => new GenerationFactRecord { Fact = fact }) .OrderBy(record => record.Fact.Id.Value, StringComparer.Ordinal) .ThenBy(record => record.Fact.Subject.Value, StringComparer.Ordinal) .ThenBy(record => Structural.FactFamily(record.Fact)) .ThenBy(record => Structural.FactDefinition(record.Fact), StringComparer.Ordinal) .ToImmutableArray(); var typeUseBindings = TypeUseBindingDerivation.Derive(baseFacts); + var diagnostics = validationDiagnostics + .Concat(typeUseBindings.Diagnostics) + .OrderBy(Canonical.Diagnostic, StringComparer.Ordinal) + .ToImmutableArray(); var derivation = new GenerationDerivationSnapshot { Rules = @@ -37,7 +49,7 @@ public static GenerationDerivationSnapshot Derive(AdapterRunSnapshot snapshot) } ], Facts = typeUseBindings.Facts, - Diagnostics = typeUseBindings.Diagnostics + Diagnostics = diagnostics }; return AdapterRunCanonicalizer.Derivation(derivation); diff --git a/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs b/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs index 7f68014..b720681 100644 --- a/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs +++ b/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs @@ -24,6 +24,24 @@ public static GenerationFactDiscriminatorValidationResult Validate(IEnumerable shape, + List diagnostics) + { + foreach (var kind in shape) + { + if (kind == TypeUseShapeKind.Unknown || !Enum.IsDefined(kind)) + { + diagnostics.Add(Unsupported( + fact, + GenerationDiagnosticCodes.UnsupportedTypeUseShapeKind, + nameof(TypeUseShapeKind), + (int)kind, + kind == TypeUseShapeKind.Unknown)); + } + } + } + + static void ValidateArtifactMemberRole( + GenerationFact fact, + ArtifactMemberRoleKind role, + List diagnostics) + { + if (role == ArtifactMemberRoleKind.Unknown || !Enum.IsDefined(role)) + { + diagnostics.Add(Unsupported( + fact, + GenerationDiagnosticCodes.UnsupportedArtifactMemberRoleKind, + nameof(ArtifactMemberRoleKind), + (int)role, + role == ArtifactMemberRoleKind.Unknown)); + } + } + static void ValidateRelationshipKind(GenerationFact fact, RelationshipKind kind, List diagnostics) { if (kind == RelationshipKind.Unknown || !Enum.IsDefined(kind)) diff --git a/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs b/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs index 715eb1d..f12d5a2 100644 --- a/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs +++ b/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs @@ -7,6 +7,20 @@ namespace Cratis.Screenplay.Generation; internal static class GenerationFactDispositionCalculator { + public static ImmutableArray Calculate( + IEnumerable records, + ResolvedApplicationGraph graph, + ScreenplayLoweringCoverage coverage, + IReadOnlyList diagnostics) + { + var inputs = records.ToArray(); + var calculated = Calculate(inputs.Select(record => record.Fact), graph, coverage, diagnostics); + return + [ + .. calculated.Select((record, index) => record with { Lineage = inputs[index].Lineage }) + ]; + } + public static ImmutableArray Calculate( IEnumerable facts, ResolvedApplicationGraph graph, @@ -47,11 +61,35 @@ static GenerationFactRecord Calculate( return Omitted(fact, coverage, diagnostics); } - if (hasConflictingIdentity || IsConflicted(fact, graph, coverage)) + if (hasConflictingIdentity || + IsConflicted(fact, graph, coverage) || + AssociatedDiagnostics(fact, coverage, diagnostics).Any(diagnostic => diagnostic.Outcome == GenerationDiagnosticOutcome.Conflict)) { return Conflicted(fact, coverage, diagnostics); } + var granularDisposition = GranularDisposition(fact, graph, coverage, diagnostics); + if (granularDisposition is not null) + { + return granularDisposition; + } + + if (fact is ArtifactFact legacyArtifact && + graph.Artifacts + .Where(resolved => resolved.Key == legacyArtifact.Definition.Key) + .SelectMany(resolved => resolved.Variants) + .Any(variant => + Structural.Artifact(legacyArtifact.Definition) != Structural.Artifact(variant.Definition) && + variant.SupportingFacts.Any(id => id == legacyArtifact.Id) && + coverage.Lowered.Contains(GenerationFactSemanticKey.Artifact(variant.Definition)))) + { + return new GenerationFactRecord + { + Fact = fact, + Disposition = GenerationFactDisposition.ProvenanceOnly + }; + } + var key = GenerationFactSemanticKey.For(fact); if (key is not null && coverage.Lowered.Contains(key)) { @@ -179,13 +217,57 @@ static IEnumerable AssociatedDiagnostics( } } - foreach (var diagnostic in diagnostics.Where(_ => - _.Outcome is not null && HasExactFactIdentity(_.Message, fact.Id.Value))) + foreach (var diagnostic in diagnostics.Where(diagnostic => + diagnostic.Outcome is not null && HasExactFactIdentity(diagnostic, fact.Id.Value))) { yield return diagnostic; } } + static GenerationFactRecord? GranularDisposition( + GenerationFact fact, + ResolvedApplicationGraph graph, + ScreenplayLoweringCoverage coverage, + IReadOnlyList diagnostics) + { + var artifact = fact switch + { + ArtifactDeclarationFact declaration => declaration.Definition.Artifact, + ArtifactMemberDeclarationFact member => member.Definition.Member.Artifact, + ArtifactMemberTypeUseFact typeUse => typeUse.Definition.Member.Artifact, + TypeUseBindingFact binding => binding.Definition.Member.Artifact, + ArtifactMemberRoleFact role => role.Definition.Member.Artifact, + _ => null + }; + if (artifact is null) + { + return null; + } + + var variants = graph.Artifacts + .Where(resolved => resolved.Key == artifact) + .SelectMany(resolved => resolved.Variants) + .ToArray(); + var appliedVariants = variants + .Where(variant => variant.SupportingFacts.Any(id => id == fact.Id)) + .ToArray(); + if (appliedVariants.Length == 0) + { + return Omitted(fact, coverage, diagnostics); + } + + var lowered = appliedVariants.Any(variant => + coverage.Lowered.Contains(GenerationFactSemanticKey.Artifact(variant.Definition))); + var disposition = fact is TypeUseBindingFact or ArtifactMemberRoleFact && lowered + ? GenerationFactDisposition.Lowered + : GenerationFactDisposition.ProvenanceOnly; + return new GenerationFactRecord + { + Fact = fact, + Disposition = disposition + }; + } + static bool IsConflicted( GenerationFact fact, ResolvedApplicationGraph graph, @@ -202,8 +284,15 @@ static bool IsConflicted( ArtifactFact artifact => graph.Artifacts.Any(resolved => resolved.IsConflicted && Structural.ArtifactKey(resolved.Key) == Structural.ArtifactKey(artifact.Definition.Key) && - resolved.Variants.Any(variant => Structural.Artifact(variant.Definition) == Structural.Artifact(artifact.Definition))), + resolved.Variants.Any(variant => variant.SupportingFacts.Any(id => id == artifact.Id))), ArtifactPlacementFact placement => ConflictingPlacement(placement, graph), + ArtifactDeclarationFact or + ArtifactMemberDeclarationFact or + ArtifactMemberTypeUseFact or + TypeUseBindingFact or + ArtifactMemberRoleFact => graph.Artifacts.Any(resolved => + resolved.IsConflicted && + resolved.Variants.Any(variant => variant.SupportingFacts.Any(id => id == fact.Id))), RelationshipFact relationship => graph.Relationships.Any(resolved => resolved.IsConflicted && Structural.RelationshipKey(resolved.Key) == Structural.RelationshipKey(relationship.Definition.Key) && @@ -260,10 +349,41 @@ static bool IsWeakerPlacement(ArtifactPlacementFact fact, ResolvedApplicationGra return resolved.EffectiveVariants.All(_ => Structural.Placement(_.Placement) != placement); } - static bool HasExactFactIdentity(string message, string factId) => - message.Contains($"Fact '{factId}'", StringComparison.Ordinal) || - message.Contains($"fact '{factId}'", StringComparison.Ordinal) || - message.Contains($"Fact identity '{factId}'", StringComparison.Ordinal); + static bool HasExactFactIdentity(GenerationDiagnostic diagnostic, string factId) => + diagnostic.Message.Contains($"Fact '{factId}'", StringComparison.Ordinal) || + diagnostic.Message.Contains($"fact '{factId}'", StringComparison.Ordinal) || + diagnostic.Message.Contains($"Fact identity '{factId}'", StringComparison.Ordinal) || + (IsGranularDiagnostic(diagnostic.Code) && InputFactIds(diagnostic.Message).Contains(factId, StringComparer.Ordinal)); + + static string[] InputFactIds(string message) + { + const string marker = "Input facts: "; + var markerIndex = message.LastIndexOf(marker, StringComparison.Ordinal); + if (markerIndex < 0) + { + return []; + } + + return + [ + .. message[(markerIndex + marker.Length)..] + .Split(", ", StringSplitOptions.RemoveEmptyEntries) + .Where(value => value.Length >= 2 && value[0] == '\'' && value[^1] == '\'') + .Select(value => value[1..^1]) + ]; + } + + static bool IsGranularDiagnostic(string code) => + string.Equals(code, GenerationDiagnosticCodes.MissingTypeUseOwner, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.MissingTypeUseMember, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.MissingTypeUseTarget, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.ConflictingMemberTypeUse, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.ConflictingTypeUseTarget, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.ConflictingTypeUseDeclaration, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.UnsupportedTypeUseShape, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.ConflictingArtifactMember, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.IncompleteArtifactMember, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.InvalidGranularFactOwnership, StringComparison.Ordinal); static bool HasSupportedDiscriminators(GenerationFact fact) { @@ -278,6 +398,17 @@ static bool HasSupportedDiscriminators(GenerationFact fact) ArtifactPlacementFact placement => Supported(placement.Artifact.Kind, ArtifactKind.Unknown) && Supported(placement.Placement.SliceKind, GenerationSliceKind.Unknown), + ArtifactDeclarationFact declaration => Supported(declaration.Definition.Artifact.Kind, ArtifactKind.Unknown), + ArtifactMemberDeclarationFact member => Supported(member.Definition.Member.Artifact.Kind, ArtifactKind.Unknown), + ArtifactMemberTypeUseFact typeUse => + Supported(typeUse.Definition.Member.Artifact.Kind, ArtifactKind.Unknown) && + typeUse.Definition.Type.Shape.All(shape => Supported(shape, TypeUseShapeKind.Unknown)), + TypeUseBindingFact binding => + Supported(binding.Definition.Member.Artifact.Kind, ArtifactKind.Unknown) && + Supported(binding.Definition.Target.Kind, ArtifactKind.Unknown), + ArtifactMemberRoleFact role => + Supported(role.Definition.Member.Artifact.Kind, ArtifactKind.Unknown) && + Supported(role.Definition.Role, ArtifactMemberRoleKind.Unknown), RelationshipFact relationship => Supported(relationship.Definition.Key.Kind, RelationshipKind.Unknown), ConceptRepresentationFact representation => Supported(representation.Definition.Kind, ConceptRepresentationKind.Unknown) && @@ -302,6 +433,11 @@ static bool Supported(TEnum value, TEnum unknown) { ArtifactFact artifact => artifact.Definition.Key.Subject, ArtifactPlacementFact placement => placement.Artifact.Subject, + ArtifactDeclarationFact declaration => declaration.Definition.Artifact.Subject, + ArtifactMemberDeclarationFact member => member.Definition.Member.Artifact.Subject, + ArtifactMemberTypeUseFact typeUse => typeUse.Definition.Member.Artifact.Subject, + TypeUseBindingFact binding => binding.Definition.Member.Artifact.Subject, + ArtifactMemberRoleFact role => role.Definition.Member.Artifact.Subject, RelationshipFact relationship => relationship.Definition.Key.Source, ConceptRepresentationFact representation => representation.Definition.Concept, ConceptAttributeFact attribute => attribute.Definition.Concept, @@ -319,6 +455,11 @@ static string SemanticIdentity(GenerationFact fact) => { ArtifactFact => "artifact", ArtifactPlacementFact => "artifact placement", + ArtifactDeclarationFact => "artifact declaration", + ArtifactMemberDeclarationFact => "artifact member declaration", + ArtifactMemberTypeUseFact => "artifact member type use", + TypeUseBindingFact => "type-use binding", + ArtifactMemberRoleFact => "artifact member role", RelationshipFact => "relationship", ConceptRepresentationFact => "concept representation", ConceptAttributeFact => "concept attribute", diff --git a/Source/DotNET/Generation/GenerationResolver.cs b/Source/DotNET/Generation/GenerationResolver.cs index a4c2215..45938d5 100644 --- a/Source/DotNET/Generation/GenerationResolver.cs +++ b/Source/DotNET/Generation/GenerationResolver.cs @@ -20,14 +20,24 @@ public ResolvedApplicationGraph Resolve(IEnumerable contrib .ThenBy(_ => _.Adapter.Version, StringComparer.Ordinal) .ToArray(); var contributedFacts = orderedContributions.SelectMany(_ => _.Facts).ToArray(); - var diagnostics = orderedContributions.SelectMany(_ => _.Diagnostics).ToList(); + var diagnostics = orderedContributions.SelectMany(_ => _.Diagnostics); + + return ResolveFacts(contributedFacts, diagnostics); + } + + internal ResolvedApplicationGraph ResolveFacts( + IEnumerable contributedFacts, + IEnumerable contributedDiagnostics) + { + var diagnostics = contributedDiagnostics.ToList(); var discriminatorValidation = GenerationFactDiscriminatorValidator.Validate(contributedFacts); var facts = discriminatorValidation.Facts; diagnostics.AddRange(discriminatorValidation.Diagnostics); diagnostics.AddRange(ConflictingFactIdentityDiagnostics(facts)); - var artifacts = ResolveArtifacts(facts.OfType(), diagnostics); + var effectiveArtifactFacts = GranularArtifactResolver.Resolve(facts, diagnostics); + var artifacts = ResolveArtifacts(effectiveArtifactFacts, diagnostics); var conceptRepresentationFacts = facts.OfType().ToArray(); diagnostics.AddRange(InvalidConceptFactDiagnostics(conceptRepresentationFacts)); var conceptRepresentations = ResolveConceptRepresentations( @@ -89,6 +99,12 @@ .. facts .Select(_ => new ResolvedArtifactVariant { Definition = _.First().Definition, + SupportingFacts = + [ + .. _.Select(fact => fact.Id) + .Distinct() + .OrderBy(id => id.Value, StringComparer.Ordinal) + ], Evidence = OrderedEvidence(_.Select(fact => fact.Evidence)) }) .ToArray(); diff --git a/Source/DotNET/Generation/GranularArtifactResolver.cs b/Source/DotNET/Generation/GranularArtifactResolver.cs new file mode 100644 index 0000000..4687c6f --- /dev/null +++ b/Source/DotNET/Generation/GranularArtifactResolver.cs @@ -0,0 +1,473 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation; + +static class GranularArtifactResolver +{ + public static ArtifactFact[] Resolve( + IReadOnlyList facts, + List diagnostics) + { + var legacy = facts.OfType().ToArray(); + var granular = ValidGranularFacts(facts.Where(IsGranularArtifactFact), diagnostics); + if (granular.Length == 0) + { + return legacy; + } + + var variants = DeclarationVariants(legacy, granular.OfType()); + var effective = new List(); + foreach (var variant in variants + .OrderBy(candidate => Structural.ArtifactKey(candidate.Definition.Key), StringComparer.Ordinal) + .ThenBy(candidate => Structural.Artifact(candidate.Definition), StringComparer.Ordinal)) + { + var overlaid = ApplyMembers(variant, granular, diagnostics); + if (overlaid is not null) + { + foreach (var support in CanonicalFacts(overlaid.Supports)) + { + effective.Add(new ArtifactFact + { + Id = support.Id, + Subject = overlaid.Definition.Key.Subject, + Evidence = support.Evidence, + Definition = overlaid.Definition + }); + } + } + } + + return [.. effective]; + } + + internal static GenerationFact[] ValidFactsForDerivation( + IEnumerable facts, + List diagnostics) + { + var input = facts.ToArray(); + return + [ + .. input.Where(fact => !IsGranularArtifactFact(fact)), + .. ValidGranularFacts(input.Where(IsGranularArtifactFact), diagnostics) + ]; + } + + static List DeclarationVariants( + IEnumerable legacy, + IEnumerable granular) + { + var variants = legacy + .GroupBy(fact => Structural.Artifact(fact.Definition), StringComparer.Ordinal) + .Select(group => new ArtifactVariant(group.First().Definition, [.. group])) + .ToList(); + foreach (var declaration in granular.OrderBy(fact => fact.Id.Value, StringComparer.Ordinal)) + { + var metadata = DeclarationKey(declaration.Definition); + var matching = variants + .Where(variant => DeclarationKey(variant.Definition) == metadata) + .ToArray(); + if (matching.Length == 0) + { + variants.Add(new ArtifactVariant( + new ArtifactDefinition + { + Key = declaration.Definition.Artifact, + Name = declaration.Definition.Name, + Description = declaration.Definition.Description, + File = declaration.Definition.File + }, + [declaration])); + continue; + } + + foreach (var variant in matching) + { + variant.Supports.Add(declaration); + } + } + + return variants; + } + + static ArtifactVariant? ApplyMembers( + ArtifactVariant variant, + IReadOnlyList granular, + List diagnostics) + { + var key = variant.Definition.Key; + var memberFacts = granular + .Where(fact => MemberFor(fact)?.Artifact == key) + .ToArray(); + if (memberFacts.Length == 0) + { + return variant; + } + + var properties = variant.Definition.Properties + .Select((property, order) => new OrderedProperty(order, property)) + .ToDictionary(item => item.Property.Name, StringComparer.Ordinal); + var supports = variant.Supports.ToList(); + var failed = false; + var memberNames = variant.Definition.Properties.Select(property => property.Name) + .Concat(memberFacts.Select(fact => MemberFor(fact)!.Name)) + .Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal); + foreach (var memberName in memberNames) + { + var currentFacts = memberFacts + .Where(fact => MemberFor(fact)!.Name == memberName) + .ToArray(); + properties.TryGetValue(memberName, out var existing); + var declarationFacts = currentFacts.OfType().ToArray(); + var orders = declarationFacts.Select(fact => fact.Definition.DeclarationOrder) + .Concat(existing is null ? [] : [existing.Order]) + .Distinct() + .ToArray(); + if (orders.Length == 0) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.IncompleteArtifactMember, + GenerationDiagnosticOutcome.Unknown, + key.Subject, + currentFacts, + $"Artifact member '{memberName}' has no declaration order"); + failed = true; + continue; + } + + if (orders.Length > 1) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.ConflictingArtifactMember, + GenerationDiagnosticOutcome.Conflict, + key.Subject, + currentFacts.Concat(variant.Supports), + $"Artifact member '{memberName}' has incompatible declaration orders"); + failed = true; + continue; + } + + var typeUseFacts = currentFacts.OfType().ToArray(); + var typeUseVariants = typeUseFacts + .GroupBy(fact => Structural.TypeUse(fact.Definition.Type), StringComparer.Ordinal) + .OrderBy(group => group.Key, StringComparer.Ordinal) + .ToArray(); + if (typeUseVariants.Length > 1) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.ConflictingArtifactMember, + GenerationDiagnosticOutcome.Conflict, + key.Subject, + typeUseFacts, + $"Artifact member '{memberName}' has incompatible exact type uses"); + failed = true; + continue; + } + + var type = existing?.Property.Type; + ArtifactMemberTypeUseFact? typeUse = null; + if (typeUseVariants.Length == 1) + { + typeUse = typeUseVariants[0].OrderBy(fact => fact.Id.Value, StringComparer.Ordinal).First(); + if (!TryTypeReference(typeUse.Definition.Type, out var granularType)) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.UnsupportedTypeUseShape, + GenerationDiagnosticOutcome.Unsupported, + key.Subject, + typeUseVariants[0], + $"Artifact member '{memberName}' uses exact shape '{Shape(typeUse.Definition.Type)}' that Screenplay cannot represent"); + failed = true; + continue; + } + + if (type is not null && !SameUnboundType(type, granularType)) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.ConflictingArtifactMember, + GenerationDiagnosticOutcome.Conflict, + key.Subject, + typeUseVariants[0].Cast().Concat(variant.Supports), + $"Artifact member '{memberName}' has incompatible legacy and granular type uses"); + failed = true; + continue; + } + + type = granularType with { Subject = type?.Subject }; + } + + if (type is null) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.IncompleteArtifactMember, + GenerationDiagnosticOutcome.Unknown, + key.Subject, + currentFacts, + $"Artifact member '{memberName}' has no exact type use"); + failed = true; + continue; + } + + var bindings = currentFacts.OfType().ToArray(); + var bindingVariants = bindings + .GroupBy(fact => Structural.ArtifactKey(fact.Definition.Target), StringComparer.Ordinal) + .OrderBy(group => group.Key, StringComparer.Ordinal) + .ToArray(); + if (bindingVariants.Length > 1) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.ConflictingArtifactMember, + GenerationDiagnosticOutcome.Conflict, + key.Subject, + bindings, + $"Artifact member '{memberName}' has incompatible exact type bindings"); + failed = true; + continue; + } + + if (bindingVariants.Length == 0 && + typeUse?.Definition.Type.ObservedTypeSubject is not null && + type.Subject is null) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.IncompleteArtifactMember, + GenerationDiagnosticOutcome.Unknown, + key.Subject, + typeUseFacts.Concat(variant.Supports), + $"Artifact member '{memberName}' has an observed exact type subject but no admitted binding"); + failed = true; + continue; + } + + if (bindingVariants.Length == 1) + { + var binding = bindingVariants[0].OrderBy(fact => fact.Id.Value, StringComparer.Ordinal).First(); + var target = binding.Definition.Target.Subject; + if ((type.Subject is not null && type.Subject != target) || + (typeUse?.Definition.Type.ObservedTypeSubject is not null && typeUse.Definition.Type.ObservedTypeSubject != target)) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.ConflictingArtifactMember, + GenerationDiagnosticOutcome.Conflict, + key.Subject, + bindingVariants[0].Cast().Concat(typeUseFacts).Concat(variant.Supports), + $"Artifact member '{memberName}' binds incompatible exact type subjects"); + failed = true; + continue; + } + + type = type with { Subject = target }; + } + + var roleFacts = currentFacts.OfType().ToArray(); + var roleVariants = roleFacts.Select(fact => fact.Definition.Role).Distinct().ToArray(); + if (roleVariants.Length > 1) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.ConflictingArtifactMember, + GenerationDiagnosticOutcome.Conflict, + key.Subject, + roleFacts, + $"Artifact member '{memberName}' has incompatible identifier roles"); + failed = true; + continue; + } + + var isIdentifier = existing?.Property.IsIdentifier == true || roleVariants.Length == 1; + properties[memberName] = new OrderedProperty( + orders[0], + new PropertyDefinition + { + Name = memberName, + Type = type, + IsIdentifier = isIdentifier + }); + supports.AddRange(currentFacts); + } + + var duplicateOrders = properties.Values + .GroupBy(property => property.Order) + .Where(group => group.Count() > 1) + .ToArray(); + foreach (var duplicateOrder in duplicateOrders.OrderBy(group => group.Key)) + { + var duplicateNames = duplicateOrder + .Select(property => property.Property.Name) + .ToHashSet(StringComparer.Ordinal); + var involvedFacts = memberFacts + .Where(fact => duplicateNames.Contains(MemberFor(fact)!.Name)) + .Concat(variant.Supports.OfType()); + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.ConflictingArtifactMember, + GenerationDiagnosticOutcome.Conflict, + key.Subject, + involvedFacts, + $"Artifact '{key.Subject.Value}' has multiple members at declaration order {duplicateOrder.Key}"); + failed = true; + } + + if (failed) + { + return variant.Supports.Exists(fact => fact is ArtifactFact) + ? variant + : null; + } + + return new ArtifactVariant( + variant.Definition with + { + Properties = + [ + .. properties.Values + .OrderBy(property => property.Order) + .ThenBy(property => property.Property.Name, StringComparer.Ordinal) + .Select(property => property.Property) + ] + }, + [.. CanonicalFacts(supports)]); + } + + static bool TryTypeReference(TypeUseDefinition type, out TypeReferenceDefinition reference) + { + var shape = type.Shape; + var isCollection = false; + var isOptional = false; + var supported = true; + switch (shape) + { + case [TypeUseShapeKind.Named]: + break; + case [TypeUseShapeKind.Optional, TypeUseShapeKind.Named]: + isOptional = true; + break; + case [TypeUseShapeKind.Collection, TypeUseShapeKind.Named]: + isCollection = true; + break; + case [TypeUseShapeKind.Optional, TypeUseShapeKind.Collection, TypeUseShapeKind.Named]: + isOptional = true; + isCollection = true; + break; + default: + supported = false; + break; + } + reference = new TypeReferenceDefinition + { + Name = type.Name, + IsCollection = isCollection, + IsOptional = isOptional + }; + return supported; + } + + static bool SameUnboundType(TypeReferenceDefinition first, TypeReferenceDefinition second) => + first.Name == second.Name && + first.IsCollection == second.IsCollection && + first.IsOptional == second.IsOptional; + + static GenerationFact[] ValidGranularFacts( + IEnumerable facts, + List diagnostics) + { + var valid = new List(); + foreach (var fact in facts.OrderBy(fact => fact.Id.Value, StringComparer.Ordinal)) + { + var owner = fact switch + { + ArtifactDeclarationFact declaration => declaration.Definition.Artifact.Subject, + _ => MemberFor(fact)?.Artifact.Subject + }; + if (owner == fact.Subject) + { + valid.Add(fact); + continue; + } + + diagnostics.Add(new GenerationDiagnostic + { + Code = GenerationDiagnosticCodes.InvalidGranularFactOwnership, + Severity = GenerationDiagnosticSeverity.Error, + Outcome = GenerationDiagnosticOutcome.Unknown, + Message = $"Granular fact '{fact.Id.Value}' describes artifact owner '{owner?.Value}' but asserts subject '{fact.Subject.Value}'; the fact was omitted", + Source = fact.Evidence.Source, + Subject = fact.Subject + }); + } + + return [.. valid]; + } + + static ArtifactMemberKey? MemberFor(GenerationFact fact) => fact switch + { + ArtifactMemberDeclarationFact declaration => declaration.Definition.Member, + ArtifactMemberTypeUseFact typeUse => typeUse.Definition.Member, + TypeUseBindingFact binding => binding.Definition.Member, + ArtifactMemberRoleFact role => role.Definition.Member, + _ => null + }; + + static bool IsGranularArtifactFact(GenerationFact fact) => fact is + ArtifactDeclarationFact or + ArtifactMemberDeclarationFact or + ArtifactMemberTypeUseFact or + TypeUseBindingFact or + ArtifactMemberRoleFact; + + static string DeclarationKey(ArtifactDeclarationDefinition definition) => + Structural.ArtifactDeclaration(definition); + + static string DeclarationKey(ArtifactDefinition definition) => + Structural.ArtifactDeclaration(new ArtifactDeclarationDefinition + { + Artifact = definition.Key, + Name = definition.Name, + Description = definition.Description, + File = definition.File + }); + + static GenerationFact[] CanonicalFacts(IEnumerable facts) => + [ + .. facts + .GroupBy(fact => fact.Id.Value, StringComparer.Ordinal) + .OrderBy(group => group.Key, StringComparer.Ordinal) + .Select(group => group.OrderBy(Structural.Fact, StringComparer.Ordinal).First()) + ]; + + static void AddDiagnostic( + List diagnostics, + string code, + GenerationDiagnosticOutcome outcome, + SubjectId subject, + IEnumerable facts, + string message) + { + var inputs = CanonicalFacts(facts); + diagnostics.Add(new GenerationDiagnostic + { + Code = code, + Severity = GenerationDiagnosticSeverity.Error, + Outcome = outcome, + Message = $"{message}. Input facts: {string.Join(", ", inputs.Select(fact => $"'{fact.Id.Value}'"))}", + Source = inputs.FirstOrDefault()?.Evidence.Source, + Subject = subject + }); + } + + static string Shape(TypeUseDefinition type) => string.Join('(', type.Shape) + new string(')', type.Shape.Count - 1); + + sealed record OrderedProperty(int Order, PropertyDefinition Property); + + sealed record ArtifactVariant(ArtifactDefinition Definition, List Supports); +} diff --git a/Source/DotNET/Generation/ResolvedApplicationGraph.cs b/Source/DotNET/Generation/ResolvedApplicationGraph.cs index e71d594..343e61f 100644 --- a/Source/DotNET/Generation/ResolvedApplicationGraph.cs +++ b/Source/DotNET/Generation/ResolvedApplicationGraph.cs @@ -13,6 +13,11 @@ public sealed record ResolvedArtifactVariant /// public required ArtifactDefinition Definition { get; init; } + /// + /// Gets the canonical fact identities supporting the effective definition. + /// + public IReadOnlyList SupportingFacts { get; init; } = []; + /// /// Gets the ordered evidence supporting the definition. /// diff --git a/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs b/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs index f78acd5..50def01 100644 --- a/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs +++ b/Source/DotNET/Generation/ScreenplayDefinitionGenerator.cs @@ -86,7 +86,16 @@ public GeneratedScreenplayDefinition Generate( IEnumerable contributions, ScreenplayGenerationOptions options) { - var graph = resolver.Resolve(contributions); + var input = contributions.ToArray(); + var baseFacts = input.SelectMany(contribution => contribution.Facts).ToArray(); + var derivation = GenerationFactDerivation.Derive(new AdapterRunSnapshot + { + Facts = [.. baseFacts.Select(fact => new GenerationFactRecord { Fact = fact })] + }); + var graph = resolver.ResolveFacts( + baseFacts.Concat(derivation.Facts.Select(record => record.Fact)), + input.SelectMany(contribution => contribution.Diagnostics) + .Concat(derivation.Rules.SelectMany(rule => rule.Diagnostics))); var lowering = lowerer.Lower(graph, options.Domain); var source = printer.Print(lowering.Application); var verification = compiler.Compile(source); @@ -102,11 +111,10 @@ public GeneratedScreenplayDefinition Generate( }); } - var diagnostics = graph.Diagnostics - .Concat(lowering.Diagnostics) - .Concat(verificationDiagnostics) - .OrderBy(Canonical.Diagnostic, StringComparer.Ordinal) - .ToArray(); + var diagnostics = CanonicalDiagnostics( + graph.Diagnostics + .Concat(lowering.Diagnostics) + .Concat(verificationDiagnostics)); return new() { @@ -150,13 +158,12 @@ public GeneratedScreenplayDefinition Generate( { Facts = [.. facts.Select(fact => new GenerationFactRecord { Fact = fact })] }); - var contributions = completed.Select(contribution => new AdapterContribution - { - Adapter = contribution.Descriptor.Identity, - Facts = contribution.Facts, - Diagnostics = contribution.Diagnostics - }); - var graph = resolver.Resolve(contributions); + var derivedFacts = derivation.Facts.Select(record => record.Fact).ToArray(); + var effectiveFacts = facts.Concat(derivedFacts).ToArray(); + var graph = resolver.ResolveFacts( + effectiveFacts, + completed.SelectMany(contribution => contribution.Diagnostics) + .Concat(derivation.Rules.SelectMany(rule => rule.Diagnostics))); var lowering = lowerer.Lower(graph, options.Domain); var source = printer.Print(lowering.Application); var verification = compiler.Compile(source); @@ -173,24 +180,32 @@ public GeneratedScreenplayDefinition Generate( } var pipelineDiagnostics = graph.Diagnostics - .Concat(derivation.Diagnostics) .Concat(lowering.Diagnostics) .Concat(verificationDiagnostics) .OrderBy(Canonical.Diagnostic, StringComparer.Ordinal) .ToArray(); + var inputRecords = facts + .Select(fact => new GenerationFactRecord { Fact = fact }) + .Concat(derivation.Facts) + .ToArray(); var factRecords = GenerationFactDispositionCalculator.Calculate( - facts, + inputRecords, graph, lowering.Coverage, pipelineDiagnostics); - var canonicalFactRecords = AdapterRunCanonicalizer.FactRecords(factRecords); + var canonicalFactRecords = AdapterRunCanonicalizer.FactRecords( + factRecords.Where(record => record.Lineage is null)); + var canonicalDerivation = AdapterRunCanonicalizer.Derivation(derivation with + { + Facts = [.. factRecords.Where(record => record.Lineage is not null)] + }); var runnerDiagnostics = RunnerDiagnostics(snapshot.Diagnostics, canonicalAdapters); var dispositionDiagnostics = canonicalFactRecords.SelectMany(record => record.Diagnostics).ToArray(); var adapterRun = new AdapterRunSnapshot { Adapters = canonicalAdapters, Facts = canonicalFactRecords, - Derivation = derivation, + Derivation = canonicalDerivation, Diagnostics = CanonicalDiagnostics(runnerDiagnostics.Concat(dispositionDiagnostics)) }; var diagnostics = CanonicalDiagnostics( diff --git a/Source/DotNET/Generation/ScreenplayLoweringCoverage.cs b/Source/DotNET/Generation/ScreenplayLoweringCoverage.cs index d091361..6ca5348 100644 --- a/Source/DotNET/Generation/ScreenplayLoweringCoverage.cs +++ b/Source/DotNET/Generation/ScreenplayLoweringCoverage.cs @@ -11,6 +11,11 @@ internal static class GenerationFactSemanticKey { ArtifactFact artifact => Artifact(artifact.Definition), ArtifactPlacementFact placement => Placement(placement.Artifact, placement.Placement), + ArtifactDeclarationFact declaration => ArtifactDeclaration(declaration.Definition), + ArtifactMemberDeclarationFact member => ArtifactMemberDeclaration(member.Definition), + ArtifactMemberTypeUseFact typeUse => ArtifactMemberTypeUse(typeUse.Definition), + TypeUseBindingFact binding => TypeUseBinding(binding.Definition), + ArtifactMemberRoleFact role => ArtifactMemberRole(role.Definition), RelationshipFact relationship => Relationship(relationship.Definition), ConceptRepresentationFact representation => ConceptRepresentation(representation.Definition), ConceptAttributeFact attribute => ConceptAttribute(attribute.Definition), @@ -24,6 +29,21 @@ internal static class GenerationFactSemanticKey public static string Artifact(ArtifactDefinition definition) => Structural.SemanticKey("artifact", Structural.Artifact(definition)); + public static string ArtifactDeclaration(ArtifactDeclarationDefinition definition) => + Structural.SemanticKey("artifact-declaration", Structural.ArtifactDeclaration(definition)); + + public static string ArtifactMemberDeclaration(ArtifactMemberDeclarationDefinition definition) => + Structural.SemanticKey("artifact-member-declaration", Structural.ArtifactMemberDeclaration(definition)); + + public static string ArtifactMemberTypeUse(ArtifactMemberTypeUseDefinition definition) => + Structural.SemanticKey("artifact-member-type-use", Structural.ArtifactMemberTypeUse(definition)); + + public static string TypeUseBinding(TypeUseBindingDefinition definition) => + Structural.SemanticKey("type-use-binding", Structural.TypeUseBinding(definition)); + + public static string ArtifactMemberRole(ArtifactMemberRoleDefinition definition) => + Structural.SemanticKey("artifact-member-role", Structural.ArtifactMemberRole(definition)); + public static string Placement(ArtifactKey artifact, ArtifactPlacement placement) => Structural.SemanticKey("placement", Structural.ArtifactKey(artifact), Structural.Placement(placement)); From 49b4250bf535679b7c515ba6b5f67e53d6d1ae39 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 09:01:01 +0200 Subject: [PATCH 4/7] Add exact DotNet type-use fact emission Let .NET adapters emit independently admitted member declarations, nested use-site shapes, exact terminal subjects, and explicit member roles while preserving the existing aggregate type-shape APIs. --- .../when_reading_exact_type_use_shapes.cs | 54 ++++++++++ .../when_emitting_member_facts.cs | 54 ++++++++++ ...erty_names_normalize_to_the_same_member.cs | 56 ++++++++++ .../Generation.DotNet/DotNetTypeShapes.cs | 68 ++++++++++-- .../Generation.DotNet/DotNetTypeUseFacts.cs | 101 ++++++++++++++++++ 5 files changed, 324 insertions(+), 9 deletions(-) create mode 100644 Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeShapes/when_reading_exact_type_use_shapes.cs create mode 100644 Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_emitting_member_facts.cs create mode 100644 Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_source_property_names_normalize_to_the_same_member.cs create mode 100644 Source/DotNET/Generation.DotNet/DotNetTypeUseFacts.cs diff --git a/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeShapes/when_reading_exact_type_use_shapes.cs b/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeShapes/when_reading_exact_type_use_shapes.cs new file mode 100644 index 0000000..60ce5b3 --- /dev/null +++ b/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeShapes/when_reading_exact_type_use_shapes.cs @@ -0,0 +1,54 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.DotNet.for_DotNetTypeShapes; + +public class when_reading_exact_type_use_shapes : given.a_compilation +{ + IReadOnlyDictionary _types = null!; + SubjectId _conceptSubject = null!; + + void Because() + { + var compilation = CompilationFrom(new SourceFile( + "/workspace/Ordering/Shapes.cs", + """ + #nullable enable + namespace Ordering; + public sealed record CustomerCode; + public sealed record Shapes( + CustomerCode Value, + CustomerCode? OptionalValue, + System.Collections.Generic.IReadOnlyList Values, + System.Collections.Generic.IReadOnlyList OptionalElements, + System.Collections.Generic.IReadOnlyList? OptionalCollection, + System.Collections.Generic.IReadOnlyList> NestedValues); + """)); + var project = new DotNetProjectCompilation + { + Name = "Ordering", + Compilation = compilation, + AuthoredSyntaxTrees = compilation.SyntaxTrees.ToHashSet() + }; + var context = new DotNetAnalysisContext([project]); + var concept = TypeNamed(compilation, "Ordering.CustomerCode"); + _conceptSubject = project.SubjectForType(concept); + _types = TypeNamed(compilation, "Ordering.Shapes").GetMembers() + .OfType() + .Where(property => !property.IsStatic && property.DeclaredAccessibility == Accessibility.Public) + .ToDictionary( + property => property.Name, + property => DotNetTypeShapes.TypeUseFor(property.Type, context), + StringComparer.Ordinal); + } + + [Fact] void should_preserve_a_named_type() => Shape("Value").ShouldEqual("Named"); + [Fact] void should_preserve_an_optional_named_type() => Shape("OptionalValue").ShouldEqual("Optional|Named"); + [Fact] void should_preserve_a_collection() => Shape("Values").ShouldEqual("Collection|Named"); + [Fact] void should_distinguish_optional_collection_elements() => Shape("OptionalElements").ShouldEqual("Collection|Optional|Named"); + [Fact] void should_distinguish_an_optional_collection() => Shape("OptionalCollection").ShouldEqual("Optional|Collection|Named"); + [Fact] void should_preserve_nested_collection_and_element_shape() => Shape("NestedValues").ShouldEqual("Collection|Collection|Optional|Named"); + [Fact] void should_bind_every_terminal_source_type_to_its_exact_subject() => string.Join('|', _types.Where(item => item.Value.ObservedTypeSubject != _conceptSubject).Select(item => $"{item.Key}:{item.Value.Name}:{item.Value.ObservedTypeSubject?.Value}")).ShouldEqual(string.Empty); + + string Shape(string property) => string.Join('|', _types[property].Shape); +} diff --git a/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_emitting_member_facts.cs b/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_emitting_member_facts.cs new file mode 100644 index 0000000..87c2294 --- /dev/null +++ b/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_emitting_member_facts.cs @@ -0,0 +1,54 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.DotNet.for_DotNetTypeUseFacts; + +public class when_emitting_member_facts : given.a_compilation +{ + readonly AdapterIdentity _adapter = new() { Id = "critter-stack", Version = "1.0.0" }; + ArtifactKey _artifact = null!; + IReadOnlyList _facts = null!; + SubjectId _conceptSubject = null!; + + void Because() + { + var compilation = CompilationFrom(new SourceFile( + "/workspace/Ordering/RegisterCustomer.cs", + """ + #nullable enable + namespace Ordering; + public sealed record CustomerCode; + public sealed record RegisterCustomer(CustomerCode CustomerCode, string? Referral); + """)); + var project = new DotNetProjectCompilation + { + Name = "Ordering", + Compilation = compilation, + AuthoredSyntaxTrees = compilation.SyntaxTrees.ToHashSet() + }; + var context = new DotNetAnalysisContext([project]); + _artifact = new ArtifactKey + { + Subject = project.SubjectForType(TypeNamed(compilation, "Ordering.RegisterCustomer")), + Kind = ArtifactKind.Command + }; + _conceptSubject = project.SubjectForType(TypeNamed(compilation, "Ordering.CustomerCode")); + _facts = DotNetTypeUseFacts.Emit( + TypeNamed(compilation, "Ordering.RegisterCustomer"), + _artifact, + context, + new Evidence { Adapter = _adapter, Strength = EvidenceStrength.Exact }, + property => property.Name == "CustomerCode" + ? ArtifactMemberRoleKind.EventSourceIdentifier + : null); + } + + [Fact] void should_emit_one_declaration_per_member() => _facts.OfType().Select(fact => fact.Definition.Member.Name).ShouldEqual("customerCode", "referral"); + [Fact] void should_preserve_zero_based_declaration_order() => _facts.OfType().Select(fact => fact.Definition.DeclarationOrder).ShouldEqual(0, 1); + [Fact] void should_emit_one_exact_type_use_per_member() => _facts.OfType().Count().ShouldEqual(2); + [Fact] void should_bind_the_terminal_source_subject_without_inspecting_other_adapters() => _facts.OfType().First().Definition.Type.ObservedTypeSubject.ShouldEqual(_conceptSubject); + [Fact] void should_preserve_optional_reference_shape() => string.Join('|', _facts.OfType().Last().Definition.Type.Shape).ShouldEqual("Optional|Named"); + [Fact] void should_emit_only_the_explicitly_established_role() => _facts.OfType().Single().Definition.ShouldEqual(new ArtifactMemberRoleDefinition { Member = new ArtifactMemberKey { Artifact = _artifact, Name = "customerCode" }, Role = ArtifactMemberRoleKind.EventSourceIdentifier }); + [Fact] void should_scope_every_fact_id_to_the_evidence_adapter() => _facts.All(fact => fact.Id.Value.StartsWith("critter-stack:", StringComparison.Ordinal)).ShouldBeTrue(); + [Fact] void should_retain_the_exact_artifact_owner_on_every_fact() => _facts.All(fact => fact.Subject == _artifact.Subject).ShouldBeTrue(); +} diff --git a/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_source_property_names_normalize_to_the_same_member.cs b/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_source_property_names_normalize_to_the_same_member.cs new file mode 100644 index 0000000..0f8cdd6 --- /dev/null +++ b/Source/DotNET/Generation.DotNet.Specs/for_DotNetTypeUseFacts/when_source_property_names_normalize_to_the_same_member.cs @@ -0,0 +1,56 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.DotNet.for_DotNetTypeUseFacts; + +public class when_source_property_names_normalize_to_the_same_member : given.a_compilation +{ + AdapterContributionAdmissionResult _admission = null!; + IReadOnlyList _facts = null!; + + void Because() + { + var compilation = CompilationFrom(new SourceFile( + "/workspace/Ordering/CaseCollision.cs", + """ + namespace Ordering; + public sealed record CaseCollision(string URL, string uRL); + """)); + var project = new DotNetProjectCompilation + { + Name = "Ordering", + Compilation = compilation, + AuthoredSyntaxTrees = compilation.SyntaxTrees.ToHashSet() + }; + var context = new DotNetAnalysisContext([project]); + var adapter = new AdapterIdentity { Id = "case-collision", Version = "1.0.0" }; + _facts = DotNetTypeUseFacts.Emit( + TypeNamed(compilation, "Ordering.CaseCollision"), + new ArtifactKey + { + Subject = project.SubjectForType(TypeNamed(compilation, "Ordering.CaseCollision")), + Kind = ArtifactKind.CompositeType + }, + context, + new Evidence { Adapter = adapter, Strength = EvidenceStrength.Exact }); + var descriptor = new AdapterDescriptor + { + Identity = adapter, + SourceLanguage = AdapterSourceLanguage.CSharp, + Category = AdapterCategory.ApplicationFramework, + EmittedFactCapabilities = + [ + GenerationFactCapability.ArtifactMemberDeclaration, + GenerationFactCapability.ArtifactMemberTypeUse + ] + }; + _admission = AdapterContributionAdmission.Admit( + descriptor, + new AdapterContribution { Adapter = adapter, Facts = _facts }); + } + + [Fact] void should_retain_both_normalized_member_assertions() => _facts.OfType().Select(fact => fact.Definition.Member.Name).ShouldEqual("uRL", "uRL"); + [Fact] void should_retain_distinct_declaration_orders_for_conflict_resolution() => _facts.OfType().Select(fact => fact.Definition.DeclarationOrder).ShouldEqual(0, 1); + [Fact] void should_encode_the_exact_source_property_in_unique_fact_ids() => _facts.Select(fact => fact.Id.Value).Distinct(StringComparer.Ordinal).Count().ShouldEqual(4); + [Fact] void should_admit_the_unique_source_assertions_atomically() => _admission.IsAdmitted.ShouldBeTrue(); +} diff --git a/Source/DotNET/Generation.DotNet/DotNetTypeShapes.cs b/Source/DotNET/Generation.DotNet/DotNetTypeShapes.cs index ceb2978..b6a9127 100644 --- a/Source/DotNET/Generation.DotNet/DotNetTypeShapes.cs +++ b/Source/DotNET/Generation.DotNet/DotNetTypeShapes.cs @@ -26,6 +26,51 @@ public static class DotNetTypeShapes public static TypeReferenceDefinition TypeReferenceFor(ITypeSymbol type, DotNetAnalysisContext context) => CreateTypeReference(type, context.SubjectForType); + /// + /// Gets the exact optionality and collection shape of a Roslyn type use. + /// + /// The Roslyn type at the use site. + /// The analyzed project context used to resolve the terminal source subject. + /// The exact source-neutral type use from outermost wrapper to terminal named type. + public static TypeUseDefinition TypeUseFor(ITypeSymbol type, DotNetAnalysisContext context) + { + var shape = new List(); + var current = type; + while (true) + { + if (current is INamedTypeSymbol nullable && + nullable.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + shape.Add(TypeUseShapeKind.Optional); + current = nullable.TypeArguments[0]; + continue; + } + + if (current.NullableAnnotation == NullableAnnotation.Annotated) + { + shape.Add(TypeUseShapeKind.Optional); + current = current.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + continue; + } + + var (elementType, isCollection) = CollectionElementOf(current); + if (isCollection) + { + shape.Add(TypeUseShapeKind.Collection); + current = elementType; + continue; + } + + shape.Add(TypeUseShapeKind.Named); + return new TypeUseDefinition + { + Name = TypeName(current), + ObservedTypeSubject = current is INamedTypeSymbol named ? context.SubjectForType(named) : null, + Shape = shape + }; + } + } + /// /// Gets the public readable instance properties of a source type in declaration order. /// @@ -42,6 +87,19 @@ public static TypeReferenceDefinition TypeReferenceFor(ITypeSymbol type, DotNetA public static IReadOnlyList PropertiesOf(INamedTypeSymbol type, DotNetAnalysisContext context) => PropertiesOf(type, propertyType => TypeReferenceFor(propertyType, context)); + internal static IReadOnlyList PublicReadablePropertiesOf(INamedTypeSymbol type) => + [ + .. type.GetMembers() + .OfType() + .Where(_ => !_.IsStatic && !_.IsIndexer && _.DeclaredAccessibility == Accessibility.Public && _.GetMethod?.DeclaredAccessibility == Accessibility.Public) + .OrderBy(SourceOrder) + .ThenBy(_ => _.Name, StringComparer.Ordinal) + ]; + + internal static string PropertyName(string name) => name.Length == 0 + ? name + : $"{char.ToLowerInvariant(name[0])}{name[1..]}"; + static TypeReferenceDefinition CreateTypeReference( ITypeSymbol type, Func? subjectForType) @@ -68,11 +126,7 @@ static IReadOnlyList PropertiesOf( INamedTypeSymbol type, Func typeReferenceFor) => [ - .. type.GetMembers() - .OfType() - .Where(_ => !_.IsStatic && !_.IsIndexer && _.DeclaredAccessibility == Accessibility.Public && _.GetMethod?.DeclaredAccessibility == Accessibility.Public) - .OrderBy(SourceOrder) - .ThenBy(_ => _.Name, StringComparer.Ordinal) + .. PublicReadablePropertiesOf(type) .Select(_ => new PropertyDefinition { Name = PropertyName(_.Name), @@ -118,10 +172,6 @@ static int SourceOrder(IPropertySymbol property) => property.Locations .Select(_ => _.SourceSpan.Start) .DefaultIfEmpty(int.MaxValue) .Min(); - - static string PropertyName(string name) => name.Length == 0 - ? name - : $"{char.ToLowerInvariant(name[0])}{name[1..]}"; } /// diff --git a/Source/DotNET/Generation.DotNet/DotNetTypeUseFacts.cs b/Source/DotNET/Generation.DotNet/DotNetTypeUseFacts.cs new file mode 100644 index 0000000..4602400 --- /dev/null +++ b/Source/DotNET/Generation.DotNet/DotNetTypeUseFacts.cs @@ -0,0 +1,101 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; + +namespace Cratis.Screenplay.Generation.DotNet; + +/// +/// Emits source-neutral member declarations, exact type uses, and optional roles from Roslyn properties. +/// +public static class DotNetTypeUseFacts +{ + /// + /// Emits granular facts for every public readable instance property declared by a source type. + /// + /// The exact source type declaring the properties. + /// The artifact role that owns the members. + /// The fixed analysis context used only to establish exact source type subjects. + /// The evidence establishing the declared member surface. + /// An optional source-framework rule that establishes a semantic role for a property. + /// Member declaration and type-use facts, followed by an optional role fact for each property. + public static IReadOnlyList Emit( + INamedTypeSymbol type, + ArtifactKey artifact, + DotNetAnalysisContext context, + Evidence evidence, + Func? roleFor = null) + { + var facts = new List(); + var properties = DotNetTypeShapes.PublicReadablePropertiesOf(type); + for (var order = 0; order < properties.Count; order++) + { + var property = properties[order]; + var name = DotNetTypeShapes.PropertyName(property.Name); + var member = new ArtifactMemberKey + { + Artifact = artifact, + Name = name + }; + facts.Add(new ArtifactMemberDeclarationFact + { + Id = FactIdFor(evidence.Adapter.Id, "artifact-member-declaration", member, property.Name), + Subject = artifact.Subject, + Evidence = evidence, + Definition = new ArtifactMemberDeclarationDefinition + { + Member = member, + DeclarationOrder = order + } + }); + facts.Add(new ArtifactMemberTypeUseFact + { + Id = FactIdFor(evidence.Adapter.Id, "artifact-member-type-use", member, property.Name), + Subject = artifact.Subject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = DotNetTypeShapes.TypeUseFor(property.Type, context) + } + }); + + var role = roleFor?.Invoke(property); + if (role is not null) + { + facts.Add(new ArtifactMemberRoleFact + { + Id = FactIdFor(evidence.Adapter.Id, $"artifact-member-role-{(int)role.Value}", member, property.Name), + Subject = artifact.Subject, + Evidence = evidence, + Definition = new ArtifactMemberRoleDefinition + { + Member = member, + Role = role.Value + } + }); + } + } + + return facts; + } + + static FactId FactIdFor( + string adapter, + string family, + ArtifactMemberKey member, + string sourceMemberName) => new() + { + Value = string.Join( + ':', + adapter, + family, + Encode(member.Artifact.Subject.Value), + ((int)member.Artifact.Kind).ToString(System.Globalization.CultureInfo.InvariantCulture), + Encode(sourceMemberName), + Encode(member.Name)) + }; + + static string Encode(string value) => string.Concat( + value.Select(character => ((int)character).ToString("X4", System.Globalization.CultureInfo.InvariantCulture))); +} From 24e327236b6c7ee6265e7453fa87353b2a72843f Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 09:15:53 +0200 Subject: [PATCH 5/7] Verify granular derivation package consumers Document fixed-snapshot type-use lineage and exercise modern Vogen/external composition, a source-independent non-.NET frontend, the 0.16 API baseline, and unchanged 0.1/0.5 consumer binaries against candidate packages. --- Directory.Build.props | 4 +- Documentation/guides/build-source-adapter.md | 41 ++- README.md | 34 +- scripts/verify-package-consumers.sh | 325 +++++++++++++++++-- 4 files changed, 354 insertions(+), 50 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 308305a..58454a8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,12 +10,12 @@ true true - 0.15.0 + 0.16.0 false diff --git a/Documentation/guides/build-source-adapter.md b/Documentation/guides/build-source-adapter.md index 84e2487..3f363cf 100644 --- a/Documentation/guides/build-source-adapter.md +++ b/Documentation/guides/build-source-adapter.md @@ -39,10 +39,10 @@ Keep runtime packages for the framework you analyze out of the adapter whenever Reference one version across all directly referenced Screenplay Generation packages. :::note -`v0.15.0` is the current public release and package-validation baseline. Exact method signatures, complete bounded source-value extraction, and authoritative invocation and assignment enumeration are included in that lockstep package set. The described-adapter execution boundary is additive on `main`. +`v0.16.0` is the current public release and package-validation baseline. Described adapters, atomic admission, deterministic runner snapshots, and final fact dispositions are included in that lockstep package set. Granular type-use derivation is additive on `main`. ::: -| Capability | Released `0.15.0` | Current `main` | +| Capability | Released `0.16.0` | Current `main` | | --- | ---: | ---: | | Adapter, context, neutral fact, evidence, and diagnostic contracts | Yes | Yes | | Stable source identity, fixed source snapshots, and strict placement | Yes | Yes | @@ -52,11 +52,14 @@ Reference one version across all directly referenced Screenplay Generation packa | Overload-safe subjects, alternate source owners, and flat compatibility placement | Yes | Yes | | Authoritative invocation and assignment enumeration | Yes | Yes | | Legacy `IDotNetScreenplayAdapter` | Yes | Yes | -| Descriptors, structured probes, and atomic public admission | No | Yes | -| Explicit modern/legacy registration and deterministic .NET runner | No | Yes | -| Immutable adapter-run snapshots and `Generate(snapshot)` | No | Yes | -| Per-fact generation dispositions | No | Yes | -| Vogen modern descriptor/probe with legacy contribution parity | No | Yes | +| Descriptors, structured probes, and atomic public admission | Yes | Yes | +| Explicit modern/legacy registration and deterministic .NET runner | Yes | Yes | +| Immutable adapter-run snapshots and `Generate(snapshot)` | Yes | Yes | +| Per-fact generation dispositions | Yes | Yes | +| Vogen modern descriptor/probe with legacy contribution parity | Yes | Yes | +| Granular artifact/member/type-use/binding/role facts | No | Yes | +| Fixed-snapshot derivation rule and input/evidence lineage | No | Yes | +| Exact nested .NET type-use fact emission | No | Yes | ## Implement the adapter contract @@ -92,7 +95,12 @@ public sealed class AcmeScreenplayAdapter : AdapterHostCapability.SemanticAnalysis ], RequiredApiCapabilities = [_commandDeclarationApi], - EmittedFactCapabilities = [GenerationFactCapability.Artifact] + EmittedFactCapabilities = + [ + GenerationFactCapability.Artifact, + GenerationFactCapability.ArtifactMemberDeclaration, + GenerationFactCapability.ArtifactMemberTypeUse + ] }; // Legacy compatibility surface. @@ -177,10 +185,11 @@ public sealed class AcmeScreenplayAdapter : Key = key, Name = type.Name, File = evidence.Source?.Path, - Properties = DotNetTypeShapes.PropertiesOf(type, context) + Properties = DotNetTypeShapes.PropertiesOf(type) }, Evidence = evidence }); + facts.AddRange(DotNetTypeUseFacts.Emit(type, key, context, evidence)); } } @@ -332,7 +341,11 @@ Fact IDs must be globally stable and unique. Prefix them with the adapter and se Use the smallest fact vocabulary that says what the source proves: -- `ArtifactFact` — a command, event, read model, projection, reaction, message, handler, concept, or another supported role; +- `ArtifactFact` — the compatibility aggregate for a command, event, read model, projection, reaction, message, handler, concept, or another supported role; +- `ArtifactDeclarationFact` and `ArtifactMemberDeclarationFact` — independent artifact metadata and one ordered member declaration without repeating a complete property list; +- `ArtifactMemberTypeUseFact` — one exact use-site type shape and observed source subject; +- `TypeUseBindingFact` — an exact member-to-artifact binding, normally produced by fixed-snapshot derivation; +- `ArtifactMemberRoleFact` — an explicitly established typed identifier or event-source-identifier role; - `ArtifactPlacementFact` — module, feature, slice, and independently established slice kind; - `RelationshipFact` — handles, reads, produces, consumes, builds, returns, cascades, publishes, starts or appends streams, or document persistence; - concept representation, attribute, and validation facts; @@ -340,7 +353,11 @@ Use the smallest fact vocabulary that says what the source proves: Do not overload a nearby role. A published message is not a persisted event. A document is not an event-built read model unless source evidence proves the projection. A response is not a cascade. -Use `TypeReferenceDefinition.Subject` when a property targets an exact discovered type or concept. `DotNetTypeShapes.PropertiesOf(type, context)` and `TypeReferenceFor(type, context)` preserve project-qualified type subjects. +Keep compatibility aggregate properties unbound with `DotNetTypeShapes.PropertiesOf(type)` when another adapter may declare their target concepts. Append `DotNetTypeUseFacts.Emit(type, artifact, context, evidence)` so each member independently records declaration order, exact use-site shape, and the terminal project-qualified source subject. Fixed-snapshot derivation then emits a granular binding without rewriting the aggregate. + +`DotNetTypeShapes.TypeUseFor(type, context)` orders shape nodes from the outermost wrapper to the terminal `Named` node. This distinguishes `Collection(Optional(Named))` from `Optional(Collection(Named))` and preserves nested collections. The current Screenplay grammar lowers only the shapes it can express exactly; unsupported distinctions remain diagnosed rather than flattened. + +Pass a `roleFor` callback to `DotNetTypeUseFacts.Emit(...)` only when source-framework semantics establish `ArtifactMemberRoleKind.Identifier` or `EventSourceIdentifier`. Never infer either role from a property name or primitive type. A framework that already knows the exact target inside one adapter may continue setting `TypeReferenceDefinition.Subject` directly with `PropertiesOf(type, context)` or `TypeReferenceFor(type, context)`. ## Nominate declared concepts @@ -530,7 +547,7 @@ The modern descriptor has category `Concepts`, source language `CSharp`, require The type-use binding rule joins an `ArtifactMemberTypeUseFact` to an exact declared artifact subject. It does not join by display name and does not replace the owning artifact's complete property list. Its derived `TypeUseBindingFact` remains separate from the admitted base facts under `AdapterRunSnapshot.Derivation`. -Each derived `GenerationFactRecord` carries `GenerationFactLineage`: the stable derivation rule identity and version, canonical input `FactId` references, and complete input evidence. `GenerationDerivationRuleRecord` records the fixed inputs, outputs, and diagnostics for that rule execution. Directly invoking derivation leaves fact dispositions unknown because disposition is a later generation decision. `Generate(snapshot, options)` currently attaches the derivation result and propagates its diagnostics; resolution and lowering still consume only the admitted base contributions until the granular overlay stage applies the derived binding. +Each derived `GenerationFactRecord` carries `GenerationFactLineage`: the stable derivation rule identity and version, canonical input `FactId` references, and complete input evidence. `GenerationDerivationRuleRecord` records the fixed inputs, outputs, and diagnostics for that rule execution. Directly invoking derivation leaves fact dispositions unknown because disposition is a later generation decision. `Generate(snapshot, options)` attaches the derivation result, propagates its diagnostics, resolves admitted and derived granular facets together, and applies an exact member binding as an overlay without publishing a replacement aggregate fact. Exact subjects can come from any source frontend. A C# member type use can bind a declaration contributed by another adapter, while a source-independent or non-.NET adapter can contribute the same neutral contracts without Roslyn or Screenplay-layout dependencies. Missing, ambiguous, or conflicting inputs produce stable diagnostics without selecting a winner. diff --git a/README.md b/README.md index b1b6300..f8f5061 100644 --- a/README.md +++ b/README.md @@ -20,26 +20,30 @@ See [Build a .NET source adapter](Documentation/guides/build-source-adapter.md) ## Public baseline and current main -`0.15.0` is the current public lockstep release and package-validation baseline. The adapter execution boundary is additive on `main`. +`0.16.0` is the current public lockstep release and package-validation baseline. Granular type-use derivation is additive on `main`. -| Capability | Released `0.15.0` | Current `main` | +| Capability | Released `0.16.0` | Current `main` | | --- | ---: | ---: | | Adapter, context, neutral fact, evidence, and diagnostic contracts | Yes | Yes | | Stable source identity, fixed source snapshots, and strict placement | Yes | Yes | | Exact method signatures and bounded scalar, payload, and collection extraction | Yes | Yes | | Authoritative invocation and assignment enumeration | Yes | Yes | | Legacy `IDotNetScreenplayAdapter` | Yes | Yes | -| Descriptors, structured probes, and atomic public admission | No | Yes | -| Explicit modern/legacy registration and deterministic .NET runner | No | Yes | -| Immutable adapter-run snapshots and `Generate(snapshot)` | No | Yes | -| Per-fact generation dispositions | No | Yes | -| Vogen modern descriptor/probe with legacy contribution parity | No | Yes | +| Descriptors, structured probes, and atomic public admission | Yes | Yes | +| Explicit modern/legacy registration and deterministic .NET runner | Yes | Yes | +| Immutable adapter-run snapshots and `Generate(snapshot)` | Yes | Yes | +| Per-fact generation dispositions | Yes | Yes | +| Vogen modern descriptor/probe with legacy contribution parity | Yes | Yes | +| Granular artifact/member/type-use/binding/role facts | No | Yes | +| Fixed-snapshot derivation rule and input/evidence lineage | No | Yes | +| Exact nested .NET type-use fact emission | No | Yes | ## Architecture ```text source adapter - -> typed facts and evidence + -> admitted typed facts and evidence + -> fixed-snapshot derivation and lineage -> resolved application graph -> lowerable Screenplay model -> Screenplay AST @@ -62,7 +66,7 @@ Duplicate adapter IDs are rejected before probe or analysis. Invalid descriptors A modern `SourceIndependent` adapter with no host requirements can run against an empty .NET context. A source adapter that declares `StableSourceLocations` requires every authored tree to have an authoritative `DotNetProjectSourceContext` mapping, and every located probe, fact, and diagnostic must use that mapping. The modern Vogen path requires stable locations; its legacy interface remains available for compatibility. -Pass the frozen snapshot to `ScreenplayDefinitionGenerator.Generate(snapshot, options)` to preserve runner diagnostics and receive final fact dispositions: `Lowered`, `ProvenanceOnly`, `OmittedWithDiagnostic`, or `Conflicted`. This snapshot records one run only. It does not add issue #19 adapter/fact lineage, and it does not add issue #24 serialization or fingerprints. +Pass the frozen snapshot to `ScreenplayDefinitionGenerator.Generate(snapshot, options)` to preserve runner diagnostics and receive final fact dispositions: `Lowered`, `ProvenanceOnly`, `OmittedWithDiagnostic`, or `Conflicted`. Generation runs one closed derivation pass over the fixed admitted base facts, attaches stable rule/input/evidence lineage under `AdapterRunSnapshot.Derivation`, and applies exact member bindings as granular overlays. The snapshot records one run only; it does not add issue #24 serialization or fingerprints. ### Adapter syntax robustness @@ -139,7 +143,15 @@ Generated members never provide primary evidence. The adapter never infers ident Adapters can contribute `ArtifactKind.Concept` together with independently proven `ConceptRepresentationFact`, `ConceptAttributeFact`, and `ConceptValidationRuleFact` assertions. Primitive/enumeration representations, named attributes, and named external predicate rules resolve deterministically and lower to top-level Screenplay concepts without module placement. -`TypeReferenceDefinition.Subject` binds an artifact property to the exact concept subject rather than a simple display name. Missing, conflicting, unsupported, or same-named concept definitions produce stable diagnostics; generation never falls back to `String`. +`TypeReferenceDefinition.Subject` continues to bind an aggregate artifact property directly when one adapter proves the exact target. Missing, conflicting, unsupported, or same-named concept definitions produce stable diagnostics; generation never falls back to `String`. + +## Granular type uses and derivation + +Adapters that establish different facets independently can emit `ArtifactDeclarationFact`, `ArtifactMemberDeclarationFact`, `ArtifactMemberTypeUseFact`, and `ArtifactMemberRoleFact` without repeating a complete `ArtifactFact`. `TypeUseDefinition.Shape` orders optional and collection wrappers from outermost to the terminal `Named` node, so optional elements, optional collections, and nested collections remain distinct. + +Generation runs the built-in `cratis.screenplay.type-use-binding@1.0.0` rule once over one fixed admitted base snapshot. It joins only exact subjects, never display names, adapter IDs, registration order, Roslyn symbols, or another adapter instance. A derived `TypeUseBindingFact` retains canonical input `FactId` values and complete evidence in `GenerationFactLineage`. Conflicting, incomplete, foreign-owned, or unsupported inputs remain diagnosed without a winner or partial artifact. + +.NET adapters keep compatibility aggregate properties unbound with `DotNetTypeShapes.PropertiesOf(type)` and append `DotNetTypeUseFacts.Emit(...)`. `TypeUseFor(...)` preserves exact nested use-site shape and terminal source subject; an optional role callback emits only roles explicitly established by source-framework semantics. Non-.NET and source-independent frontends contribute the same contracts directly. Concept validation stays independent from identity, representation, attributes, and optionality. A rule uses an adapter-authored `RuleIdentity` for deterministic resolution, while `Predicate` is the authored predicate name emitted by lowering. Adapters contribute framework-neutral data and provenance only; they never reference Screenplay syntax: @@ -187,7 +199,7 @@ dotnet pack Screenplay.Generation.slnx --no-build --configuration Release -o Art ./scripts/verify-package-consumers.sh 9999.0.0 Artifacts/NuGet ``` -Package validation runs during pack against the latest released API baseline, `0.15.0`, for all four packages. Baseline strict mode remains disabled so intentional compatible additions are accepted while removals and signature changes still fail; no compatibility diagnostics are suppressed. The sentinel version must be applied to both the Release build and the no-build pack so package and assembly versions agree. The consumer smoke keeps clean legacy binaries compiled against the `0.1.0` core and `0.5.0` Vogen ancestry and runs them unchanged with current packages. A separate clean current-source consumer compiles only against the candidate packages and verifies the current authored-source, shared symbol helpers, declared concept nomination, neutral-fact, resolver, Vogen, adapter-composition, and deterministic compiler-verified generation APIs. +Package validation runs during pack against the latest released API baseline, `0.16.0`, for all four packages. Baseline strict mode remains disabled so intentional compatible additions are accepted while removals and signature changes still fail; no compatibility diagnostics are suppressed. The sentinel version must be applied to both the Release build and the no-build pack so package and assembly versions agree. The consumer smoke keeps clean legacy binaries compiled against the `0.1.0` core and `0.5.0` Vogen ancestry and runs them unchanged with current packages. A separate clean current-source consumer compiles only against the candidate packages and verifies the current authored-source, shared symbol helpers, declared concept nomination, neutral-fact, resolver, Vogen, adapter-composition, and deterministic compiler-verified generation APIs. All builds require zero errors and zero warnings. Generated Screenplay output must compile and remain stable through print/compile/print. diff --git a/scripts/verify-package-consumers.sh b/scripts/verify-package-consumers.sh index 2bd2502..fc2f242 100755 --- a/scripts/verify-package-consumers.sh +++ b/scripts/verify-package-consumers.sh @@ -4,7 +4,7 @@ # Compiles consumers against the compatibility ancestry, then runs those unchanged binaries # beside the packages being validated. A separate source consumer compiles only against the -# current candidate packages and exercises the public 0.15+ composition and adapter-run surface. Together these +# current candidate packages and exercises the public 0.16+ composition and adapter-run surface. Together these # catch binary breaks that a source rebuild hides and ensure the current package set is usable. # # Usage: verify-package-consumers.sh [current-version] [local-package-feed] @@ -361,6 +361,8 @@ internal static class Program (int)GenerationPrimitiveKind.Unknown == -1 && (int)ConceptAttributeKind.Unknown == -1 && (int)ConceptValidationRuleKind.Unknown == -1 && + (int)TypeUseShapeKind.Unknown == -1 && + (int)ArtifactMemberRoleKind.Unknown == -1 && (int)EvidenceStrength.Unknown == -1 && (int)DotNetProjectRole.Unknown == -1 && (int)DotNetProjectRole.Application == 0 && @@ -371,7 +373,18 @@ internal static class Program (int)GenerationSliceKind.Translate == 3 && (int)ArtifactKind.Command == 3 && (int)ArtifactKind.Query == 8 && - (int)ArtifactKind.Reducer == 10, + (int)ArtifactKind.Reducer == 10 && + (int)GenerationFactCapability.SpecificationValue == 8 && + (int)GenerationFactCapability.ArtifactDeclaration == 9 && + (int)GenerationFactCapability.ArtifactMemberDeclaration == 10 && + (int)GenerationFactCapability.ArtifactMemberTypeUse == 11 && + (int)GenerationFactCapability.TypeUseBinding == 12 && + (int)GenerationFactCapability.ArtifactMemberRole == 13 && + (int)TypeUseShapeKind.Named == 0 && + (int)TypeUseShapeKind.Optional == 1 && + (int)TypeUseShapeKind.Collection == 2 && + (int)ArtifactMemberRoleKind.Identifier == 0 && + (int)ArtifactMemberRoleKind.EventSourceIdentifier == 1, "CSC0019", "The additive public Unknown discriminator values are unavailable or were renumbered."); Require( @@ -392,8 +405,9 @@ internal static class Program "The named Failures constructor argument did not retain an immutable failure snapshot."); ExercisePublicAdapterContracts(); ExerciseCandidatePackageClosure(); + ExerciseSourceIndependentGranularFrontend(); - var authoredTree = CSharpSyntaxTree.ParseText( + var vogenApiTree = CSharpSyntaxTree.ParseText( """ namespace Vogen { @@ -413,7 +427,23 @@ internal static class Program } } } + """, + path: "/api/Vogen.SharedTypes.cs"); + var vogenApiCompilation = CSharpCompilation.Create( + "Vogen.SharedTypes", + [vogenApiTree], + TrustedPlatformReferences(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + using var vogenApiImage = new MemoryStream(); + var vogenApiEmit = vogenApiCompilation.Emit(vogenApiImage); + Require( + vogenApiEmit.Success, + "CSC0066", + $"The exact Vogen API failed to compile: {string.Join(" | ", vogenApiEmit.Diagnostics)}"); + var authoredTree = CSharpSyntaxTree.ParseText( + new string('\n', 19) + + """ namespace Ordering { [Vogen.ValueObject] @@ -470,7 +500,7 @@ internal static class Program var compilation = CSharpCompilation.Create( "Ordering", [authoredTree, generatedLookalikeTree], - TrustedPlatformReferences(), + TrustedPlatformReferences().Append(MetadataReference.CreateFromImage(vogenApiImage.ToArray())), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var compilationErrors = compilation.GetDiagnostics() .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) @@ -841,9 +871,34 @@ internal static class Program var eventFact = externalContribution.Facts.OfType().Single(); var eventType = eventFact.Definition.Properties.Single().Type; Require( - eventType.Name == "UnresolvedCustomerCode" && eventType.Subject == concept.Subject, + eventType.Name == "CustomerCode" && eventType.Subject is null && + externalContribution.Facts.OfType().Single().Definition.Type.ObservedTypeSubject == concept.Subject, "CSC0009", - "The external adapter did not bind TypeReferenceDefinition.Subject to the exact concept subject."); + "The external adapter did not keep aggregate type use unbound while contributing the exact observed concept subject independently."); + var modernSnapshot = DotNetAdapterRunner.Run( + [ + DotNetAdapterRegistration.For((IDescribedDotNetScreenplayAdapter)adapters[0]), + DotNetAdapterRegistration.For((IDescribedDotNetScreenplayAdapter)adapters[1]) + ], + context, + new DotNetAdapterOptions()); + var modernGenerated = new ScreenplayDefinitionGenerator().Generate( + modernSnapshot, + new ScreenplayGenerationOptions { Domain = "Ordering" }); + var directGenerated = new ScreenplayDefinitionGenerator().Generate( + contributions, + new ScreenplayGenerationOptions { Domain = "Ordering" }); + Require( + modernSnapshot.Adapters.All(record => record.Disposition == AdapterRunDisposition.Admitted) && + modernSnapshot.Adapters.Single(record => record.Descriptor.Identity.Id == ExternalAdapterId) + .Descriptor.EmittedFactCapabilities.Contains(GenerationFactCapability.ArtifactMemberDeclaration) && + modernSnapshot.Adapters.Single(record => record.Descriptor.Identity.Id == ExternalAdapterId) + .Descriptor.EmittedFactCapabilities.Contains(GenerationFactCapability.ArtifactMemberTypeUse) && + modernGenerated.IsSuccess && + modernGenerated.Source == directGenerated.Source && + modernGenerated.AdapterRun!.Derivation!.Facts.Single().Disposition == GenerationFactDisposition.Lowered, + "CSC0065", + "The modern external and Vogen adapters were not admitted, derived, and lowered with direct-path parity."); var specificationAdapter = new AdapterIdentity { Id = "specification-smoke", Version = "1.0.0" }; var commandSubject = new SubjectId { Value = "dotnet://Ordering/Ordering/RegisterCustomer" }; @@ -947,6 +1002,39 @@ internal static class Program ] }; AdapterContribution[] allContributions = [.. contributions, specificationContribution]; + var derivation = GenerationFactDerivation.Derive(new AdapterRunSnapshot + { + Facts = + [ + .. allContributions + .SelectMany(contribution => contribution.Facts) + .Select(fact => new GenerationFactRecord { Fact = fact }) + ] + }); + var derivedBindingRecord = derivation.Facts.Single(); + var derivedBinding = (TypeUseBindingFact)derivedBindingRecord.Fact; + var expectedLineageFacts = new GenerationFact[] + { + eventFact, + externalContribution.Facts.OfType().Single(), + externalContribution.Facts.OfType().Single(), + concept + }.OrderBy(fact => fact.Id.Value, StringComparer.Ordinal).ToArray(); + Require( + derivation.Rules.Single().Rule == new GenerationDerivationRuleIdentity + { + Id = "cratis.screenplay.type-use-binding", + Version = "1.0.0" + } && + derivedBinding.Definition.Member.Artifact == eventFact.Definition.Key && + derivedBinding.Definition.Member.Name == "customerCode" && + derivedBinding.Definition.Target == concept.Definition.Key && + derivedBindingRecord.Lineage is { Inputs.Length: 4, Evidence.Length: 4 } lineage && + lineage.Inputs.Select(input => input.Value) + .SequenceEqual(expectedLineageFacts.Select(fact => fact.Id.Value), StringComparer.Ordinal) && + lineage.Evidence.SequenceEqual(expectedLineageFacts.Select(fact => fact.Evidence)), + "CSC0062", + "Fixed-snapshot derivation did not bind the independent external type use to the exact Vogen concept with complete lineage."); var generated = new ScreenplayDefinitionGenerator().Generate( allContributions, @@ -1184,6 +1272,144 @@ internal static class Program "The runtime dependency closure did not load all four candidate package assemblies independently."); } + static void ExerciseSourceIndependentGranularFrontend() + { + var adapter = new AdapterIdentity { Id = "typescript-smoke", Version = "1.0.0" }; + var eventSubject = new SubjectId { Value = "typescript://catalog/events/CustomerRegistered" }; + var conceptSubject = new SubjectId { Value = "typescript://catalog/concepts/CustomerCode" }; + var artifact = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; + var member = new ArtifactMemberKey { Artifact = artifact, Name = "customerCode" }; + var evidence = new Evidence { Adapter = adapter, Strength = EvidenceStrength.Exact }; + var descriptor = new AdapterDescriptor + { + Identity = adapter, + SourceLanguage = AdapterSourceLanguage.SourceIndependent, + Category = AdapterCategory.Integration, + EmittedFactCapabilities = + [ + GenerationFactCapability.Artifact, + GenerationFactCapability.ArtifactPlacement, + GenerationFactCapability.ConceptRepresentation, + GenerationFactCapability.ArtifactDeclaration, + GenerationFactCapability.ArtifactMemberDeclaration, + GenerationFactCapability.ArtifactMemberTypeUse + ] + }; + var contribution = new AdapterContribution + { + Adapter = adapter, + Facts = + [ + new ArtifactDeclarationFact + { + Id = new FactId { Value = "typescript-smoke:event" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactDeclarationDefinition + { + Artifact = artifact, + Name = "CustomerRegistered" + } + }, + new ArtifactMemberDeclarationFact + { + Id = new FactId { Value = "typescript-smoke:member" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactMemberDeclarationDefinition + { + Member = member, + DeclarationOrder = 0 + } + }, + new ArtifactMemberTypeUseFact + { + Id = new FactId { Value = "typescript-smoke:type-use" }, + Subject = eventSubject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = new TypeUseDefinition + { + Name = "CustomerCode", + ObservedTypeSubject = conceptSubject + } + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "typescript-smoke:placement" }, + Subject = eventSubject, + Evidence = evidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Features = ["Registration"], + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + }, + new ArtifactFact + { + Id = new FactId { Value = "typescript-smoke:concept" }, + Subject = conceptSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = new ArtifactKey { Subject = conceptSubject, Kind = ArtifactKind.Concept }, + Name = "CustomerCode" + } + }, + new ConceptRepresentationFact + { + Id = new FactId { Value = "typescript-smoke:representation" }, + Subject = conceptSubject, + Evidence = evidence, + Definition = new ConceptRepresentationDefinition + { + Concept = conceptSubject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + ] + }; + var admission = AdapterContributionAdmission.Admit(descriptor, contribution); + Require(admission.IsAdmitted, "CSC0063", "The source-independent granular frontend was not admitted atomically."); + var admitted = admission.Snapshot!; + var snapshot = new AdapterRunSnapshot + { + Adapters = + [ + new AdapterRunRecord + { + Considered = true, + Probed = true, + Executed = true, + Descriptor = admitted.Descriptor, + Probe = new AdapterProbeApplicable(), + Execution = new AdapterExecutionCompleted { Contribution = admitted }, + Disposition = AdapterRunDisposition.Admitted + } + ], + Facts = [.. admitted.Facts.Select(fact => new GenerationFactRecord { Fact = fact })] + }; + var generated = new ScreenplayDefinitionGenerator().Generate( + snapshot, + new ScreenplayGenerationOptions { Domain = "Catalog" }); + var binding = (TypeUseBindingFact)generated.AdapterRun!.Derivation!.Facts.Single().Fact; + Require( + generated.IsSuccess && + generated.Source.Contains("customerCode CustomerCode", StringComparison.Ordinal) && + binding.Definition.Target.Subject == conceptSubject && + generated.AdapterRun.Facts.Concat(generated.AdapterRun.Derivation.Facts) + .All(record => record.Disposition != GenerationFactDisposition.Unknown), + "CSC0064", + "The source-independent granular frontend did not derive, lower, and classify its exact subject binding."); + } + static void ExerciseAdapterRunnerContracts( DotNetAnalysisContext context, DotNetProjectCompilation project, @@ -1559,13 +1785,68 @@ internal static class Program } } - sealed class ExternalCustomerAdapter : IDotNetScreenplayAdapter + sealed class ExternalCustomerAdapter : IDescribedDotNetScreenplayAdapter, IDotNetScreenplayAdapter { - public AdapterIdentity Identity { get; } = new() { Id = ExternalAdapterId, Version = "0.7.0" }; + static readonly AdapterApiCapability _customerEventApi = new() + { + Id = "external-smoke.customer-registered" + }; + + public AdapterDescriptor Descriptor { get; } = new() + { + Identity = new AdapterIdentity { Id = ExternalAdapterId, Version = "0.7.0" }, + SourceLanguage = AdapterSourceLanguage.CSharp, + Category = AdapterCategory.ApplicationFramework, + RequiredHostCapabilities = + [ + AdapterHostCapability.AuthoredSource, + AdapterHostCapability.StableSourceLocations, + AdapterHostCapability.SemanticAnalysis + ], + RequiredApiCapabilities = [_customerEventApi], + EmittedFactCapabilities = + [ + GenerationFactCapability.Artifact, + GenerationFactCapability.ArtifactPlacement, + GenerationFactCapability.ArtifactMemberDeclaration, + GenerationFactCapability.ArtifactMemberTypeUse + ] + }; + + public AdapterIdentity Identity => Descriptor.Identity; - public bool CanAnalyze(DotNetAnalysisContext context) => - context.Projects.Any(project => - project.Compilation.GetTypeByMetadataName("Ordering.CustomerRegistered") is not null); + public bool CanAnalyze(DotNetAnalysisContext context) => Probe(context) is AdapterProbeApplicable; + + public AdapterProbeResult Probe(DotNetAnalysisContext context) + { + var declaration = context.Projects + .Select(project => new + { + Project = project, + Type = project.Compilation.GetTypeByMetadataName("Ordering.CustomerRegistered") + }) + .SingleOrDefault(candidate => candidate.Type is not null); + if (declaration is null) + { + return new AdapterProbeNotApplicable(); + } + + return new AdapterProbeApplicable + { + Evidence = + [ + new AdapterProbeEvidence + { + Description = "The authored customer event declaration is available", + ApiCapability = _customerEventApi, + Subject = declaration.Project.SubjectForType(declaration.Type!), + Source = DotNetSource.RangeForProject( + declaration.Type!.DeclaringSyntaxReferences.Single().GetSyntax().GetLocation(), + declaration.Project) + } + ] + }; + } public AdapterContribution Analyze(DotNetAnalysisContext context, DotNetAdapterOptions options) { @@ -1575,10 +1856,14 @@ internal static class Program var eventType = project.Compilation.GetTypeByMetadataName("Ordering.CustomerRegistered"); Require(conceptType is not null, "CSC0017", "The external adapter could not resolve CustomerCode."); Require(eventType is not null, "CSC0018", "The external adapter could not resolve CustomerRegistered."); - var conceptSubject = project.SubjectForType(conceptType!); var eventSubject = project.SubjectForType(eventType!); var eventKey = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; - var evidence = new Evidence { Adapter = Identity, Strength = EvidenceStrength.Exact }; + var evidence = DotNetSource.EvidenceFor( + eventType!, + Identity, + project, + EvidenceStrength.Exact, + "The authored customer event declares its exact member type uses"); return new AdapterContribution { @@ -1595,20 +1880,10 @@ internal static class Program Key = eventKey, Name = "CustomerRegistered", File = "Customers/Register/CustomerRegistered.cs", - Properties = - [ - new PropertyDefinition - { - Name = "customerCode", - Type = new TypeReferenceDefinition - { - Name = "UnresolvedCustomerCode", - Subject = conceptSubject - } - } - ] + Properties = DotNetTypeShapes.PropertiesOf(eventType!) } }, + .. DotNetTypeUseFacts.Emit(eventType!, eventKey, context, evidence), new ArtifactPlacementFact { Id = new FactId { Value = "external-smoke:placement:customer-registered" }, From c0643b6df6fecc54c1329b213a03e864e5d8b518 Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 09:48:12 +0200 Subject: [PATCH 6/7] Fail closed on lossy granular overlays Block complete legacy artifacts when exact member evidence conflicts or cannot be represented, retain complete binding target roles through lowering, distinguish ordinary and event-source identifiers, and validate malformed shape structures without throwing. --- .../AdapterContributionAdmissionValidator.cs | 14 ++ .../AdapterContributionFreezer.cs | 1 + .../DotNET/Generation.Contracts/Artifacts.cs | 5 + .../given/a_contribution.cs | 7 +- .../with_missing_kind_operands.cs | 18 ++- .../when_mutating_admitted_input.cs | 7 +- ...ormed_type_use_shape_bypasses_admission.cs | 71 ++++++++++ ..._exact_type_use_shape_cannot_be_lowered.cs | 25 +++- ...en_binding_targets_exact_artifact_roles.cs | 130 ++++++++++++++++++ ...e_overlays_a_legacy_non_concept_binding.cs | 123 +++++++++++++++++ .../when_identifier_roles_conflict.cs | 4 +- ...owering_an_event_source_identifier_role.cs | 67 ++++++++- .../when_type_use_bindings_conflict.cs | 4 +- .../Generation/AdapterRunCanonicalizer.cs | 1 + Source/DotNET/Generation/Canonical.cs | 2 + .../Generation/GenerationDiagnosticCodes.cs | 5 + .../GenerationFactDiscriminatorValidator.cs | 54 +++++++- .../GenerationFactDispositionCalculator.cs | 12 +- .../DotNET/Generation/GenerationResolver.cs | 5 +- .../Generation/GranularArtifactResolver.cs | 60 ++++++-- Source/DotNET/Generation/ScreenplayLowerer.cs | 4 +- Source/DotNET/Generation/Structural.cs | 1 + .../Generation/TypeUseBindingDerivation.cs | 28 +++- 23 files changed, 614 insertions(+), 34 deletions(-) create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_malformed_type_use_shape_bypasses_admission.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_binding_targets_exact_artifact_roles.cs create mode 100644 Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_granular_type_use_overlays_a_legacy_non_concept_binding.cs diff --git a/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs b/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs index cc09f17..1ad33a7 100644 --- a/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs +++ b/Source/DotNET/Generation.Contracts/AdapterContributionAdmissionValidator.cs @@ -89,6 +89,20 @@ public static void ValidateType( { ValidateSubject(type.Subject, $"{path}.Subject", fact, context); } + + if (type.TargetArtifactKind is { } targetArtifactKind) + { + context.Enum(targetArtifactKind, ArtifactKind.Unknown, $"{path}.TargetArtifactKind", fact, subject); + if (type.Subject is null) + { + context.Add( + AdapterContributionAdmissionDiagnosticCode.InvalidKindOperand, + $"{path}.TargetArtifactKind", + "A target artifact kind requires an exact target subject", + fact, + subject); + } + } } internal static void ValidateDescriptor( diff --git a/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs b/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs index 20c5a11..b0d090d 100644 --- a/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs +++ b/Source/DotNET/Generation.Contracts/AdapterContributionFreezer.cs @@ -379,6 +379,7 @@ static TypeReferenceDefinition FreezeType( { Name = type.Name ?? string.Empty, Subject = type.Subject is null ? null : FreezeSubject(type.Subject, $"{path}.Subject", context), + TargetArtifactKind = type.TargetArtifactKind, IsCollection = type.IsCollection, IsOptional = type.IsOptional }; diff --git a/Source/DotNET/Generation.Contracts/Artifacts.cs b/Source/DotNET/Generation.Contracts/Artifacts.cs index 8f25848..08a4f18 100644 --- a/Source/DotNET/Generation.Contracts/Artifacts.cs +++ b/Source/DotNET/Generation.Contracts/Artifacts.cs @@ -187,6 +187,11 @@ public sealed record TypeReferenceDefinition /// public SubjectId? Subject { get; init; } + /// + /// Gets the exact artifact role of when a granular binding established one. + /// + public ArtifactKind? TargetArtifactKind { get; init; } + /// /// Gets whether the value is a collection. /// diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs index a021930..ba124af 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/given/a_contribution.cs @@ -79,7 +79,12 @@ protected static List EveryFact( new PropertyDefinition { Name = "second", - Type = new TypeReferenceDefinition { Name = "External", Subject = ExternalSubject } + Type = new TypeReferenceDefinition + { + Name = "External", + Subject = ExternalSubject, + TargetArtifactKind = ArtifactKind.Concept + } }, new PropertyDefinition { diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs index 699f7c7..3d883eb 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_admitting_malformed_contributions/with_missing_kind_operands.cs @@ -10,6 +10,22 @@ public class with_missing_kind_operands : given.a_contribution void Because() { var facts = EveryFact(); + var artifact = (ArtifactFact)facts[0]; + var firstProperty = artifact.Definition.Properties[0]; + facts[0] = artifact with + { + Definition = artifact.Definition with + { + Properties = + [ + firstProperty with + { + Type = firstProperty.Type with { Subject = null } + }, + .. artifact.Definition.Properties.Skip(1) + ] + } + }; var representation = (ConceptRepresentationFact)facts[8]; facts[8] = representation with { @@ -33,5 +49,5 @@ void Because() } [Fact] void should_reject_the_whole_contribution() => _result.Snapshot.ShouldBeNull(); - [Fact] void should_report_each_missing_or_invalid_kind_operand() => _result.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.InvalidKindOperand).ShouldEqual(3); + [Fact] void should_report_each_missing_or_invalid_kind_operand() => _result.Diagnostics.Count(diagnostic => diagnostic.Code == AdapterContributionAdmissionDiagnosticCode.InvalidKindOperand).ShouldEqual(4); } diff --git a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs index b7fa497..4fb0941 100644 --- a/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs +++ b/Source/DotNET/Generation.Specs/for_AdapterContributionAdmission/when_mutating_admitted_input.cs @@ -10,7 +10,12 @@ public class when_mutating_admitted_input : given.a_contribution new PropertyDefinition { Name = "second", - Type = new TypeReferenceDefinition { Name = "External", Subject = ExternalSubject } + Type = new TypeReferenceDefinition + { + Name = "External", + Subject = ExternalSubject, + TargetArtifactKind = ArtifactKind.Concept + } }, new PropertyDefinition { diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_malformed_type_use_shape_bypasses_admission.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_malformed_type_use_shape_bypasses_admission.cs new file mode 100644 index 0000000..5a0c57d --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_a_malformed_type_use_shape_bypasses_admission.cs @@ -0,0 +1,71 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_a_malformed_type_use_shape_bypasses_admission : given.a_generator +{ + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var subject = new SubjectId { Value = "dotnet://Ordering/Events.CustomerRegistered" }; + var artifact = new ArtifactKey { Subject = subject, Kind = ArtifactKind.Event }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var facts = new GenerationFact[] + { + new ArtifactFact + { + Id = new FactId { Value = "event:legacy" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = artifact, + Name = "CustomerRegistered", + Properties = + [ + new PropertyDefinition + { + Name = "customerCode", + Type = new TypeReferenceDefinition { Name = "String" } + } + ] + } + }, + new ArtifactMemberTypeUseFact + { + Id = new FactId { Value = "event:type-use" }, + Subject = subject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = new ArtifactMemberKey { Artifact = artifact, Name = "customerCode" }, + Type = new TypeUseDefinition { Name = "String", Shape = [] } + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "event:placement" }, + Subject = subject, + Evidence = evidence, + Artifact = artifact, + Placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + } + } + }; + + _result = Generator.Generate( + [new AdapterContribution { Adapter = Adapter, Facts = facts }], + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_return_a_deterministic_error_instead_of_throwing() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.UnsupportedTypeUseShape); + [Fact] void should_type_the_missing_shape_as_unknown() => _result.Diagnostics.Single(diagnostic => diagnostic.Code == GenerationDiagnosticCodes.UnsupportedTypeUseShape).Outcome.ShouldEqual(GenerationDiagnosticOutcome.Unknown); + [Fact] void should_omit_the_malformed_type_use() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Kind == ArtifactKind.Event).ShouldBeFalse(); + [Fact] void should_not_emit_partial_source() => _result.Source.ShouldNotContain("event CustomerRegistered"); +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs index b2d5c9d..8529ad8 100644 --- a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_an_exact_type_use_shape_cannot_be_lowered.cs @@ -14,15 +14,28 @@ void Because() var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; var facts = new GenerationFact[] { - new ArtifactDeclarationFact + new ArtifactFact { - Id = new FactId { Value = "critter-stack:declaration" }, + Id = new FactId { Value = "critter-stack:legacy-artifact" }, Subject = subject, Evidence = evidence, - Definition = new ArtifactDeclarationDefinition + Definition = new ArtifactDefinition { - Artifact = artifact, - Name = "CustomerRegistered" + Key = artifact, + Name = "CustomerRegistered", + Properties = + [ + new PropertyDefinition + { + Name = "codes", + Type = new TypeReferenceDefinition + { + Name = "String", + IsCollection = true, + IsOptional = true + } + } + ] } }, new ArtifactMemberDeclarationFact @@ -79,9 +92,11 @@ void Because() [Fact] void should_fail_closed() => _result.IsSuccess.ShouldBeFalse(); [Fact] void should_report_the_exact_shape_as_unsupported() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.UnsupportedTypeUseShape); [Fact] void should_omit_the_incomplete_granular_only_artifact_atomically() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Kind == ArtifactKind.Event).ShouldBeFalse(); + [Fact] void should_not_emit_the_flattened_legacy_artifact() => _result.Source.ShouldNotContain("event CustomerRegistered"); [Fact] void should_not_emit_the_unsupported_member() => _result.Source.ShouldNotContain("codes String"); [Fact] void should_omit_the_type_use_with_its_diagnostic() => TypeUseRecord().Disposition.ShouldEqual(GenerationFactDisposition.OmittedWithDiagnostic); [Fact] void should_associate_the_shape_diagnostic_with_the_type_use() => TypeUseRecord().Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.UnsupportedTypeUseShape); + [Fact] void should_omit_the_complete_legacy_fact_as_diagnosed_provenance() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "critter-stack:legacy-artifact").Disposition.ShouldEqual(GenerationFactDisposition.OmittedWithDiagnostic); GenerationFactRecord TypeUseRecord() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "critter-stack:type-use"); } diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_binding_targets_exact_artifact_roles.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_binding_targets_exact_artifact_roles.cs new file mode 100644 index 0000000..2fbea72 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_binding_targets_exact_artifact_roles.cs @@ -0,0 +1,130 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_binding_targets_exact_artifact_roles : given.a_generator +{ + readonly SubjectId _targetSubject = new() { Value = "dotnet://Ordering/Types.CustomerCode" }; + GeneratedScreenplayDefinition _exact = null!; + GeneratedScreenplayDefinition _missingRole = null!; + + void Because() + { + _missingRole = Generate(includeTargetEvent: false); + _exact = Generate(includeTargetEvent: true); + } + + [Fact] void should_reject_an_undeclared_exact_target_role() => _missingRole.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.MissingTypeUseBindingTarget); + [Fact] void should_omit_the_owner_when_the_exact_target_role_is_missing() => _missingRole.Graph.Artifacts.Any(artifact => artifact.Key.Subject.Value == "dotnet://Ordering/Events.RegistrationAttempted").ShouldBeFalse(); + [Fact] void should_omit_the_invalid_binding_with_its_diagnostic() => Binding(_missingRole).Disposition.ShouldEqual(GenerationFactDisposition.OmittedWithDiagnostic); + [Fact] void should_retain_the_complete_exact_target_key() => Owner(_exact).Definition.Properties.Single().Type.TargetArtifactKind.ShouldEqual(ArtifactKind.Event); + [Fact] void should_not_resolve_an_event_binding_as_the_same_subject_concept() => _exact.Source.ShouldContain("customerCode CustomerCodeEvent"); + [Fact] void should_not_substitute_the_same_subject_concept_name() => _exact.Source.ShouldNotContain("customerCode CustomerCode\n"); + [Fact] void should_lower_the_exact_declared_event_binding() => Binding(_exact).Disposition.ShouldEqual(GenerationFactDisposition.Lowered); + + GeneratedScreenplayDefinition Generate(bool includeTargetEvent) + { + var ownerSubject = new SubjectId { Value = "dotnet://Ordering/Events.RegistrationAttempted" }; + var owner = new ArtifactKey { Subject = ownerSubject, Kind = ArtifactKind.Event }; + var targetEvent = new ArtifactKey { Subject = _targetSubject, Kind = ArtifactKind.Event }; + var concept = new ArtifactKey { Subject = _targetSubject, Kind = ArtifactKind.Concept }; + var member = new ArtifactMemberKey { Artifact = owner, Name = "customerCode" }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + }; + var facts = new List + { + new ArtifactFact + { + Id = new FactId { Value = "owner:event" }, + Subject = ownerSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = owner, + Name = "RegistrationAttempted", + Properties = + [ + new PropertyDefinition + { + Name = "customerCode", + Type = new TypeReferenceDefinition { Name = "CustomerCodeEvent" } + } + ] + } + }, + new TypeUseBindingFact + { + Id = new FactId { Value = "owner:binding" }, + Subject = ownerSubject, + Evidence = evidence, + Definition = new TypeUseBindingDefinition + { + Member = member, + Target = targetEvent + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "owner:placement" }, + Subject = ownerSubject, + Evidence = evidence, + Artifact = owner, + Placement = placement + }, + new ArtifactFact + { + Id = new FactId { Value = "target:concept" }, + Subject = _targetSubject, + Evidence = evidence, + Definition = new ArtifactDefinition { Key = concept, Name = "CustomerCode" } + }, + new ConceptRepresentationFact + { + Id = new FactId { Value = "target:representation" }, + Subject = _targetSubject, + Evidence = evidence, + Definition = new ConceptRepresentationDefinition + { + Concept = _targetSubject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + }; + if (includeTargetEvent) + { + facts.Add(new ArtifactFact + { + Id = new FactId { Value = "target:event" }, + Subject = _targetSubject, + Evidence = evidence, + Definition = new ArtifactDefinition { Key = targetEvent, Name = "CustomerCodeEvent" } + }); + facts.Add(new ArtifactPlacementFact + { + Id = new FactId { Value = "target:placement" }, + Subject = _targetSubject, + Evidence = evidence, + Artifact = targetEvent, + Placement = placement + }); + } + + return Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + ResolvedArtifactVariant Owner(GeneratedScreenplayDefinition result) => result.Graph.Artifacts + .Single(artifact => artifact.Key.Subject.Value == "dotnet://Ordering/Events.RegistrationAttempted") + .Variants.Single(); + + static GenerationFactRecord Binding(GeneratedScreenplayDefinition result) => result.AdapterRun!.Facts + .Single(record => record.Fact.Id.Value == "owner:binding"); +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_granular_type_use_overlays_a_legacy_non_concept_binding.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_granular_type_use_overlays_a_legacy_non_concept_binding.cs new file mode 100644 index 0000000..5698d92 --- /dev/null +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_granular_type_use_overlays_a_legacy_non_concept_binding.cs @@ -0,0 +1,123 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Screenplay.Generation.for_ScreenplayDefinitionGenerator; + +public class when_granular_type_use_overlays_a_legacy_non_concept_binding : given.a_generator +{ + readonly SubjectId _targetSubject = new() { Value = "dotnet://Ordering/Types.CustomerCode" }; + GeneratedScreenplayDefinition _result = null!; + + void Because() + { + var ownerSubject = new SubjectId { Value = "dotnet://Ordering/Events.RegistrationAttempted" }; + var owner = new ArtifactKey { Subject = ownerSubject, Kind = ArtifactKind.Event }; + var targetEvent = new ArtifactKey { Subject = _targetSubject, Kind = ArtifactKind.Event }; + var concept = new ArtifactKey { Subject = _targetSubject, Kind = ArtifactKind.Concept }; + var member = new ArtifactMemberKey { Artifact = owner, Name = "customerCode" }; + var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; + var placement = new ArtifactPlacement + { + Module = "Customers", + Slice = "Register", + SliceKind = GenerationSliceKind.StateChange + }; + var facts = new GenerationFact[] + { + new ArtifactFact + { + Id = new FactId { Value = "owner:event" }, + Subject = ownerSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = owner, + Name = "RegistrationAttempted", + Properties = + [ + new PropertyDefinition + { + Name = "customerCode", + Type = new TypeReferenceDefinition + { + Name = "CustomerCodeEvent", + Subject = _targetSubject, + TargetArtifactKind = ArtifactKind.Event + } + } + ] + } + }, + new ArtifactMemberTypeUseFact + { + Id = new FactId { Value = "owner:type-use" }, + Subject = ownerSubject, + Evidence = evidence, + Definition = new ArtifactMemberTypeUseDefinition + { + Member = member, + Type = new TypeUseDefinition + { + Name = "CustomerCodeEvent", + ObservedTypeSubject = _targetSubject + } + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "owner:placement" }, + Subject = ownerSubject, + Evidence = evidence, + Artifact = owner, + Placement = placement + }, + new ArtifactFact + { + Id = new FactId { Value = "target:event" }, + Subject = _targetSubject, + Evidence = evidence, + Definition = new ArtifactDefinition { Key = targetEvent, Name = "CustomerCodeEvent" } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "target:placement" }, + Subject = _targetSubject, + Evidence = evidence, + Artifact = targetEvent, + Placement = placement + }, + new ArtifactFact + { + Id = new FactId { Value = "target:concept" }, + Subject = _targetSubject, + Evidence = evidence, + Definition = new ArtifactDefinition { Key = concept, Name = "CustomerCode" } + }, + new ConceptRepresentationFact + { + Id = new FactId { Value = "target:representation" }, + Subject = _targetSubject, + Evidence = evidence, + Definition = new ConceptRepresentationDefinition + { + Concept = _targetSubject, + Kind = ConceptRepresentationKind.Primitive, + Primitive = GenerationPrimitiveKind.Text + } + } + }; + + _result = Generator.Generate( + Snapshot(Completed(Adapter, facts)), + new ScreenplayGenerationOptions { Domain = "Ordering" }); + } + + [Fact] void should_generate_successfully() => _result.IsSuccess.ShouldBeTrue(); + [Fact] void should_preserve_the_legacy_exact_target_role() => Owner().Definition.Properties.Single().Type.TargetArtifactKind.ShouldEqual(ArtifactKind.Event); + [Fact] void should_derive_the_existing_exact_event_target() => ((TypeUseBindingFact)_result.AdapterRun!.Derivation!.Facts.Single().Fact).Definition.Target.ShouldEqual(new ArtifactKey { Subject = _targetSubject, Kind = ArtifactKind.Event }); + [Fact] void should_not_substitute_the_same_subject_concept() => _result.Source.ShouldContain("customerCode CustomerCodeEvent"); + + ResolvedArtifactVariant Owner() => _result.Graph.Artifacts + .Single(artifact => artifact.Key.Subject.Value == "dotnet://Ordering/Events.RegistrationAttempted") + .Variants.Single(); +} diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs index 6227833..20bb17e 100644 --- a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_identifier_roles_conflict.cs @@ -111,13 +111,11 @@ void Because() [Fact] void should_fail_closed() => _result.IsSuccess.ShouldBeFalse(); [Fact] void should_report_the_role_conflict() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.ConflictingArtifactMember); - [Fact] void should_not_choose_either_role() => Command().Definition.Properties.Single().IsIdentifier.ShouldBeFalse(); + [Fact] void should_omit_the_command_instead_of_choosing_either_role() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Kind == ArtifactKind.Command).ShouldBeFalse(); [Fact] void should_conflict_both_role_facts() => Dispositions().ShouldContainOnly(GenerationFactDisposition.Conflicted, GenerationFactDisposition.Conflicted); [Fact] void should_not_emit_identifier_semantics() => _result.Source.ShouldNotContain("orderId Uuid identifier"); [Fact] void should_not_associate_a_quoted_member_name_with_an_unrelated_legacy_fact_id() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "orderId").Disposition.ShouldEqual(GenerationFactDisposition.Lowered); - ResolvedArtifactVariant Command() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Command).Variants.Single(); - GenerationFactDisposition[] Dispositions() => [ .. _result.AdapterRun!.Facts diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs index 0f7c06c..4ac3df9 100644 --- a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_lowering_an_event_source_identifier_role.cs @@ -10,8 +10,10 @@ public class when_lowering_an_event_source_identifier_role : given.a_generator void Because() { var commandSubject = new SubjectId { Value = "dotnet://Ordering/Commands.SubmitOrder" }; + var trackingSubject = new SubjectId { Value = "dotnet://Ordering/Commands.TrackOrder" }; var eventSubject = new SubjectId { Value = "dotnet://Ordering/Events.OrderSubmitted" }; var command = new ArtifactKey { Subject = commandSubject, Kind = ArtifactKind.Command }; + var trackingCommand = new ArtifactKey { Subject = trackingSubject, Kind = ArtifactKind.Command }; var @event = new ArtifactKey { Subject = eventSubject, Kind = ArtifactKind.Event }; var evidence = new Evidence { Adapter = Adapter, Strength = EvidenceStrength.Exact }; var placement = new ArtifactPlacement @@ -61,6 +63,44 @@ void Because() Placement = placement }, new ArtifactFact + { + Id = new FactId { Value = "command:track" }, + Subject = trackingSubject, + Evidence = evidence, + Definition = new ArtifactDefinition + { + Key = trackingCommand, + Name = "TrackOrder", + Properties = + [ + new PropertyDefinition + { + Name = "trackingId", + Type = new TypeReferenceDefinition { Name = "Uuid" } + } + ] + } + }, + new ArtifactMemberRoleFact + { + Id = new FactId { Value = "command:track:identifier" }, + Subject = trackingSubject, + Evidence = evidence, + Definition = new ArtifactMemberRoleDefinition + { + Member = new ArtifactMemberKey { Artifact = trackingCommand, Name = "trackingId" }, + Role = ArtifactMemberRoleKind.Identifier + } + }, + new ArtifactPlacementFact + { + Id = new FactId { Value = "command:track:placement" }, + Subject = trackingSubject, + Evidence = evidence, + Artifact = trackingCommand, + Placement = placement + }, + new ArtifactFact { Id = new FactId { Value = "event:submitted" }, Subject = eventSubject, @@ -76,6 +116,21 @@ void Because() Placement = placement }, new RelationshipFact + { + Id = new FactId { Value = "command:track:produces" }, + Subject = trackingSubject, + Evidence = evidence, + Definition = new RelationshipDefinition + { + Key = new RelationshipKey + { + Kind = RelationshipKind.Produces, + Source = trackingSubject, + Target = eventSubject + } + } + }, + new RelationshipFact { Id = new FactId { Value = "command:submit:produces" }, Subject = commandSubject, @@ -98,9 +153,15 @@ void Because() } [Fact] void should_generate_successfully() => _result.IsSuccess.ShouldBeTrue(); - [Fact] void should_mark_the_exact_command_member_as_identifying() => Command().Definition.Properties.Single().IsIdentifier.ShouldBeTrue(); + [Fact] void should_mark_the_exact_event_source_member_as_identifying() => Command("SubmitOrder").Definition.Properties.Single().IsIdentifier.ShouldBeTrue(); + [Fact] void should_not_collapse_an_ordinary_identifier_into_event_source_semantics() => Command("TrackOrder").Definition.Properties.Single().IsIdentifier.ShouldBeFalse(); [Fact] void should_lower_the_event_source_identifier_role() => _result.Source.ShouldContain("orderId Uuid identifier"); - [Fact] void should_classify_the_role_as_lowered() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "command:submit:event-source-identifier").Disposition.ShouldEqual(GenerationFactDisposition.Lowered); + [Fact] void should_classify_the_event_source_role_as_lowered() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "command:submit:event-source-identifier").Disposition.ShouldEqual(GenerationFactDisposition.Lowered); + [Fact] void should_retain_the_ordinary_identifier_role_as_provenance() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "command:track:identifier").Disposition.ShouldEqual(GenerationFactDisposition.ProvenanceOnly); + [Fact] void should_not_emit_ordinary_identifier_semantics() => _result.Source.ShouldNotContain("trackingId Uuid identifier"); - ResolvedArtifactVariant Command() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Command).Variants.Single(); + ResolvedArtifactVariant Command(string name) => _result.Graph.Artifacts + .Where(artifact => artifact.Key.Kind == ArtifactKind.Command) + .SelectMany(artifact => artifact.Variants) + .Single(variant => variant.Definition.Name == name); } diff --git a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs index 94ad5cf..18c551e 100644 --- a/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs +++ b/Source/DotNET/Generation.Specs/for_ScreenplayDefinitionGenerator/when_type_use_bindings_conflict.cs @@ -90,13 +90,11 @@ void Because() [Fact] void should_fail_closed() => _result.IsSuccess.ShouldBeFalse(); [Fact] void should_report_the_binding_conflict() => _result.Diagnostics.Select(diagnostic => diagnostic.Code).ShouldContain(GenerationDiagnosticCodes.ConflictingArtifactMember); - [Fact] void should_not_choose_either_target_subject() => Event().Definition.Properties.Single().Type.Subject.ShouldBeNull(); + [Fact] void should_omit_the_artifact_instead_of_choosing_either_target() => _result.Graph.Artifacts.Any(artifact => artifact.Key.Kind == ArtifactKind.Event).ShouldBeFalse(); [Fact] void should_conflict_the_direct_binding() => DirectBinding().Disposition.ShouldEqual(GenerationFactDisposition.Conflicted); [Fact] void should_conflict_the_derived_binding() => DerivedBinding().Disposition.ShouldEqual(GenerationFactDisposition.Conflicted); [Fact] void should_associate_the_same_conflict_with_both_bindings() => DirectBinding().Diagnostics.Select(diagnostic => diagnostic.Code).Concat(DerivedBinding().Diagnostics.Select(diagnostic => diagnostic.Code)).ShouldContainOnly(GenerationDiagnosticCodes.ConflictingArtifactMember, GenerationDiagnosticCodes.ConflictingArtifactMember); - ResolvedArtifactVariant Event() => _result.Graph.Artifacts.Single(artifact => artifact.Key.Kind == ArtifactKind.Event).Variants.Single(); - GenerationFactRecord DirectBinding() => _result.AdapterRun!.Facts.Single(record => record.Fact.Id.Value == "application:legacy-binding"); GenerationFactRecord DerivedBinding() => _result.AdapterRun!.Derivation!.Facts.Single(); diff --git a/Source/DotNET/Generation/AdapterRunCanonicalizer.cs b/Source/DotNET/Generation/AdapterRunCanonicalizer.cs index 1fcfaa5..39edf7b 100644 --- a/Source/DotNET/Generation/AdapterRunCanonicalizer.cs +++ b/Source/DotNET/Generation/AdapterRunCanonicalizer.cs @@ -356,6 +356,7 @@ .. diagnostics { Name = type.Name, Subject = type.Subject is null ? null : Subject(type.Subject), + TargetArtifactKind = type.TargetArtifactKind, IsCollection = type.IsCollection, IsOptional = type.IsOptional }; diff --git a/Source/DotNET/Generation/Canonical.cs b/Source/DotNET/Generation/Canonical.cs index fbd3b87..422404d 100644 --- a/Source/DotNET/Generation/Canonical.cs +++ b/Source/DotNET/Generation/Canonical.cs @@ -17,6 +17,7 @@ public static string Artifact(ArtifactDefinition definition) _.Name, _.Type.Name, _.Type.Subject?.Value, + Invariant(_.Type.TargetArtifactKind is null ? null : (int)_.Type.TargetArtifactKind.Value), _.Type.IsCollection.ToString(), _.Type.IsOptional.ToString(), _.IsIdentifier.ToString()))); @@ -153,6 +154,7 @@ public static string Diagnostic(GenerationDiagnostic diagnostic) : Structural( type.Name, type.Subject?.Value, + Invariant(type.TargetArtifactKind is null ? null : (int)type.TargetArtifactKind.Value), type.IsCollection.ToString(), type.IsOptional.ToString()); diff --git a/Source/DotNET/Generation/GenerationDiagnosticCodes.cs b/Source/DotNET/Generation/GenerationDiagnosticCodes.cs index 49b4966..79c2d3e 100644 --- a/Source/DotNET/Generation/GenerationDiagnosticCodes.cs +++ b/Source/DotNET/Generation/GenerationDiagnosticCodes.cs @@ -267,4 +267,9 @@ public static class GenerationDiagnosticCodes /// A granular fact's asserted subject does not equal its nested artifact owner. /// public const string InvalidGranularFactOwnership = "GEN0054"; + + /// + /// A type-use binding targets an artifact role that was not declared for the exact subject. + /// + public const string MissingTypeUseBindingTarget = "GEN0055"; } diff --git a/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs b/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs index b720681..924c400 100644 --- a/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs +++ b/Source/DotNET/Generation/GenerationFactDiscriminatorValidator.cs @@ -8,6 +8,7 @@ static class GenerationFactDiscriminatorValidator public static GenerationFactDiscriminatorValidationResult Validate(IEnumerable facts) { var validFacts = new List(); + var rejectedFacts = new List(); var diagnostics = new List(); foreach (var fact in facts) @@ -19,6 +20,10 @@ public static GenerationFactDiscriminatorValidationResult Validate(IEnumerable diagnostics) @@ -96,6 +109,17 @@ static void ValidateArtifactKind(GenerationFact fact, ArtifactKind kind, List diagnostics) + { + if (type.TargetArtifactKind is { } targetArtifactKind) + { + ValidateArtifactKind(fact, targetArtifactKind, diagnostics); + } + } + static void ValidateSliceKind(GenerationFact fact, GenerationSliceKind kind, List diagnostics) { if (kind == GenerationSliceKind.Unknown || !Enum.IsDefined(kind)) @@ -114,7 +138,8 @@ static void ValidateTypeUseShape( IEnumerable shape, List diagnostics) { - foreach (var kind in shape) + var nodes = shape.ToArray(); + foreach (var kind in nodes) { if (kind == TypeUseShapeKind.Unknown || !Enum.IsDefined(kind)) { @@ -126,8 +151,32 @@ static void ValidateTypeUseShape( kind == TypeUseShapeKind.Unknown)); } } + + var nodesAreDefined = nodes.All(kind => + kind != TypeUseShapeKind.Unknown && Enum.IsDefined(kind)); + var hasExactNamedTerminal = nodes.Length > 0 && + nodes[^1] == TypeUseShapeKind.Named && + nodes[..^1].All(kind => kind != TypeUseShapeKind.Named); + if (nodesAreDefined && !hasExactNamedTerminal) + { + diagnostics.Add(new GenerationDiagnostic + { + Code = GenerationDiagnosticCodes.UnsupportedTypeUseShape, + Severity = GenerationDiagnosticSeverity.Error, + Outcome = nodes.Length == 0 + ? GenerationDiagnosticOutcome.Unknown + : GenerationDiagnosticOutcome.Unsupported, + Message = $"Fact '{fact.Id.Value}' uses malformed exact type-use shape '{Shape(nodes)}'; the affected fact was omitted", + Source = fact.Evidence.Source, + Subject = fact.Subject + }); + } } + static string Shape(TypeUseShapeKind[] shape) => shape.Length == 0 + ? "" + : string.Join(" -> ", shape); + static void ValidateArtifactMemberRole( GenerationFact fact, ArtifactMemberRoleKind role, @@ -295,4 +344,5 @@ static GenerationDiagnostic Unsupported( sealed record GenerationFactDiscriminatorValidationResult( GenerationFact[] Facts, + GenerationFact[] RejectedFacts, GenerationDiagnostic[] Diagnostics); diff --git a/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs b/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs index f12d5a2..9034fd7 100644 --- a/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs +++ b/Source/DotNET/Generation/GenerationFactDispositionCalculator.cs @@ -258,7 +258,14 @@ static IEnumerable AssociatedDiagnostics( var lowered = appliedVariants.Any(variant => coverage.Lowered.Contains(GenerationFactSemanticKey.Artifact(variant.Definition))); - var disposition = fact is TypeUseBindingFact or ArtifactMemberRoleFact && lowered + var contributesSyntax = fact switch + { + TypeUseBindingFact => lowered, + ArtifactMemberRoleFact role => + lowered && role.Definition.Role == ArtifactMemberRoleKind.EventSourceIdentifier, + _ => false + }; + var disposition = contributesSyntax ? GenerationFactDisposition.Lowered : GenerationFactDisposition.ProvenanceOnly; return new GenerationFactRecord @@ -383,7 +390,8 @@ static bool IsGranularDiagnostic(string code) => string.Equals(code, GenerationDiagnosticCodes.UnsupportedTypeUseShape, StringComparison.Ordinal) || string.Equals(code, GenerationDiagnosticCodes.ConflictingArtifactMember, StringComparison.Ordinal) || string.Equals(code, GenerationDiagnosticCodes.IncompleteArtifactMember, StringComparison.Ordinal) || - string.Equals(code, GenerationDiagnosticCodes.InvalidGranularFactOwnership, StringComparison.Ordinal); + string.Equals(code, GenerationDiagnosticCodes.InvalidGranularFactOwnership, StringComparison.Ordinal) || + string.Equals(code, GenerationDiagnosticCodes.MissingTypeUseBindingTarget, StringComparison.Ordinal); static bool HasSupportedDiscriminators(GenerationFact fact) { diff --git a/Source/DotNET/Generation/GenerationResolver.cs b/Source/DotNET/Generation/GenerationResolver.cs index 45938d5..9b9a7f1 100644 --- a/Source/DotNET/Generation/GenerationResolver.cs +++ b/Source/DotNET/Generation/GenerationResolver.cs @@ -36,7 +36,10 @@ internal ResolvedApplicationGraph ResolveFacts( diagnostics.AddRange(ConflictingFactIdentityDiagnostics(facts)); - var effectiveArtifactFacts = GranularArtifactResolver.Resolve(facts, diagnostics); + var effectiveArtifactFacts = GranularArtifactResolver.Resolve( + facts, + discriminatorValidation.RejectedFacts, + diagnostics); var artifacts = ResolveArtifacts(effectiveArtifactFacts, diagnostics); var conceptRepresentationFacts = facts.OfType().ToArray(); diagnostics.AddRange(InvalidConceptFactDiagnostics(conceptRepresentationFacts)); diff --git a/Source/DotNET/Generation/GranularArtifactResolver.cs b/Source/DotNET/Generation/GranularArtifactResolver.cs index 4687c6f..8401017 100644 --- a/Source/DotNET/Generation/GranularArtifactResolver.cs +++ b/Source/DotNET/Generation/GranularArtifactResolver.cs @@ -7,22 +7,38 @@ static class GranularArtifactResolver { public static ArtifactFact[] Resolve( IReadOnlyList facts, + IReadOnlyList rejectedFacts, List diagnostics) { + var blockedArtifacts = rejectedFacts + .Select(fact => new { Fact = fact, Member = MemberFor(fact) }) + .Where(item => item.Member is not null && item.Fact.Subject == item.Member.Artifact.Subject) + .Select(item => item.Member!.Artifact) + .ToHashSet(); var legacy = facts.OfType().ToArray(); var granular = ValidGranularFacts(facts.Where(IsGranularArtifactFact), diagnostics); if (granular.Length == 0) { - return legacy; + return [.. legacy.Where(fact => !blockedArtifacts.Contains(fact.Definition.Key))]; } - var variants = DeclarationVariants(legacy, granular.OfType()); + var granularDeclarations = granular.OfType().ToArray(); + var declaredArtifacts = legacy.Select(fact => fact.Definition.Key) + .Concat(granularDeclarations.Select(fact => fact.Definition.Artifact)) + .Distinct() + .ToHashSet(); + var variants = DeclarationVariants(legacy, granularDeclarations); var effective = new List(); foreach (var variant in variants .OrderBy(candidate => Structural.ArtifactKey(candidate.Definition.Key), StringComparer.Ordinal) .ThenBy(candidate => Structural.Artifact(candidate.Definition), StringComparer.Ordinal)) { - var overlaid = ApplyMembers(variant, granular, diagnostics); + if (blockedArtifacts.Contains(variant.Definition.Key)) + { + continue; + } + + var overlaid = ApplyMembers(variant, granular, declaredArtifacts, diagnostics); if (overlaid is not null) { foreach (var support in CanonicalFacts(overlaid.Supports)) @@ -93,6 +109,7 @@ static List DeclarationVariants( static ArtifactVariant? ApplyMembers( ArtifactVariant variant, IReadOnlyList granular, + HashSet declaredArtifacts, List diagnostics) { var key = variant.Definition.Key; @@ -199,7 +216,11 @@ static List DeclarationVariants( continue; } - type = granularType with { Subject = type?.Subject }; + type = granularType with + { + Subject = type?.Subject, + TargetArtifactKind = type?.TargetArtifactKind + }; } if (type is null) @@ -251,8 +272,22 @@ static List DeclarationVariants( if (bindingVariants.Length == 1) { var binding = bindingVariants[0].OrderBy(fact => fact.Id.Value, StringComparer.Ordinal).First(); + if (!declaredArtifacts.Contains(binding.Definition.Target)) + { + AddDiagnostic( + diagnostics, + GenerationDiagnosticCodes.MissingTypeUseBindingTarget, + GenerationDiagnosticOutcome.Unknown, + key.Subject, + bindingVariants[0], + $"Artifact member '{memberName}' binds undeclared target role '{binding.Definition.Target.Kind}' for subject '{binding.Definition.Target.Subject.Value}'"); + failed = true; + continue; + } + var target = binding.Definition.Target.Subject; if ((type.Subject is not null && type.Subject != target) || + (type.TargetArtifactKind is not null && type.TargetArtifactKind != binding.Definition.Target.Kind) || (typeUse?.Definition.Type.ObservedTypeSubject is not null && typeUse.Definition.Type.ObservedTypeSubject != target)) { AddDiagnostic( @@ -266,7 +301,11 @@ static List DeclarationVariants( continue; } - type = type with { Subject = target }; + type = type with + { + Subject = target, + TargetArtifactKind = binding.Definition.Target.Kind + }; } var roleFacts = currentFacts.OfType().ToArray(); @@ -284,7 +323,8 @@ static List DeclarationVariants( continue; } - var isIdentifier = existing?.Property.IsIdentifier == true || roleVariants.Length == 1; + var isIdentifier = existing?.Property.IsIdentifier == true || + roleVariants.SingleOrDefault() == ArtifactMemberRoleKind.EventSourceIdentifier; properties[memberName] = new OrderedProperty( orders[0], new PropertyDefinition @@ -320,9 +360,7 @@ static List DeclarationVariants( if (failed) { - return variant.Supports.Exists(fact => fact is ArtifactFact) - ? variant - : null; + return null; } return new ArtifactVariant( @@ -465,7 +503,9 @@ static void AddDiagnostic( }); } - static string Shape(TypeUseDefinition type) => string.Join('(', type.Shape) + new string(')', type.Shape.Count - 1); + static string Shape(TypeUseDefinition type) => type.Shape.Count == 0 + ? "" + : string.Join(" -> ", type.Shape); sealed record OrderedProperty(int Order, PropertyDefinition Property); diff --git a/Source/DotNET/Generation/ScreenplayLowerer.cs b/Source/DotNET/Generation/ScreenplayLowerer.cs index 6fcfbc3..dbd83a4 100644 --- a/Source/DotNET/Generation/ScreenplayLowerer.cs +++ b/Source/DotNET/Generation/ScreenplayLowerer.cs @@ -929,7 +929,9 @@ .. _relationships _artifacts.FirstOrDefault(_ => _.Key.Subject == subject && _.Key.Kind == kind)?.Name; public string TypeName(TypeReferenceDefinition type) => - type.Subject is not null && conceptNames.TryGetValue(type.Subject.Value, out var conceptName) + type.Subject is not null && + type.TargetArtifactKind is null or ArtifactKind.Concept && + conceptNames.TryGetValue(type.Subject.Value, out var conceptName) ? conceptName : type.Name; diff --git a/Source/DotNET/Generation/Structural.cs b/Source/DotNET/Generation/Structural.cs index 2d0355e..805874f 100644 --- a/Source/DotNET/Generation/Structural.cs +++ b/Source/DotNET/Generation/Structural.cs @@ -229,6 +229,7 @@ static string TypeReference(TypeReferenceDefinition type) => Node( type.Name, type.Subject?.Value, + NullableInteger(type.TargetArtifactKind is null ? null : (int)type.TargetArtifactKind.Value), Boolean(type.IsCollection), Boolean(type.IsOptional)); diff --git a/Source/DotNET/Generation/TypeUseBindingDerivation.cs b/Source/DotNET/Generation/TypeUseBindingDerivation.cs index 1e6267b..42f11e2 100644 --- a/Source/DotNET/Generation/TypeUseBindingDerivation.cs +++ b/Source/DotNET/Generation/TypeUseBindingDerivation.cs @@ -126,8 +126,34 @@ static void DeriveBinding( return; } + var legacyTargets = ownerDeclarations + .SelectMany(declaration => declaration.Fact is ArtifactFact artifact + ? artifact.Definition.Properties + .Where(property => property.Name == member.Name && + property.Type.Subject == observedType && + property.Type.TargetArtifactKind is not null) + .Select(property => new ArtifactKey + { + Subject = observedType, + Kind = property.Type.TargetArtifactKind!.Value + }) + : []) + .Distinct() + .ToArray(); + if (legacyTargets.Length > 1) + { + diagnostics.Add(Diagnostic( + GenerationDiagnosticCodes.ConflictingTypeUseTarget, + GenerationDiagnosticOutcome.Conflict, + member, + ownerDeclarations.Select(declaration => declaration.Fact).Concat(typeUses), + $"Artifact member '{member.Name}' retains incompatible exact legacy target roles")); + return; + } + var targetDeclarations = declarations - .Where(declaration => declaration.Key.Subject == observedType) + .Where(declaration => declaration.Key.Subject == observedType && + (legacyTargets.Length == 0 || declaration.Key == legacyTargets[0])) .ToArray(); if (targetDeclarations.Length == 0) { From 0c991c3d236b5abcc5be8faa5fde217ffa609caf Mon Sep 17 00:00:00 2001 From: woksin Date: Fri, 28 Aug 2026 09:49:21 +0200 Subject: [PATCH 7/7] Document fail-closed target role semantics Clarify exact ArtifactKey retention, role-specific dispositions, and atomic omission of malformed or lossy member evidence, and exercise target-role preservation in the package consumer. --- Documentation/guides/build-source-adapter.md | 6 +++--- README.md | 4 ++-- scripts/verify-package-consumers.sh | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Documentation/guides/build-source-adapter.md b/Documentation/guides/build-source-adapter.md index 3f363cf..f76c602 100644 --- a/Documentation/guides/build-source-adapter.md +++ b/Documentation/guides/build-source-adapter.md @@ -357,7 +357,7 @@ Keep compatibility aggregate properties unbound with `DotNetTypeShapes.Propertie `DotNetTypeShapes.TypeUseFor(type, context)` orders shape nodes from the outermost wrapper to the terminal `Named` node. This distinguishes `Collection(Optional(Named))` from `Optional(Collection(Named))` and preserves nested collections. The current Screenplay grammar lowers only the shapes it can express exactly; unsupported distinctions remain diagnosed rather than flattened. -Pass a `roleFor` callback to `DotNetTypeUseFacts.Emit(...)` only when source-framework semantics establish `ArtifactMemberRoleKind.Identifier` or `EventSourceIdentifier`. Never infer either role from a property name or primitive type. A framework that already knows the exact target inside one adapter may continue setting `TypeReferenceDefinition.Subject` directly with `PropertiesOf(type, context)` or `TypeReferenceFor(type, context)`. +Pass a `roleFor` callback to `DotNetTypeUseFacts.Emit(...)` only when source-framework semantics establish `ArtifactMemberRoleKind.Identifier` or `EventSourceIdentifier`. Never infer either role from a property name or primitive type. `EventSourceIdentifier` lowers through the existing identifier syntax; the distinct ordinary `Identifier` role remains provenance until Screenplay has separate syntax for it. A framework that already knows the exact target inside one adapter may continue setting `TypeReferenceDefinition.Subject` directly with `PropertiesOf(type, context)` or `TypeReferenceFor(type, context)`. ## Nominate declared concepts @@ -545,11 +545,11 @@ The modern descriptor has category `Concepts`, source language `CSharp`, require `GenerationFactDerivation.Derive(...)` runs the closed built-in rule set once over `AdapterRunSnapshot.Facts`. Every rule sees the same deeply frozen base array. A rule never consumes another rule's output, inspects adapter registrations or instances, or reopens source-language state. -The type-use binding rule joins an `ArtifactMemberTypeUseFact` to an exact declared artifact subject. It does not join by display name and does not replace the owning artifact's complete property list. Its derived `TypeUseBindingFact` remains separate from the admitted base facts under `AdapterRunSnapshot.Derivation`. +The type-use binding rule joins an `ArtifactMemberTypeUseFact` to an exact declared `ArtifactKey`, including its role and subject. It does not join by display name and does not replace the owning artifact's complete property list. Its derived `TypeUseBindingFact` remains separate from the admitted base facts under `AdapterRunSnapshot.Derivation`; the resolved compatibility reference retains the role in `TypeReferenceDefinition.TargetArtifactKind` so a same-subject non-concept can never be substituted as a concept. Each derived `GenerationFactRecord` carries `GenerationFactLineage`: the stable derivation rule identity and version, canonical input `FactId` references, and complete input evidence. `GenerationDerivationRuleRecord` records the fixed inputs, outputs, and diagnostics for that rule execution. Directly invoking derivation leaves fact dispositions unknown because disposition is a later generation decision. `Generate(snapshot, options)` attaches the derivation result, propagates its diagnostics, resolves admitted and derived granular facets together, and applies an exact member binding as an overlay without publishing a replacement aggregate fact. -Exact subjects can come from any source frontend. A C# member type use can bind a declaration contributed by another adapter, while a source-independent or non-.NET adapter can contribute the same neutral contracts without Roslyn or Screenplay-layout dependencies. Missing, ambiguous, or conflicting inputs produce stable diagnostics without selecting a winner. +Exact subjects can come from any source frontend. A C# member type use can bind a declaration contributed by another adapter, while a source-independent or non-.NET adapter can contribute the same neutral contracts without Roslyn or Screenplay-layout dependencies. Missing, malformed, ambiguous, conflicting, or currently unrepresentable member evidence omits the affected artifact instead of lowering a flattened legacy fallback. The execution and derivation snapshots are not a history model. They have no issue #24 serializer or stable fingerprints; keep them in process and compare canonical generated bytes when determinism matters. diff --git a/README.md b/README.md index f8f5061..d67a5d7 100644 --- a/README.md +++ b/README.md @@ -149,9 +149,9 @@ Adapters can contribute `ArtifactKind.Concept` together with independently prove Adapters that establish different facets independently can emit `ArtifactDeclarationFact`, `ArtifactMemberDeclarationFact`, `ArtifactMemberTypeUseFact`, and `ArtifactMemberRoleFact` without repeating a complete `ArtifactFact`. `TypeUseDefinition.Shape` orders optional and collection wrappers from outermost to the terminal `Named` node, so optional elements, optional collections, and nested collections remain distinct. -Generation runs the built-in `cratis.screenplay.type-use-binding@1.0.0` rule once over one fixed admitted base snapshot. It joins only exact subjects, never display names, adapter IDs, registration order, Roslyn symbols, or another adapter instance. A derived `TypeUseBindingFact` retains canonical input `FactId` values and complete evidence in `GenerationFactLineage`. Conflicting, incomplete, foreign-owned, or unsupported inputs remain diagnosed without a winner or partial artifact. +Generation runs the built-in `cratis.screenplay.type-use-binding@1.0.0` rule once over one fixed admitted base snapshot. It joins only exact artifact keys and subjects, never display names, adapter IDs, registration order, Roslyn symbols, or another adapter instance. A derived `TypeUseBindingFact` retains canonical input `FactId` values and complete evidence in `GenerationFactLineage`; resolved compatibility references retain the exact target role in `TypeReferenceDefinition.TargetArtifactKind`. Conflicting, incomplete, foreign-owned, malformed, or unsupported inputs omit the affected artifact without a winner, flattened fallback, or partial output. -.NET adapters keep compatibility aggregate properties unbound with `DotNetTypeShapes.PropertiesOf(type)` and append `DotNetTypeUseFacts.Emit(...)`. `TypeUseFor(...)` preserves exact nested use-site shape and terminal source subject; an optional role callback emits only roles explicitly established by source-framework semantics. Non-.NET and source-independent frontends contribute the same contracts directly. +.NET adapters keep compatibility aggregate properties unbound with `DotNetTypeShapes.PropertiesOf(type)` and append `DotNetTypeUseFacts.Emit(...)`. `TypeUseFor(...)` preserves exact nested use-site shape and terminal source subject; an optional role callback emits only roles explicitly established by source-framework semantics. `EventSourceIdentifier` lowers through the existing identifier syntax, while the distinct ordinary `Identifier` role remains provenance until Screenplay owns separate syntax for it. Non-.NET and source-independent frontends contribute the same contracts directly. Concept validation stays independent from identity, representation, attributes, and optionality. A rule uses an adapter-authored `RuleIdentity` for deterministic resolution, while `Predicate` is the authored predicate name emitted by lowering. Adapters contribute framework-neutral data and provenance only; they never reference Screenplay syntax: diff --git a/scripts/verify-package-consumers.sh b/scripts/verify-package-consumers.sh index fc2f242..5e5f170 100755 --- a/scripts/verify-package-consumers.sh +++ b/scripts/verify-package-consumers.sh @@ -1404,6 +1404,9 @@ internal static class Program generated.IsSuccess && generated.Source.Contains("customerCode CustomerCode", StringComparison.Ordinal) && binding.Definition.Target.Subject == conceptSubject && + generated.Graph.Artifacts + .Single(resolved => resolved.Key == artifact) + .Variants.Single().Definition.Properties.Single().Type.TargetArtifactKind == ArtifactKind.Concept && generated.AdapterRun.Facts.Concat(generated.AdapterRun.Derivation.Facts) .All(record => record.Disposition != GenerationFactDisposition.Unknown), "CSC0064",