diff --git a/.gitignore b/.gitignore index 414a9b3..c704992 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/DocumentAutomation/README.md b/DocumentAutomation/README.md new file mode 100644 index 0000000..91a0282 --- /dev/null +++ b/DocumentAutomation/README.md @@ -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`. diff --git a/DocumentAutomation/docs/architecture.md b/DocumentAutomation/docs/architecture.md new file mode 100644 index 0000000..9e85b97 --- /dev/null +++ b/DocumentAutomation/docs/architecture.md @@ -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. diff --git a/DocumentAutomation/docs/development.md b/DocumentAutomation/docs/development.md new file mode 100644 index 0000000..522e14d --- /dev/null +++ b/DocumentAutomation/docs/development.md @@ -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`. diff --git a/DocumentAutomation/docs/docx-template-contract.md b/DocumentAutomation/docs/docx-template-contract.md new file mode 100644 index 0000000..315f658 --- /dev/null +++ b/DocumentAutomation/docs/docx-template-contract.md @@ -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. diff --git a/DocumentAutomation/docs/standards-rule-model.md b/DocumentAutomation/docs/standards-rule-model.md new file mode 100644 index 0000000..ffe737a --- /dev/null +++ b/DocumentAutomation/docs/standards-rule-model.md @@ -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. diff --git a/DocumentAutomation/docs/word-com-lifecycle.md b/DocumentAutomation/docs/word-com-lifecycle.md new file mode 100644 index 0000000..be1bfbf --- /dev/null +++ b/DocumentAutomation/docs/word-com-lifecycle.md @@ -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. diff --git a/DocumentAutomation/samples/.gitkeep b/DocumentAutomation/samples/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/DocumentAutomation/samples/README.md b/DocumentAutomation/samples/README.md new file mode 100644 index 0000000..20f34da --- /dev/null +++ b/DocumentAutomation/samples/README.md @@ -0,0 +1,3 @@ +# Samples + +Реальные проектные DOCX/PDF и нормативные документы сюда не добавляются. Тесты создают синтетические документы во временном каталоге. Для локальной проверки укажите входные файлы через GUI. diff --git a/DocumentAutomation/tests/DocumentAutomation.IntegrationTests/WordComIntegrationTests.cs b/DocumentAutomation/tests/DocumentAutomation.IntegrationTests/WordComIntegrationTests.cs new file mode 100644 index 0000000..d2b1bcd --- /dev/null +++ b/DocumentAutomation/tests/DocumentAutomation.IntegrationTests/WordComIntegrationTests.cs @@ -0,0 +1,10 @@ +namespace DocumentAutomation.IntegrationTests; + +public sealed class WordComIntegrationTests +{ + [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."); + } +} diff --git a/DocumentAutomation/tests/DocxWorkbench.Tests/DocumentInspectorTests.cs b/DocumentAutomation/tests/DocxWorkbench.Tests/DocumentInspectorTests.cs new file mode 100644 index 0000000..59ef137 --- /dev/null +++ b/DocumentAutomation/tests/DocxWorkbench.Tests/DocumentInspectorTests.cs @@ -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); } + } +} diff --git a/DocumentAutomation/tests/StandardsWorkbench.Tests/RulesEngineTests.cs b/DocumentAutomation/tests/StandardsWorkbench.Tests/RulesEngineTests.cs new file mode 100644 index 0000000..3be8e76 --- /dev/null +++ b/DocumentAutomation/tests/StandardsWorkbench.Tests/RulesEngineTests.cs @@ -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); + } +}