From 272314a09b3811626707f38136b43b31dc34830c Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Mon, 22 Jun 2026 14:36:05 +0200 Subject: [PATCH 01/12] feat: add CMT data, OPC content-types and ImportConfig validation schemas --- .../Schemas/CmtData.xsd | 115 ++++++++++++++++++ .../Schemas/ContentTypes.xsd | 29 +++++ .../Schemas/ImportConfig.xsd | 103 ++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/ContentTypes.xsd create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/ImportConfig.xsd diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd new file mode 100644 index 0000000..5f8bb26 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/ContentTypes.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/ContentTypes.xsd new file mode 100644 index 0000000..8ed44f0 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/ContentTypes.xsd @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/ImportConfig.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/ImportConfig.xsd new file mode 100644 index 0000000..a284628 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/ImportConfig.xsd @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d86d19ab1a505ba89f47b2f3c782664dc6da9f84 Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Mon, 22 Jun 2026 14:51:06 +0200 Subject: [PATCH 02/12] feat: tag validation messages with their pipeline stage --- .../ValidationResult.cs | 30 +++++++++++++- .../WorkspaceValidator.cs | 39 ++++++++++++------- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/TALXIS.Platform.Metadata.Validation/ValidationResult.cs b/src/TALXIS.Platform.Metadata.Validation/ValidationResult.cs index 9883260..e805245 100644 --- a/src/TALXIS.Platform.Metadata.Validation/ValidationResult.cs +++ b/src/TALXIS.Platform.Metadata.Validation/ValidationResult.cs @@ -14,7 +14,10 @@ public sealed record ValidationResult( string? FilePath, int? Line, int? Column -); +) +{ + public ValidationStage Stage { get; init; } = ValidationStage.Workspace; +} /// /// Severity level for a . @@ -31,3 +34,28 @@ public enum ValidationSeverity /// Warning } + +public enum ValidationStage +{ + Workspace, + Schema, + Json, + DuplicateGuid, + ModelLoad, + Flow, + Relationship +} + +public static class ValidationStageExtensions +{ + public static string Label(this ValidationStage stage) => stage switch + { + ValidationStage.Schema => "Schema", + ValidationStage.Json => "JSON", + ValidationStage.DuplicateGuid => "GUID", + ValidationStage.ModelLoad => "Model", + ValidationStage.Flow => "Flow", + ValidationStage.Relationship => "Relationship", + _ => "Workspace" + }; +} diff --git a/src/TALXIS.Platform.Metadata.Validation/WorkspaceValidator.cs b/src/TALXIS.Platform.Metadata.Validation/WorkspaceValidator.cs index 2780fdb..8fc070c 100644 --- a/src/TALXIS.Platform.Metadata.Validation/WorkspaceValidator.cs +++ b/src/TALXIS.Platform.Metadata.Validation/WorkspaceValidator.cs @@ -44,8 +44,8 @@ public WorkspaceValidationReport ValidateDirectory(string workspacePath) if (!Directory.Exists(workspacePath)) { results.Add(new ValidationResult(ValidationSeverity.Error, - $"Directory not found: {workspacePath}", null, null, null)); - return new WorkspaceValidationReport(results, null); + $"Directory not found: {workspacePath}", null, null, null) { Stage = ValidationStage.Workspace }); + return BuildReport(results, null); } // Layer 1: Schema validation @@ -54,17 +54,17 @@ public WorkspaceValidationReport ValidateDirectory(string workspacePath) { try { - results.AddRange(schemaValidator.ValidateFile(xmlFile)); + results.AddRange(WithStage(schemaValidator.ValidateFile(xmlFile), ValidationStage.Schema)); } catch (IOException ex) { results.Add(new ValidationResult(ValidationSeverity.Warning, - $"Cannot read file: {ex.Message}", xmlFile, null, null)); + $"Cannot read file: {ex.Message}", xmlFile, null, null) { Stage = ValidationStage.Schema }); } catch (UnauthorizedAccessException ex) { results.Add(new ValidationResult(ValidationSeverity.Warning, - $"Access denied: {ex.Message}", xmlFile, null, null)); + $"Access denied: {ex.Message}", xmlFile, null, null) { Stage = ValidationStage.Schema }); } } @@ -74,23 +74,23 @@ public WorkspaceValidationReport ValidateDirectory(string workspacePath) { try { - results.AddRange(jsonValidator.ValidateFile(jsonFile)); + results.AddRange(WithStage(jsonValidator.ValidateFile(jsonFile), ValidationStage.Json)); } catch (IOException ex) { results.Add(new ValidationResult(ValidationSeverity.Warning, - $"Cannot read file: {ex.Message}", jsonFile, null, null)); + $"Cannot read file: {ex.Message}", jsonFile, null, null) { Stage = ValidationStage.Json }); } catch (UnauthorizedAccessException ex) { results.Add(new ValidationResult(ValidationSeverity.Warning, - $"Access denied: {ex.Message}", jsonFile, null, null)); + $"Access denied: {ex.Message}", jsonFile, null, null) { Stage = ValidationStage.Json }); } } // Layer 1: GUID duplicate detection var guidValidator = new GuidValidator(); - results.AddRange(guidValidator.ValidateDirectory(workspacePath)); + results.AddRange(WithStage(guidValidator.ValidateDirectory(workspacePath), ValidationStage.DuplicateGuid)); // Layer 2: Model loading Workspace? workspace = null; @@ -107,7 +107,7 @@ public WorkspaceValidationReport ValidateDirectory(string workspacePath) $"Load error: {loadError.Message}", loadError.FilePath, loadError.Line, - loadError.Column)); + loadError.Column) { Stage = ValidationStage.ModelLoad }); } foreach (var diagnostic in workspace.FlowDefinitions.SelectMany(f => f.Diagnostics)) @@ -117,22 +117,33 @@ public WorkspaceValidationReport ValidateDirectory(string workspacePath) $"Flow {diagnostic.Code}: {diagnostic.Message}", diagnostic.FilePath, diagnostic.Line, - diagnostic.Column)); + diagnostic.Column) { Stage = ValidationStage.Flow }); } // Layer 3: Referential integrity across the loaded model. var relationshipValidator = new RelationshipValidator(); - results.AddRange(relationshipValidator.Validate(workspace)); + results.AddRange(WithStage(relationshipValidator.Validate(workspace), ValidationStage.Relationship)); } catch (Exception ex) { results.Add(new ValidationResult( ValidationSeverity.Error, $"Failed to load workspace into model: {ex.Message}", - workspacePath, null, null)); + workspacePath, null, null) { Stage = ValidationStage.ModelLoad }); } - return new WorkspaceValidationReport(results, workspace); + return BuildReport(results, workspace); + } + + private static IEnumerable WithStage(IEnumerable results, ValidationStage stage) => + results.Select(r => r with { Stage = stage }); + + private static WorkspaceValidationReport BuildReport(IEnumerable results, Workspace? workspace) + { + var labeled = results + .Select(r => r with { Message = $"[{r.Stage.Label()}] {r.Message}" }) + .ToList(); + return new WorkspaceValidationReport(labeled, workspace); } private static ValidationSeverity MapFlowSeverity(FlowDiagnosticSeverity severity) => From 09b6dd002cbbc903a58d9e4664fc641526470d2d Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Mon, 22 Jun 2026 15:04:31 +0200 Subject: [PATCH 03/12] fix: loosen ribbon schema for solution-merged RibbonDiff.xml --- .../Schemas/Ribbon.xsd | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Ribbon.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Ribbon.xsd index 1f49165..6bca2ea 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/Ribbon.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Ribbon.xsd @@ -14,21 +14,17 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + @@ -117,12 +113,13 @@ - - - - - + + + + + + @@ -141,9 +138,10 @@ - - - + + + + @@ -193,29 +191,30 @@ - - - - - + + + + + - - - - - + + + + + - + + @@ -225,7 +224,7 @@ - + @@ -298,7 +297,7 @@ - + From ca65b38b5ca5e10f99a826572a3f8cc1d73581a6 Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Mon, 22 Jun 2026 15:22:02 +0200 Subject: [PATCH 04/12] fix: accept solutionaction and Descriptions labels in form schema --- .../Schemas/Form.xsd | 11 +++++++---- .../Schemas/SharedTypes.xsd | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Form.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Form.xsd index 9d5c0d6..adb2bd2 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/Form.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Form.xsd @@ -244,7 +244,8 @@ - + + @@ -561,7 +562,7 @@ - + @@ -741,7 +742,8 @@ - + + @@ -749,7 +751,8 @@ - + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/SharedTypes.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/SharedTypes.xsd index 36fda5e..76f78ea 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/SharedTypes.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/SharedTypes.xsd @@ -62,6 +62,8 @@ + + From 437c1b8b59dd9ef66d21f66bdfb554663e24863e Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Tue, 23 Jun 2026 09:46:49 +0200 Subject: [PATCH 05/12] fix: allow duplicate workflow flags and msdyn_dataflows in customizations --- .../Schemas/Solution.xsd | 7 +++++++ .../Schemas/Workflow.xsd | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd index 808c440..4373781 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd @@ -218,6 +218,13 @@ + + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Workflow.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Workflow.xsd index 766dd88..a5cbfeb 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/Workflow.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Workflow.xsd @@ -7,7 +7,7 @@ - + @@ -79,7 +79,7 @@ - + From 332f62a258beae6f15e9622ed054b46fd967a491 Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Tue, 23 Jun 2026 09:50:14 +0200 Subject: [PATCH 06/12] feat: add catalog and catalogassignments validation schema --- .../Schemas/Catalog.xsd | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/Catalog.xsd diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Catalog.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Catalog.xsd new file mode 100644 index 0000000..b3f8ff5 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Catalog.xsd @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ea47acb0c58d77f0fa3461393075e3ae18335706 Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Tue, 23 Jun 2026 10:13:46 +0200 Subject: [PATCH 07/12] fix: only apply flow schema to Workflows json files --- .../JsonValidator.cs | 50 +++++++++++++++---- .../JsonValidatorTests.cs | 7 +-- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/TALXIS.Platform.Metadata.Validation/JsonValidator.cs b/src/TALXIS.Platform.Metadata.Validation/JsonValidator.cs index 962e4eb..034bab4 100644 --- a/src/TALXIS.Platform.Metadata.Validation/JsonValidator.cs +++ b/src/TALXIS.Platform.Metadata.Validation/JsonValidator.cs @@ -12,10 +12,14 @@ namespace TALXIS.Platform.Metadata.Validation; /// public sealed class JsonValidator { - private readonly List _schemas = new(); + private readonly Dictionary _routedSchemas = new(StringComparer.OrdinalIgnoreCase); + private readonly List _explicitSchemas = new(); + private readonly bool _useRouting; /// - /// Creates a validator pre-loaded with all embedded JSON schemas. + /// Creates a validator pre-loaded with all embedded JSON schemas. Each file is + /// validated only against the schema that applies to it (resolved by file name + /// and location); files that match no schema are left untouched. /// public JsonValidator() { @@ -23,8 +27,12 @@ public JsonValidator() { using var stream = SchemaResourceLoader.OpenSchema(name); using var reader = new StreamReader(stream); - _schemas.Add(JSchema.Parse(reader.ReadToEnd())); + var dotIndex = name.IndexOf('.'); + var stem = dotIndex > 0 ? name.Substring(0, dotIndex) : name; + _routedSchemas[stem] = JSchema.Parse(reader.ReadToEnd()); } + + _useRouting = true; } internal JsonValidator(IEnumerable schemas) @@ -32,18 +40,22 @@ internal JsonValidator(IEnumerable schemas) if (schemas is null) throw new ArgumentNullException(nameof(schemas)); - _schemas.AddRange(schemas); + _explicitSchemas.AddRange(schemas); + _useRouting = false; } /// - /// Validates a JSON file against all loaded schemas. - /// A file passes if it is valid against at least one schema. + /// Validates a JSON file against the schema that applies to it. /// public IReadOnlyList ValidateFile(string filePath) { if (!File.Exists(filePath)) return new[] { new ValidationResult(ValidationSeverity.Error, $"File not found: {filePath}", filePath, null, null) }; + var schemas = _useRouting ? ResolveSchemas(filePath) : _explicitSchemas; + if (schemas.Count == 0) + return System.Array.Empty(); + JToken token; try { @@ -62,12 +74,9 @@ public IReadOnlyList ValidateFile(string filePath) }; } - if (_schemas.Count == 0) - return System.Array.Empty(); - - // File passes if valid against ANY schema + // File passes if valid against ANY of the applicable schemas. var allErrors = new List(); - foreach (var schema in _schemas) + foreach (var schema in schemas) { var schemaErrors = ValidateAgainstSchema(token, schema, filePath); if (schemaErrors.Count == 0) @@ -79,6 +88,25 @@ public IReadOnlyList ValidateFile(string filePath) return allErrors; } + /// + /// Returns the embedded schemas that apply to based on + /// its name and location. Returns an empty list for files no schema targets, so + /// non-metadata JSON (package.json, tsconfig.json, settings.json, ...) is skipped. + /// + private IReadOnlyList ResolveSchemas(string filePath) + { + if (_routedSchemas.TryGetValue("Flow", out var flow) && IsFlowDefinition(filePath)) + return new[] { flow }; + + return System.Array.Empty(); + } + + private static bool IsFlowDefinition(string filePath) + { + var segments = filePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return segments.Any(s => s.Equals("Workflows", StringComparison.OrdinalIgnoreCase)); + } + private static JToken LoadToken(string filePath) { using var textReader = File.OpenText(filePath); diff --git a/tests/TALXIS.Platform.Metadata.Tests/JsonValidatorTests.cs b/tests/TALXIS.Platform.Metadata.Tests/JsonValidatorTests.cs index 5ea62da..451d2e6 100644 --- a/tests/TALXIS.Platform.Metadata.Tests/JsonValidatorTests.cs +++ b/tests/TALXIS.Platform.Metadata.Tests/JsonValidatorTests.cs @@ -9,7 +9,7 @@ public class JsonValidatorTests [Fact] public void ValidateFile_InvalidJson_ReturnsLineAndColumn() { - WithJsonFile("broken.json", "{\n \"properties\": {\n", file => + WithJsonFile("Workflows/broken.json", "{\n \"properties\": {\n", file => { var result = new ValidationJsonValidator().ValidateFile(file).Single(); @@ -23,7 +23,7 @@ public void ValidateFile_InvalidJson_ReturnsLineAndColumn() [Fact] public void ValidateFile_ValidFlowSchema_ReturnsNoErrors() { - WithJsonFile("flow.json", """ + WithJsonFile("Workflows/flow.json", """ { "properties": { "connectionReferences": {}, @@ -126,7 +126,7 @@ public void ValidateFile_InvalidAgainstAllSchemas_AggregatesErrors() [Fact] public void ValidateFile_InvalidFlowSchema_ReturnsLineAndColumn() { - WithJsonFile("flow.json", """ + WithJsonFile("Workflows/flow.json", """ { "properties": { "connectionReferences": {} @@ -152,6 +152,7 @@ private static void WithJsonFile(string fileName, string content, Action { Directory.CreateDirectory(dir); var file = Path.Combine(dir, fileName); + Directory.CreateDirectory(Path.GetDirectoryName(file)!); File.WriteAllText(file, content); assertion(file); } From 545b1cebac456d6a22d5f55d48a8922f8696b316 Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Tue, 23 Jun 2026 12:56:39 +0200 Subject: [PATCH 08/12] feat: add map.xml build mapping validation schema --- .../Schemas/Map.xsd | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/Map.xsd diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Map.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Map.xsd new file mode 100644 index 0000000..8087b5b --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Map.xsd @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + From 0d2a255bf90174a001b622d3ba22f17d021fb8da Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Tue, 23 Jun 2026 14:19:12 +0200 Subject: [PATCH 09/12] feat: add CanvasApp metadata validation schema --- .../Schemas/CanvasApp.xsd | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd new file mode 100644 index 0000000..14beabc --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ca709ffd57da211a80c9929b28d7b15446e0bd0b Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Tue, 23 Jun 2026 14:41:29 +0200 Subject: [PATCH 10/12] feat: add PCF control manifest validation schema --- .../Schemas/ControlManifest.xsd | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/ControlManifest.xsd diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/ControlManifest.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/ControlManifest.xsd new file mode 100644 index 0000000..3285274 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/ControlManifest.xsd @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5db8f3ee6a5ad1760337673b2c4a7b8658dbe26a Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Wed, 24 Jun 2026 12:03:58 +0200 Subject: [PATCH 11/12] fix: scope duplicate GUID detection to component identity --- .../GuidValidator.cs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs b/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs index c3ef67d..d1a245b 100644 --- a/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs +++ b/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; +using TALXIS.Platform.Metadata; namespace TALXIS.Platform.Metadata.Validation; @@ -57,8 +58,11 @@ private static readonly (string AttributeName, string FilePattern)[] AttributeId }; private static readonly Dictionary> IdentityLookup; + private static readonly Dictionary> AttributeIdentityLookup; + private static readonly HashSet ComponentRootDirectories = new(ComponentDefinitionRegistry.GetAll().Select(d => d.Directory), StringComparer.OrdinalIgnoreCase); + static GuidValidator() { IdentityLookup = BuildLookup(IdentityRules); @@ -83,6 +87,7 @@ private static Dictionary> BuildLookup((string Name, string private struct GuidLocation { public string FilePath; + public string RelativePath; public string ElementName; public int Line; public int Column; @@ -142,12 +147,13 @@ public IReadOnlyList ValidateDirectory(string workspacePath) for (int i = 0; i < locations.Count; i++) { var loc = locations[i]; - var locComponent = GetComponentKey(loc.FilePath); + var locComponent = GetComponentIdentity(loc.RelativePath); var otherFiles = locations .Where((l, idx) => idx != i && - !string.Equals(GetComponentKey(l.FilePath), locComponent, StringComparison.OrdinalIgnoreCase)) - .Select(l => Path.GetFileName(l.FilePath)) + !string.Equals(GetComponentIdentity(l.RelativePath), locComponent, StringComparison.OrdinalIgnoreCase)) + .Select(l => l.RelativePath) + .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (otherFiles.Length == 0) @@ -189,6 +195,7 @@ private static void ScanElements(XElement element, string filePath, string relat AddGuid(guidMap, normalized, new GuidLocation { FilePath = filePath, + RelativePath = relativePath, ElementName = $"@{attr.Name.LocalName}", Line = lineInfo.HasLineInfo() ? lineInfo.LineNumber : 0, Column = lineInfo.HasLineInfo() ? lineInfo.LinePosition : 0 @@ -215,6 +222,7 @@ private static void ScanElements(XElement element, string filePath, string relat AddGuid(guidMap, normalized, new GuidLocation { FilePath = filePath, + RelativePath = relativePath, ElementName = localName, Line = lineInfo.HasLineInfo() ? lineInfo.LineNumber : 0, Column = lineInfo.HasLineInfo() ? lineInfo.LinePosition : 0 @@ -277,18 +285,28 @@ private static bool MatchesLookup(Dictionary> lookup, strin return null; } - // Identity key = path with the managed-layer suffix stripped, so {name}.xml and - // {name}_managed.xml map to the same component (never flagged as duplicates). - private static string GetComponentKey(string filePath) + private static string GetComponentIdentity(string relativePath) { - var directory = Path.GetDirectoryName(filePath) ?? string.Empty; - var nameNoExt = Path.GetFileNameWithoutExtension(filePath); - var ext = Path.GetExtension(filePath); + var segments = relativePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + var anchor = Array.FindIndex(segments, s => ComponentRootDirectories.Contains(s)); + var scoped = anchor >= 0 ? segments.Skip(anchor).ToArray() : segments; + + if (scoped.Length > 0) + { + var file = scoped[scoped.Length - 1]; + var ext = Path.GetExtension(file); + var name = Path.GetFileNameWithoutExtension(file); - if (nameNoExt.EndsWith(ManagedLayerSuffix, StringComparison.OrdinalIgnoreCase)) - nameNoExt = nameNoExt.Substring(0, nameNoExt.Length - ManagedLayerSuffix.Length); + if (name.EndsWith(ManagedLayerSuffix, StringComparison.OrdinalIgnoreCase)) + { + name = name.Substring(0, name.Length - ManagedLayerSuffix.Length); + } + + scoped[scoped.Length - 1] = name + ext; + } - return Path.Combine(directory, nameNoExt + ext); + return string.Join(Path.DirectorySeparatorChar.ToString(), scoped); } private static void AddGuid(Dictionary> map, string guid, GuidLocation location) From 7cd3790546ec9e1eb0193afc5b9dfe4b98668ec0 Mon Sep 17 00:00:00 2001 From: Alexander Zekelin Date: Thu, 25 Jun 2026 13:33:03 +0200 Subject: [PATCH 12/12] fix: extend validation schemas and scope GUID duplicate detection --- .../GuidValidator.cs | 12 +++- .../Schemas/AppAction.xsd | 55 +++++++++++++++++++ .../Schemas/CanvasApp.xsd | 9 +++ .../Schemas/CmtData.xsd | 5 +- .../Schemas/CustomApi.xsd | 8 ++- .../Schemas/EnvironmentVariable.xsd | 7 +++ .../Schemas/Fetch.xsd | 3 + .../Schemas/SavedQuery.xsd | 19 +++++++ .../Schemas/Solution.xsd | 14 +++++ .../GuidValidatorTests.cs | 30 ++++++++++ 10 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 src/TALXIS.Platform.Metadata.Validation/Schemas/AppAction.xsd diff --git a/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs b/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs index d1a245b..3c7cbee 100644 --- a/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs +++ b/src/TALXIS.Platform.Metadata.Validation/GuidValidator.cs @@ -268,10 +268,18 @@ private static bool MatchesLookup(Dictionary> lookup, strin if (patterns.Count == 0) return true; + var segments = filePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + foreach (var pattern in patterns) { - if (filePath.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0) - return true; + var isFilePattern = pattern.IndexOf('.') >= 0; + foreach (var segment in segments) + { + if (isFilePattern + ? segment.EndsWith(pattern, StringComparison.OrdinalIgnoreCase) + : segment.Equals(pattern, StringComparison.OrdinalIgnoreCase)) + return true; + } } return false; diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/AppAction.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/AppAction.xsd new file mode 100644 index 0000000..dc51f59 --- /dev/null +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/AppAction.xsd @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd index 14beabc..4cdd783 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/CanvasApp.xsd @@ -32,6 +32,9 @@ + + + @@ -42,4 +45,10 @@ + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd index 5f8bb26..5e94cd8 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/CmtData.xsd @@ -15,7 +15,10 @@ - + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/CustomApi.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/CustomApi.xsd index d58c9d5..fa17a08 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/CustomApi.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/CustomApi.xsd @@ -30,7 +30,13 @@ - + + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/EnvironmentVariable.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/EnvironmentVariable.xsd index ad9e13b..a95dc58 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/EnvironmentVariable.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/EnvironmentVariable.xsd @@ -37,6 +37,13 @@ + + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Fetch.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Fetch.xsd index 1901865..0110ba5 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/Fetch.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Fetch.xsd @@ -117,6 +117,7 @@ + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/SavedQuery.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/SavedQuery.xsd index 9acd600..7d9adad 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/SavedQuery.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/SavedQuery.xsd @@ -127,6 +127,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd b/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd index 4373781..b98e58e 100644 --- a/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd +++ b/src/TALXIS.Platform.Metadata.Validation/Schemas/Solution.xsd @@ -225,6 +225,20 @@ + + + + + + + + + + + + + + diff --git a/tests/TALXIS.Platform.Metadata.Tests/GuidValidatorTests.cs b/tests/TALXIS.Platform.Metadata.Tests/GuidValidatorTests.cs index d940b8f..e7bda4e 100644 --- a/tests/TALXIS.Platform.Metadata.Tests/GuidValidatorTests.cs +++ b/tests/TALXIS.Platform.Metadata.Tests/GuidValidatorTests.cs @@ -146,6 +146,36 @@ public void RootComponentReference_InProjectFolderNamedRoles_NotFlaggedAsDuplica Assert.Empty(results); } + [Fact] + public void RootComponentReference_ModuleFolderNameContainingRoles_NotFlaggedAsDuplicate() + { + var roleGuid = "{ffca9a2a-db15-f011-9989-000d3ab911a5}"; + + var module = Path.Combine(_tempDir, "PVSecurityRoles", "Declarations"); + + var rolesDir = Path.Combine(module, "Roles"); + Directory.CreateDirectory(rolesDir); + File.WriteAllText(Path.Combine(rolesDir, "role.xml"), $""" + + """); + + var otherDir = Path.Combine(module, "Other"); + Directory.CreateDirectory(otherDir); + File.WriteAllText(Path.Combine(otherDir, "Solution.xml"), $""" + + + + + + + + """); + + var results = _validator.ValidateDirectory(_tempDir); + + Assert.Empty(results); + } + [Fact] public void DuplicateRoleDeclarations_StillDetected() {