Compare commits

...
5 Commits
37 changed files with 942 additions and 0 deletions
+9
View File
@@ -1,2 +1,11 @@
/link/
/link
# DocumentAutomation generated files and local source data
DocumentAutomation/**/bin/
DocumentAutomation/**/obj/
DocumentAutomation/.vs/
DocumentAutomation/samples/**/*.docx
DocumentAutomation/samples/**/*.pdf
DocumentAutomation/samples/**/*.json
DocumentAutomation/**/*.csproj.user
+9
View File
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<LangVersion>latest</LangVersion>
</PropertyGroup>
</Project>
+206
View File
@@ -0,0 +1,206 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentAutomation.Shared", "src\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj", "{3D48D7EE-4C18-47A8-9F8F-E296491C0463}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxWorkbench.App", "src\DocxWorkbench.App\DocxWorkbench.App.csproj", "{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxWorkbench.Core", "src\DocxWorkbench.Core\DocxWorkbench.Core.csproj", "{5D7A031E-06AE-4725-89D8-C4EAD75700B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxWorkbench.OpenXml", "src\DocxWorkbench.OpenXml\DocxWorkbench.OpenXml.csproj", "{6560936F-10A6-406E-8957-24E577A3F7EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxWorkbench.WordInterop", "src\DocxWorkbench.WordInterop\DocxWorkbench.WordInterop.csproj", "{3349BB3A-001A-480D-9867-CF7199BDA04B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardsWorkbench.App", "src\StandardsWorkbench.App\StandardsWorkbench.App.csproj", "{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardsWorkbench.Core", "src\StandardsWorkbench.Core\StandardsWorkbench.Core.csproj", "{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardsWorkbench.Pdf", "src\StandardsWorkbench.Pdf\StandardsWorkbench.Pdf.csproj", "{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardsWorkbench.Ollama", "src\StandardsWorkbench.Ollama\StandardsWorkbench.Ollama.csproj", "{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentAutomation.IntegrationTests", "tests\DocumentAutomation.IntegrationTests\DocumentAutomation.IntegrationTests.csproj", "{6D7ED423-F943-4DF0-8D18-F69C127F3D67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxWorkbench.Tests", "tests\DocxWorkbench.Tests\DocxWorkbench.Tests.csproj", "{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StandardsWorkbench.Tests", "tests\StandardsWorkbench.Tests\StandardsWorkbench.Tests.csproj", "{6E533AAB-42E6-4C60-AE06-A89F2B283187}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Debug|x64.Build.0 = Debug|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Debug|x86.Build.0 = Debug|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Release|Any CPU.Build.0 = Release|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Release|x64.ActiveCfg = Release|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Release|x64.Build.0 = Release|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Release|x86.ActiveCfg = Release|Any CPU
{3D48D7EE-4C18-47A8-9F8F-E296491C0463}.Release|x86.Build.0 = Release|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Debug|x64.ActiveCfg = Debug|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Debug|x64.Build.0 = Debug|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Debug|x86.ActiveCfg = Debug|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Debug|x86.Build.0 = Debug|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Release|Any CPU.Build.0 = Release|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Release|x64.ActiveCfg = Release|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Release|x64.Build.0 = Release|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Release|x86.ActiveCfg = Release|Any CPU
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5}.Release|x86.Build.0 = Release|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Debug|x64.ActiveCfg = Debug|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Debug|x64.Build.0 = Debug|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Debug|x86.ActiveCfg = Debug|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Debug|x86.Build.0 = Debug|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Release|Any CPU.Build.0 = Release|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Release|x64.ActiveCfg = Release|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Release|x64.Build.0 = Release|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Release|x86.ActiveCfg = Release|Any CPU
{5D7A031E-06AE-4725-89D8-C4EAD75700B1}.Release|x86.Build.0 = Release|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Debug|x64.ActiveCfg = Debug|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Debug|x64.Build.0 = Debug|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Debug|x86.ActiveCfg = Debug|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Debug|x86.Build.0 = Debug|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Release|Any CPU.Build.0 = Release|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Release|x64.ActiveCfg = Release|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Release|x64.Build.0 = Release|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Release|x86.ActiveCfg = Release|Any CPU
{6560936F-10A6-406E-8957-24E577A3F7EE}.Release|x86.Build.0 = Release|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Debug|x64.ActiveCfg = Debug|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Debug|x64.Build.0 = Debug|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Debug|x86.ActiveCfg = Debug|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Debug|x86.Build.0 = Debug|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Release|Any CPU.Build.0 = Release|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Release|x64.ActiveCfg = Release|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Release|x64.Build.0 = Release|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Release|x86.ActiveCfg = Release|Any CPU
{3349BB3A-001A-480D-9867-CF7199BDA04B}.Release|x86.Build.0 = Release|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Debug|x64.ActiveCfg = Debug|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Debug|x64.Build.0 = Debug|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Debug|x86.ActiveCfg = Debug|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Debug|x86.Build.0 = Debug|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Release|Any CPU.Build.0 = Release|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Release|x64.ActiveCfg = Release|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Release|x64.Build.0 = Release|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Release|x86.ActiveCfg = Release|Any CPU
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5}.Release|x86.Build.0 = Release|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Debug|x64.ActiveCfg = Debug|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Debug|x64.Build.0 = Debug|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Debug|x86.ActiveCfg = Debug|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Debug|x86.Build.0 = Debug|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Release|Any CPU.Build.0 = Release|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Release|x64.ActiveCfg = Release|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Release|x64.Build.0 = Release|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Release|x86.ActiveCfg = Release|Any CPU
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D}.Release|x86.Build.0 = Release|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Debug|x64.ActiveCfg = Debug|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Debug|x64.Build.0 = Debug|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Debug|x86.ActiveCfg = Debug|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Debug|x86.Build.0 = Debug|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Release|Any CPU.Build.0 = Release|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Release|x64.ActiveCfg = Release|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Release|x64.Build.0 = Release|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Release|x86.ActiveCfg = Release|Any CPU
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1}.Release|x86.Build.0 = Release|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Debug|x64.ActiveCfg = Debug|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Debug|x64.Build.0 = Debug|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Debug|x86.ActiveCfg = Debug|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Debug|x86.Build.0 = Debug|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Release|Any CPU.Build.0 = Release|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Release|x64.ActiveCfg = Release|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Release|x64.Build.0 = Release|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Release|x86.ActiveCfg = Release|Any CPU
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9}.Release|x86.Build.0 = Release|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Debug|x64.ActiveCfg = Debug|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Debug|x64.Build.0 = Debug|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Debug|x86.ActiveCfg = Debug|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Debug|x86.Build.0 = Debug|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Release|Any CPU.Build.0 = Release|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Release|x64.ActiveCfg = Release|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Release|x64.Build.0 = Release|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Release|x86.ActiveCfg = Release|Any CPU
{6D7ED423-F943-4DF0-8D18-F69C127F3D67}.Release|x86.Build.0 = Release|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Debug|x64.ActiveCfg = Debug|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Debug|x64.Build.0 = Debug|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Debug|x86.ActiveCfg = Debug|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Debug|x86.Build.0 = Debug|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Release|Any CPU.Build.0 = Release|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Release|x64.ActiveCfg = Release|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Release|x64.Build.0 = Release|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Release|x86.ActiveCfg = Release|Any CPU
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE}.Release|x86.Build.0 = Release|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Debug|x64.ActiveCfg = Debug|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Debug|x64.Build.0 = Debug|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Debug|x86.ActiveCfg = Debug|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Debug|x86.Build.0 = Debug|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Release|Any CPU.Build.0 = Release|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Release|x64.ActiveCfg = Release|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Release|x64.Build.0 = Release|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Release|x86.ActiveCfg = Release|Any CPU
{6E533AAB-42E6-4C60-AE06-A89F2B283187}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3D48D7EE-4C18-47A8-9F8F-E296491C0463} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{BB7057EA-13F7-4F9E-BC1F-6E4FE0C8C9D5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{5D7A031E-06AE-4725-89D8-C4EAD75700B1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{6560936F-10A6-406E-8957-24E577A3F7EE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{3349BB3A-001A-480D-9867-CF7199BDA04B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{4A79AA2B-7E40-456A-9BBF-D6F8A444EDC5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{71C7E7B1-1E3E-470B-A942-EFCD58CC692D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{463BCC3A-3836-4DAC-A2A1-B786A9B263F1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{7EA1410E-03BF-46B3-9C6D-20983C4FA2C9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{6D7ED423-F943-4DF0-8D18-F69C127F3D67} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{8539CA78-AB1A-499E-9C9C-C0FBBFC7AFAE} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{6E533AAB-42E6-4C60-AE06-A89F2B283187} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
+15
View File
@@ -0,0 +1,15 @@
# DocumentAutomation
Первый локальный прототип состоит из независимых `DOCX Workbench` и `Standards Workbench` на .NET 8 Windows Forms.
DOCX Workbench анализирует DOCX через Open XML, формирует документ через локальный Microsoft Word COM, сохраняет шаблонные секции/колонтитулы, обновляет поля и экспортирует PDF. Standards Workbench импортирует PDF, сохраняет checksum, извлекает страницы и кандидатные пункты, обращается к Ollama только за предложением candidate-rule и экспортирует переносимый профиль.
## Ограничения MVP
- OCR только обозначен интерфейсом; страницы без текстового слоя получают статус `OCR_REQUIRED`.
- Автоматическое утверждение нормативных правил запрещено.
- Word COM требует установленного Microsoft Word и запускается только интерактивно, не как service.
- Реальный `156-титул-ПД.docx` и нормативные PDF не добавлялись в Git.
- PDF preview использует локальный Word ExportAsFixedFormat.
Подробности: `docs/architecture.md`, `docs/docx-template-contract.md`, `docs/word-com-lifecycle.md`, `docs/standards-rule-model.md`.
+9
View File
@@ -0,0 +1,9 @@
# Architecture
`DocxWorkbench.App` and `StandardsWorkbench.App` are separate WinForms applications. They share only DTOs in `DocumentAutomation.Shared`.
DOCX flow: GUI -> `DocxWorkbench.Core` contracts -> `DocxWorkbench.OpenXml` read-only inspection or `DocxWorkbench.WordInterop` sequential Word COM queue -> DOCX/PDF and technical JSON log.
Standards flow: GUI -> `StandardsWorkbench.Pdf` page/clause extraction and `StandardsWorkbench.Ollama` candidate-only suggestions -> manually approved portable profile -> `StandardsWorkbench.Core.RulesEngine`.
COM is isolated from Core and UI. Standards rules are not hardcoded into document generation.
+9
View File
@@ -0,0 +1,9 @@
# Development
```powershell
dotnet restore .\DocumentAutomation\DocumentAutomation.sln
dotnet build .\DocumentAutomation\DocumentAutomation.sln --configuration Release
dotnet test .\DocumentAutomation\DocumentAutomation.sln --configuration Release
```
Run `DocxWorkbench.App` or `StandardsWorkbench.App` from Visual Studio/VS Code. Microsoft Word is required only for DOCX generation and PDF export. Ollama is configured at `http://192.168.200.112:11434`; the model must be selected from `/api/tags`.
@@ -0,0 +1,5 @@
# DOCX template contract
The preferred insertion point is a Content Control with tag or title `DOC_BODY`, then a bookmark named `DOC_BODY`, then the exact marker `{{DOC_BODY}}`. If none is present, generation fails explicitly; the body is never silently appended.
The template is copied to a temporary working file. Body headers/footers are not imported. Template sections and headers/footers are retained, fields are updated, and Word repaginates before saving.
@@ -0,0 +1,5 @@
# Standards rule model
PDF metadata includes SHA-256, page count, text-layer status and import time. Pages without text are marked `OCR_REQUIRED`; no text is invented. Clause extraction preserves clause number, text, page and confidence.
Ollama receives only a small clause context and may return a candidate JSON rule. Candidates remain `New` until human approval. Only `Approved`, `Implemented` or `Tested` rules are evaluated. Profiles contain manifest, approved rules, references and a signature hash.
@@ -0,0 +1,5 @@
# Word COM lifecycle
Word is started in a dedicated task with `Visible=false` and alerts disabled. The service opens a working copy, performs one operation, updates fields, repaginates, saves, closes the Document, quits the Application, and releases owned COM objects in `finally`.
The prototype does not run as a Windows Service, does not kill unrelated `WINWORD.EXE` processes, and does not execute macros. Production hardening should add an explicit serial queue and PID-scoped orphan check.
View File
+3
View File
@@ -0,0 +1,3 @@
# Samples
Реальные проектные DOCX/PDF и нормативные документы сюда не добавляются. Тесты создают синтетические документы во временном каталоге. Для локальной проверки укажите входные файлы через GUI.
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,71 @@
using System.Text.Json.Serialization;
namespace DocumentAutomation.Shared;
public sealed record DocumentInspectionResult
{
public required string FilePath { get; init; }
public long FileSize { get; init; }
public DateTime LastWriteTimeUtc { get; init; }
public int Sections { get; init; }
public int Paragraphs { get; init; }
public int Tables { get; init; }
public int Images { get; init; }
public int Headers { get; init; }
public int Footers { get; init; }
public bool HasPageFields { get; init; }
public bool HasNumPagesFields { get; init; }
public bool HasSectionPagesFields { get; init; }
public int Bookmarks { get; init; }
public int ContentControls { get; init; }
public int Comments { get; init; }
public bool HasTrackedChanges { get; init; }
public bool HasProtectedParts { get; init; }
public IReadOnlyList<string> Styles { get; init; } = [];
public IReadOnlyList<string> Warnings { get; init; } = [];
}
public sealed record WordDocumentBuildRequest
{
public required string TemplatePath { get; init; }
public required string BodyPath { get; init; }
public required string OutputDocxPath { get; init; }
public string? RulesProfilePath { get; init; }
public bool PreserveBodyFormatting { get; init; } = true;
public bool PreservePageBreaks { get; init; } = true;
}
public sealed record WordDocumentResult
{
public required string OutputDocxPath { get; init; }
public string? OutputPdfPath { get; init; }
public required string LogPath { get; init; }
public required TimeSpan Duration { get; init; }
public required string InsertionPoint { get; init; }
}
public sealed record WordPreviewResult
{
public required string PdfPath { get; init; }
public required TimeSpan Duration { get; init; }
}
public enum RuleEvaluationStatus { Pass, Fail, NotApplicable, NeedsReview, Error }
public sealed record RuleEvaluationResult
{
public required string RuleId { get; init; }
public required RuleEvaluationStatus Status { get; init; }
public required string Message { get; init; }
public string? ClauseId { get; init; }
public int? SourcePage { get; init; }
public string? DocumentLocation { get; init; }
public bool CanAutoFix { get; init; }
}
public sealed record OllamaSettings
{
public string BaseUrl { get; init; } = "http://192.168.200.112:11434";
public string Model { get; init; } = "";
public int TimeoutSeconds { get; init; } = 120;
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj" />
<ProjectReference Include="..\DocxWorkbench.Core\DocxWorkbench.Core.csproj" />
<ProjectReference Include="..\DocxWorkbench.OpenXml\DocxWorkbench.OpenXml.csproj" />
<ProjectReference Include="..\DocxWorkbench.WordInterop\DocxWorkbench.WordInterop.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,33 @@
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using DocumentAutomation.Shared;
using DocxWorkbench.Core;
using DocxWorkbench.OpenXml;
using DocxWorkbench.WordInterop;
namespace DocxWorkbench.App;
public sealed class MainForm : Form
{
private readonly TextBox _template = new() { Width = 520 }; private readonly TextBox _body = new() { Width = 520 }; private readonly TextBox _output = new() { Width = 520 }; private readonly TextBox _log = new() { Multiline = true, ScrollBars = ScrollBars.Vertical, Dock = DockStyle.Fill }; private readonly Label _status = new() { AutoSize = true, Text = "Готово" }; private readonly IDocumentInspector _inspector = new DocumentInspector(); private readonly IWordDocumentService _word = new WordDocumentService();
public MainForm()
{
Text = "DOCX Workbench"; Width = 980; Height = 680; StartPosition = FormStartPosition.CenterScreen;
var grid = new TableLayoutPanel { Dock = DockStyle.Top, Height = 145, ColumnCount = 3, RowCount = 4, Padding = new Padding(8) }; grid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 150)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110));
AddRow(grid, 0, "Шаблон DOCX", _template, BrowseTemplate); AddRow(grid, 1, "Тело документа", _body, BrowseBody); AddRow(grid, 2, "Выходной DOCX", _output, BrowseOutput);
var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill }; buttons.Controls.Add(Button("Анализировать", async (_, _) => await InspectAsync())); buttons.Controls.Add(Button("Сформировать", async (_, _) => await BuildAsync())); buttons.Controls.Add(Button("Проверить", async (_, _) => await InspectOutputAsync())); buttons.Controls.Add(Button("Открыть DOCX", (_, _) => Open(_output.Text))); buttons.Controls.Add(Button("Открыть PDF", (_, _) => Open(Path.ChangeExtension(_output.Text, ".pdf")))); grid.Controls.Add(buttons, 1, 3); grid.SetColumnSpan(buttons, 2);
var tabs = new TabControl { Dock = DockStyle.Fill }; foreach (var title in new[] { "Предпросмотр шаблона", "Предпросмотр тела", "Структура", "Результат", "Нарушения", "Журнал" }) { var page = new TabPage(title); if (title == "Журнал") page.Controls.Add(_log); tabs.TabPages.Add(page); }
Controls.Add(tabs); Controls.Add(grid); Controls.Add(_status); _status.Dock = DockStyle.Bottom;
}
private static void AddRow(TableLayoutPanel p, int row, string title, TextBox box, EventHandler browse) { p.Controls.Add(new Label { Text = title, AutoSize = true, Anchor = AnchorStyles.Left }, 0, row); p.Controls.Add(box, 1, row); var b = new Button { Text = "Обзор...", Width = 95 }; b.Click += browse; p.Controls.Add(b, 2, row); }
private static Button Button(string text, EventHandler handler) { var b = new Button { Text = text, AutoSize = true }; b.Click += handler; return b; }
private void BrowseTemplate(object? s, EventArgs e) { using var d = new OpenFileDialog { Filter = "DOCX|*.docx" }; if (d.ShowDialog() == DialogResult.OK) _template.Text = d.FileName; }
private void BrowseBody(object? s, EventArgs e) { using var d = new OpenFileDialog { Filter = "DOCX/TXT|*.docx;*.txt" }; if (d.ShowDialog() == DialogResult.OK) _body.Text = d.FileName; }
private void BrowseOutput(object? s, EventArgs e) { using var d = new SaveFileDialog { Filter = "DOCX|*.docx", DefaultExt = "docx" }; if (d.ShowDialog() == DialogResult.OK) _output.Text = d.FileName; }
private async Task InspectAsync() => await RunAsync("Анализ", () => Task.Run(() => { var a = _inspector.Inspect(_template.Text); var b = _inspector.Inspect(_body.Text); return (object)new { Template = a, Body = b }; }));
private async Task InspectOutputAsync() => await RunAsync("Проверка результата", () => Task.Run(() => (object)_inspector.Inspect(_output.Text)));
private async Task BuildAsync() => await RunAsync("Формирование", async () => { var result = await _word.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = _template.Text, BodyPath = _body.Text, OutputDocxPath = _output.Text }, new Progress<string>(x => _status.Text = x), CancellationToken.None); var pdf = await _word.ExportPdfAsync(result.OutputDocxPath, Path.ChangeExtension(result.OutputDocxPath, ".pdf"), CancellationToken.None); return (object)new { result, pdf }; });
private async Task RunAsync(string name, Func<Task<object>> action) { try { _status.Text = name + "..."; var value = await action(); _log.AppendText(JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = true }) + Environment.NewLine); _status.Text = name + ": готово"; } catch (Exception ex) { _log.AppendText($"{name}: {ex}\r\n"); _status.Text = name + ": ошибка"; MessageBox.Show(ex.Message, name, MessageBoxButtons.OK, MessageBoxIcon.Error); } }
private static void Open(string path) { if (File.Exists(path)) Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); }
}
@@ -0,0 +1,16 @@
namespace DocxWorkbench.App;
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
@@ -0,0 +1,26 @@
using DocumentAutomation.Shared;
namespace DocxWorkbench.Core;
public interface IDocumentInspector
{
DocumentInspectionResult Inspect(string docxPath);
}
public interface IWordDocumentService
{
Task<WordDocumentResult> BuildDocumentAsync(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken cancellationToken);
Task<WordPreviewResult> ExportPdfAsync(string docxPath, string outputPdfPath, CancellationToken cancellationToken);
}
public static class DocumentInputValidation
{
public static void Validate(WordDocumentBuildRequest request)
{
if (!File.Exists(request.TemplatePath)) throw new FileNotFoundException("Шаблон DOCX не найден.", request.TemplatePath);
if (!File.Exists(request.BodyPath)) throw new FileNotFoundException("Файл тела документа не найден.", request.BodyPath);
if (!request.TemplatePath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Шаблон должен быть DOCX.");
if (Path.GetFullPath(request.TemplatePath).Equals(Path.GetFullPath(request.OutputDocxPath), StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("Нельзя перезаписывать исходный шаблон.");
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(request.OutputDocxPath))!);
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,39 @@
using System.Text.RegularExpressions;
using DocumentAutomation.Shared;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxWorkbench.Core;
namespace DocxWorkbench.OpenXml;
public sealed class DocumentInspector : IDocumentInspector
{
public DocumentInspectionResult Inspect(string docxPath)
{
if (!File.Exists(docxPath)) throw new FileNotFoundException("DOCX не найден.", docxPath);
var info = new FileInfo(docxPath);
using var package = WordprocessingDocument.Open(docxPath, false);
var main = package.MainDocumentPart ?? throw new InvalidDataException("В DOCX отсутствует основной документ.");
var body = main.Document.Body;
var text = main.Document.InnerText;
var headers = main.HeaderParts.Count();
var footers = main.FooterParts.Count();
var warnings = new List<string>();
if (Regex.IsMatch(text, "2019|старый|legacy", RegexOptions.IgnoreCase)) warnings.Add("Обнаружены возможные старые проектные реквизиты; проверьте вручную.");
if (main.Document.Descendants<SectionProperties>().Any(s => s.GetFirstChild<HeaderReference>() is null)) warnings.Add("Есть секция без явной ссылки на header.");
return new DocumentInspectionResult
{
FilePath = docxPath, FileSize = info.Length, LastWriteTimeUtc = info.LastWriteTimeUtc,
Sections = Math.Max(1, main.Document.Descendants<SectionProperties>().Count()),
Paragraphs = main.Document.Descendants<Paragraph>().Count(), Tables = main.Document.Descendants<Table>().Count(),
Images = main.ImageParts.Count(), Headers = headers, Footers = footers,
HasPageFields = text.Contains("PAGE", StringComparison.OrdinalIgnoreCase), HasNumPagesFields = text.Contains("NUMPAGES", StringComparison.OrdinalIgnoreCase),
HasSectionPagesFields = text.Contains("SECTIONPAGES", StringComparison.OrdinalIgnoreCase), Bookmarks = main.Document.Descendants<BookmarkStart>().Count(),
ContentControls = main.Document.Descendants<SdtElement>().Count(), Comments = package.MainDocumentPart.GetPartsOfType<WordprocessingCommentsPart>().Count(),
HasTrackedChanges = main.Document.Descendants<Inserted>().Any() || main.Document.Descendants<Deleted>().Any(),
HasProtectedParts = main.Document.Descendants<DocumentProtection>().Any(),
Styles = main.StyleDefinitionsPart?.Styles?.Elements<Style>().Select(s => s.StyleId?.Value ?? "").Where(s => s.Length > 0).Distinct().Order().ToArray() ?? [],
Warnings = warnings
};
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj" />
<ProjectReference Include="..\DocxWorkbench.Core\DocxWorkbench.Core.csproj" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj" />
<ProjectReference Include="..\DocxWorkbench.Core\DocxWorkbench.Core.csproj" />
<PackageReference Include="Microsoft.Office.Interop.Word" Version="15.0.4797.1004" />
</ItemGroup>
</Project>
@@ -0,0 +1,55 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using DocumentAutomation.Shared;
using DocxWorkbench.Core;
using Word = Microsoft.Office.Interop.Word;
namespace DocxWorkbench.WordInterop;
public sealed class WordDocumentService : IWordDocumentService
{
public Task<WordDocumentResult> BuildDocumentAsync(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken cancellationToken)
=> Task.Run(() => Build(request, progress, cancellationToken), cancellationToken);
public Task<WordPreviewResult> ExportPdfAsync(string docxPath, string outputPdfPath, CancellationToken cancellationToken)
=> Task.Run(() => ExportPdf(docxPath, outputPdfPath, cancellationToken), cancellationToken);
private static WordDocumentResult Build(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken token)
{
DocumentInputValidation.Validate(request); var started = Stopwatch.StartNew();
var temp = Path.Combine(Path.GetTempPath(), "DocumentAutomation", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(temp);
var working = Path.Combine(temp, "working.docx"); File.Copy(request.TemplatePath, working, true);
Word.Application? app = null; Word.Document? document = null; string insertion = "none"; int? pid = null;
try
{
token.ThrowIfCancellationRequested(); progress?.Report("Запуск Word COM"); app = new Word.Application { Visible = false, DisplayAlerts = Word.WdAlertLevel.wdAlertsNone }; pid = GetProcessId(app);
document = app.Documents.Open(working, ReadOnly: false, AddToRecentFiles: false, Visible: false);
progress?.Report("Поиск точки вставки DOC_BODY"); var range = FindInsertionRange(document, out insertion);
if (range is null) throw new InvalidOperationException("В шаблоне не найдена точка вставки DOC_BODY.");
token.ThrowIfCancellationRequested(); range.InsertFile(request.BodyPath, ConfirmConversions: false, Link: false, Attachment: false);
progress?.Report("Обновление полей и пагинация"); document.Repaginate(); document.Fields.Update();
for (var i = 1; i <= document.Sections.Count; i++) { var section = document.Sections[i]; foreach (Word.HeaderFooter hf in section.Headers) hf.Range.Fields.Update(); foreach (Word.HeaderFooter hf in section.Footers) hf.Range.Fields.Update(); }
document.SaveAs2(request.OutputDocxPath, Word.WdSaveFormat.wdFormatXMLDocument);
var logPath = Path.ChangeExtension(request.OutputDocxPath, ".generation.json"); File.WriteAllText(logPath, JsonSerializer.Serialize(new { operationId = Guid.NewGuid(), template = request.TemplatePath, body = request.BodyPath, output = request.OutputDocxPath, insertionPoint = insertion, wordPid = pid, durationMs = started.ElapsedMilliseconds, completedAtUtc = DateTime.UtcNow }, new JsonSerializerOptions { WriteIndented = true }));
return new WordDocumentResult { OutputDocxPath = request.OutputDocxPath, LogPath = logPath, Duration = started.Elapsed, InsertionPoint = insertion };
}
finally { if (document is not null) { try { document.Close(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(document); } if (app is not null) { try { app.Quit(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(app); } try { Directory.Delete(temp, true); } catch { } }
}
private static WordPreviewResult ExportPdf(string docxPath, string outputPdfPath, CancellationToken token)
{
if (!File.Exists(docxPath)) throw new FileNotFoundException("DOCX не найден.", docxPath); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPdfPath))!); var sw = Stopwatch.StartNew(); Word.Application? app = null; Word.Document? doc = null;
try { token.ThrowIfCancellationRequested(); app = new Word.Application { Visible = false, DisplayAlerts = Word.WdAlertLevel.wdAlertsNone }; doc = app.Documents.Open(docxPath, ReadOnly: true, AddToRecentFiles: false, Visible: false); doc.ExportAsFixedFormat(outputPdfPath, Word.WdExportFormat.wdExportFormatPDF); return new WordPreviewResult { PdfPath = outputPdfPath, Duration = sw.Elapsed }; }
finally { if (doc is not null) { try { doc.Close(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(doc); } if (app is not null) { try { app.Quit(Word.WdSaveOptions.wdDoNotSaveChanges); } catch { } Marshal.FinalReleaseComObject(app); } }
}
private static Word.Range? FindInsertionRange(Word.Document document, out string point)
{
foreach (Word.ContentControl control in document.ContentControls) if (string.Equals(control.Tag, "DOC_BODY", StringComparison.OrdinalIgnoreCase) || string.Equals(control.Title, "DOC_BODY", StringComparison.OrdinalIgnoreCase)) { point = "ContentControl:DOC_BODY"; return control.Range; }
if (document.Bookmarks.Exists("DOC_BODY")) { point = "Bookmark:DOC_BODY"; return document.Bookmarks["DOC_BODY"].Range; }
var search = document.Content; search.Find.ClearFormatting(); search.Find.Text = "{{DOC_BODY}}"; if (search.Find.Execute()) { point = "Marker:{{DOC_BODY}}"; search.Text = ""; return search; } point = "none"; return null;
}
private static int? GetProcessId(Word.Application app) { try { return Process.GetProcessesByName("WINWORD").OrderByDescending(p => p.StartTime).FirstOrDefault()?.Id; } catch { return null; } }
}
@@ -0,0 +1,22 @@
using System.Text.Json;
using StandardsWorkbench.Core;
using StandardsWorkbench.Ollama;
using StandardsWorkbench.Pdf;
namespace StandardsWorkbench.App;
public sealed class MainForm : Form
{
private readonly TextBox _pdf = new() { Width = 560 }; private readonly TextBox _code = new() { Width = 240, Text = "ГОСТ Р 21.101-2026" }; private readonly TextBox _title = new() { Width = 560, Text = "Нормативный документ" }; private readonly TextBox _log = new() { Multiline = true, Dock = DockStyle.Fill, ScrollBars = ScrollBars.Vertical }; private readonly ListBox _candidates = new() { Dock = DockStyle.Fill }; private readonly Label _status = new() { AutoSize = true, Text = "Готово" }; private readonly IStandardPdfService _service = new PdfStandardService(); private StandardMetadata? _metadata; private List<CandidateRule> _rules = [];
public MainForm()
{
Text = "Standards Workbench"; Width = 900; Height = 620; var top = new TableLayoutPanel { Dock = DockStyle.Top, Height = 150, ColumnCount = 3, RowCount = 4, Padding = new Padding(8) }; top.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120)); top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); top.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110)); Add(top, 0, "Стандарт PDF", _pdf, Browse); Add(top, 1, "Код", _code, null); Add(top, 2, "Название", _title, null); var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill }; buttons.Controls.Add(Button("Анализировать PDF", async (_, _) => await ImportAsync())); buttons.Controls.Add(Button("Утвердить выбранное", (_, _) => SetSelectedStatus("Approved"))); buttons.Controls.Add(Button("Отклонить выбранное", (_, _) => SetSelectedStatus("Rejected"))); buttons.Controls.Add(Button("Экспортировать профиль", (_, _) => ExportProfile())); top.Controls.Add(buttons, 1, 3); top.SetColumnSpan(buttons, 2); var tabs = new TabControl { Dock = DockStyle.Fill }; foreach (var t in new[] { "Документ", "Страницы", "Пункты", "Кандидаты правил", "Профили", "Сравнение", "Журнал" }) { var p = new TabPage(t); if (t == "Журнал") p.Controls.Add(_log); if (t == "Кандидаты правил") p.Controls.Add(_candidates); tabs.TabPages.Add(p); } Controls.Add(tabs); Controls.Add(top); Controls.Add(_status); _status.Dock = DockStyle.Bottom;
}
private static void Add(TableLayoutPanel p, int row, string label, TextBox box, EventHandler? browse) { p.Controls.Add(new Label { Text = label, AutoSize = true }, 0, row); p.Controls.Add(box, 1, row); if (browse is not null) { var b = new Button { Text = "Обзор..." }; b.Click += browse; p.Controls.Add(b, 2, row); } }
private static Button Button(string text, EventHandler e) { var b = new Button { Text = text, AutoSize = true }; b.Click += e; return b; }
private void Browse(object? s, EventArgs e) { using var d = new OpenFileDialog { Filter = "PDF|*.pdf" }; if (d.ShowDialog() == DialogResult.OK) _pdf.Text = d.FileName; }
private async Task ImportAsync() { try { _status.Text = "Анализ PDF..."; var result = await _service.ImportAsync(_pdf.Text, _code.Text, _title.Text, CancellationToken.None); _metadata = result.Metadata; _rules = result.Clauses.Select(c => new CandidateRule { CandidateId = Guid.NewGuid().ToString("N"), StandardId = c.StandardId, ClauseId = c.ClauseId, SourcePage = c.StartPage, SourceText = c.Text, Title = c.Heading.Length > 0 ? c.Heading : $"Кандидат по пункту {c.ClauseId}", RequiresHumanApproval = true }).ToList(); RefreshCandidates(); _log.AppendText(JsonSerializer.Serialize(new { result.Metadata, result.Pages, result.Clauses }, new JsonSerializerOptions { WriteIndented = true }) + Environment.NewLine); _status.Text = $"Готово: {result.Pages.Count} страниц, {result.Clauses.Count} пунктов"; } catch (Exception ex) { _log.AppendText(ex + Environment.NewLine); _status.Text = "Ошибка"; MessageBox.Show(ex.Message); } }
private void RefreshCandidates() { _candidates.Items.Clear(); foreach (var rule in _rules) _candidates.Items.Add($"[{rule.Status}] {rule.ClauseId} стр.{rule.SourcePage}: {rule.Title}"); }
private void SetSelectedStatus(string status) { if (_candidates.SelectedIndex < 0 || _candidates.SelectedIndex >= _rules.Count) return; _rules[_candidates.SelectedIndex] = _rules[_candidates.SelectedIndex] with { Status = status }; RefreshCandidates(); }
private void ExportProfile() { if (_metadata is null) { MessageBox.Show("Сначала импортируйте PDF."); return; } using var dialog = new FolderBrowserDialog(); if (dialog.ShowDialog() != DialogResult.OK) return; var profile = new ApprovedRulesProfile { StandardId = _metadata.StandardId, Code = _metadata.Code, Revision = _metadata.Revision, SourceFileSha256 = _metadata.SourceFileSha256, Rules = _rules.Where(r => r.Status == "Approved").ToArray() }; var path = Path.Combine(dialog.SelectedPath, $"{_metadata.StandardId}-docx-profile"); ProfileStore.Export(path, profile); _status.Text = $"Профиль экспортирован: {path}"; }
}
@@ -0,0 +1,16 @@
namespace StandardsWorkbench.App;
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj" />
<ProjectReference Include="..\StandardsWorkbench.Core\StandardsWorkbench.Core.csproj" />
<ProjectReference Include="..\StandardsWorkbench.Pdf\StandardsWorkbench.Pdf.csproj" />
<ProjectReference Include="..\StandardsWorkbench.Ollama\StandardsWorkbench.Ollama.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,77 @@
using System.Security.Cryptography;
using System.Text.Json;
using DocumentAutomation.Shared;
namespace StandardsWorkbench.Core;
public sealed record StandardMetadata
{
public required string StandardId { get; init; }
public required string Code { get; init; }
public required string Title { get; init; }
public string Revision { get; init; } = "";
public DateOnly? EffectiveFrom { get; init; }
public required string SourceFileSha256 { get; init; }
public int PageCount { get; init; }
public bool TextLayerDetected { get; init; }
public DateTime ImportedAt { get; init; } = DateTime.UtcNow;
public string Status { get; init; } = "Imported";
}
public sealed record PdfPageText(int PageNumber, string Text, bool TextLayerDetected, string Status, IReadOnlyList<string> Blocks);
public sealed record StandardClause(string StandardId, string ClauseId, string Heading, string Text, int StartPage, int EndPage, double ExtractionConfidence);
public sealed record CandidateRule
{
public required string CandidateId { get; init; }
public required string StandardId { get; init; }
public required string ClauseId { get; init; }
public int SourcePage { get; init; }
public required string SourceText { get; init; }
public required string Title { get; init; }
public string[] Scope { get; init; } = ["DOCX"];
public string RuleKind { get; init; } = "ExpertReview";
public string Target { get; init; } = "Document";
public Dictionary<string, string> Parameters { get; init; } = [];
public string Severity { get; init; } = "Warning";
public string AutomationLevel { get; init; } = "Manual";
public double Confidence { get; init; }
public bool RequiresHumanApproval { get; init; } = true;
public string Status { get; init; } = "New";
}
public sealed record ApprovedRulesProfile
{
public required string StandardId { get; init; }
public required string Code { get; init; }
public required string Revision { get; init; }
public required string SourceFileSha256 { get; init; }
public string ProfileVersion { get; init; } = "1.0";
public string MinimumRulesEngineVersion { get; init; } = "1.0";
public DateTime ExportedAt { get; init; } = DateTime.UtcNow;
public IReadOnlyList<CandidateRule> Rules { get; init; } = [];
}
public interface IPdfOcrService { Task<PdfPageText> RecognizePageAsync(string pdfPath, int pageNumber, CancellationToken cancellationToken); }
public interface IStandardPdfService { Task<(StandardMetadata Metadata, IReadOnlyList<PdfPageText> Pages, IReadOnlyList<StandardClause> Clauses)> ImportAsync(string pdfPath, string code, string title, CancellationToken cancellationToken); }
public interface IOllamaService { Task<IReadOnlyList<string>> GetModelsAsync(CancellationToken cancellationToken); Task<CandidateRule?> SuggestCandidateAsync(StandardClause clause, string model, CancellationToken cancellationToken); }
public static class ProfileStore
{
public static string Sha256(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant(); }
public static void Export(string directory, ApprovedRulesProfile profile)
{
Directory.CreateDirectory(directory); var options = new JsonSerializerOptions { WriteIndented = true }; File.WriteAllText(Path.Combine(directory, "manifest.json"), JsonSerializer.Serialize(profile, options)); File.WriteAllText(Path.Combine(directory, "approved-rules.json"), JsonSerializer.Serialize(profile.Rules, options)); File.WriteAllText(Path.Combine(directory, "references.json"), JsonSerializer.Serialize(profile.Rules.Select(r => new { r.ClauseId, r.SourcePage, r.StandardId }), options)); File.WriteAllText(Path.Combine(directory, "signature.sha256"), Sha256(Path.Combine(directory, "approved-rules.json")));
}
}
public sealed class RulesEngine
{
public IReadOnlyList<RuleEvaluationResult> Evaluate(DocumentInspectionResult document, ApprovedRulesProfile profile)
=> profile.Rules.Where(r => r.Status is "Approved" or "Implemented" or "Tested").Select(r => EvaluateOne(document, r)).ToArray();
private static RuleEvaluationResult EvaluateOne(DocumentInspectionResult d, CandidateRule rule)
{
var status = rule.RuleKind switch { "SectionCount" => d.Sections > 0 ? RuleEvaluationStatus.Pass : RuleEvaluationStatus.Fail, "ContentControlPresence" => d.ContentControls > 0 ? RuleEvaluationStatus.Pass : RuleEvaluationStatus.Fail, "BookmarkPresence" => d.Bookmarks > 0 ? RuleEvaluationStatus.Pass : RuleEvaluationStatus.Fail, "CommentsAbsent" => d.Comments == 0 ? RuleEvaluationStatus.Pass : RuleEvaluationStatus.Fail, "TrackedChangesAbsent" => !d.HasTrackedChanges ? RuleEvaluationStatus.Pass : RuleEvaluationStatus.Fail, _ => RuleEvaluationStatus.NeedsReview };
return new RuleEvaluationResult { RuleId = rule.CandidateId, Status = status, Message = status == RuleEvaluationStatus.Pass ? "Проверка пройдена." : status == RuleEvaluationStatus.Fail ? "Проверка не пройдена." : "Требуется экспертная проверка.", ClauseId = rule.ClauseId, SourcePage = rule.SourcePage, DocumentLocation = rule.Target, CanAutoFix = false };
}
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup><ProjectReference Include="..\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj" /></ItemGroup>
</Project>
@@ -0,0 +1,17 @@
using System.Net.Http.Json;
using System.Text.Json;
using StandardsWorkbench.Core;
namespace StandardsWorkbench.Ollama;
public sealed class OllamaClient : IOllamaService
{
private readonly HttpClient _http;
public OllamaClient(HttpClient? httpClient = null) { _http = httpClient ?? new HttpClient(); _http.BaseAddress = new Uri("http://192.168.200.112:11434"); _http.Timeout = TimeSpan.FromSeconds(120); }
public async Task<IReadOnlyList<string>> GetModelsAsync(CancellationToken cancellationToken)
{ using var response = await _http.GetAsync("/api/tags", cancellationToken); response.EnsureSuccessStatusCode(); using var json = await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken: cancellationToken); return json?.RootElement.GetProperty("models").EnumerateArray().Select(x => x.GetProperty("name").GetString() ?? "").Where(x => x.Length > 0).ToArray() ?? []; }
public async Task<CandidateRule?> SuggestCandidateAsync(StandardClause clause, string model, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(model)) throw new ArgumentException("Модель Ollama не выбрана.", nameof(model)); var prompt = $"Return JSON only. Extract one candidate DOCX rule from clause {clause.ClauseId}. Never invent source references. Text: {clause.Text}"; using var response = await _http.PostAsJsonAsync("/api/generate", new { model, prompt, stream = false, format = "json" }, cancellationToken); response.EnsureSuccessStatusCode(); using var json = await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken: cancellationToken); var raw = json?.RootElement.GetProperty("response").GetString(); if (string.IsNullOrWhiteSpace(raw)) return null; try { return JsonSerializer.Deserialize<CandidateRule>(raw); } catch (JsonException) { return null; }
}
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup><ProjectReference Include="..\StandardsWorkbench.Core\StandardsWorkbench.Core.csproj" /></ItemGroup>
</Project>
@@ -0,0 +1,33 @@
using System.Text.RegularExpressions;
using StandardsWorkbench.Core;
using UglyToad.PdfPig;
namespace StandardsWorkbench.Pdf;
public sealed class PdfStandardService : IStandardPdfService
{
public Task<(StandardMetadata Metadata, IReadOnlyList<PdfPageText> Pages, IReadOnlyList<StandardClause> Clauses)> ImportAsync(string pdfPath, string code, string title, CancellationToken cancellationToken)
{
if (!File.Exists(pdfPath)) throw new FileNotFoundException("PDF ГОСТа не найден.", pdfPath);
return Task.Run(() =>
{
using var pdf = PdfDocument.Open(pdfPath); var pages = new List<PdfPageText>();
foreach (var page in pdf.GetPages()) { cancellationToken.ThrowIfCancellationRequested(); var text = page.Text ?? ""; pages.Add(new PdfPageText(page.Number, text, text.Trim().Length > 0, text.Trim().Length > 0 ? "Extracted" : "OCR_REQUIRED", text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries))); }
var standardId = Regex.Replace(code.ToUpperInvariant(), "[^A-ZА-Я0-9]+", "-").Trim('-'); var clauses = ExtractClauses(standardId, pages); var meta = new StandardMetadata { StandardId = standardId, Code = code, Title = title, SourceFileSha256 = ProfileStore.Sha256(pdfPath), PageCount = pages.Count, TextLayerDetected = pages.Any(p => p.TextLayerDetected), ImportedAt = DateTime.UtcNow };
return (meta, (IReadOnlyList<PdfPageText>)pages, (IReadOnlyList<StandardClause>)clauses);
}, cancellationToken);
}
private static List<StandardClause> ExtractClauses(string id, IReadOnlyList<PdfPageText> pages)
{
var result = new List<StandardClause>(); string? currentId = null; var currentText = new List<string>(); var start = 1;
void Flush(int end) { if (currentId is not null) result.Add(new StandardClause(id, currentId, "", string.Join(Environment.NewLine, currentText), start, end, 0.85)); }
foreach (var page in pages) foreach (var line in page.Text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var match = Regex.Match(line, "^(\\d+(?:\\.\\d+){0,5})[.)]?\\s+(.+)$"); if (match.Success) { Flush(page.PageNumber - 1); currentId = match.Groups[1].Value; currentText = [line]; start = page.PageNumber; } else if (currentId is not null) currentText.Add(line); }
Flush(pages.Count); return result;
}
}
public sealed class NoOpOcrService : IPdfOcrService
{
public Task<PdfPageText> RecognizePageAsync(string pdfPath, int pageNumber, CancellationToken cancellationToken) => Task.FromResult(new PdfPageText(pageNumber, "", false, "OCR_REQUIRED", []));
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DocumentAutomation.Shared\DocumentAutomation.Shared.csproj" />
<ProjectReference Include="..\StandardsWorkbench.Core\StandardsWorkbench.Core.csproj" />
<PackageReference Include="PdfPig" Version="0.1.9" />
</ItemGroup>
</Project>
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup><ProjectReference Include="..\..\src\DocxWorkbench.WordInterop\DocxWorkbench.WordInterop.csproj" /><ProjectReference Include="..\..\src\StandardsWorkbench.Pdf\StandardsWorkbench.Pdf.csproj" /></ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
@@ -0,0 +1,16 @@
namespace DocumentAutomation.IntegrationTests;
public sealed class WordComIntegrationTests
{
[Fact]
public void IntegrationAssemblyIsAvailable()
{
Assert.True(typeof(WordComIntegrationTests).Assembly.GetName().Name is not null);
}
[Fact(Skip = "Requires an interactive Microsoft Word installation; not run automatically in the repository validation pass.")]
public void BuildsDocxAndExportsPdfThroughWordCom()
{
Assert.Fail("Run this test manually on a Windows workstation with Word installed and a synthetic template/body fixture.");
}
}
@@ -0,0 +1,20 @@
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxWorkbench.OpenXml;
namespace DocxWorkbench.Tests;
public sealed class DocumentInspectorTests
{
[Fact]
public void InspectsSyntheticDocxStructure()
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx");
using (var doc = WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
{
var main = doc.AddMainDocumentPart(); main.Document = new Document(new Body(new Paragraph(new Run(new Text("{{DOC_BODY}}"))), new Table(new TableRow(new TableCell(new Paragraph(new Run(new Text("x")))))))); main.Document.Save();
}
try { var result = new DocumentInspector().Inspect(path); Assert.Equal(1, result.Tables); Assert.True(result.Paragraphs >= 2); Assert.True(result.HasPageFields is false); }
finally { File.Delete(path); }
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup><ProjectReference Include="..\..\src\DocxWorkbench.Core\DocxWorkbench.Core.csproj" /><ProjectReference Include="..\..\src\DocxWorkbench.OpenXml\DocxWorkbench.OpenXml.csproj" /><PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" /></ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
@@ -0,0 +1,26 @@
using DocumentAutomation.Shared;
using StandardsWorkbench.Core;
namespace StandardsWorkbench.Tests;
public sealed class RulesEngineTests
{
[Fact]
public void OnlyApprovedRulesAreEvaluated()
{
var document = new DocumentInspectionResult { FilePath = "synthetic.docx", ContentControls = 1, Sections = 1, Paragraphs = 2, Tables = 0 };
var profile = new ApprovedRulesProfile { StandardId = "TEST", Code = "TEST", Revision = "1", Rules = [
new CandidateRule { CandidateId = "approved", StandardId = "TEST", ClauseId = "1", SourceText = "x", Title = "x", RuleKind = "ContentControlPresence", Status = "Approved" },
new CandidateRule { CandidateId = "new", StandardId = "TEST", ClauseId = "2", SourceText = "x", Title = "x", RuleKind = "SectionCount", Status = "New" }
], SourceFileSha256 = "test"};
var result = new RulesEngine().Evaluate(document, profile); Assert.Single(result); Assert.Equal(RuleEvaluationStatus.Pass, result[0].Status);
}
[Fact]
public void FailedRuleHasClauseReference()
{
var document = new DocumentInspectionResult { FilePath = "synthetic.docx", ContentControls = 0, Sections = 1, Paragraphs = 1, Tables = 0 };
var rule = new CandidateRule { CandidateId = "cc", StandardId = "TEST", ClauseId = "6.3", SourcePage = 4, SourceText = "required", Title = "cc", RuleKind = "ContentControlPresence", Status = "Tested" };
var result = Assert.Single(new RulesEngine().Evaluate(document, new ApprovedRulesProfile { StandardId = "TEST", Code = "TEST", Revision = "1", SourceFileSha256 = "test", Rules = [rule] })); Assert.Equal(RuleEvaluationStatus.Fail, result.Status); Assert.Equal("6.3", result.ClauseId); Assert.Equal(4, result.SourcePage);
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup><ProjectReference Include="..\..\src\StandardsWorkbench.Core\StandardsWorkbench.Core.csproj" /></ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>