Compare commits

...
Author SHA1 Message Date
codex 36178374db audit: document launcher source architecture 2026-07-31 14:06:39 +03:00
codex b33351117b audit: clarify legacy bootstrap evidence 2026-07-31 13:39:55 +03:00
codex 56dcce42f6 audit: document legacy P_Rec_Annot loading chain 2026-07-31 13:39:29 +03:00
codex ea3a9e157e test: cover deterministic Ortho planning 2026-07-31 13:08:22 +03:00
codex 837ac1d994 feat: read Plant 3D Ortho context and viewports 2026-07-31 13:08:16 +03:00
codex 142b5f8b93 test: add controlled Plant 3D 2026 runtime smoke mode 2026-07-31 12:55:22 +03:00
codex dcf95d7e3a test: validate Plant 3D 2026 build and bundle 2026-07-31 11:37:36 +03:00
codex c733a27cbb feat: register real Plant 3D 2026 commands and ribbon 2026-07-31 11:37:31 +03:00
codex 9a7a6cd678 fix: bind P_Rec_Annot to Plant 3D 2026 assemblies 2026-07-31 11:36:46 +03:00
codex 66e52e5f50 chore: accept user-approved report presentation updates
Accept the three existing user changes as approved by the project owner. The changes are intentionally committed as-is without semantic normalization or regeneration.
2026-07-31 10:03:39 +03:00
codex 60b3934cd2 feat: scaffold P_Rec_Annot .NET 8 architecture 2026-07-31 09:57:53 +03:00
codex 34222cedc8 audit: inventory P_Rec_Annot v1.0.4 sources 2026-07-31 09:37:32 +03:00
codex 596c95a4b6 fix: aggregate Plant3D plugin usage without computer field 2026-07-31 09:07:06 +03:00
codex 6f29a5bce4 audit: parse Plant3D usage from Excel reports 2026-07-31 08:45:35 +03:00
codex 871499fa03 audit: build plugin usage matrix from logs 2026-07-31 08:00:19 +03:00
codex bbe87611bf audit: inventory P_Rec_BOP Plant 3D 2019 sources 2026-07-31 07:34:41 +03:00
codex 25e1def353 fix: make STA Word queue lifetime safe 2026-07-30 16:09:15 +03:00
codex f7fa0363e3 feat: add learned document template workflow 2026-07-30 15:24:51 +03:00
codex 11e6e5d806 fix: import DOCX body without source layout 2026-07-30 14:24:08 +03:00
codex 77a62d4564 fix: restore DOCX workbench runtime path 2026-07-30 14:00:31 +03:00
184 changed files with 49518 additions and 739 deletions
@@ -3,3 +3,5 @@
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.
Before Word starts, the template is checked for the insertion point. If none is found, the build is rejected without starting Word. The GUI routes `.docx` through Open XML and `.txt` through `TextDocumentInspector`; template and body analysis results remain independent.
@@ -0,0 +1,14 @@
# Learned document templates
DOCX Workbench supports two deterministic workflows:
1. `TemplateLearningService` normalizes a `.doc` or copies a `.docx` into a temporary working directory, inspects it, adds a safe `{{DOC_BODY}}` insertion marker to the working copy, and writes a portable `.dtemplate` ZIP package.
2. `TemplateDocumentGenerator` opens the package, replaces explicitly supplied scalar tokens, imports a `.doc`, `.docx`, or `.txt` body through the existing main-story importer, and creates DOCX, PDF, generation, and validation outputs.
The package contains `manifest.json`, `template.docx`, source metadata, page archetypes, regions, slots, field bindings, mapping suggestions, and a validation profile. It contains no original source binary unless it is the normalized template copy.
All Word automation is late-bound and serialized through the existing STA queue. Binary `.doc` sources are opened read-only, macros are disabled through Word automation security, and the source SHA-256 is checked before and after normalization. The source body’s headers, footers, section properties, and page setup are excluded by `DocxBodyImporter`.
The current UI exposes three modes: template learning, document generation, and the existing quick `DOC_BODY` path. Page archetypes and slot proposals are deterministic suggestions and require later user confirmation; Ollama is not required for the workflow.
Known limitation: the first slice stores the learned structural model and default slots, but does not yet provide a pixel-level page editor or automatic extraction of every scalar field from an arbitrary engineering drawing. Those operations remain explicit user mapping work.
@@ -1,5 +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`.
Word is resolved late-bound through `Word.Application`; the Word PIA and `office.dll` are not required at application load time. Each service owns one serial `StaWordQueue`, and every Word job runs on its dedicated STA thread 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 proxies 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.
The prototype does not run as a Windows Service, does not kill unrelated `WINWORD.EXE` processes, and does not execute macros. Integration tests compare process IDs before and after each job and verify that no newly-created Word process remains.
@@ -2,6 +2,99 @@ using System.Text.Json.Serialization;
namespace DocumentAutomation.Shared;
public sealed record InspectionWarning
{
public required string Code { get; init; }
public required string Message { get; init; }
public string? Part { get; init; }
public string? Location { get; init; }
public string? MatchedValue { get; init; }
public string? DetectionRule { get; init; }
}
public sealed record DocumentFieldInfo
{
public required string Type { get; init; }
public required string Part { get; init; }
public int? Section { get; init; }
public string? Instruction { get; init; }
}
public sealed record DocumentImageSummary
{
public int RasterImages { get; init; }
public int DrawingObjects { get; init; }
public int VmlShapes { get; init; }
public int ExternalImages { get; init; }
}
public sealed record DocumentStyleInfo
{
public required string StyleId { get; init; }
public string? Name { get; init; }
public string? Type { get; init; }
public bool IsDefault { get; init; }
public string? BasedOn { get; init; }
public string? LinkedStyle { get; init; }
public int UsageCount { get; init; }
}
public sealed record SectionPartAssignment
{
public required int Section { get; init; }
public IReadOnlyList<string> Headers { get; init; } = [];
public IReadOnlyList<string> Footers { get; init; } = [];
public bool LinkToPrevious { get; init; }
}
public sealed record InsertionPointInfo
{
public required string Type { get; init; }
public string? Location { get; init; }
}
public enum SectionBreakPolicy { Remove, ConvertToParagraph, ConvertToPageBreak }
public enum RevisionImportPolicy { PreserveAndWarn, AcceptAll, RejectAll, BlockImport }
public enum CommentImportPolicy { PreserveAndWarn, Exclude, BlockImport }
public enum StyleConflictPolicy { KeepSourceFormatting, UseDestinationStyles, MapKnownStyles }
public sealed record WordOperationProgress
{
public required string Stage { get; init; }
public string? Message { get; init; }
public int? Percent { get; init; }
}
public sealed record BodyImportPlan
{
public required int ParagraphCount { get; init; }
public required int TableCount { get; init; }
public required int InlineShapeCount { get; init; }
public required int FloatingShapeCount { get; init; }
public required int InternalSectionBreakCount { get; init; }
public required int PageBreakCount { get; init; }
public required IReadOnlyList<string> IncludedStories { get; init; }
public required IReadOnlyList<string> ExcludedStories { get; init; }
public required IReadOnlyList<string> Warnings { get; init; }
}
public sealed record DocxBodyImportRequest
{
public required string SourcePath { get; init; }
public required object DestinationRangeHandle { get; init; }
public SectionBreakPolicy SectionBreakPolicy { get; init; } = SectionBreakPolicy.ConvertToPageBreak;
public RevisionImportPolicy RevisionPolicy { get; init; } = RevisionImportPolicy.PreserveAndWarn;
public CommentImportPolicy CommentPolicy { get; init; } = CommentImportPolicy.Exclude;
public StyleConflictPolicy StyleConflictPolicy { get; init; } = StyleConflictPolicy.KeepSourceFormatting;
}
public sealed record BodyImportResult
{
public required BodyImportPlan Plan { get; init; }
public required string InsertionMode { get; init; }
public required IReadOnlyList<string> Warnings { get; init; }
}
public sealed record DocumentInspectionResult
{
public required string FilePath { get; init; }
@@ -11,18 +104,24 @@ public sealed record DocumentInspectionResult
public int Paragraphs { get; init; }
public int Tables { get; init; }
public int Images { get; init; }
public DocumentImageSummary ImageSummary { get; init; } = new();
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 IReadOnlyList<DocumentFieldInfo> Fields { 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 IReadOnlyList<DocumentStyleInfo> StyleDetails { get; init; } = [];
public IReadOnlyList<SectionPartAssignment> SectionAssignments { get; init; } = [];
public InsertionPointInfo InsertionPoint { get; init; } = new() { Type = "None" };
public BodyImportPlan? BodyImportPlan { get; init; }
public IReadOnlyList<InspectionWarning> Warnings { get; init; } = [];
public IReadOnlyList<string> Styles => StyleDetails.Select(x => x.StyleId).ToArray();
}
public sealed record WordDocumentBuildRequest
@@ -42,6 +141,7 @@ public sealed record WordDocumentResult
public required string LogPath { get; init; }
public required TimeSpan Duration { get; init; }
public required string InsertionPoint { get; init; }
public BodyImportResult? BodyImport { get; init; }
}
public sealed record WordPreviewResult
@@ -0,0 +1,153 @@
namespace DocumentAutomation.Shared;
public enum TemplateSlotKind { PlainText, RichText, MainBody, Table, RepeatingTableRows, Image, Signature, Date, DocumentField }
public enum TemplateRegionKind { Fixed, Variable, Repeating, Ignored }
public sealed record TemplateSlotOccurrence
{
public required string Location { get; init; }
public int? Page { get; init; }
public string? Story { get; init; }
public string? XPath { get; init; }
}
public sealed record TemplateSlot
{
public required string Id { get; init; }
public required string Name { get; init; }
public required TemplateSlotKind Kind { get; init; }
public required string ContentControlTag { get; init; }
public bool IsRequired { get; init; }
public bool IsRepeating { get; init; }
public string? PageArchetypeId { get; init; }
public string? RepeatGroup { get; init; }
public required IReadOnlyList<TemplateSlotOccurrence> Occurrences { get; init; }
}
public sealed record FixedRegion
{
public required string Id { get; init; }
public required string Name { get; init; }
public required TemplateRegionKind Kind { get; init; }
public required IReadOnlyList<int> Pages { get; init; }
public required string Fingerprint { get; init; }
public string? Story { get; init; }
public string? Location { get; init; }
public double Confidence { get; init; }
}
public sealed record RepeatingRegion
{
public required string Id { get; init; }
public required string Name { get; init; }
public required IReadOnlyList<int> Pages { get; init; }
public required string Fingerprint { get; init; }
public required TemplateRegionKind Kind { get; init; }
public string? Location { get; init; }
}
public sealed record PageArchetype
{
public required string Id { get; init; }
public required string Name { get; init; }
public bool IsFirstPage { get; init; }
public bool IsRepeating { get; init; }
public required IReadOnlyList<int> ExamplePages { get; init; }
public required IReadOnlyList<string> FixedRegionIds { get; init; }
public required IReadOnlyList<string> SlotIds { get; init; }
}
public sealed record TemplateFieldBinding
{
public required string SourceField { get; init; }
public required string TargetSlot { get; init; }
public required string Scope { get; init; }
public string? SourceStyle { get; init; }
public string? TargetStyle { get; init; }
}
public sealed record SourceDocumentStructure
{
public required string SourcePath { get; init; }
public required string Sha256 { get; init; }
public required string NormalizedPath { get; init; }
public required string NormalizedSha256 { get; init; }
public required int PageCount { get; init; }
public required int SectionCount { get; init; }
public required IReadOnlyList<string> Stories { get; init; }
public required IReadOnlyList<string> RepeatedFingerprints { get; init; }
public required DocumentInspectionResult Inspection { get; init; }
}
public sealed record TemplateLearningSuggestion
{
public required string SuggestionType { get; init; }
public required string SourceLocation { get; init; }
public string? TargetSlot { get; init; }
public string? ProposedName { get; init; }
public double Confidence { get; init; }
public required string Reason { get; init; }
public bool RequiresConfirmation { get; init; } = true;
}
public sealed record LearnedTemplate
{
public required string TemplateId { get; init; }
public required string Name { get; init; }
public required string SourceFileName { get; init; }
public required string SourceSha256 { get; init; }
public required string NormalizedSha256 { get; init; }
public required string CreatedAt { get; init; }
public string MinimumEngineVersion { get; init; } = "1.0";
public required SourceDocumentStructure Source { get; init; }
public required IReadOnlyList<PageArchetype> PageArchetypes { get; init; }
public required IReadOnlyList<FixedRegion> FixedRegions { get; init; }
public required IReadOnlyList<RepeatingRegion> RepeatingRegions { get; init; }
public required IReadOnlyList<TemplateSlot> Slots { get; init; }
public required IReadOnlyList<TemplateFieldBinding> FieldBindings { get; init; }
public required IReadOnlyList<TemplateLearningSuggestion> Suggestions { get; init; }
}
public sealed record DocumentNormalizationRequest
{
public required string SourcePath { get; init; }
public required string WorkingDirectory { get; init; }
public bool OpenReadOnly { get; init; } = true;
public bool DisableMacros { get; init; } = true;
}
public sealed record DocumentNormalizationResult
{
public required string SourcePath { get; init; }
public required string NormalizedPath { get; init; }
public required string SourceSha256 { get; init; }
public required string NormalizedSha256 { get; init; }
public required bool WasConverted { get; init; }
public required TimeSpan Duration { get; init; }
public int? PageCount { get; init; }
}
public sealed record TemplateLearningRequest
{
public required string SourcePath { get; init; }
public required string TemplateName { get; init; }
public required string OutputPackagePath { get; init; }
public bool CreateDefaultSlots { get; init; } = true;
}
public sealed record TemplateGenerationRequest
{
public required string TemplatePackagePath { get; init; }
public required string ContentSourcePath { get; init; }
public required string OutputDocxPath { get; init; }
public IReadOnlyDictionary<string, string> ScalarValues { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public sealed record TemplateGenerationResult
{
public required string OutputDocxPath { get; init; }
public required string OutputPdfPath { get; init; }
public required string GenerationLogPath { get; init; }
public required string ValidationLogPath { get; init; }
public required IReadOnlyList<RuleEvaluationResult> Validation { get; init; }
}
@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using DocumentAutomation.Shared;
using DocxWorkbench.Core;
@@ -10,24 +9,63 @@ 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();
private readonly ComboBox _mode = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 230 };
private readonly TextBox _template = new() { Width = 560 };
private readonly TextBox _body = new() { Width = 560 };
private readonly TextBox _package = new() { Width = 560 };
private readonly TextBox _templateName = new() { Width = 560, Text = "Learned document template" };
private readonly TextBox _output = new() { Width = 560 };
private readonly RichTextBox _structure = new() { Dock = DockStyle.Fill, ReadOnly = true };
private readonly RichTextBox _preview = new() { Dock = DockStyle.Fill, ReadOnly = true };
private readonly TextBox _log = new() { Multiline = true, ScrollBars = ScrollBars.Vertical, Dock = DockStyle.Fill, ReadOnly = true };
private readonly TextBox _technicalError = new() { Multiline = true, ScrollBars = ScrollBars.Vertical, Dock = DockStyle.Fill, ReadOnly = true };
private readonly Label _status = new() { AutoSize = true, Text = "Ready" };
private readonly Button _analyze; private readonly Button _learn; private readonly Button _generate; private readonly Button _build; private readonly Button _validate; private readonly Button _openDocx; private readonly Button _openPdf;
private readonly WorkbenchStateController _state = new();
private readonly IDocumentInspector _docx = new OpenXmlDocumentInspector(); private readonly IDocumentInspector _txt = new TextDocumentInspector(); private readonly IWordDocumentService _word = new WordDocumentService();
private DocumentInspectionResult? _templateResult; private DocumentInspectionResult? _bodyResult; private string? _builtDocx; private string? _builtPdf;
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;
Text = "DOCX Workbench"; Width = 1200; Height = 780; StartPosition = FormStartPosition.CenterScreen;
_mode.Items.AddRange(["Template learning", "Document generation", "Quick DOC_BODY"]); _mode.SelectedIndex = 2; _mode.SelectedIndexChanged += (_, _) => RefreshButtons();
var grid = new TableLayoutPanel { Dock = DockStyle.Top, Height = 245, ColumnCount = 3, RowCount = 7, Padding = new Padding(8) }; grid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 155)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110));
AddControlRow(grid, 0, "Mode", _mode, null); AddControlRow(grid, 1, "Template DOC/DOCX", _template, BrowseTemplate); AddControlRow(grid, 2, "Template name", _templateName, null); AddControlRow(grid, 3, "Content DOC/DOCX/TXT", _body, BrowseBody); AddControlRow(grid, 4, "Package .dtemplate", _package, BrowsePackage); AddControlRow(grid, 5, "Output DOCX", _output, BrowseOutput);
var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill }; _analyze = Button("Analyze", async (_, _) => await AnalyzeAsync()); _learn = Button("Learn template", async (_, _) => await LearnAsync()); _generate = Button("Generate", async (_, _) => await GenerateAsync()); _build = Button("Quick build", async (_, _) => await QuickBuildAsync()); _validate = Button("Validate", async (_, _) => await ValidateAsync()); _openDocx = Button("Open DOCX", (_, _) => Open(_builtDocx)); _openPdf = Button("Open PDF", (_, _) => Open(_builtPdf)); buttons.Controls.AddRange([_analyze, _learn, _generate, _build, _validate, _openDocx, _openPdf]); grid.Controls.Add(buttons, 1, 6); grid.SetColumnSpan(buttons, 2);
var tabs = new TabControl { Dock = DockStyle.Fill }; AddTab(tabs, "Structure", _structure); AddTab(tabs, "Preview", _preview); AddTab(tabs, "Log", _log); AddTab(tabs, "Technical error", _technicalError); Controls.Add(tabs); Controls.Add(grid); _status.Dock = DockStyle.Bottom; Controls.Add(_status); foreach (var box in new[] { _template, _body, _package, _output }) box.TextChanged += (_, _) => InputsChanged(); FormClosed += (_, _) => (_word as IDisposable)?.Dispose(); RefreshButtons();
}
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 }); }
private static void AddTab(TabControl tabs, string title, Control content) { var tab = new TabPage(title); tab.Controls.Add(content); tabs.TabPages.Add(tab); }
private static void AddControlRow(TableLayoutPanel panel, int row, string title, Control control, EventHandler? browse) { panel.Controls.Add(new Label { Text = title, AutoSize = true, Anchor = AnchorStyles.Left }, 0, row); panel.Controls.Add(control, 1, row); if (browse is not null) { var button = new Button { Text = "Browse...", Width = 95 }; button.Click += browse; panel.Controls.Add(button, 2, row); } }
private static Button Button(string text, EventHandler handler) { var button = new Button { Text = text, AutoSize = true }; button.Click += handler; return button; }
private void BrowseTemplate(object? sender, EventArgs e) { using var dialog = new OpenFileDialog { Filter = "Word document|*.doc;*.docx" }; if (dialog.ShowDialog() == DialogResult.OK) _template.Text = dialog.FileName; }
private void BrowseBody(object? sender, EventArgs e) { using var dialog = new OpenFileDialog { Filter = "Word or text|*.doc;*.docx;*.txt" }; if (dialog.ShowDialog() == DialogResult.OK) _body.Text = dialog.FileName; }
private void BrowsePackage(object? sender, EventArgs e) { using var dialog = new OpenFileDialog { Filter = "Learned template|*.dtemplate" }; if (dialog.ShowDialog() == DialogResult.OK) _package.Text = dialog.FileName; }
private void BrowseOutput(object? sender, EventArgs e) { using var dialog = new SaveFileDialog { Filter = "DOCX|*.docx", DefaultExt = "docx" }; if (dialog.ShowDialog() == DialogResult.OK) _output.Text = dialog.FileName; }
private async Task LearnAsync()
{
try { _status.Text = "Learning template..."; using var normalizer = new WordDocumentNormalizer(); var service = new TemplateLearningService(normalizer, new TemplatePackageStore(), _docx); var learned = await service.LearnAsync(new TemplateLearningRequest { SourcePath = _template.Text, TemplateName = _templateName.Text, OutputPackagePath = _package.Text }, null, CancellationToken.None); _structure.Text = JsonSerializer.Serialize(learned, new JsonSerializerOptions { WriteIndented = true }); AppendLog($"Template package created: {_package.Text}"); _status.Text = "Template learned"; } catch (Exception ex) { ShowError(ex, "Template learning failed"); }
}
private async Task GenerateAsync()
{
try { _status.Text = "Generating..."; using var normalizer = new WordDocumentNormalizer(); using var generator = new TemplateDocumentGenerator(new TemplatePackageStore(), normalizer, _docx); var result = await generator.GenerateAsync(new TemplateGenerationRequest { TemplatePackagePath = _package.Text, ContentSourcePath = _body.Text, OutputDocxPath = _output.Text }, null, CancellationToken.None); _builtDocx = result.OutputDocxPath; _builtPdf = result.OutputPdfPath; _preview.Text = JsonSerializer.Serialize(result.Validation, new JsonSerializerOptions { WriteIndented = true }); AppendLog(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true })); _status.Text = "Generation succeeded"; } catch (Exception ex) { ShowError(ex, "Generation failed"); }
}
private async Task AnalyzeAsync()
{
_status.Text = "Analyzing..."; var template = await InspectPathAsync(_template.Text); var body = await InspectPathAsync(_body.Text); _templateResult = template.Result; _bodyResult = body.Result; if (_templateResult is not null) ShowStructure(_templateResult); if (_bodyResult is not null) _preview.Text = _body.Text.EndsWith(".txt", StringComparison.OrdinalIgnoreCase) ? File.ReadAllText(_body.Text) : JsonSerializer.Serialize(_bodyResult, new JsonSerializerOptions { WriteIndented = true }); LogInspection("Template", template); LogInspection("Body", body); _state.InputsSelected(template.Result is not null && body.Result is not null); _state.AnalysisCompleted(template.Result is not null, body.Result is not null, template.Result?.InsertionPoint.Type != "None"); _status.Text = _state.State == WorkbenchState.ReadyToBuild ? "Ready" : "Analysis completed with warnings"; RefreshButtons();
}
private Task<(DocumentInspectionResult? Result, Exception? Error, string Inspector)> InspectPathAsync(string path) => Task.Run(() => { try { if (path.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) return (_docx.Inspect(path), (Exception?)null, "OpenXmlDocumentInspector"); if (path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) return (_txt.Inspect(path), (Exception?)null, "TextDocumentInspector"); throw new UnsupportedDocumentFormatException($"Unsupported format {Path.GetExtension(path)}. Use DOC/DOCX/TXT or normalize DOC first."); } catch (Exception ex) { return ((DocumentInspectionResult?)null, ex, "format-router"); } });
private async Task QuickBuildAsync() { if (!_state.CanBuild) { await AnalyzeAsync(); if (!_state.CanBuild) return; } _state.BeginBuild(); RefreshButtons(); try { var result = await _word.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = _template.Text, BodyPath = _body.Text, OutputDocxPath = _output.Text }, null, CancellationToken.None); var pdf = await _word.ExportPdfAsync(result.OutputDocxPath, Path.ChangeExtension(result.OutputDocxPath, ".pdf"), CancellationToken.None); _builtDocx = result.OutputDocxPath; _builtPdf = pdf.PdfPath; _state.BuildSucceeded(); AppendLog(JsonSerializer.Serialize(new { result, pdf }, new JsonSerializerOptions { WriteIndented = true })); _status.Text = "Quick build succeeded"; } catch (Exception ex) { _state.BuildFailed(); ShowError(ex, "Build failed"); } finally { RefreshButtons(); } }
private async Task ValidateAsync() { if (!_state.CanValidate || _builtDocx is null || !File.Exists(_builtDocx)) return; try { var result = await Task.Run(() => _docx.Inspect(_builtDocx)); AppendLog(JsonSerializer.Serialize(new { label = "Output", result }, new JsonSerializerOptions { WriteIndented = true })); _status.Text = "Validation succeeded"; } catch (Exception ex) { ShowError(ex, "Validation failed"); } }
private void ShowStructure(DocumentInspectionResult result) => _structure.Text = JsonSerializer.Serialize(new { result.FilePath, result.Sections, result.Paragraphs, result.Tables, result.Headers, result.Footers, result.Fields, result.ImageSummary, result.SectionAssignments, result.StyleDetails, result.Warnings }, new JsonSerializerOptions { WriteIndented = true });
private void LogInspection(string label, (DocumentInspectionResult? Result, Exception? Error, string Inspector) value) { AppendLog(JsonSerializer.Serialize(new { label, inspector = value.Inspector, result = value.Result, error = value.Error?.Message }, new JsonSerializerOptions { WriteIndented = true })); if (value.Error is not null) _technicalError.Text = value.Error.Message; }
private void InputsChanged() { _builtDocx = null; _builtPdf = null; RefreshButtons(); }
private void RefreshButtons() { var mode = _mode.SelectedIndex; var templateSupported = _template.Text.EndsWith(".doc", StringComparison.OrdinalIgnoreCase) || _template.Text.EndsWith(".docx", StringComparison.OrdinalIgnoreCase); _analyze.Enabled = mode != 1 && File.Exists(_template.Text) && File.Exists(_body.Text); _learn.Enabled = mode == 0 && templateSupported && File.Exists(_template.Text) && !string.IsNullOrWhiteSpace(_package.Text); _generate.Enabled = mode == 1 && File.Exists(_package.Text) && File.Exists(_body.Text) && !string.IsNullOrWhiteSpace(_output.Text); _build.Enabled = mode == 2 && _state.CanBuild; _validate.Enabled = _state.CanValidate && _builtDocx is not null && File.Exists(_builtDocx); _openDocx.Enabled = _builtDocx is not null && File.Exists(_builtDocx); _openPdf.Enabled = _builtPdf is not null && File.Exists(_builtPdf); }
private void ShowError(Exception ex, string title) { _technicalError.Text = ex.ToString(); AppendLog(title + ": " + ex.Message); _status.Text = title; MessageBox.Show(ex.Message, title, MessageBoxButtons.OK, MessageBoxIcon.Error); }
private void AppendLog(string text) => _log.AppendText(text + Environment.NewLine);
private static void Open(string? path) { if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); }
}
@@ -7,12 +7,59 @@ public interface IDocumentInspector
DocumentInspectionResult Inspect(string docxPath);
}
public sealed class UnsupportedDocumentFormatException(string message) : InvalidOperationException(message);
public enum WorkbenchState { Empty, InputsSelected, Analyzed, ReadyToBuild, Building, BuildSucceeded, BuildFailed, Validating, ValidationSucceeded, ValidationFailed }
public sealed class WorkbenchStateController
{
public WorkbenchState State { get; private set; } = WorkbenchState.Empty;
public bool CanBuild => State == WorkbenchState.ReadyToBuild;
public bool CanValidate => State == WorkbenchState.BuildSucceeded;
public bool CanOpenDocx { get; private set; }
public bool CanOpenPdf { get; private set; }
public void InputsSelected(bool selected) { State = selected ? WorkbenchState.InputsSelected : WorkbenchState.Empty; if (!selected) ClearArtifacts(); }
public void AnalysisCompleted(bool templateOk, bool bodyOk, bool insertionPointFound) { State = templateOk && bodyOk && insertionPointFound ? WorkbenchState.ReadyToBuild : WorkbenchState.Analyzed; ClearArtifacts(); }
public void BeginBuild() { if (!CanBuild) throw new InvalidOperationException("Build is unavailable in the current state."); State = WorkbenchState.Building; ClearArtifacts(); }
public void BuildSucceeded() { State = WorkbenchState.BuildSucceeded; CanOpenDocx = true; CanOpenPdf = true; }
public void BuildFailed() { State = WorkbenchState.BuildFailed; ClearArtifacts(); }
public void BeginValidation() { if (!CanValidate) throw new InvalidOperationException("Validation is available only after a successful build."); State = WorkbenchState.Validating; }
public void ValidationCompleted(bool success) => State = success ? WorkbenchState.ValidationSucceeded : WorkbenchState.ValidationFailed;
private void ClearArtifacts() { CanOpenDocx = false; CanOpenPdf = false; }
}
public interface IWordDocumentService
{
Task<WordDocumentResult> BuildDocumentAsync(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken cancellationToken);
Task<WordPreviewResult> ExportPdfAsync(string docxPath, string outputPdfPath, CancellationToken cancellationToken);
}
public interface IDocxBodyImporter
{
Task<BodyImportResult> ImportAsync(DocxBodyImportRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken);
}
public interface IWordDocumentNormalizer
{
Task<DocumentNormalizationResult> NormalizeAsync(DocumentNormalizationRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken);
}
public interface ITemplateLearningService
{
Task<LearnedTemplate> LearnAsync(TemplateLearningRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken);
}
public interface ITemplatePackageStore
{
void Save(string packagePath, LearnedTemplate template, string normalizedDocxPath);
(LearnedTemplate Template, string TemplateDocxPath, string WorkingDirectory) Open(string packagePath);
}
public interface ITemplateDocumentGenerator
{
Task<TemplateGenerationResult> GenerateAsync(TemplateGenerationRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken);
}
public static class DocumentInputValidation
{
public static void Validate(WordDocumentBuildRequest request)
@@ -1,39 +1,77 @@
using System.Text.RegularExpressions;
using DocumentAutomation.Shared;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxWorkbench.Core;
namespace DocxWorkbench.OpenXml;
public sealed class DocumentInspector : IDocumentInspector
public sealed class OpenXmlDocumentInspector : IDocumentInspector
{
private static readonly string[] KnownFields = ["PAGE", "NUMPAGES", "SECTIONPAGES", "FILENAME"];
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.");
if (!File.Exists(docxPath)) throw new FileNotFoundException("DOCX was not found.", docxPath);
if (!docxPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) throw new UnsupportedDocumentFormatException($"Open XML inspector accepts DOCX only, received {Path.GetExtension(docxPath)}.");
var info = new FileInfo(docxPath); using var package = WordprocessingDocument.Open(docxPath, false); var main = package.MainDocumentPart ?? throw new InvalidDataException("The DOCX has no main document part."); var roots = GetRoots(main); var fields = roots.SelectMany(x => FindFields(x.Root, x.Part, x.Section)).ToArray(); var images = CountImages(main, roots);
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
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 = images.RasterImages, ImageSummary = images,
Headers = main.HeaderParts.Count(), Footers = main.FooterParts.Count(), HasPageFields = fields.Any(x => x.Type == "PAGE"), HasNumPagesFields = fields.Any(x => x.Type == "NUMPAGES"), HasSectionPagesFields = fields.Any(x => x.Type == "SECTIONPAGES"), Fields = fields,
Bookmarks = main.Document.Descendants<BookmarkStart>().Count(), ContentControls = main.Document.Descendants<SdtElement>().Count(), Comments = main.GetPartsOfType<WordprocessingCommentsPart>().Count(), HasTrackedChanges = main.Document.Descendants<Inserted>().Any() || main.Document.Descendants<Deleted>().Any(), HasProtectedParts = main.Document.Descendants<DocumentProtection>().Any(),
StyleDetails = GetStyles(main), SectionAssignments = GetSectionAssignments(main), InsertionPoint = FindInsertionPoint(main.Document), BodyImportPlan = CreateBodyImportPlan(main), Warnings = FindWarnings(roots)
};
}
private static IReadOnlyList<(OpenXmlPartRootElement Root, string Part, int? Section)> GetRoots(MainDocumentPart main)
{
var roots = new List<(OpenXmlPartRootElement, string, int?)> { (main.Document, "MainDocumentPart", null) }; roots.AddRange(main.HeaderParts.Select((p, i) => ((OpenXmlPartRootElement)p.Header, $"HeaderPart[{i + 1}]", (int?)null))); roots.AddRange(main.FooterParts.Select((p, i) => ((OpenXmlPartRootElement)p.Footer, $"FooterPart[{i + 1}]", (int?)null))); if (main.DocumentSettingsPart?.Settings is not null) roots.Add((main.DocumentSettingsPart.Settings, "SettingsPart", null)); if (main.FootnotesPart?.Footnotes is not null) roots.Add((main.FootnotesPart.Footnotes, "FootnotesPart", null)); if (main.EndnotesPart?.Endnotes is not null) roots.Add((main.EndnotesPart.Endnotes, "EndnotesPart", null)); foreach (var comments in main.GetPartsOfType<WordprocessingCommentsPart>()) if (comments.Comments is not null) roots.Add((comments.Comments, "CommentsPart", null)); return roots;
}
private static IEnumerable<DocumentFieldInfo> FindFields(OpenXmlElement root, string part, int? section)
{
foreach (var simple in root.Descendants<SimpleField>()) { var type = NormalizeField(simple.Instruction?.Value); if (type is not null) yield return new DocumentFieldInfo { Type = type, Part = part, Section = section, Instruction = simple.Instruction?.Value }; }
var active = new List<string>(); foreach (var element in root.Descendants()) { if (element is FieldChar begin && begin.FieldCharType?.Value == FieldCharValues.Begin) active.Add(""); else if (element is FieldCode code && active.Count > 0) active[^1] += " " + code.Text; else if (element is FieldChar end && end.FieldCharType?.Value == FieldCharValues.End && active.Count > 0) { var raw = active[^1]; active.RemoveAt(active.Count - 1); var type = NormalizeField(raw); if (type is not null) yield return new DocumentFieldInfo { Type = type, Part = part, Section = section, Instruction = raw.Trim() }; } }
}
private static string? NormalizeField(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var normalized = Regex.Replace(value, @"\s+", " ").Trim(); var name = Regex.Match(normalized, @"(?i)^(PAGE|NUMPAGES|SECTIONPAGES|FILENAME)\b").Value.ToUpperInvariant(); return KnownFields.Contains(name) ? name : null; }
private static DocumentImageSummary CountImages(MainDocumentPart main, IReadOnlyList<(OpenXmlPartRootElement Root, string Part, int? Section)> roots)
{
var imageParts = new HashSet<OpenXmlPart>(); var drawings = roots.Sum(x => x.Root.Descendants<Drawing>().Count()); var shapes = roots.Sum(x => x.Root.Descendants<DocumentFormat.OpenXml.Vml.Shape>().Count()); var external = 0;
foreach (var image in main.GetPartsOfType<ImagePart>()) imageParts.Add(image); foreach (var header in main.HeaderParts) { foreach (var image in header.GetPartsOfType<ImagePart>()) imageParts.Add(image); external += header.ExternalRelationships.Count(x => x.Uri?.ToString().Contains("image", StringComparison.OrdinalIgnoreCase) == true); } foreach (var footer in main.FooterParts) { foreach (var image in footer.GetPartsOfType<ImagePart>()) imageParts.Add(image); external += footer.ExternalRelationships.Count(x => x.Uri?.ToString().Contains("image", StringComparison.OrdinalIgnoreCase) == true); }
external += main.ExternalRelationships.Count(x => x.Uri?.ToString().Contains("image", StringComparison.OrdinalIgnoreCase) == true); return new DocumentImageSummary { RasterImages = imageParts.Count, DrawingObjects = drawings, VmlShapes = shapes, ExternalImages = external };
}
private static IReadOnlyList<DocumentStyleInfo> GetStyles(MainDocumentPart main)
{
var usage = main.Document.Descendants<ParagraphStyleId>().GroupBy(x => x.Val?.Value ?? "").ToDictionary(x => x.Key, x => x.Count()); return main.StyleDefinitionsPart?.Styles?.Elements<Style>().Select(style => new DocumentStyleInfo { StyleId = style.StyleId?.Value ?? "", Name = style.StyleName?.Val?.Value, Type = style.Type is null ? null : style.Type.Value.ToString(), IsDefault = style.Default?.Value == true, BasedOn = style.BasedOn?.Val?.Value, UsageCount = usage.GetValueOrDefault(style.StyleId?.Value ?? "") }).Where(x => x.StyleId.Length > 0).ToArray() ?? [];
}
private static IReadOnlyList<SectionPartAssignment> GetSectionAssignments(MainDocumentPart main)
{
var result = new List<SectionPartAssignment>(); var sections = main.Document.Body?.Elements<SectionProperties>().ToArray() ?? []; for (var i = 0; i < sections.Length; i++) { var section = sections[i]; var headers = section.Elements<HeaderReference>().Select(x => $"{x.Type?.Value}:{PartName(main, x.Id?.Value)}").ToArray(); var footers = section.Elements<FooterReference>().Select(x => $"{x.Type?.Value}:{PartName(main, x.Id?.Value)}").ToArray(); result.Add(new SectionPartAssignment { Section = i + 1, Headers = headers, Footers = footers, LinkToPrevious = i > 0 && headers.Length == 0 && footers.Length == 0 }); } return result;
}
private static string PartName(MainDocumentPart main, string? id) { if (id is null) return "missing"; try { return main.GetPartById(id).Uri.ToString(); } catch { return "unresolved:" + id; } }
private static InsertionPointInfo FindInsertionPoint(Document root) { if (root.Descendants<SdtElement>().Any(x => string.Equals(x.SdtProperties?.GetFirstChild<Tag>()?.Val?.Value, "DOC_BODY", StringComparison.OrdinalIgnoreCase))) return new InsertionPointInfo { Type = "ContentControl", Location = "MainDocumentPart" }; if (root.Descendants<BookmarkStart>().Any(x => string.Equals(x.Name?.Value, "DOC_BODY", StringComparison.OrdinalIgnoreCase))) return new InsertionPointInfo { Type = "Bookmark", Location = "MainDocumentPart" }; if (root.InnerText.Contains("{{DOC_BODY}}", StringComparison.Ordinal)) return new InsertionPointInfo { Type = "TextMarker", Location = "MainDocumentPart" }; return new InsertionPointInfo { Type = "None" }; }
private static IReadOnlyList<InspectionWarning> FindWarnings(IReadOnlyList<(OpenXmlPartRootElement Root, string Part, int? Section)> roots) { var result = new List<InspectionWarning>(); foreach (var root in roots) foreach (Match match in Regex.Matches(root.Root.InnerText, @"(?i)\b(2019|legacy|старый)\b")) result.Add(new InspectionWarning { Code = "PossibleLegacyProjectData", Message = "A value matched the legacy-project heuristic.", Part = root.Part, Location = "text", MatchedValue = match.Value, DetectionRule = "word-boundary: 2019|legacy|старый" }); return result; }
private static BodyImportPlan CreateBodyImportPlan(MainDocumentPart main)
{
var body = main.Document.Body; if (body is null) return new BodyImportPlan { ParagraphCount = 0, TableCount = 0, InlineShapeCount = 0, FloatingShapeCount = 0, InternalSectionBreakCount = 0, PageBreakCount = 0, IncludedStories = ["wdMainTextStory"], ExcludedStories = ["wdPrimaryHeaderStory", "wdFirstPageHeaderStory", "wdEvenPagesHeaderStory", "wdPrimaryFooterStory", "wdFirstPageFooterStory", "wdEvenPagesFooterStory"], Warnings = [] };
var drawings = body.Descendants<Drawing>(); return new BodyImportPlan { ParagraphCount = body.Descendants<Paragraph>().Count(), TableCount = body.Descendants<Table>().Count(), InlineShapeCount = drawings.Count(x => x.Ancestors<DocumentFormat.OpenXml.Drawing.Wordprocessing.Inline>().Any()), FloatingShapeCount = drawings.Count(x => x.Ancestors<DocumentFormat.OpenXml.Drawing.Wordprocessing.Anchor>().Any()), InternalSectionBreakCount = Math.Max(0, body.Descendants<SectionProperties>().Count() - 1), PageBreakCount = body.Descendants<Break>().Count(x => x.Type?.Value == BreakValues.Page), IncludedStories = ["wdMainTextStory"], ExcludedStories = ["wdPrimaryHeaderStory", "wdFirstPageHeaderStory", "wdEvenPagesHeaderStory", "wdPrimaryFooterStory", "wdFirstPageFooterStory", "wdEvenPagesFooterStory"], Warnings = ["Source headers, footers, page setup and final section properties are excluded."] };
}
}
public sealed class TextDocumentInspector : IDocumentInspector
{
public DocumentInspectionResult Inspect(string path) { if (!File.Exists(path)) throw new FileNotFoundException("TXT was not found.", path); if (!path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) throw new UnsupportedDocumentFormatException($"TXT inspector accepts TXT only, received {Path.GetExtension(path)}."); var info = new FileInfo(path); return new DocumentInspectionResult { FilePath = path, FileSize = info.Length, LastWriteTimeUtc = info.LastWriteTimeUtc, Sections = 1, Paragraphs = File.ReadAllLines(path).Length, InsertionPoint = new InsertionPointInfo { Type = "None" } }; }
}
public sealed class DocumentInspector : IDocumentInspector
{
private readonly OpenXmlDocumentInspector _inner = new();
public DocumentInspectionResult Inspect(string docxPath) => _inner.Inspect(docxPath);
}
@@ -0,0 +1,73 @@
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text.Json;
using DocumentAutomation.Shared;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxWorkbench.Core;
namespace DocxWorkbench.OpenXml;
public sealed class TemplatePackageStore : ITemplatePackageStore
{
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
public void Save(string packagePath, LearnedTemplate template, string normalizedDocxPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(packagePath))!);
if (File.Exists(packagePath)) File.Delete(packagePath);
using var archive = ZipFile.Open(packagePath, ZipArchiveMode.Create);
WriteJson(archive, "manifest.json", new { schemaVersion = "1.0", templateId = template.TemplateId, name = template.Name, sourceFileName = template.SourceFileName, sourceSha256 = template.SourceSha256, normalizedSha256 = template.NormalizedSha256, createdAt = template.CreatedAt, minimumEngineVersion = template.MinimumEngineVersion });
WriteJson(archive, "source-metadata.json", template.Source);
WriteJson(archive, "page-archetypes.json", template.PageArchetypes);
WriteJson(archive, "fixed-regions.json", template.FixedRegions.Concat(template.RepeatingRegions.Select(x => new FixedRegion { Id = x.Id, Name = x.Name, Kind = x.Kind, Pages = x.Pages, Fingerprint = x.Fingerprint, Location = x.Location })).ToArray());
WriteJson(archive, "slots.json", template.Slots);
WriteJson(archive, "field-bindings.json", template.FieldBindings);
WriteJson(archive, "mapping-rules.json", template.Suggestions);
WriteJson(archive, "validation-profile.json", new { schemaVersion = "1.0", requiredSlots = template.Slots.Where(x => x.IsRequired).Select(x => x.ContentControlTag).ToArray(), allowTrackedChanges = false, allowExternalLinks = false });
var entry = archive.CreateEntry("template.docx", CompressionLevel.Optimal); using var input = File.OpenRead(normalizedDocxPath); using var output = entry.Open(); input.CopyTo(output);
}
public (LearnedTemplate Template, string TemplateDocxPath, string WorkingDirectory) Open(string packagePath)
{
if (!File.Exists(packagePath)) throw new FileNotFoundException("Template package was not found.", packagePath);
var work = Path.Combine(Path.GetTempPath(), "DocumentAutomation", "template-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(work); ZipFile.ExtractToDirectory(packagePath, work);
var manifest = ReadJson<Manifest>(Path.Combine(work, "manifest.json")); if (manifest.SchemaVersion != "1.0") throw new InvalidDataException($"Unsupported template schema: {manifest.SchemaVersion}");
var template = new LearnedTemplate { TemplateId = manifest.TemplateId, Name = manifest.Name, SourceFileName = manifest.SourceFileName, SourceSha256 = manifest.SourceSha256, NormalizedSha256 = manifest.NormalizedSha256, CreatedAt = manifest.CreatedAt, MinimumEngineVersion = manifest.MinimumEngineVersion, Source = ReadJson<SourceDocumentStructure>(Path.Combine(work, "source-metadata.json")), PageArchetypes = ReadJson<List<PageArchetype>>(Path.Combine(work, "page-archetypes.json")), FixedRegions = ReadJson<List<FixedRegion>>(Path.Combine(work, "fixed-regions.json")).Where(x => x.Kind != TemplateRegionKind.Repeating).ToArray(), RepeatingRegions = ReadJson<List<FixedRegion>>(Path.Combine(work, "fixed-regions.json")).Where(x => x.Kind == TemplateRegionKind.Repeating).Select(x => new RepeatingRegion { Id = x.Id, Name = x.Name, Pages = x.Pages, Fingerprint = x.Fingerprint, Kind = x.Kind, Location = x.Location }).ToArray(), Slots = ReadJson<List<TemplateSlot>>(Path.Combine(work, "slots.json")), FieldBindings = ReadJson<List<TemplateFieldBinding>>(Path.Combine(work, "field-bindings.json")), Suggestions = ReadJson<List<TemplateLearningSuggestion>>(Path.Combine(work, "mapping-rules.json")) };
var docx = Path.Combine(work, "template.docx"); if (!File.Exists(docx)) throw new InvalidDataException("Template package does not contain template.docx."); return (template, docx, work);
}
private static void WriteJson<T>(ZipArchive archive, string name, T value) { var entry = archive.CreateEntry(name, CompressionLevel.Optimal); using var writer = new StreamWriter(entry.Open()); writer.Write(JsonSerializer.Serialize(value, JsonOptions)); }
private static T ReadJson<T>(string path) => JsonSerializer.Deserialize<T>(File.ReadAllText(path), JsonOptions) ?? throw new InvalidDataException($"Invalid template package entry: {Path.GetFileName(path)}");
private sealed record Manifest(string SchemaVersion, string TemplateId, string Name, string SourceFileName, string SourceSha256, string NormalizedSha256, string CreatedAt, string MinimumEngineVersion);
}
public sealed class TemplateLearningService : ITemplateLearningService
{
private readonly IWordDocumentNormalizer _normalizer;
private readonly ITemplatePackageStore _packages;
private readonly IDocumentInspector _inspector;
public TemplateLearningService(IWordDocumentNormalizer normalizer, ITemplatePackageStore packages, IDocumentInspector? inspector = null) { _normalizer = normalizer; _packages = packages; _inspector = inspector ?? new OpenXmlDocumentInspector(); }
public async Task<LearnedTemplate> LearnAsync(TemplateLearningRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken)
{
if (!File.Exists(request.SourcePath)) throw new FileNotFoundException("Template source was not found.", request.SourcePath);
var normalized = await _normalizer.NormalizeAsync(new DocumentNormalizationRequest { SourcePath = request.SourcePath, WorkingDirectory = Path.Combine(Path.GetTempPath(), "DocumentAutomation", "learning-" + Guid.NewGuid().ToString("N")) }, progress, cancellationToken);
try
{
progress?.Report(new WordOperationProgress { Stage = "AnalyzeTemplate", Message = "Inspecting normalized template" });
EnsureBodyMarker(normalized.NormalizedPath); var normalizedHash = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(normalized.NormalizedPath))); var inspection = _inspector.Inspect(normalized.NormalizedPath) with { FilePath = "template.docx" }; var pages = Math.Max(1, normalized.PageCount ?? inspection.Sections); var archetypes = CreateArchetypes(pages, inspection); var repeated = FindRepeatedRegions(inspection); var slots = request.CreateDefaultSlots ? DefaultSlots() : [];
var source = new SourceDocumentStructure { SourcePath = Path.GetFileName(request.SourcePath), Sha256 = normalized.SourceSha256, NormalizedPath = "template.docx", NormalizedSha256 = normalizedHash, PageCount = pages, SectionCount = inspection.Sections, Stories = ["wdMainTextStory", "wdPrimaryHeaderStory", "wdFirstPageHeaderStory", "wdEvenPagesHeaderStory", "wdPrimaryFooterStory", "wdFootnotesStory", "wdEndnotesStory", "wdCommentsStory", "wdTextFrameStory"], RepeatedFingerprints = repeated.Select(x => x.Fingerprint).ToArray(), Inspection = inspection };
var learned = new LearnedTemplate { TemplateId = Guid.NewGuid().ToString("D"), Name = request.TemplateName, SourceFileName = Path.GetFileName(request.SourcePath), SourceSha256 = normalized.SourceSha256, NormalizedSha256 = normalizedHash, CreatedAt = DateTimeOffset.Now.ToString("O"), Source = source, PageArchetypes = archetypes, FixedRegions = [], RepeatingRegions = repeated, Slots = slots, FieldBindings = [], Suggestions = BuildSuggestions(inspection, slots) };
_packages.Save(request.OutputPackagePath, learned, normalized.NormalizedPath); return learned;
}
finally { TryDeleteDirectory(normalized.NormalizedPath, normalized.SourcePath); }
}
private static void EnsureBodyMarker(string path) { using var doc = WordprocessingDocument.Open(path, true); var main = doc.MainDocumentPart ?? throw new InvalidDataException("Main document part is missing."); var body = main.Document.Body ?? throw new InvalidDataException("Document body is missing."); if (main.Document.InnerText.Contains("{{DOC_BODY}}", StringComparison.Ordinal)) return; body.AppendChild(new Paragraph(new Run(new Text("{{DOC_BODY}}")))); main.Document.Save(); }
private static IReadOnlyList<PageArchetype> CreateArchetypes(int pages, DocumentInspectionResult inspection) { var result = new List<PageArchetype> { new() { Id = "FirstPage", Name = "FirstPage", IsFirstPage = true, IsRepeating = false, ExamplePages = [1], FixedRegionIds = [], SlotIds = [] } }; if (pages > 1) result.Add(new PageArchetype { Id = "RepeatingPage", Name = "RepeatingPage", IsRepeating = true, ExamplePages = Enumerable.Range(2, pages - 1).ToArray(), FixedRegionIds = [], SlotIds = [] }); return result; }
private static IReadOnlyList<RepeatingRegion> FindRepeatedRegions(DocumentInspectionResult inspection) => inspection.SectionAssignments.GroupBy(x => string.Join("|", x.Headers.Concat(x.Footers))).Where(x => x.Count() > 1 && x.Key.Length > 0).Select((x, i) => new RepeatingRegion { Id = "region-" + (i + 1), Name = "Repeated header/footer", Pages = x.Select(y => y.Section).ToArray(), Fingerprint = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(x.Key))), Kind = TemplateRegionKind.Repeating, Location = "section assignments" }).ToArray();
private static IReadOnlyList<TemplateSlot> DefaultSlots() => new[] { "PROJECT.CODE", "PROJECT.NAME", "PROJECT.OBJECT", "DOCUMENT.CODE", "DOCUMENT.TITLE", "DOCUMENT.STAGE", "DOCUMENT.SHEET", "DOCUMENT.SHEET_COUNT", "REGISTER.CONTENTS", "REGISTER.MAIN_DRAWING_SETS", "REGISTER.DRAWINGS", "REGISTER.REFERENCE_DOCUMENTS", "DOCUMENT.GENERAL_NOTES", "DOCUMENT.MAIN_BODY" }.Select(x => new TemplateSlot { Id = x, Name = x, Kind = x == "DOCUMENT.MAIN_BODY" ? TemplateSlotKind.MainBody : TemplateSlotKind.PlainText, ContentControlTag = x, IsRequired = x is "DOCUMENT.CODE" or "DOCUMENT.TITLE" or "DOCUMENT.MAIN_BODY", Occurrences = [] }).ToArray();
private static IReadOnlyList<TemplateLearningSuggestion> BuildSuggestions(DocumentInspectionResult inspection, IReadOnlyList<TemplateSlot> slots) => [new TemplateLearningSuggestion { SuggestionType = "PageArchetype", SourceLocation = "document", ProposedName = inspection.Sections > 1 ? "FirstPage + RepeatingPage" : "SinglePage", Confidence = 0.5, Reason = "Initial deterministic proposal based on document sections." }];
private static void TryDeleteDirectory(string normalized, string source) { var directory = Path.GetDirectoryName(normalized); if (directory is not null && !Path.GetFullPath(source).Equals(Path.GetFullPath(normalized), StringComparison.OrdinalIgnoreCase)) try { if (Directory.Exists(directory)) Directory.Delete(directory, true); } catch { } }
}
@@ -0,0 +1,80 @@
using System.Runtime.InteropServices;
using System.IO.Compression;
using System.Text;
using System.Xml.Linq;
using DocumentAutomation.Shared;
using DocxWorkbench.Core;
namespace DocxWorkbench.WordInterop;
public sealed class DocxBodyImporter : IDocxBodyImporter, IDisposable
{
private readonly StaWordQueue _queue;
private readonly bool _ownsQueue;
public DocxBodyImporter() : this(new StaWordQueue(), true) { }
internal DocxBodyImporter(StaWordQueue queue) : this(queue, false) { }
private DocxBodyImporter(StaWordQueue queue, bool ownsQueue) { _queue = queue; _ownsQueue = ownsQueue; }
public Task<BodyImportResult> ImportAsync(DocxBodyImportRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken)
=> _queue.Enqueue(() => ImportStandalone(request, progress, cancellationToken), cancellationToken);
private BodyImportResult ImportStandalone(DocxBodyImportRequest request, IProgress<WordOperationProgress>? progress, CancellationToken token)
{
dynamic? application = null; var proxies = new List<object?>(); try { var type = Type.GetTypeFromProgID("Word.Application", false) ?? throw new WordNotInstalledException("Microsoft Word is not installed or is not registered in COM."); application = Activator.CreateInstance(type) ?? throw new WordAutomationException("Could not create Word.Application."); application.Visible = false; application.DisplayAlerts = 0; return ImportOnSta(application, null, request.DestinationRangeHandle, request, progress, token, proxies); } finally { try { if (application is not null) application.Quit(0); } catch { } Release(application); ReleaseAll(proxies); }
}
internal BodyImportResult ImportOnSta(dynamic application, dynamic destinationDocument, dynamic destinationRange, DocxBodyImportRequest request, IProgress<WordOperationProgress>? progress, CancellationToken token, List<object?> proxies)
{
if (!File.Exists(request.SourcePath)) throw new FileNotFoundException("Source DOCX was not found.", request.SourcePath); if (!request.SourcePath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) throw new UnsupportedDocumentFormatException("DocxBodyImporter accepts DOCX only.");
var plan = BuildPlanFromOpenXml(request.SourcePath);
var temp = Path.Combine(Path.GetTempPath(), "DocumentAutomation", "body-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(temp); var sourceCopy = Path.Combine(temp, "source.docx"); File.Copy(request.SourcePath, sourceCopy, true); NormalizeSourceSections(sourceCopy, request.SectionBreakPolicy); dynamic? sourceDocument = null; dynamic? sourceDocuments = null; dynamic? sourceRange = null; dynamic? importRange = null; var localProxies = new List<object?>();
try
{
token.ThrowIfCancellationRequested(); progress?.Report(new WordOperationProgress { Stage = "OpenSource", Message = "Opening source DOCX read-only" }); sourceDocuments = Own(application.Documents, localProxies); sourceDocument = Own(sourceDocuments.Open(sourceCopy, true, false, false), localProxies); dynamic stories = Own(sourceDocument.StoryRanges, localProxies); sourceRange = Own(stories[1], localProxies); var start = (int)sourceRange.Start; var end = (int)sourceRange.End; if (end > start) end -= 1; importRange = Own(sourceDocument.Range(start, end), localProxies);
var warnings = new List<string>(); var sourceRevisions = TryCount(importRange.Revisions); var sourceComments = TryCount(importRange.Comments); if (sourceRevisions > 0) { if (request.RevisionPolicy == RevisionImportPolicy.BlockImport) throw new WordAutomationException("Source DOCX contains tracked changes and RevisionPolicy is BlockImport."); if (request.RevisionPolicy == RevisionImportPolicy.AcceptAll) importRange.Revisions.AcceptAll(); if (request.RevisionPolicy == RevisionImportPolicy.RejectAll) importRange.Revisions.RejectAll(); if (request.RevisionPolicy == RevisionImportPolicy.PreserveAndWarn) warnings.Add($"Tracked changes preserved: {sourceRevisions}."); } if (sourceComments > 0) { if (request.CommentPolicy == CommentImportPolicy.BlockImport) throw new WordAutomationException("Source DOCX contains comments and CommentPolicy is BlockImport."); if (request.CommentPolicy == CommentImportPolicy.PreserveAndWarn) warnings.Add($"Comments preserved: {sourceComments}."); }
progress?.Report(new WordOperationProgress { Stage = "FormattedText", Message = "Copying only the main text story" }); destinationRange.FormattedText = importRange.FormattedText; if (request.CommentPolicy == CommentImportPolicy.Exclude) RemoveImportedComments(destinationRange); if (request.RevisionPolicy == RevisionImportPolicy.PreserveAndWarn) warnings.Add("Revision policy: PreserveAndWarn.");
plan = plan with { Warnings = warnings }; return new BodyImportResult { Plan = plan, InsertionMode = "FormattedText", Warnings = warnings };
}
finally { try { if (sourceDocument is not null) sourceDocument.Close(0); } catch { } ReleaseAll(localProxies); TryDelete(temp); }
}
private static BodyImportPlan BuildPlanFromOpenXml(string path)
{
using var archive = ZipFile.OpenRead(path);
var entry = archive.GetEntry("word/document.xml") ?? throw new InvalidDataException("Source DOCX document.xml is missing.");
using var reader = new StreamReader(entry.Open());
var document = XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
var w = XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main");
var wp = XNamespace.Get("http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing");
var body = document.Root?.Element(w + "body") ?? throw new InvalidDataException("Source DOCX body is missing.");
var sectionMarks = body.Descendants(w + "sectPr").ToList();
var internalSections = Math.Max(0, sectionMarks.Count - 1);
var pageBreaks = body.Descendants(w + "br").Count(x => string.Equals((string?)x.Attribute(w + "type"), "page", StringComparison.OrdinalIgnoreCase));
return new BodyImportPlan
{
ParagraphCount = body.Descendants(w + "p").Count(),
TableCount = body.Descendants(w + "tbl").Count(),
InlineShapeCount = body.Descendants(wp + "inline").Count(),
FloatingShapeCount = body.Descendants(wp + "anchor").Count(),
InternalSectionBreakCount = internalSections,
PageBreakCount = pageBreaks,
IncludedStories = ["wdMainTextStory"],
ExcludedStories = ["wdPrimaryHeaderStory", "wdFirstPageHeaderStory", "wdEvenPagesHeaderStory", "wdPrimaryFooterStory", "wdFirstPageFooterStory", "wdEvenPagesFooterStory"],
Warnings = []
};
}
private static void NormalizeSourceSections(string path, SectionBreakPolicy policy)
{
if (policy == SectionBreakPolicy.ConvertToParagraph) policy = SectionBreakPolicy.Remove;
using var archive = ZipFile.Open(path, ZipArchiveMode.Update); var entry = archive.GetEntry("word/document.xml") ?? throw new InvalidDataException("Source DOCX document.xml is missing."); string xml; using (var reader = new StreamReader(entry.Open())) xml = reader.ReadToEnd(); var w = XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main"); var document = XDocument.Parse(xml, LoadOptions.PreserveWhitespace); var body = document.Root?.Element(w + "body") ?? throw new InvalidDataException("Source DOCX body is missing."); var sections = body.Descendants(w + "sectPr").ToList(); for (var i = 0; i < sections.Count; i++) { var section = sections[i]; var parent = section.Parent; var paragraph = parent?.Name == w + "pPr" ? parent.Parent : null; section.Remove(); if (i < sections.Count - 1 && policy == SectionBreakPolicy.ConvertToPageBreak && paragraph is not null) paragraph.Add(new XElement(w + "r", new XElement(w + "br", new XAttribute(w + "type", "page")))); } entry.Delete(); var replacement = archive.CreateEntry("word/document.xml", CompressionLevel.Optimal); using var stream = replacement.Open(); using var writer = new StreamWriter(stream, new UTF8Encoding(false)); document.Save(writer, SaveOptions.DisableFormatting);
}
private static void RemoveImportedComments(dynamic range) { try { dynamic comments = range.Comments; for (var i = comments.Count; i >= 1; i--) comments[i].Delete(); } catch { } }
private static int TryCount(dynamic collection) { try { return (int)collection.Count; } catch { return 0; } }
private static T? Own<T>(T? value, List<object?> proxies) where T : class { if (value is not null) proxies.Add(value); return value; }
private static void Release(object? value) { try { if (value is not null && Marshal.IsComObject(value)) Marshal.FinalReleaseComObject(value); } catch { } }
private static void ReleaseAll(IEnumerable<object?> values) { foreach (var value in values.Reverse()) Release(value); }
private static void TryDelete(string path) { try { if (Directory.Exists(path)) Directory.Delete(path, true); } catch { } }
public void Dispose() { if (_ownsQueue) _queue.Dispose(); }
}
@@ -9,7 +9,6 @@
<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,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("DocxWorkbench.Tests")]
@@ -0,0 +1,60 @@
using System.Collections.Concurrent;
namespace DocxWorkbench.WordInterop;
internal sealed class StaWordQueue : IDisposable
{
private readonly BlockingCollection<Action> _jobs = new();
private readonly Thread _thread;
private readonly ManualResetEventSlim _stopped = new(false);
private readonly object _lifetimeGate = new();
private bool _accepting = true;
public StaWordQueue()
{
_thread = new Thread(Run) { IsBackground = true, Name = "DocumentAutomation.Word.STA" };
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
}
public Task<T> Enqueue<T>(Func<T> operation, CancellationToken cancellationToken)
{
if (operation is null) throw new ArgumentNullException(nameof(operation));
if (cancellationToken.IsCancellationRequested) return Task.FromCanceled<T>(cancellationToken);
var completion = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
lock (_lifetimeGate)
{
if (!_accepting) throw new ObjectDisposedException(nameof(StaWordQueue));
try { _jobs.Add(() => Execute(operation, completion, cancellationToken), cancellationToken); }
catch (InvalidOperationException) { throw new ObjectDisposedException(nameof(StaWordQueue)); }
catch (OperationCanceledException) { completion.TrySetCanceled(cancellationToken); }
}
return completion.Task;
}
private static void Execute<T>(Func<T> operation, TaskCompletionSource<T> completion, CancellationToken cancellationToken)
{
try { cancellationToken.ThrowIfCancellationRequested(); completion.TrySetResult(operation()); }
catch (OperationCanceledException ex) { completion.TrySetCanceled(ex.CancellationToken); }
catch (Exception ex) { completion.TrySetException(ex); }
}
private void Run()
{
try { foreach (var job in _jobs.GetConsumingEnumerable()) job(); }
finally { _stopped.Set(); }
}
public void Dispose()
{
lock (_lifetimeGate)
{
if (!_accepting) return;
_accepting = false;
_jobs.CompleteAdding();
}
if (Thread.CurrentThread != _thread) _stopped.Wait(TimeSpan.FromSeconds(30));
// Do not dispose the collection or stop event while the STA worker may
// still be draining it. The worker owns the final enumeration lifetime.
}
}
@@ -0,0 +1,49 @@
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
using DocumentAutomation.Shared;
using DocxWorkbench.Core;
namespace DocxWorkbench.WordInterop;
public sealed class TemplateDocumentGenerator : ITemplateDocumentGenerator, IDisposable
{
private readonly ITemplatePackageStore _packages;
private readonly IWordDocumentNormalizer _normalizer;
private readonly WordDocumentService _word = new();
private readonly IDocumentInspector _inspector;
public TemplateDocumentGenerator(ITemplatePackageStore packages, IWordDocumentNormalizer normalizer, IDocumentInspector inspector) { _packages = packages; _normalizer = normalizer; _inspector = inspector; }
public async Task<TemplateGenerationResult> GenerateAsync(TemplateGenerationRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken)
{
var opened = _packages.Open(request.TemplatePackagePath); DocumentNormalizationResult? normalizedBody = null; var outputPdf = Path.ChangeExtension(request.OutputDocxPath, ".pdf");
try
{
var bodyPath = request.ContentSourcePath;
if (Path.GetExtension(bodyPath).Equals(".doc", StringComparison.OrdinalIgnoreCase)) { normalizedBody = await _normalizer.NormalizeAsync(new DocumentNormalizationRequest { SourcePath = bodyPath, WorkingDirectory = Path.Combine(opened.WorkingDirectory, "body") }, progress, cancellationToken); bodyPath = normalizedBody.NormalizedPath; }
var workingTemplate = Path.Combine(opened.WorkingDirectory, "working-template.docx"); File.Copy(opened.TemplateDocxPath, workingTemplate, true); ReplaceScalarTokens(workingTemplate, request.ScalarValues); progress?.Report(new WordOperationProgress { Stage = "Generate", Message = "Building document from learned template" });
var result = await _word.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = workingTemplate, BodyPath = bodyPath, OutputDocxPath = request.OutputDocxPath }, progress is null ? null : new Progress<string>(x => progress.Report(new WordOperationProgress { Stage = "Generate", Message = x })), cancellationToken); await _word.ExportPdfAsync(result.OutputDocxPath, outputPdf, cancellationToken);
var validation = Validate(result.OutputDocxPath, opened.Template.TemplateId); var validationPath = Path.ChangeExtension(request.OutputDocxPath, ".validation.json"); File.WriteAllText(validationPath, JsonSerializer.Serialize(validation, new JsonSerializerOptions { WriteIndented = true })); return new TemplateGenerationResult { OutputDocxPath = result.OutputDocxPath, OutputPdfPath = outputPdf, GenerationLogPath = result.LogPath, ValidationLogPath = validationPath, Validation = validation };
}
finally { TryDeleteDirectory(opened.WorkingDirectory); }
}
private IReadOnlyList<RuleEvaluationResult> Validate(string output, string templateId)
{
var inspection = _inspector.Inspect(output); var result = new List<RuleEvaluationResult> { new() { RuleId = "DOCX_EXISTS", Status = File.Exists(output) ? RuleEvaluationStatus.Pass : RuleEvaluationStatus.Fail, Message = "Output DOCX exists.", DocumentLocation = output }, new() { RuleId = "NO_BODY_MARKER", Status = inspection.InsertionPoint.Type == "None" ? RuleEvaluationStatus.Pass : RuleEvaluationStatus.NeedsReview, Message = inspection.InsertionPoint.Type == "None" ? "No unresolved insertion marker remains." : "An insertion marker remains in the output.", DocumentLocation = "word/document.xml" }, new() { RuleId = "NO_TRACKED_CHANGES", Status = inspection.HasTrackedChanges ? RuleEvaluationStatus.Fail : RuleEvaluationStatus.Pass, Message = inspection.HasTrackedChanges ? "Tracked changes remain." : "No tracked changes detected." }, new() { RuleId = "TEMPLATE_ID", Status = RuleEvaluationStatus.Pass, Message = $"Generated from template {templateId}." } }; return result;
}
private static void ReplaceScalarTokens(string path, IReadOnlyDictionary<string, string> values)
{
if (values.Count == 0) return; using var archive = ZipFile.Open(path, ZipArchiveMode.Update); var w = XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main");
foreach (var entry in archive.Entries.Where(x => x.FullName.StartsWith("word/", StringComparison.OrdinalIgnoreCase) && x.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)).ToArray())
{
XDocument document; using (var reader = new StreamReader(entry.Open())) document = XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); var changed = false; foreach (var text in document.Descendants(w + "t")) { var value = text.Value; foreach (var pair in values) { var token = "{{" + pair.Key + "}}"; if (!value.Contains(token, StringComparison.OrdinalIgnoreCase)) continue; value = value.Replace(token, pair.Value, StringComparison.OrdinalIgnoreCase); changed = true; } text.Value = value; } if (!changed) continue; entry.Delete(); var replacement = archive.CreateEntry(entry.FullName, CompressionLevel.Optimal); using var stream = replacement.Open(); using var writer = new StreamWriter(stream, new UTF8Encoding(false)); document.Save(writer, SaveOptions.DisableFormatting);
}
}
private static void TryDeleteDirectory(string path) { try { if (Directory.Exists(path)) Directory.Delete(path, true); } catch { } }
public void Dispose() { _word.Dispose(); if (_normalizer is IDisposable disposable) disposable.Dispose(); }
}
@@ -0,0 +1,42 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using DocumentAutomation.Shared;
using DocxWorkbench.Core;
namespace DocxWorkbench.WordInterop;
public sealed class WordDocumentNormalizer : IWordDocumentNormalizer, IDisposable
{
private readonly StaWordQueue _queue;
public WordDocumentNormalizer() : this(new StaWordQueue(), true) { }
internal WordDocumentNormalizer(StaWordQueue queue) : this(queue, false) { }
private readonly bool _ownsQueue;
private WordDocumentNormalizer(StaWordQueue queue, bool ownsQueue) { _queue = queue; _ownsQueue = ownsQueue; }
public Task<DocumentNormalizationResult> NormalizeAsync(DocumentNormalizationRequest request, IProgress<WordOperationProgress>? progress, CancellationToken cancellationToken)
=> _queue.Enqueue(() => NormalizeOnSta(request, progress, cancellationToken), cancellationToken);
private static DocumentNormalizationResult NormalizeOnSta(DocumentNormalizationRequest request, IProgress<WordOperationProgress>? progress, CancellationToken token)
{
if (!File.Exists(request.SourcePath)) throw new FileNotFoundException("Document source was not found.", request.SourcePath);
var extension = Path.GetExtension(request.SourcePath);
if (!extension.Equals(".doc", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".docx", StringComparison.OrdinalIgnoreCase)) throw new UnsupportedDocumentFormatException("Template source must be DOC or DOCX.");
var started = Stopwatch.StartNew(); var sourceHash = Hash(request.SourcePath); var work = Path.GetFullPath(request.WorkingDirectory); Directory.CreateDirectory(work); var normalized = Path.Combine(work, "normalized.docx"); token.ThrowIfCancellationRequested();
if (extension.Equals(".docx", StringComparison.OrdinalIgnoreCase)) { File.Copy(request.SourcePath, normalized, true); return new DocumentNormalizationResult { SourcePath = request.SourcePath, NormalizedPath = normalized, SourceSha256 = sourceHash, NormalizedSha256 = Hash(normalized), WasConverted = false, Duration = started.Elapsed }; }
dynamic? application = null; dynamic? documents = null; dynamic? document = null; var proxies = new List<object?>();
try
{
progress?.Report(new WordOperationProgress { Stage = "NormalizeDoc", Message = "Opening binary DOC read-only" }); var wordType = Type.GetTypeFromProgID("Word.Application", false) ?? throw new WordNotInstalledException("Microsoft Word is not installed or is not registered in COM."); application = Own(Activator.CreateInstance(wordType), proxies) ?? throw new WordAutomationException("Could not create Word.Application."); application.Visible = false; application.DisplayAlerts = 0; if (request.DisableMacros) { try { application.AutomationSecurity = 3; } catch { } } documents = Own(application.Documents, proxies); document = Own(documents.Open(request.SourcePath, false, true, false), proxies) ?? throw new WordAutomationException("Word could not open the DOC source read-only."); token.ThrowIfCancellationRequested(); progress?.Report(new WordOperationProgress { Stage = "NormalizeDoc", Message = "Saving temporary DOCX" }); document.SaveAs2(normalized, 12); int? pageCount = null; try { document.Repaginate(); pageCount = (int)document.ComputeStatistics(2); } catch { } document.Close(0); document = null; return new DocumentNormalizationResult { SourcePath = request.SourcePath, NormalizedPath = normalized, SourceSha256 = sourceHash, NormalizedSha256 = Hash(normalized), WasConverted = true, Duration = started.Elapsed, PageCount = pageCount };
}
catch (WordNotInstalledException) { throw; }
catch (WordAutomationException) { throw; }
catch (Exception ex) { throw new WordAutomationException("DOC normalization failed.", ex); }
finally { try { if (document is not null) document.Close(0); } catch { } try { if (application is not null) application.Quit(0); } catch { } ReleaseAll(proxies); }
}
private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));
private static T? Own<T>(T? value, List<object?> proxies) where T : class { if (value is not null) proxies.Add(value); return value; }
private static void ReleaseAll(IEnumerable<object?> proxies) { foreach (var value in proxies.Reverse()) try { if (value is not null && Marshal.IsComObject(value)) Marshal.FinalReleaseComObject(value); } catch { } }
public void Dispose() { if (_ownsQueue) _queue.Dispose(); }
}
@@ -1,55 +1,132 @@
using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using DocumentAutomation.Shared;
using DocxWorkbench.Core;
using Word = Microsoft.Office.Interop.Word;
namespace DocxWorkbench.WordInterop;
public sealed class WordDocumentService : IWordDocumentService
public sealed class WordNotInstalledException(string message) : InvalidOperationException(message);
public sealed class WordAutomationException(string message, Exception? inner = null) : InvalidOperationException(message, inner);
public sealed class WordDocumentService : IWordDocumentService, IDisposable
{
private readonly StaWordQueue _queue = new();
private readonly DocxBodyImporter _bodyImporter;
public WordDocumentService() => _bodyImporter = new DocxBodyImporter(_queue);
public Task<WordDocumentResult> BuildDocumentAsync(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken cancellationToken)
=> Task.Run(() => Build(request, progress, cancellationToken), cancellationToken);
=> _queue.Enqueue(() => BuildOnSta(request, progress, cancellationToken), cancellationToken);
public Task<WordPreviewResult> ExportPdfAsync(string docxPath, string outputPdfPath, CancellationToken cancellationToken)
=> Task.Run(() => ExportPdf(docxPath, outputPdfPath, cancellationToken), cancellationToken);
=> _queue.Enqueue(() => ExportPdfOnSta(docxPath, outputPdfPath, cancellationToken), cancellationToken);
private static WordDocumentResult Build(WordDocumentBuildRequest request, IProgress<string>? progress, CancellationToken token)
private WordDocumentResult BuildOnSta(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;
DocumentInputValidation.Validate(request);
if (!request.BodyPath.EndsWith(".docx", StringComparison.OrdinalIgnoreCase) && !request.BodyPath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) throw new UnsupportedDocumentFormatException($"Unsupported body format: {Path.GetExtension(request.BodyPath)}. Use DOCX or TXT.");
var insertion = TemplateInsertionPointReader.Find(request.TemplatePath);
if (insertion.Type == "None") throw new WordAutomationException("No DOC_BODY insertion point was found. Add a DOC_BODY content control, DOC_BODY bookmark, or {{DOC_BODY}} marker.");
var started = Stopwatch.StartNew(); var temp = Path.Combine(Path.GetTempPath(), "DocumentAutomation", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(temp); var workingTemplate = Path.Combine(temp, "template.docx"); var stagedOutput = Path.Combine(temp, "result.docx"); File.Copy(request.TemplatePath, workingTemplate, true);
dynamic? application = null; dynamic? documents = null; dynamic? document = null; dynamic? range = null; var proxies = new List<object?>(); int? wordPid = null; BodyImportResult? bodyImport = 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 };
token.ThrowIfCancellationRequested(); progress?.Report("Checking Word.Application COM registration"); var wordType = Type.GetTypeFromProgID("Word.Application", false) ?? throw new WordNotInstalledException("Microsoft Word is not installed or is not registered in COM."); application = Own(Activator.CreateInstance(wordType), proxies) ?? throw new WordAutomationException("Could not create Word.Application."); application.Visible = false; application.DisplayAlerts = 0; TrySetAutomationSecurity(application); wordPid = TryGetPid(application); progress?.Report($"Word COM started (PID {wordPid?.ToString() ?? "unknown"})");
documents = Own(application.Documents, proxies); document = Own(documents.Open(workingTemplate, false, false, false), proxies) ?? throw new WordAutomationException("Word could not open the working template copy.");
range = Own(FindInsertionRange(document, insertion.Type, proxies), proxies) ?? throw new WordAutomationException("The insertion point disappeared while opening the template.");
token.ThrowIfCancellationRequested(); progress?.Report($"Inserting {Path.GetExtension(request.BodyPath).ToUpperInvariant()} body"); if (request.BodyPath.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) range.Text = File.ReadAllText(request.BodyPath).Replace("\r\n", "\r").Replace('\n', '\r'); else bodyImport = _bodyImporter.ImportOnSta(application, document, range, new DocxBodyImportRequest { SourcePath = request.BodyPath, DestinationRangeHandle = range }, progress is null ? null : new Progress<WordOperationProgress>(x => progress.Report(x.Message ?? x.Stage)), token, proxies);
token.ThrowIfCancellationRequested(); progress?.Report("Updating fields and pagination"); document.Repaginate(); document.Fields.Update(); UpdateHeadersFooters(document, proxies); document.SaveAs2(stagedOutput, 12); document.Close(0); document = null; RemoveUnreferencedHeaderFooterParts(stagedOutput); CommitOutput(stagedOutput, request.OutputDocxPath);
var logPath = Path.ChangeExtension(request.OutputDocxPath, ".generation.json"); File.WriteAllText(logPath, JsonSerializer.Serialize(new { operationId = Guid.NewGuid(), operationType = "BuildDocument", startedAtUtc = DateTime.UtcNow.Subtract(started.Elapsed), completedAtUtc = DateTime.UtcNow, template = request.TemplatePath, templateSha256 = Hash(request.TemplatePath), templateVersion = "1.0", body = request.BodyPath, sourceSha256 = Hash(request.BodyPath), output = request.OutputDocxPath, resultSha256 = Hash(request.OutputDocxPath), insertionPoint = insertion.Type, wordPid, staThread = Thread.CurrentThread.ManagedThreadId, staApartment = Thread.CurrentThread.GetApartmentState().ToString(), durationMs = started.ElapsedMilliseconds, status = "Succeeded", errorCode = (string?)null }, new JsonSerializerOptions { WriteIndented = true }));
return new WordDocumentResult { OutputDocxPath = request.OutputDocxPath, LogPath = logPath, Duration = started.Elapsed, InsertionPoint = insertion.Type, BodyImport = bodyImport };
}
catch (WordNotInstalledException) { throw; }
catch (WordAutomationException) { throw; }
catch (Exception ex) when (ex is not OperationCanceledException) { throw new WordAutomationException("Word automation failed. See technical details in the operation log.", ex); }
finally
{
try { if (document is not null) document.Close(0); } catch { }
try { if (application is not null) application.Quit(0); } catch { }
ReleaseAll(proxies); TryDeleteDirectory(temp);
}
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)
private static WordPreviewResult ExportPdfOnSta(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); } }
if (!File.Exists(docxPath)) throw new FileNotFoundException("DOCX was not found.", docxPath); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPdfPath))!); var sw = Stopwatch.StartNew(); dynamic? application = null; dynamic? documents = null; dynamic? document = null; var proxies = new List<object?>();
try { token.ThrowIfCancellationRequested(); var wordType = Type.GetTypeFromProgID("Word.Application", false) ?? throw new WordNotInstalledException("Microsoft Word is not installed or is not registered in COM."); application = Own(Activator.CreateInstance(wordType), proxies) ?? throw new WordAutomationException("Could not create Word.Application."); application.Visible = false; application.DisplayAlerts = 0; TrySetAutomationSecurity(application); documents = Own(application.Documents, proxies); document = Own(documents.Open(docxPath, true, false, false), proxies) ?? throw new WordAutomationException("Word could not open the DOCX."); document.ExportAsFixedFormat(outputPdfPath, 17); return new WordPreviewResult { PdfPath = outputPdfPath, Duration = sw.Elapsed }; }
catch (WordNotInstalledException) { throw; }
catch (WordAutomationException) { throw; }
catch (Exception ex) when (ex is not OperationCanceledException) { throw new WordAutomationException("PDF export through Word failed.", ex); }
finally { try { if (document is not null) document.Close(0); } catch { } try { if (application is not null) application.Quit(0); } catch { } ReleaseAll(proxies); }
}
private static Word.Range? FindInsertionRange(Word.Document document, out string point)
private static dynamic? FindInsertionRange(dynamic document, string type, List<object?> proxies)
{
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;
if (type == "ContentControl") { dynamic controls = Own(document.ContentControls, proxies); for (var i = 1; i <= controls.Count; i++) { dynamic control = Own(controls[i], proxies); if (string.Equals((string?)control.Tag, "DOC_BODY", StringComparison.OrdinalIgnoreCase) || string.Equals((string?)control.Title, "DOC_BODY", StringComparison.OrdinalIgnoreCase)) return control.Range; } }
if (type == "Bookmark") return document.Bookmarks["DOC_BODY"].Range;
dynamic search = Own(document.Content, proxies); search.Find.ClearFormatting(); search.Find.Text = "{{DOC_BODY}}"; if (search.Find.Execute()) { search.Text = ""; return search; } return null;
}
private static int? GetProcessId(Word.Application app) { try { return Process.GetProcessesByName("WINWORD").OrderByDescending(p => p.StartTime).FirstOrDefault()?.Id; } catch { return null; } }
private static void UpdateHeadersFooters(dynamic document, List<object?> proxies)
{
dynamic sections = Own(document.Sections, proxies); for (var i = 1; i <= sections.Count; i++) { dynamic section = Own(sections[i], proxies); UpdateCollection(section.Headers, proxies); UpdateCollection(section.Footers, proxies); }
void UpdateCollection(dynamic collection, List<object?> proxies) { dynamic owned = WordDocumentService.Own(collection, proxies); for (var i = 1; i <= owned.Count; i++) { dynamic item = WordDocumentService.Own(owned[i], proxies); dynamic range = WordDocumentService.Own(item.Range, proxies); range.Fields.Update(); } }
}
private static void TrySetAutomationSecurity(dynamic application) { try { application.AutomationSecurity = 3; application.ScreenUpdating = false; } catch { } }
private static int? TryGetPid(dynamic application) { try { return Process.GetProcessById((int)application.Hwnd).Id; } catch { return null; } }
private static T? Own<T>(T? value, List<object?> proxies) where T : class { if (value is not null) proxies.Add(value); return value; }
private static void ReleaseAll(IEnumerable<object?> proxies) { foreach (var proxy in proxies.Reverse()) try { if (proxy is not null && Marshal.IsComObject(proxy)) Marshal.FinalReleaseComObject(proxy); } catch { } }
private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));
private static void CommitOutput(string staged, string output) { var full = Path.GetFullPath(output); if (File.Exists(full)) File.Copy(full, full + ".bak-" + DateTime.UtcNow.ToString("yyyyMMddHHmmss"), true); File.Copy(staged, full, true); }
private static void RemoveUnreferencedHeaderFooterParts(string path)
{
using var archive = ZipFile.Open(path, ZipArchiveMode.Update);
var documentEntry = archive.GetEntry("word/document.xml");
var relsEntry = archive.GetEntry("word/_rels/document.xml.rels");
if (documentEntry is null || relsEntry is null) return;
XDocument document;
XDocument relationships;
using (var reader = new StreamReader(documentEntry.Open())) document = XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
using (var reader = new StreamReader(relsEntry.Open())) relationships = XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
var w = XNamespace.Get("http://schemas.openxmlformats.org/wordprocessingml/2006/main");
var rel = XNamespace.Get("http://schemas.openxmlformats.org/package/2006/relationships");
var used = document.Descendants(w + "headerReference").Concat(document.Descendants(w + "footerReference")).Select(x => (string?)x.Attribute(XNamespace.Get("http://schemas.openxmlformats.org/officeDocument/2006/relationships") + "id")).Where(x => x is not null).ToHashSet(StringComparer.Ordinal);
var removedTargets = new List<string>();
foreach (var relationship in relationships.Root?.Elements(rel + "Relationship").ToList() ?? [])
{
var type = (string?)relationship.Attribute("Type") ?? string.Empty;
var id = (string?)relationship.Attribute("Id");
if (id is null || (!type.EndsWith("/header", StringComparison.OrdinalIgnoreCase) && !type.EndsWith("/footer", StringComparison.OrdinalIgnoreCase)) || used.Contains(id)) continue;
var target = (string?)relationship.Attribute("Target");
if (!string.IsNullOrWhiteSpace(target)) removedTargets.Add(ResolveWordPart(target));
relationship.Remove();
}
foreach (var target in removedTargets.Distinct(StringComparer.OrdinalIgnoreCase)) archive.GetEntry(target)?.Delete();
relsEntry.Delete();
var replacement = archive.CreateEntry("word/_rels/document.xml.rels", CompressionLevel.Optimal);
using var stream = replacement.Open();
using var writer = new StreamWriter(stream, new UTF8Encoding(false));
relationships.Save(writer, SaveOptions.DisableFormatting);
}
private static string ResolveWordPart(string target) => target.StartsWith("/", StringComparison.Ordinal) ? target.TrimStart('/') : "word/" + target.Replace("\\", "/").TrimStart("./".ToCharArray());
private static void TryDeleteDirectory(string path) { try { if (Directory.Exists(path)) Directory.Delete(path, true); } catch { } }
public void Dispose() => _queue.Dispose();
}
internal static class TemplateInsertionPointReader
{
public static InsertionPointInfo Find(string docxPath)
{
using var archive = ZipFile.OpenRead(docxPath); var entry = archive.GetEntry("word/document.xml") ?? throw new InvalidDataException("DOCX document.xml is missing."); using var reader = new StreamReader(entry.Open()); var xml = reader.ReadToEnd();
if (Regex.IsMatch(xml, @"w:tag[^>]+w:val=""DOC_BODY""", RegexOptions.IgnoreCase)) return new InsertionPointInfo { Type = "ContentControl", Location = "word/document.xml" };
if (Regex.IsMatch(xml, @"w:bookmarkStart[^>]+w:name=""DOC_BODY""", RegexOptions.IgnoreCase)) return new InsertionPointInfo { Type = "Bookmark", Location = "word/document.xml" };
if (xml.Contains("{{DOC_BODY}}", StringComparison.Ordinal)) return new InsertionPointInfo { Type = "TextMarker", Location = "word/document.xml" };
return new InsertionPointInfo { Type = "None" };
}
}
@@ -9,7 +9,7 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup><ProjectReference Include="..\..\src\DocxWorkbench.WordInterop\DocxWorkbench.WordInterop.csproj" /><ProjectReference Include="..\..\src\StandardsWorkbench.Pdf\StandardsWorkbench.Pdf.csproj" /></ItemGroup>
<ItemGroup><ProjectReference Include="..\..\src\DocxWorkbench.WordInterop\DocxWorkbench.WordInterop.csproj" /><ProjectReference Include="..\..\src\DocxWorkbench.OpenXml\DocxWorkbench.OpenXml.csproj" /><ProjectReference Include="..\..\src\StandardsWorkbench.Pdf\StandardsWorkbench.Pdf.csproj" /><PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" /></ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
@@ -1,16 +1,72 @@
using System.Diagnostics;
using System.Text.Json;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocumentAutomation.Shared;
using DocxWorkbench.WordInterop;
using DocxWorkbench.OpenXml;
using Xunit.Sdk;
namespace DocumentAutomation.IntegrationTests;
public sealed class WordComIntegrationTests
{
[Fact]
public void IntegrationAssemblyIsAvailable()
public void IntegrationAssemblyIsAvailable() => Assert.True(typeof(WordComIntegrationTests).Assembly.GetName().Name is not null);
[Fact]
public void WordAvailability_DoesNotRequireOfficePia()
{
Assert.True(typeof(WordComIntegrationTests).Assembly.GetName().Name is not null);
var names = typeof(WordDocumentService).Assembly.GetReferencedAssemblies().Select(x => x.Name).ToArray(); Assert.DoesNotContain(names, name => string.Equals(name, "office", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "Microsoft.Office.Interop.Word", StringComparison.OrdinalIgnoreCase));
}
[Fact(Skip = "Requires an interactive Microsoft Word installation; not run automatically in the repository validation pass.")]
public void BuildsDocxAndExportsPdfThroughWordCom()
[Fact]
public async Task Build_WithoutInsertionPoint_DoesNotStartWord()
{
Assert.Fail("Run this test manually on a Windows workstation with Word installed and a synthetic template/body fixture.");
var template = CreateDocx(false); var body = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); File.WriteAllText(body, "text"); var before = ProcessIds(); try { using var service = new WordDocumentService(); await Assert.ThrowsAsync<WordAutomationException>(() => service.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = template, BodyPath = body, OutputDocxPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx") }, null, CancellationToken.None)); Assert.Empty(ProcessIds().Except(before)); } finally { File.Delete(template); File.Delete(body); }
}
[Fact] public async Task Build_RunsOnStaThread() { await RequireWord(); var result = await BuildTxt(); var log = JsonSerializer.Deserialize<JsonElement>(File.ReadAllText(result.LogPath)); Assert.Equal("STA", log.GetProperty("staApartment").GetString()); }
[Fact] public async Task Build_WithTxtBody_InsertsText() { await RequireWord(); var result = await BuildTxt(); Assert.Contains("TXT integration body", ReadMainText(result.OutputDocxPath)); }
[Fact] public async Task Build_WithDocxBody_InsertsDocument() { await RequireWord(); var template = CreateDocx(true); var body = CreateDocx(false, "DOCX integration body"); var output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); try { using var service = new WordDocumentService(); await service.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = template, BodyPath = body, OutputDocxPath = output }, null, CancellationToken.None); Assert.Contains("DOCX integration body", ReadMainText(output)); } finally { File.Delete(template); File.Delete(body); File.Delete(output); } }
[Fact] public async Task Build_ExportsPdf() { await RequireWord(); var result = await BuildTxt(); using var service = new WordDocumentService(); var pdf = await service.ExportPdfAsync(result.OutputDocxPath, Path.ChangeExtension(result.OutputDocxPath, ".pdf"), CancellationToken.None); Assert.True(File.Exists(pdf.PdfPath)); File.Delete(result.OutputDocxPath); File.Delete(pdf.PdfPath); File.Delete(result.LogPath); }
[Fact] public async Task Build_DoesNotLeaveCreatedWinwordProcess() { await RequireWord(); var before = ProcessIds(); var result = await BuildTxt(); await Task.Delay(500); Assert.Empty(ProcessIds().Except(before)); File.Delete(result.OutputDocxPath); File.Delete(result.LogPath); }
[Fact] public async Task Build_Failure_ReleasesWord() { await RequireWord(); var before = ProcessIds(); var template = CreateDocx(true); var body = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); var outputDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); Directory.CreateDirectory(outputDirectory); File.WriteAllText(body, "failure body"); try { using var service = new WordDocumentService(); await Assert.ThrowsAnyAsync<Exception>(() => service.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = template, BodyPath = body, OutputDocxPath = outputDirectory }, null, CancellationToken.None)); await Task.Delay(500); Assert.Empty(ProcessIds().Except(before)); } finally { File.Delete(template); File.Delete(body); Directory.Delete(outputDirectory, true); } }
[Fact]
public async Task RuntimeGate_DesktopDocxBodyUsesTemplateCopy()
{
await RequireWord(); var template = @"C:\Users\codex\Desktop\156-титул-ПД.docx"; var body = @"C:\Users\codex\Desktop\НХС-33-УК3-ОЗХ-13011-000-НК-ОД.docx"; if (!File.Exists(template) || !File.Exists(body)) throw SkipException.ForSkip("The supplied Desktop DOCX fixtures are not available."); var copy = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); var output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); File.Copy(template, copy); try { using (var document = WordprocessingDocument.Open(copy, true)) { var main = document.MainDocumentPart ?? throw new InvalidDataException("Main part missing."); main.Document.Body!.Append(new Paragraph(new Run(new Text("{{DOC_BODY}}")))); main.Document.Save(); } using var service = new WordDocumentService(); var result = await service.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = copy, BodyPath = body, OutputDocxPath = output }, null, CancellationToken.None); Assert.True(File.Exists(result.OutputDocxPath)); } finally { File.Delete(copy); File.Delete(output); File.Delete(Path.ChangeExtension(output, ".generation.json")); }
}
[Fact]
public async Task ImportDocxBody_ExcludesSourceHeaders()
{
await RequireWord(); var template = CreateDocx(true); var source = CreateComplexSource(); var output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); var sourceHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(source))); try { using var service = new WordDocumentService(); var result = await service.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = template, BodyPath = source, OutputDocxPath = output }, null, CancellationToken.None); using var document = WordprocessingDocument.Open(result.OutputDocxPath, false); var main = document.MainDocumentPart!; Assert.DoesNotContain(main.HeaderParts.SelectMany(x => x.Header?.Descendants<Text>() ?? []), x => x.Text == "SOURCE HEADER"); Assert.DoesNotContain(main.FooterParts.SelectMany(x => x.Footer?.Descendants<Text>() ?? []), x => x.Text == "SOURCE FOOTER"); Assert.Single(main.Document.Descendants<SectionProperties>()); Assert.NotNull(result.BodyImport); Assert.Contains("wdMainTextStory", result.BodyImport!.Plan.IncludedStories); Assert.Equal(sourceHash, Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(source)))); } finally { File.Delete(template); File.Delete(source); File.Delete(output); File.Delete(Path.ChangeExtension(output, ".generation.json")); }
}
[Fact]
public async Task ImportDocxBody_DoesNotChangeDestinationPageSetup()
{
await RequireWord(); var template = CreateDocx(true); var source = CreateComplexSource(); var output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); try { using var service = new WordDocumentService(); var result = await service.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = template, BodyPath = source, OutputDocxPath = output }, null, CancellationToken.None); using var document = WordprocessingDocument.Open(result.OutputDocxPath, false); Assert.Single(document.MainDocumentPart!.Document.Descendants<SectionProperties>()); Assert.Contains("DOCX complex body", document.MainDocumentPart.Document.InnerText); } finally { File.Delete(template); File.Delete(source); File.Delete(output); File.Delete(Path.ChangeExtension(output, ".generation.json")); }
}
[Fact]
public async Task TemplateLearning_EndToEnd_RealDocToDtemplateToDocxAndPdf()
{
await RequireWord(); var source = @"C:\Users\codex\Desktop\НХС-33-УК3-ОЗХ-13011-000-НК-ОД.doc"; var content = @"C:\Users\codex\Desktop\НХС-33-УК3-ОЗХ-13011-000-НК-ОД.docx"; if (!File.Exists(source) || !File.Exists(content)) throw SkipException.ForSkip("The supplied real DOC/DOCX fixtures are not available.");
var package = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dtemplate"); var output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); var sourceHash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(source))); var before = ProcessIds();
try
{
using var normalizer = new WordDocumentNormalizer(); var learning = new TemplateLearningService(normalizer, new TemplatePackageStore(), new OpenXmlDocumentInspector()); var learned = await learning.LearnAsync(new TemplateLearningRequest { SourcePath = source, TemplateName = "Real sample", OutputPackagePath = package }, null, CancellationToken.None); Assert.NotEmpty(learned.Slots); Assert.True(File.Exists(package)); using var generator = new TemplateDocumentGenerator(new TemplatePackageStore(), new WordDocumentNormalizer(), new OpenXmlDocumentInspector()); var result = await generator.GenerateAsync(new TemplateGenerationRequest { TemplatePackagePath = package, ContentSourcePath = content, OutputDocxPath = output }, null, CancellationToken.None); Assert.True(File.Exists(result.OutputDocxPath)); Assert.True(File.Exists(result.OutputPdfPath)); Assert.Equal(sourceHash, Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(source)))); Assert.Empty(ProcessIds().Except(before));
}
finally { File.Delete(package); File.Delete(output); File.Delete(Path.ChangeExtension(output, ".pdf")); File.Delete(Path.ChangeExtension(output, ".generation.json")); File.Delete(Path.ChangeExtension(output, ".validation.json")); }
}
private static async Task RequireWord() { if (!OperatingSystem.IsWindows() || Type.GetTypeFromProgID("Word.Application", false) is null) throw SkipException.ForSkip("Microsoft Word COM is not installed or registered on this workstation."); await Task.CompletedTask; }
private static async Task<WordDocumentResult> BuildTxt() { var template = CreateDocx(true); var body = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); var output = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); File.WriteAllText(body, "TXT integration body"); try { using var service = new WordDocumentService(); return await service.BuildDocumentAsync(new WordDocumentBuildRequest { TemplatePath = template, BodyPath = body, OutputDocxPath = output }, null, CancellationToken.None); } finally { File.Delete(template); File.Delete(body); } }
private static HashSet<int> ProcessIds() => Process.GetProcessesByName("WINWORD").Select(x => x.Id).ToHashSet();
private static string ReadMainText(string path) { using var doc = WordprocessingDocument.Open(path, false); return doc.MainDocumentPart?.Document.InnerText ?? ""; }
private static string CreateComplexSource()
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); using var doc = WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document); var main = doc.AddMainDocumentPart(); var header1 = main.AddNewPart<HeaderPart>(); header1.Header = new Header(new Paragraph(new Run(new Text("SOURCE HEADER")))); var footer1 = main.AddNewPart<FooterPart>(); footer1.Footer = new Footer(new Paragraph(new Run(new Text("SOURCE FOOTER")))); var body = new Body(); var first = new Paragraph(new ParagraphProperties(new SectionProperties(new HeaderReference { Type = HeaderFooterValues.Default, Id = main.GetIdOfPart(header1) }, new FooterReference { Type = HeaderFooterValues.Default, Id = main.GetIdOfPart(footer1) })), new Run(new Text("DOCX complex body"))); body.Append(first); body.Append(new Table(new TableRow(new TableCell(new Paragraph(new Run(new Text("source table"))))))); body.Append(new SectionProperties(new HeaderReference { Type = HeaderFooterValues.Default, Id = main.GetIdOfPart(header1) }, new FooterReference { Type = HeaderFooterValues.Default, Id = main.GetIdOfPart(footer1) })); main.Document = new Document(body); main.Document.Save(); return path;
}
private static string CreateDocx(bool marker, string text = "template body") { 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(marker ? "{{DOC_BODY}}" : text))))); main.Document.Save(); return path; }
}
@@ -1,5 +1,6 @@
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxWorkbench.Core;
using DocxWorkbench.OpenXml;
namespace DocxWorkbench.Tests;
@@ -7,14 +8,47 @@ namespace DocxWorkbench.Tests;
public sealed class DocumentInspectorTests
{
[Fact]
public void InspectsSyntheticDocxStructure()
public void Analyze_DocxTemplateAndTxtBody_Succeeds()
{
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); }
var docx = CreateDocx(marker: true); var txt = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); File.WriteAllText(txt, "line one\nline two"); try { var template = new OpenXmlDocumentInspector().Inspect(docx); var body = new TextDocumentInspector().Inspect(txt); Assert.Equal("TextMarker", template.InsertionPoint.Type); Assert.Equal(2, body.Paragraphs); } finally { File.Delete(docx); File.Delete(txt); }
}
[Fact]
public void Analyze_DocxTemplateAndDocxBody_Succeeds()
{
var template = CreateDocx(marker: true); var body = CreateDocx(marker: false); try { var inspector = new OpenXmlDocumentInspector(); Assert.Equal("TextMarker", inspector.Inspect(template).InsertionPoint.Type); Assert.Equal(1, inspector.Inspect(body).Sections); } finally { File.Delete(template); File.Delete(body); }
}
[Fact]
public void Inspect_DetectsMarkerInsertionPoint()
{
var path = CreateDocx(marker: true); try { Assert.Equal("TextMarker", new OpenXmlDocumentInspector().Inspect(path).InsertionPoint.Type); } finally { File.Delete(path); }
}
[Fact]
public void Inspect_FindsPageFieldsInFooter()
{
var path = CreateDocx(marker: true, fields: true); try { var result = new OpenXmlDocumentInspector().Inspect(path); Assert.Contains(result.Fields, field => field.Type == "PAGE" && field.Part.StartsWith("FooterPart", StringComparison.Ordinal)); Assert.Contains(result.Fields, field => field.Type == "NUMPAGES"); } finally { File.Delete(path); }
}
[Fact]
public void Inspect_CountsHeaderFooterGraphics()
{
var path = CreateDocx(marker: true, graphics: true); try { var result = new OpenXmlDocumentInspector().Inspect(path); Assert.True(result.ImageSummary.RasterImages >= 2); } finally { File.Delete(path); }
}
[Fact]
public void Inspect_UnsupportedFormatIsDomainError()
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".bin"); File.WriteAllText(path, "not a document"); try { var exception = Assert.Throws<UnsupportedDocumentFormatException>(() => new OpenXmlDocumentInspector().Inspect(path)); Assert.Contains("Open XML inspector", exception.Message); } finally { File.Delete(path); }
}
private static string CreateDocx(bool marker, bool fields = false, bool graphics = false)
{
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); using var document = WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document); var main = document.AddMainDocumentPart(); var body = new Body(new Paragraph(new Run(new Text(marker ? "{{DOC_BODY}}" : "body")))); main.Document = new Document(body);
var header = main.AddNewPart<HeaderPart>(); header.Header = new Header(new Paragraph(new Run(new Text("header")))); var footer = main.AddNewPart<FooterPart>();
if (fields) { var paragraph = new Paragraph(); paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.Begin })); paragraph.Append(new Run(new FieldCode(" PAGE "))); paragraph.Append(new Run(new FieldChar { FieldCharType = FieldCharValues.End })); paragraph.Append(new SimpleField { Instruction = " NUMPAGES " }); footer.Footer = new Footer(paragraph); } else footer.Footer = new Footer(new Paragraph(new Run(new Text("footer"))));
if (graphics) { var image = header.AddImagePart(ImagePartType.Png); image.FeedData(new MemoryStream([137, 80, 78, 71])); var image2 = footer.AddImagePart(ImagePartType.Png); image2.FeedData(new MemoryStream([137, 80, 78, 71])); }
body.Append(new SectionProperties(new HeaderReference { Type = HeaderFooterValues.Default, Id = main.GetIdOfPart(header) }, new FooterReference { Type = HeaderFooterValues.Default, Id = main.GetIdOfPart(footer) })); main.Document.Save(); return path;
}
}
@@ -9,7 +9,7 @@
<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><ProjectReference Include="..\..\src\DocxWorkbench.Core\DocxWorkbench.Core.csproj" /><ProjectReference Include="..\..\src\DocxWorkbench.OpenXml\DocxWorkbench.OpenXml.csproj" /><ProjectReference Include="..\..\src\DocxWorkbench.WordInterop\DocxWorkbench.WordInterop.csproj" /><PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" /></ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
@@ -0,0 +1,22 @@
using DocxWorkbench.WordInterop;
namespace DocxWorkbench.Tests;
public sealed class StaWordQueueTests
{
[Fact]
public async Task Dispose_DrainsActiveJobAndIsIdempotent()
{
using var queue = new StaWordQueue(); using var started = new ManualResetEventSlim(false); using var release = new ManualResetEventSlim(false);
var job = queue.Enqueue(() => { started.Set(); Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState()); release.Wait(); return 42; }, CancellationToken.None); Assert.True(started.Wait(TimeSpan.FromSeconds(2)));
var dispose = Task.Run(queue.Dispose); var completed = await Task.WhenAny(dispose, Task.Delay(100)); Assert.NotSame(dispose, completed); release.Set(); await dispose; Assert.Equal(42, await job); queue.Dispose(); Assert.Throws<ObjectDisposedException>(() => EnqueueAfterDispose(queue));
}
[Fact]
public async Task Enqueue_CancellationCompletesCanceledTask()
{
using var queue = new StaWordQueue(); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); await Assert.ThrowsAnyAsync<OperationCanceledException>(() => queue.Enqueue(() => 1, cancellation.Token));
}
private static void EnqueueAfterDispose(StaWordQueue queue) => queue.Enqueue(() => 1, CancellationToken.None);
}
@@ -0,0 +1,25 @@
using System.IO.Compression;
using System.Security.Cryptography;
using DocumentAutomation.Shared;
using DocxWorkbench.OpenXml;
using DocxWorkbench.WordInterop;
namespace DocxWorkbench.Tests;
public sealed class TemplateLearningTests
{
[Fact]
public async Task NormalizeDocx_UsesSafeWorkingCopy()
{
var source = CreateDocx("template"); var hash = Hash(source); var working = Path.Combine(Path.GetTempPath(), "learning-test-" + Guid.NewGuid().ToString("N")); using var normalizer = new WordDocumentNormalizer(); try { var result = await normalizer.NormalizeAsync(new DocumentNormalizationRequest { SourcePath = source, WorkingDirectory = working }, null, CancellationToken.None); Assert.False(result.WasConverted); Assert.NotEqual(Path.GetFullPath(source), Path.GetFullPath(result.NormalizedPath)); Assert.Equal(hash, Hash(source)); Assert.Equal(hash, result.SourceSha256); Assert.True(File.Exists(result.NormalizedPath)); } finally { File.Delete(source); if (Directory.Exists(working)) Directory.Delete(working, true); }
}
[Fact]
public async Task TemplatePackage_CanBeCreatedAndReopened()
{
var source = CreateDocx("{{DOC_BODY}}"); var package = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dtemplate"); using var normalizer = new WordDocumentNormalizer(); try { var service = new TemplateLearningService(normalizer, new TemplatePackageStore()); var learned = await service.LearnAsync(new TemplateLearningRequest { SourcePath = source, TemplateName = "Unit template", OutputPackagePath = package }, null, CancellationToken.None); Assert.NotEmpty(learned.Slots); Assert.Contains(learned.PageArchetypes, x => x.IsFirstPage); using var archive = ZipFile.OpenRead(package); Assert.Contains(archive.Entries, x => x.FullName == "manifest.json"); Assert.Contains(archive.Entries, x => x.FullName == "template.docx"); var opened = new TemplatePackageStore().Open(package); Assert.Equal(learned.TemplateId, opened.Template.TemplateId); Assert.True(File.Exists(opened.TemplateDocxPath)); Directory.Delete(opened.WorkingDirectory, true); } finally { File.Delete(source); File.Delete(package); }
}
private static string CreateDocx(string text) { var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".docx"); using var document = DocumentFormat.OpenXml.Packaging.WordprocessingDocument.Create(path, DocumentFormat.OpenXml.WordprocessingDocumentType.Document); var main = document.AddMainDocumentPart(); main.Document = new DocumentFormat.OpenXml.Wordprocessing.Document(new DocumentFormat.OpenXml.Wordprocessing.Body(new DocumentFormat.OpenXml.Wordprocessing.Paragraph(new DocumentFormat.OpenXml.Wordprocessing.Run(new DocumentFormat.OpenXml.Wordprocessing.Text(text))))); main.Document.Save(); return path; }
private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)));
}
@@ -0,0 +1,14 @@
using DocxWorkbench.Core;
namespace DocxWorkbench.Tests;
public sealed class WorkbenchStateTests
{
[Fact]
public void VerifyButton_DisabledBeforeBuild() { var state = new WorkbenchStateController(); Assert.False(state.CanValidate); }
[Fact]
public void VerifyButton_DisabledAfterBuildFailure() { var state = Ready(); state.BeginBuild(); state.BuildFailed(); Assert.False(state.CanValidate); Assert.False(state.CanOpenDocx); }
[Fact]
public void VerifyButton_EnabledAfterBuildSuccess() { var state = Ready(); state.BeginBuild(); state.BuildSucceeded(); Assert.True(state.CanValidate); Assert.True(state.CanOpenDocx); Assert.True(state.CanOpenPdf); }
private static WorkbenchStateController Ready() { var state = new WorkbenchStateController(); state.InputsSelected(true); state.AnalysisCompleted(true, true, true); return state; }
}
+6
View File
@@ -0,0 +1,6 @@
artifacts/
.local-dotnet-sdk/
.local-tools/
bin/
obj/
.vs/
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<DebugType>portable</DebugType>
<AutoCAD2026ManagedDir Condition="'$(AutoCAD2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)</AutoCAD2026ManagedDir>
<Plant3D2026ManagedDir Condition="'$(Plant3D2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)\PLNT3D</Plant3D2026ManagedDir>
<Plant3D2026InstallDir Condition="'$(Plant3D2026InstallDir)' == '' and '$(AutoCAD2026ManagedDir)' != ''">$(AutoCAD2026ManagedDir)</Plant3D2026InstallDir>
</PropertyGroup>
</Project>
+54
View File
@@ -0,0 +1,54 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Abstractions", "src\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj", "{10000000-0000-0000-0000-000000000001}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Core", "src\PRecAnnot.Core\PRecAnnot.Core.csproj", "{10000000-0000-0000-0000-000000000002}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Application", "src\PRecAnnot.Application\PRecAnnot.Application.csproj", "{10000000-0000-0000-0000-000000000003}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Plant3D2026", "src\PRecAnnot.Plant3D2026\PRecAnnot.Plant3D2026.csproj", "{10000000-0000-0000-0000-000000000004}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.UI", "src\PRecAnnot.UI\PRecAnnot.UI.csproj", "{10000000-0000-0000-0000-000000000005}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Telemetry", "src\PRecAnnot.Telemetry\PRecAnnot.Telemetry.csproj", "{10000000-0000-0000-0000-000000000006}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Core.Tests", "tests\PRecAnnot.Core.Tests\PRecAnnot.Core.Tests.csproj", "{10000000-0000-0000-0000-000000000007}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{10000000-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10000000-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10000000-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10000000-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
{10000000-0000-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10000000-0000-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10000000-0000-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10000000-0000-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU
{10000000-0000-0000-0000-000000000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10000000-0000-0000-0000-000000000003}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10000000-0000-0000-0000-000000000003}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10000000-0000-0000-0000-000000000003}.Release|Any CPU.Build.0 = Release|Any CPU
{10000000-0000-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10000000-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10000000-0000-0000-0000-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10000000-0000-0000-0000-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU
{10000000-0000-0000-0000-000000000005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10000000-0000-0000-0000-000000000005}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10000000-0000-0000-0000-000000000005}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10000000-0000-0000-0000-000000000005}.Release|Any CPU.Build.0 = Release|Any CPU
{10000000-0000-0000-0000-000000000006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10000000-0000-0000-0000-000000000006}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10000000-0000-0000-0000-000000000006}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10000000-0000-0000-0000-000000000006}.Release|Any CPU.Build.0 = Release|Any CPU
{10000000-0000-0000-0000-000000000007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10000000-0000-0000-0000-000000000007}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10000000-0000-0000-0000-000000000007}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10000000-0000-0000-0000-000000000007}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+24
View File
@@ -0,0 +1,24 @@
# P_Rec_Annot Plant 3D 2026 foundation
Foundation for migration from the legacy P_Rec_Annot v1.0.4. Legacy sources remain read-only and are not copied here.
Current slice:
* `PRecAnnot.Abstractions` — Autodesk-free immutable contracts and DTOs;
* `PRecAnnot.Core` — deterministic annotation planning and duplicate filtering;
* `PRecAnnot.Application` — command registry and use-case orchestration;
* `PRecAnnot.Plant3D2026` — guarded adapter boundary; compilation against Plant 3D 2026 is blocked until the SDK/runtime is installed;
* `PRecAnnot.UI` — idempotent Ribbon registration model, without legacy `_netload`;
* `PRecAnnot.Telemetry` — local JSONL command-scope events, without project path/number;
* `PRecAnnot.Bundle.Tests` and `PRecAnnot.Core.Tests` — architecture/core/packaging checks.
The first vertical slice is intentionally a dry-run planning slice. It does not mutate DWG until the Plant 3D 2026 adapter is compiled and runtime-tested on a controlled project.
Build:
```powershell
dotnet build .\PRecAnnot2026.sln --configuration Release
dotnet test .\PRecAnnot2026.sln --configuration Release --no-build
```
When Plant 3D 2026 is installed, pass `/p:Plant3D2026InstallDir=...`. Autodesk assemblies are `Private=false`, `CopyLocal=false` and are never committed.
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ApplicationPackage SchemaVersion="1.0" Name="P_Rec_Annot" Description="P_Rec_Annot Plant 3D 2026" AppVersion="0.1.0" Author="NBIM">
<Components>
<!-- SeriesMin/SeriesMax and platform token are pending official/runtime verification. -->
<RuntimeRequirements OS="Win64" />
<ComponentEntry AppName="PRecAnnot2026" ModuleName="Contents\Windows\PRecAnnot.Plant3D2026.dll" AppDescription="P_Rec_Annot entry assembly" LoadReasons="LoadOnAutoCADStartup" />
</Components>
</ApplicationPackage>
@@ -0,0 +1,3 @@
# Target architecture
Core and Abstractions have no Autodesk, WinForms or WPF references. Only `PRecAnnot.Plant3D2026` may reference installed Autodesk assemblies. DWG mutation remains synchronous on AutoCAD's thread; no `Task.Run` is used for database operations. Ribbon registration is guarded and unloadable. The bundle has one entry assembly and no legacy `_netload` chain.
+29
View File
@@ -0,0 +1,29 @@
# Build
The workstation has .NET SDK 8.0.423 and Autodesk AutoCAD Plant 3D 2026 installed. The paths are deliberately supplied at build time; no machine-specific Autodesk path is stored in project files.
Detected assembly inventory:
| Assembly | Assembly version | Build input |
|---|---:|---|
| AcMgd.dll | 25.1.0.0 | `AutoCAD2026ManagedDir` |
| AcDbMgd.dll | 25.1.0.0 | `AutoCAD2026ManagedDir` |
| AcCoreMgd.dll | 25.1.0.0 | `AutoCAD2026ManagedDir` |
| AdWindows.dll | 5.2.1.1 | `AutoCAD2026ManagedDir` |
| PnP3dObjectsMgd.dll | 17.0.60.1 | `Plant3D2026ManagedDir` |
| PnPProjectManagerMgd.dll | 17.0.60.1 | `Plant3D2026ManagedDir` |
The actual build used:
```powershell
dotnet restore .\Plant3D2026\P_Rec_Annot\PRecAnnot2026.sln
dotnet build .\Plant3D2026\P_Rec_Annot\PRecAnnot2026.sln --configuration Release `
/p:AutoCAD2026ManagedDir="<AutoCAD managed directory>" `
/p:Plant3D2026ManagedDir="<Plant 3D managed directory>"
```
`PRecAnnot.Plant3D2026` now compiles against the real AutoCAD/Plant 3D 2026 metadata. The adapter has real `IExtensionApplication`, `CommandMethod`, `PlantApplication.CurrentProject`, `Document`, `Editor`, and `Autodesk.Windows` Ribbon references. The previous conditional foundation-only adapter was removed. Autodesk references are `Private=false` and `CopyLocal=false`.
Release emits one non-fatal WindowsBase reference conflict caused by Autodesk's legacy reference surface versus the .NET 8 reference pack. It does not copy Autodesk assemblies to output. Autodesk DLLs are excluded from Git, NuGet, bundle, and publish output.
The local smoke bundle is created at `Plant3D2026\P_Rec_Annot\artifacts\P_Rec_Annot.bundle`. Deploy is NOT PERFORMED.
@@ -0,0 +1,18 @@
# Command migration matrix
Initial active set is based on non-archive command registrations and Ribbon definitions in audit evidence. This matrix is deliberately explicit; commands remain blocked until Plant 3D 2026 runtime evidence exists.
| Legacy command | Legacy module | Target use case | Decision | Status | Blocker/test |
|---|---|---|---|---|---|
| PXP-AAV | AutoAnnotateView | automatic annotations in viewport | port after isolation | BLOCKED_WITH_REASON | 2026 assemblies/runtime |
| PXP-OA | OrthoAutoAnnotate | axis/support graphical annotations | port after isolation | BLOCKED_WITH_REASON | exact legacy rules/golden DWG |
| PXP-ALC | AutoLinenumberConnections | line-number connection annotations | reimplement | BLOCKED_WITH_REASON | DataLinks adapter |
| PXP-LN, PXP-ELN, PXP-ULN, PXP-ESN, PXP-RMP | AnnotateLineNumbers | line-number list/updates | reimplement | BLOCKED_WITH_REASON | golden cases |
| PXP-AE, PXP-IE | AutoElevation/InsertElevation | elevations | reimplement | BLOCKED_WITH_REASON | geometry/runtime |
| PXP-IOD | UpdateOrthoDrawings | batch update | defer to Wave 5 | BLOCKED_WITH_REASON | multi-DWG transaction |
| PXP-UOB | UpdateOrthoViewBlocks | viewport layers | adapter | BLOCKED_WITH_REASON | layer API |
| PXP-OSI, PXP-COV | OrthoAutoAnnotate/CheckOrthoView | locate/check objects | compare native | BLOCKED_WITH_REASON | runtime comparison |
| PXP-MA, CAC, BJ, mto-ma | PHPAnnotations/P3DNewMa | manual annotation blocks | port/review | BLOCKED_WITH_REASON | UI and block contract |
| PXP-AC | SwapAttributesMenuItem | swap annotation attributes | port after rules | BLOCKED_WITH_REASON | block schema |
Archive-only registrations are not published. No command is silently discarded.
@@ -0,0 +1,24 @@
{
"executionMode": "DryRun",
"runtimeExecuted": false,
"plantProjectDetected": null,
"orthoDetection": "NotExecuted",
"viewportCount": null,
"dataLinksAvailable": null,
"objectsRead": null,
"plan": {
"create": null,
"update": null,
"skip": null,
"conflict": null,
"unsupported": null
},
"planDeterministic": true,
"dbmodBefore": null,
"dbmodAfter": null,
"dwgModified": null,
"dcfModified": null,
"projectXmlModified": null,
"unhandledExceptions": null,
"blocker": "No controlled test Ortho DWG/project path was supplied or discovered; runtime DryRun was not claimed."
}
@@ -0,0 +1,26 @@
{
"product": "Autodesk AutoCAD Plant 3D 2026",
"manualNetload": true,
"bundleLoaded": true,
"automaticAppLoaderChecked": false,
"smokeModeConfirmedByOperator": true,
"observedCommandLogRuntimeMode": "Normal",
"assemblyInitializeCountObserved": 1,
"commands": {
"PHP_REC_ANNOT": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" },
"PXP-AAV": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" },
"PXP-OA": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" }
},
"ribbon": { "created": true, "duplicateCheckedExhaustively": false },
"plantContext": { "projectDetected": true, "projectDisplayName": "DefaultProject", "observedOrthoDetected": false, "dataLinksAvailable": true },
"writesBlocked": true,
"dbmodChecked": false,
"dwgModified": false,
"dcfModified": false,
"projectXmlModified": false,
"telemetryEventsExpected": 3,
"telemetryEventsActual": 3,
"unhandledExceptions": 0,
"temporaryInstallationRemoved": true,
"productionDeploy": false
}
@@ -0,0 +1,3 @@
# Golden test plan
Capture a minimal 2019 Ortho fixture with input objects, output blocks/attributes, text, layer, style, coordinates, rotation, scale, DataLinks, before/after usage event and repeat-run behavior. Recreate the same fixture in 2026 only after the adapter is available. Do not commit production DWG/DCF.
@@ -0,0 +1,5 @@
RuleId,TargetClass,TargetMethod,LegacyFile,LegacyLines,Behavior,MigrationDecision,TestName
RULE-001,AnnotationRuleEngine,BuildPlan,AutoAnnotateView/Commands.cs,476-500,Remove prior annotations then insert calculated annotations,REIMPLEMENT_SAME_BEHAVIOR,ExistingAnnotation_IsUpdatedNotDuplicated
RULE-002,AnnotationRuleEngine,BuildPlan,OrthoAutoAnnotate/Commands.cs,6444-6488,Require Plant project and registered Ortho drawing,KEEP_AFTER_ISOLATION,PlantProjectAndOrthoDrawingRequired
RULE-003,PlantObjectReader,ReadObjects,README.md,96-101,Read EngineeringItems and line-group relationships through PnP API,REIMPLEMENT_SAME_BEHAVIOR,PlantAdapter_ReadsGoldenObjects
RULE-004,CommandRegistry,Foundation,OrthoLoader/Ribbon.cs,109-129,Expose annotation commands in annotation panel,REIMPLEMENT_SAME_BEHAVIOR,AllActiveCommands_AreMapped
1 RuleId TargetClass TargetMethod LegacyFile LegacyLines Behavior MigrationDecision TestName
2 RULE-001 AnnotationRuleEngine BuildPlan AutoAnnotateView/Commands.cs 476-500 Remove prior annotations then insert calculated annotations REIMPLEMENT_SAME_BEHAVIOR ExistingAnnotation_IsUpdatedNotDuplicated
3 RULE-002 AnnotationRuleEngine BuildPlan OrthoAutoAnnotate/Commands.cs 6444-6488 Require Plant project and registered Ortho drawing KEEP_AFTER_ISOLATION PlantProjectAndOrthoDrawingRequired
4 RULE-003 PlantObjectReader ReadObjects README.md 96-101 Read EngineeringItems and line-group relationships through PnP API REIMPLEMENT_SAME_BEHAVIOR PlantAdapter_ReadsGoldenObjects
5 RULE-004 CommandRegistry Foundation OrthoLoader/Ribbon.cs 109-129 Expose annotation commands in annotation panel REIMPLEMENT_SAME_BEHAVIOR AllActiveCommands_AreMapped
@@ -0,0 +1,18 @@
# Migration dashboard
The first real Plant 3D 2026 build is complete against the workstation assemblies (AutoCAD managed 25.1.0.0; Plant managed 17.0.60.1; AdWindows 5.2.1.1). Core remains Autodesk-free.
| Area | Status |
|---|---|
| `PHP_REC_ANNOT` | Registered real `CommandMethod`; primary diagnostic entry point |
| `PXP-AAV` | Registered real `CommandMethod`; diagnostic alias |
| `PXP-OA` | Registered real `CommandMethod`; diagnostic alias |
| Ribbon | Implemented with `Autodesk.Windows`; idempotent tab/panel guard |
| Adapter | Compiled against real 2026 assemblies |
| Bundle | Local smoke package built and checked for Autodesk DLL exclusion |
| Runtime smoke | Manual NETLOAD confirmed on Plant 3D 2026; automatic AppLoader and Ortho DryRun remain unverified |
| Ortho read path | Compiled against `OrthoDocUtils`, `OrthoProject`, `DwgSheetDefinitions`, `DwgViewDefs`, and `DataLinksManager`; runtime fixture pending |
| Dry-run plan | Deterministic Core builder with DTO boundary and Apply guard; no annotation writes |
| Production deploy | NOT PERFORMED |
Fixed API assumptions: foundation-only lifecycle became `IExtensionApplication`; command descriptors became actual `CommandMethod` registrations; Ortho detection uses `OrthoDocUtils.IsValidOrthoDrawing` plus the Ortho project part; viewport metadata uses `DwgSheetDefinitions`/`DwgViewDefs`; object properties use read-only `DataLinksManager.SelectAcPpRowIds` and `GetAllProperties`; Core receives only immutable DTOs. Remaining blockers are a controlled Ortho fixture, geometry/object-to-viewport mapping, legacy source provenance, and future Apply implementation.
@@ -0,0 +1,15 @@
# Module migration matrix
| Legacy module | Target project | Classification | Wave |
|---|---|---|---|
| AnnotateLineNumbers | PRecAnnot.Core + Plant3D2026 | KEEP_AFTER_ISOLATION | 3 |
| AutoAnnotateView | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 1 |
| OrthoAutoAnnotate | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 1 |
| AutoLinenumberConnections | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 2 |
| AutoElevation/InsertElevation | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 3 |
| UpdateOrthoDrawings | Plant3D2026/Application | DEFER_AND_RETEST | 5 |
| OrthoLoader | bundle/entry composition root | DO_NOT_REUSE | 1 |
| Ribbon | PRecAnnot.UI | REIMPLEMENT | 1 |
| GetLinenumberAnnotationText | Plant adapter + Core mapper | KEEP_AFTER_ISOLATION | 2 |
| PHPAnnotations/P3DNewMa | UI + block adapter | REVIEW_USAGE | 4 |
| telemetry | PRecAnnot.Telemetry | REIMPLEMENT | 1 |
@@ -0,0 +1,9 @@
# PXP-AAV read-path analysis
Status: traced to the existing command registration and the shared dry-run plan path; annotation writes are not implemented in this slice.
| RuleId | LegacyCommand | LegacyProject | LegacyFile | ObservedBehavior | TargetUseCase | TargetClass | Test | Status |
|---|---|---|---|---|---|---|---|---|
| PXP-AAV-001 | PXP-AAV | legacy audit | `docs/legacy-rule-provenance.csv` | legacy provenance is recorded, but the requested legacy checkout is absent on this workstation | read objects visible to an Ortho view and build a dry-run plan | adapter DTO -> `AnnotationPlan` | `AnnotationPlan_InputOrderDoesNotChangeResult` | partial |
The implementation deliberately does not claim behavioral equivalence. The next required input is a read-only copy of the legacy source or a golden test Ortho drawing with expected diagnostics.
@@ -0,0 +1,9 @@
# PXP-OA read-path analysis
Status: command is a real `CommandMethod` and uses the same safe context/read-plan pipeline. No Ortho annotation is created, updated, or deleted.
| RuleId | LegacyCommand | LegacyProject | LegacyFile | ObservedBehavior | TargetUseCase | TargetClass | Test | Status |
|---|---|---|---|---|---|---|---|---|
| PXP-OA-001 | PXP-OA | legacy audit | `docs/legacy-rule-provenance.csv` | legacy provenance is available, but source checkout was not found locally | Ortho context and DataLinks read diagnostics | adapter DTO -> `AnnotationPlan` | `ApplyMode_IsBlocked` | partial |
Exact object filters, annotation text, placement rules, and existing-annotation detection remain unconfirmed until the legacy source or a controlled golden fixture is supplied.
@@ -0,0 +1,31 @@
# Plant 3D 2026 runtime smoke results
## Scope
The smoke test used a temporary per-user bundle installation only. Production deploy was not performed. The temporary installation was removed after AutoCAD closed.
## Confirmed
- Manual `NETLOAD` loaded the P_Rec_Annot entry assembly in AutoCAD Plant 3D 2026.
- `PHP_REC_ANNOT`, `PXP-AAV`, and `PXP-OA` were accepted by the command line.
- The active Plant project was detected as `DefaultProject`.
- The user confirmed Smoke mode behavior and no DWG mutation during the manual smoke interaction.
- The runtime log contains one assembly initialization and one normal termination for the observed process.
- The runtime log records Ribbon creation with status `Created`.
- The runtime log records exactly one `CommandUsageEvent` for each of the three commands.
- The observed Plant context reported `DataLinksAvailable=true` and `IsOrthoDrawing=false` for `Floor Plan Sample.dwg`.
- No unhandled exception was reported in the captured runtime log.
## Not confirmed
- Automatic AppLoader loading without `NETLOAD` was not tested.
- DBMOD and byte-level DWG/DCF/Project.xml comparisons were not captured by the smoke evidence.
- Ribbon duplicate behavior across workspace/document lifecycle was not exhaustively tested.
- A command invocation logged as `RuntimeMode=Smoke` is not present in the captured JSONL; the three detailed command records have `RuntimeMode=Normal`.
## Evidence handling
The detailed runtime JSONL remains local under `%LOCALAPPDATA%\Promhimproekt\PRecAnnot2026\Logs\` and is not committed. No DWG, DCF, Project.xml, Autodesk DLL, user identity, project path, or full local runtime path is included in the repository.
Temporary installation: PERFORMED AND REMOVED.
Production deploy: NOT PERFORMED.
@@ -0,0 +1,19 @@
# Runtime test plan
Runtime smoke is ready but has not been executed in AutoCAD. The local package is `Plant3D2026\P_Rec_Annot\artifacts\P_Rec_Annot.bundle`.
Next-step installation command (intentionally not executed):
```powershell
Copy-Item -Recurse -Force .\Plant3D2026\P_Rec_Annot\artifacts\P_Rec_Annot.bundle `
"$env:APPDATA\Autodesk\ApplicationPlugins\P_Rec_Annot.bundle"
```
In a controlled Plant 3D 2026 session, verify that the entry assembly loads, the `Промхимпроект` tab and `Аннотации` panel occur once, and the `Аннотировать` button invokes `PHP_REC_ANNOT`. Open a Plant project and active DWG, run all three commands, verify the project guard and diagnostic-only behavior, and confirm no unhandled exception. Repeat initialization and confirm no Ribbon duplicate. DWG mutation, Ortho viewport/data-link behavior, undo, repeat-run idempotence, and golden-DWG comparison remain runtime blockers for the implementation wave.
Deploy status: NOT PERFORMED.
# Current slice: Ortho read / dry-run
`P_REC_ANNOT_RUNTIME_MODE=Smoke` reads context only. `DryRun` reads Ortho view metadata and DataLinks properties, builds an immutable deterministic `AnnotationPlan`, and never applies it. `Apply` is blocked with `APPLY_MODE_NOT_IMPLEMENTED`; unknown values resolve to `DryRun`.
The read adapter uses `OrthoDocUtils.IsValidOrthoDrawing`, `OrthoProject.DwgSheetDefinitions`, `IDwgViewDef`, and read-only `DataLinksManager` methods. The adapter does not expose AutoCAD or Plant objects to Core and contains no write calls. A runtime DryRun result is not published until a controlled test Ortho DWG is available; see `docs/evidence/ortho-dry-run-summary.json`.
@@ -0,0 +1,212 @@
namespace PRecAnnot.Abstractions;
public sealed record RuntimeDiagnostic
{
public required string Code { get; init; }
public required string Message { get; init; }
public string Severity { get; init; } = "Warning";
}
public enum OrthoDetectionStatus { Confirmed, NotOrtho, Ambiguous, Unsupported, Failed }
public sealed record OrthoDetectionResult
{
public required OrthoDetectionStatus Status { get; init; }
public required IReadOnlyList<string> Evidence { get; init; }
public required IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; }
}
public readonly record struct Point2Dto(double X, double Y);
public readonly record struct Point3Dto(double X, double Y, double Z);
public readonly record struct Vector2Dto(double X, double Y);
public sealed record Bounds2Dto(Point2Dto Minimum, Point2Dto Maximum);
public sealed record GeometrySnapshot
{
public Point3Dto? WorldPosition { get; init; }
public Point2Dto? PaperPosition { get; init; }
public Bounds2Dto? ProjectedBounds { get; init; }
public double? Rotation { get; init; }
public double? Elevation { get; init; }
}
public sealed record OrthoViewportInfo
{
public required string Id { get; init; }
public required string DisplayName { get; init; }
public double CenterX { get; init; }
public double CenterY { get; init; }
public double Width { get; init; }
public double Height { get; init; }
public double Scale { get; init; }
public double TwistAngle { get; init; }
public bool IsOn { get; init; }
public bool IsLocked { get; init; }
public string? SourceDrawingName { get; init; }
public string? ViewName { get; init; }
}
public sealed record PlantObjectSnapshot
{
public required string SnapshotId { get; init; }
public required string ObjectClass { get; init; }
public string? Tag { get; init; }
public string? LineNumber { get; init; }
public string? SupportNumber { get; init; }
public string? EquipmentNumber { get; init; }
public required GeometrySnapshot Geometry { get; init; }
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
public IReadOnlyList<string> Diagnostics { get; init; } = Array.Empty<string>();
}
public sealed record PlantRuntimeContext
{
public bool HasActiveDocument { get; init; }
public bool HasPlantProject { get; init; }
public bool IsOrthoDrawing { get; init; }
public string? DrawingName { get; init; }
public string? ProjectDisplayName { get; init; }
public IReadOnlyList<OrthoViewportInfo> Viewports { get; init; } = Array.Empty<OrthoViewportInfo>();
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
public OrthoDetectionResult Detection { get; init; } = new() { Status = OrthoDetectionStatus.Unsupported, Evidence = Array.Empty<string>(), Diagnostics = Array.Empty<RuntimeDiagnostic>() };
}
public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
public interface IOrthoViewportReader { IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken); }
public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported }
public sealed record AnnotationOperationPlan
{
public required string SourceSnapshotId { get; init; }
public required AnnotationOperationKind Kind { get; init; }
public string? AnnotationType { get; init; }
public string? Text { get; init; }
public Point2Dto? Position { get; init; }
public double? Rotation { get; init; }
public string? ExistingAnnotationId { get; init; }
public required IReadOnlyList<string> Reasons { get; init; }
}
public sealed record AnnotationPlanSummary
{
public int ObjectsExamined { get; init; }
public int CreateCount { get; init; }
public int UpdateCount { get; init; }
public int SkipCount { get; init; }
public int ConflictCount { get; init; }
public int UnsupportedCount { get; init; }
public int WarningCount { get; init; }
public int ErrorCount { get; init; }
}
public sealed record AnnotationPlanningRequest
{
public required string CommandName { get; init; }
public required OrthoViewportInfo Viewport { get; init; }
public IReadOnlyList<PlantObjectSnapshot> Objects { get; init; } = Array.Empty<PlantObjectSnapshot>();
public IReadOnlyList<ExistingAnnotation> Existing { get; init; } = Array.Empty<ExistingAnnotation>();
}
public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); }
public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply }
public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); }
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
public sealed record PlantObjectData
{
public required string ObjectId { get; init; }
public required string ClassName { get; init; }
public string? LineNumber { get; init; }
public string? Tag { get; init; }
public string? DisplayName { get; init; }
public required AnnotationPoint Position { get; init; }
public double Rotation { get; init; }
}
public readonly record struct AnnotationPoint(double X, double Y, double Z = 0);
public sealed record ExistingAnnotation(string StableKey, string? Text, AnnotationPoint Position);
public sealed record AnnotationRequest
{
public required OrthoViewportRef Viewport { get; init; }
public required IReadOnlyList<PlantObjectData> Objects { get; init; }
public required IReadOnlyList<ExistingAnnotation> Existing { get; init; }
}
public sealed record AnnotationIntent
{
public required string StableKey { get; init; }
public required string Text { get; init; }
public required AnnotationPoint Position { get; init; }
public required double Rotation { get; init; }
public required string ClassName { get; init; }
public required string Layer { get; init; }
}
public sealed record AnnotationPlan
{
public string PlanId { get; init; } = string.Empty;
public string CommandName { get; init; } = string.Empty;
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
public AnnotationPlanSummary Summary { get; init; } = new();
public required IReadOnlyList<AnnotationIntent> Create { get; init; }
public required IReadOnlyList<AnnotationIntent> Update { get; init; }
public required IReadOnlyList<string> Delete { get; init; }
public required IReadOnlyList<string> SkipReasons { get; init; }
public int ExaminedCount { get; init; }
public bool IsDeterministic { get; init; } = true;
}
public interface IPlantProjectContext
{
bool IsPlantProjectOpen { get; }
string? ProjectName { get; }
}
public interface IOrthoDrawingContext
{
bool IsOrthoDrawing { get; }
IReadOnlyList<OrthoViewportRef> GetViewports();
}
public interface IPlantObjectReader
{
IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken);
}
public interface IAnnotationRepository
{
IReadOnlyList<ExistingAnnotation> FindExisting(OrthoViewportRef viewport);
AnnotationWriteResult Apply(AnnotationPlan plan, CancellationToken cancellationToken);
}
public sealed record AnnotationWriteResult(int Created, int Updated, int Deleted, int Skipped);
public interface IAnnotationRuleEngine
{
AnnotationPlan BuildPlan(AnnotationRequest request);
}
public sealed record CommandUsageEvent
{
public required string CommandName { get; init; }
public required string ResultStatus { get; init; }
public int ObjectsExamined { get; init; }
public int AnnotationsCreated { get; init; }
public int AnnotationsUpdated { get; init; }
public int AnnotationsDeleted { get; init; }
public int AnnotationsSkipped { get; init; }
public int WarningCount { get; init; }
public int ErrorCount { get; init; }
public long DurationMilliseconds { get; init; }
public DateTimeOffset TimestampUtc { get; init; } = DateTimeOffset.UtcNow;
}
public interface ICommandUsageSink
{
void Record(CommandUsageEvent usageEvent);
}
@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
</Project>
@@ -0,0 +1,36 @@
namespace PRecAnnot.Application;
public sealed record PRecAnnotCommandDescriptor
{
public required string CommandName { get; init; }
public required string DisplayName { get; init; }
public required string UseCaseId { get; init; }
public required string RibbonPanel { get; init; }
public bool RequiresPlantProject { get; init; }
public bool RequiresOrthoDrawing { get; init; }
public bool RequiresSelection { get; init; }
public string Status { get; init; } = "BLOCKED_WITH_REASON";
}
public sealed class CommandRegistry
{
private readonly IReadOnlyDictionary<string, PRecAnnotCommandDescriptor> _commands;
public CommandRegistry(IEnumerable<PRecAnnotCommandDescriptor> commands)
{
var list = commands.ToList();
if (list.GroupBy(x => x.CommandName, StringComparer.OrdinalIgnoreCase).Any(x => x.Count() > 1))
throw new ArgumentException("Command names must be unique.", nameof(commands));
_commands = list.ToDictionary(x => x.CommandName, StringComparer.OrdinalIgnoreCase);
}
public IReadOnlyCollection<PRecAnnotCommandDescriptor> Commands => _commands.Values.ToArray();
public bool TryGet(string name, out PRecAnnotCommandDescriptor? descriptor) => _commands.TryGetValue(name, out descriptor);
public static CommandRegistry Foundation() => new(new[]
{
new PRecAnnotCommandDescriptor { CommandName = "PHP_REC_ANNOT", DisplayName = "Аннотировать", UseCaseId = "ortho-annotation", RibbonPanel = "Аннотации", RequiresPlantProject = true, RequiresOrthoDrawing = true, Status = "BLOCKED_WITH_REASON" },
new PRecAnnotCommandDescriptor { CommandName = "PXP-AAV", DisplayName = "Разместить аннотации автоматически", UseCaseId = "auto-annotate-view", RibbonPanel = "Аннотации", RequiresPlantProject = true, RequiresOrthoDrawing = true, Status = "BLOCKED_WITH_REASON" },
new PRecAnnotCommandDescriptor { CommandName = "PXP-OA", DisplayName = "Разместить осевые линии и опоры", UseCaseId = "ortho-axes-supports", RibbonPanel = "Осевые линии и опоры", RequiresPlantProject = true, RequiresOrthoDrawing = true, Status = "BLOCKED_WITH_REASON" }
});
}
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
<ItemGroup><ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" /></ItemGroup>
</Project>
@@ -0,0 +1,109 @@
using System.Security.Cryptography;
using System.Text;
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
{
public AnnotationPlan Build(AnnotationPlanningRequest request)
{
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase);
var operations = new List<AnnotationOperationPlan>(objects.Length);
var creates = new List<AnnotationIntent>();
var updates = new List<AnnotationIntent>();
var diagnostics = new List<RuntimeDiagnostic>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in objects)
{
if (!seen.Add(item.SnapshotId))
{
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Conflict, Reasons = new[] { "duplicate-snapshot-id" } });
continue;
}
if (string.IsNullOrWhiteSpace(item.ObjectClass))
{
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Unsupported, Reasons = new[] { "missing-object-class" } });
continue;
}
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
if (string.IsNullOrWhiteSpace(text))
{
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "missing-line-number-or-tag" } });
continue;
}
var position = item.Geometry.PaperPosition;
if (position is null)
{
diagnostics.Add(new RuntimeDiagnostic { Code = "MISSING_GEOMETRY", Message = $"No paper position for {item.SnapshotId}." });
}
var stableKey = $"{request.Viewport.SourceDrawingName ?? string.Empty}:{request.Viewport.Id}:{item.SnapshotId}";
var existingAnnotation = existing.TryGetValue(stableKey, out var match) ? match : existing.TryGetValue(item.SnapshotId, out var directMatch) ? directMatch : null;
var kind = existingAnnotation is null ? AnnotationOperationKind.Create : AnnotationOperationKind.Update;
operations.Add(new AnnotationOperationPlan
{
SourceSnapshotId = item.SnapshotId,
Kind = kind,
AnnotationType = "PlantObject",
Text = text,
Position = position,
Rotation = item.Geometry.Rotation,
ExistingAnnotationId = existingAnnotation?.StableKey,
Reasons = Array.Empty<string>()
});
var intent = new AnnotationIntent
{
StableKey = stableKey,
Text = text,
Position = position is { } point ? new AnnotationPoint(point.X, point.Y) : new AnnotationPoint(0, 0),
Rotation = item.Geometry.Rotation ?? 0,
ClassName = item.ObjectClass,
Layer = "PXP_ANNOTATION"
};
if (kind == AnnotationOperationKind.Create) creates.Add(intent); else updates.Add(intent);
}
var summary = new AnnotationPlanSummary
{
ObjectsExamined = objects.Length,
CreateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Create),
UpdateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Update),
SkipCount = operations.Count(x => x.Kind == AnnotationOperationKind.Skip),
ConflictCount = operations.Count(x => x.Kind == AnnotationOperationKind.Conflict),
UnsupportedCount = operations.Count(x => x.Kind == AnnotationOperationKind.Unsupported),
WarningCount = diagnostics.Count(x => !string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase)),
ErrorCount = diagnostics.Count(x => string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase))
};
return new AnnotationPlan
{
PlanId = BuildPlanId(request, objects),
CommandName = request.CommandName,
Operations = operations,
Diagnostics = diagnostics,
Summary = summary,
Create = creates,
Update = updates,
Delete = Array.Empty<string>(),
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).ToArray(),
ExaminedCount = objects.Length,
IsDeterministic = true
};
}
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects)
{
var canonical = new StringBuilder(request.CommandName).Append('|').Append(request.Viewport.SourceDrawingName).Append('|').Append(request.Viewport.Id);
foreach (var item in objects)
canonical.Append('|').Append(item.SnapshotId).Append('|').Append(item.ObjectClass).Append('|').Append(item.Tag).Append('|').Append(item.LineNumber).Append('|').Append(item.Geometry.PaperPosition);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
return "plan-" + Convert.ToHexString(hash)[..24];
}
}
@@ -0,0 +1,26 @@
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public sealed class AnnotationRuleEngine : IAnnotationRuleEngine
{
public AnnotationPlan BuildPlan(AnnotationRequest request)
{
var objects = request.Objects.Select(item => new PlantObjectSnapshot
{
SnapshotId = item.ObjectId,
ObjectClass = item.ClassName,
Tag = item.Tag,
LineNumber = item.LineNumber,
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation }
}).ToArray();
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
{
CommandName = "PHP_REC_ANNOT",
Viewport = new OrthoViewportInfo { Id = request.Viewport.ViewportId, DisplayName = request.Viewport.ViewportId, SourceDrawingName = request.Viewport.DrawingId },
Objects = objects,
Existing = request.Existing
});
}
}
@@ -0,0 +1,8 @@
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public sealed class ExecutionModeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
{
public void EnsureWriteAllowed(string operation) => throw new InvalidOperationException(mode == PRecAnnotExecutionMode.Apply ? "APPLY_MODE_NOT_IMPLEMENTED" : "SMOKE_WRITE_BLOCKED");
}
@@ -0,0 +1,23 @@
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public static class OrthoDetectionClassifier
{
public static OrthoDetectionResult Classify(bool hasPlantProject, bool hasOrthoPart, bool apiConfirmed, bool metadataAmbiguous = false)
{
if (!hasPlantProject) return Result(OrthoDetectionStatus.NotOrtho, "NO_PLANT_PROJECT", "Active Plant project is absent.");
if (metadataAmbiguous) return Result(OrthoDetectionStatus.Ambiguous, "AMBIGUOUS_ORTHO_METADATA", "Plant metadata is insufficient to confirm Ortho.");
if (!hasOrthoPart) return Result(OrthoDetectionStatus.Unsupported, "ORTHO_PART_UNAVAILABLE", "Ortho project part is unavailable.");
return apiConfirmed
? Result(OrthoDetectionStatus.Confirmed, "ORTHO_API_CONFIRMED", "OrthoDocUtils confirmed the active drawing.")
: Result(OrthoDetectionStatus.NotOrtho, "ORTHO_API_NEGATIVE", "OrthoDocUtils did not confirm the active drawing.");
}
private static OrthoDetectionResult Result(OrthoDetectionStatus status, string code, string message) => new()
{
Status = status,
Evidence = new[] { code },
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } }
};
}
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
<ItemGroup><ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" /></ItemGroup>
</Project>
@@ -0,0 +1,195 @@
using System.Diagnostics;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.Windows;
using PRecAnnot.Abstractions;
using PRecAnnot.Core;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
namespace PRecAnnot.Plant3D2026;
public sealed class EntryPoint : IExtensionApplication
{
public void Initialize()
{
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
RibbonIntegration.Register();
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
}
public void Terminate()
{
RuntimeDiagnostics.Write("AssemblyTerminateStarted");
RibbonIntegration.Terminate();
RuntimeDiagnostics.Write("AssemblyTerminateCompleted");
}
[CommandMethod("PHP_REC_ANNOT")]
public void PrimaryCommand() => RunDiagnostic("PHP_REC_ANNOT");
[CommandMethod("PXP-AAV")]
public void AutoAnnotateViewCommand() => RunDiagnostic("PXP-AAV");
[CommandMethod("PXP-OA")]
public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA");
private static void RunDiagnostic(string commandName)
{
var operationId = Guid.NewGuid().ToString("N");
var stopwatch = Stopwatch.StartNew();
Document? document = null;
RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName);
try
{
document = AcadApplication.DocumentManager.MdiActiveDocument;
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
var context = new PlantRuntimeContextReader().ReadCurrentContext();
var details = new Dictionary<string, object?>
{
["ExecutionMode"] = mode.ToString(),
["IsPlantProjectDetected"] = context.HasPlantProject,
["IsPlantProjectOpen"] = context.HasPlantProject,
["IsOrthoDrawing"] = context.IsOrthoDrawing,
["ViewportCount"] = context.Viewports.Count,
["DataLinksAvailable"] = DataLinksAvailable(context.HasPlantProject)
};
if (document is null || !context.HasPlantProject)
{
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён.");
WriteUsage(operationId, commandName, "PreflightRejected", document, details, stopwatch.ElapsedMilliseconds);
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
return;
}
if (mode == PRecAnnotExecutionMode.Smoke || !context.IsOrthoDrawing)
{
var status = context.IsOrthoDrawing ? "PreflightAccepted" : "NotOrtho";
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
WriteEditor(document, $"\nP_Rec_Annot {commandName}: проект '{context.ProjectDisplayName}' найден; тип чертежа: {(context.IsOrthoDrawing ? "Ortho" : "NotOrtho")}; DWG не изменён.");
WriteUsage(operationId, commandName, status, document, details, stopwatch.ElapsedMilliseconds);
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
return;
}
var builder = new AnnotationPlanBuilder();
var plans = new List<AnnotationPlan>();
foreach (var viewport in context.Viewports)
{
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects }));
}
var summary = new
{
ObjectsExamined = plans.Sum(x => x.Summary.ObjectsExamined),
CreateCount = plans.Sum(x => x.Summary.CreateCount),
UpdateCount = plans.Sum(x => x.Summary.UpdateCount),
SkipCount = plans.Sum(x => x.Summary.SkipCount),
ConflictCount = plans.Sum(x => x.Summary.ConflictCount),
UnsupportedCount = plans.Sum(x => x.Summary.UnsupportedCount),
WarningCount = plans.Sum(x => x.Summary.WarningCount),
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
};
details["ObjectsExamined"] = summary.ObjectsExamined;
details["PlanCreateCount"] = summary.CreateCount;
details["PlanUpdateCount"] = summary.UpdateCount;
details["PlanSkipCount"] = summary.SkipCount;
details["PlanConflictCount"] = summary.ConflictCount;
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
details["ErrorCount"] = summary.ErrorCount;
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
WriteEditor(document, $"\nP_Rec_Annot — диагностический план\nПроект: {context.ProjectDisplayName}\nТип чертежа: Ortho\nViewport: {context.Viewports.Count}\nОбъектов найдено: {summary.ObjectsExamined}\nБудет создано: {summary.CreateCount}\nБудет обновлено: {summary.UpdateCount}\nБудет пропущено: {summary.SkipCount}\nКонфликтов: {summary.ConflictCount}\nОшибок: {summary.ErrorCount}\nDWG не изменён.");
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
}
catch (System.Exception exception)
{
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document);
WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception);
WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}");
}
}
private static PRecAnnotExecutionMode ParseMode(string value) =>
value.Equals("Smoke", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Smoke :
value.Equals("Apply", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Apply : PRecAnnotExecutionMode.DryRun;
private static bool DataLinksAvailable(bool hasPlantProject)
{
try { return hasPlantProject && Autodesk.ProcessPower.PlantInstance.PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; }
catch { return false; }
}
private static void WriteUsage(string operationId, string commandName, string status, Document? document, IReadOnlyDictionary<string, object?>? details, long duration, System.Exception? exception = null)
{
var eventDetails = new Dictionary<string, object?>
{
["ObjectsExamined"] = 0, ["AnnotationsCreated"] = 0, ["AnnotationsUpdated"] = 0,
["AnnotationsDeleted"] = 0, ["AnnotationsSkipped"] = 0, ["WarningCount"] = 0,
["ErrorCount"] = exception is null ? 0 : 1, ["DurationMilliseconds"] = duration
};
if (details is not null) foreach (var item in details) eventDetails[item.Key] = item.Value;
RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, status, exception, document, eventDetails);
}
private static void WriteEditor(Document? document, string message)
{
try { document?.Editor.WriteMessage(message); } catch { }
}
}
internal static class RibbonIntegration
{
private const string TabId = "PRecAnnot.Plant3D2026";
private const string PanelId = "PRecAnnot.Plant3D2026.Annotations";
private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT";
private static bool deferred;
public static void Register()
{
RuntimeDiagnostics.Write("RibbonInitializationStarted");
try
{
if (ComponentManager.Ribbon is null)
{
if (!deferred) { ComponentManager.ItemInitialized += OnItemInitialized; deferred = true; }
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred");
return;
}
RegisterNow();
}
catch (System.Exception exception) { RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); }
}
private static void OnItemInitialized(object? sender, EventArgs args)
{
if (ComponentManager.Ribbon is null) return;
ComponentManager.ItemInitialized -= OnItemInitialized;
deferred = false;
Register();
}
private static void RegisterNow()
{
var ribbon = ComponentManager.Ribbon!;
var tab = ribbon.Tabs.FirstOrDefault(x => string.Equals(x.Id, TabId, StringComparison.OrdinalIgnoreCase));
if (tab is null) { tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; ribbon.Tabs.Add(tab); }
var panel = tab.Panels.FirstOrDefault(x => string.Equals(x.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; }
var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" };
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " });
tab.Panels.Add(new RibbonPanel { Source = source });
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created");
}
public static void Terminate()
{
if (!deferred) return;
ComponentManager.ItemInitialized -= OnItemInitialized;
deferred = false;
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><PlatformTarget>x64</PlatformTarget><UseWindowsForms>true</UseWindowsForms><EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition></PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" />
<ProjectReference Include="..\PRecAnnot.Application\PRecAnnot.Application.csproj" />
<ProjectReference Include="..\PRecAnnot.Core\PRecAnnot.Core.csproj" />
</ItemGroup>
<ItemGroup Condition="Exists('$(AutoCAD2026ManagedDir)') and Exists('$(Plant3D2026ManagedDir)')">
<Reference Include="AcMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="AcDbMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcDbMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="AcCoreMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcCoreMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="AdWindows"><HintPath>$(AutoCAD2026ManagedDir)\AdWindows.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDataLinks"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataLinks.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDataObjects"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataObjects.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPProjectManagerMgd"><HintPath>$(Plant3D2026ManagedDir)\PnPProjectManagerMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnP3dObjectsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dObjectsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
</ItemGroup>
</Project>
@@ -0,0 +1,14 @@
namespace PRecAnnot.Plant3D2026;
public sealed record Plant3D2026Availability(bool IsAvailable, string InstallDirectory, IReadOnlyList<string> MissingAssemblies)
{
public static Plant3D2026Availability Detect(string installDirectory)
{
var expected = new[] { "AcMgd.dll", "AcDbMgd.dll", "AcCoreMgd.dll", "PnPDataLinks.dll", Path.Combine("PLNT3D", "PnPProjectManagerMgd.dll"), Path.Combine("PLNT3D", "PnP3dObjectsMgd.dll") };
var missing = expected.Where(x => !File.Exists(Path.Combine(installDirectory, x))).ToArray();
return new(missing.Length == 0, installDirectory, missing);
}
}
public sealed class Plant3D2026BlockedException(Plant3D2026Availability availability)
: InvalidOperationException($"Plant 3D 2026 assemblies are unavailable at '{availability.InstallDirectory}'. Missing: {string.Join(", ", availability.MissingAssemblies)}") { }
@@ -0,0 +1,206 @@
using System.Security.Cryptography;
using System.Text;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.ProcessPower.DataLinks;
using Autodesk.ProcessPower.P3dProjectParts;
using Autodesk.ProcessPower.PlantInstance;
using Autodesk.ProcessPower.ProjectManager;
using PRecAnnot.Abstractions;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
namespace PRecAnnot.Plant3D2026;
internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
{
public PlantRuntimeContext ReadCurrentContext()
{
var document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null)
{
return Empty(false, false, "NO_ACTIVE_DOCUMENT", "Активный документ отсутствует.");
}
PlantProject? project;
try { project = PlantApplication.CurrentProject; }
catch (Exception exception)
{
return Empty(true, false, "PLANT_PROJECT_READ_FAILED", exception.GetType().Name);
}
if (project is null)
{
return Empty(true, false, "NO_PLANT_PROJECT", "Активный Plant 3D-проект не найден.");
}
OrthoProject? orthoProject;
try { orthoProject = PlantApplication.ProjectPart("Ortho") as OrthoProject; }
catch (Exception exception)
{
return Empty(true, true, "ORTHO_PROJECT_PART_READ_FAILED", exception.GetType().Name);
}
if (orthoProject is null)
{
return Empty(true, true, "ORTHO_PROJECT_PART_UNAVAILABLE", "Ortho project part недоступен.", OrthoDetectionStatus.Unsupported, "ProjectPart(Ortho)");
}
try
{
string resolvedPath;
var isOrtho = OrthoDocUtils.IsValidOrthoDrawing(document.Name, orthoProject, out resolvedPath);
if (!isOrtho)
{
return Empty(true, true, "NOT_ORTHO_DRAWING", "OrthoDocUtils.IsValidOrthoDrawing вернул false.", OrthoDetectionStatus.NotOrtho, "Ortho project part", "OrthoDocUtils.IsValidOrthoDrawing=false");
}
var viewports = new PlantOrthoViewportReader(orthoProject, document.Name).ReadViewports(CancellationToken.None);
return new PlantRuntimeContext
{
HasActiveDocument = true,
HasPlantProject = true,
IsOrthoDrawing = true,
DrawingName = Path.GetFileName(document.Name),
ProjectDisplayName = project.Name,
Viewports = viewports,
Diagnostics = Array.Empty<RuntimeDiagnostic>(),
Detection = new OrthoDetectionResult
{
Status = OrthoDetectionStatus.Confirmed,
Evidence = new[] { "ProjectPart(Ortho)", "OrthoDocUtils.IsValidOrthoDrawing=true", "DwgSheetDefinitions" },
Diagnostics = Array.Empty<RuntimeDiagnostic>()
}
};
}
catch (Exception exception)
{
return Empty(true, true, "ORTHO_DETECTION_FAILED", exception.GetType().Name, OrthoDetectionStatus.Failed, "ProjectPart(Ortho)");
}
}
private static PlantRuntimeContext Empty(bool hasDocument, bool hasProject, string code, string message, OrthoDetectionStatus status = OrthoDetectionStatus.NotOrtho, params string[] evidence) => new()
{
HasActiveDocument = hasDocument,
HasPlantProject = hasProject,
IsOrthoDrawing = false,
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } },
Detection = new OrthoDetectionResult { Status = status, Evidence = evidence, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } }
};
}
internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
{
private readonly OrthoProject project;
private readonly string drawingName;
public PlantOrthoViewportReader(OrthoProject project, string drawingName)
{
this.project = project;
this.drawingName = Path.GetFileName(drawingName);
}
public IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken)
{
var result = new List<OrthoViewportInfo>();
foreach (var sheet in project.DwgSheetDefinitions.AsArray())
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.Equals(Path.GetFileName(sheet.DwgName), drawingName, StringComparison.OrdinalIgnoreCase)) continue;
foreach (var view in sheet.DwgViewDefs.AsArray())
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var id = StableId(sheet.DwgGuid.ToString(), view.ViewName);
result.Add(new OrthoViewportInfo
{
Id = id,
DisplayName = view.ViewName,
CenterX = view.CenterPoint.X,
CenterY = view.CenterPoint.Y,
Width = view.ViewWidth,
Height = view.ViewHeight,
Scale = view.ViewScale,
TwistAngle = 0,
IsOn = true,
IsLocked = false,
SourceDrawingName = sheet.DwgName,
ViewName = view.ViewName
});
}
catch
{
// One malformed view must not abort the remaining read-only analysis.
}
}
}
return result.OrderByDescending(x => x.CenterY).ThenBy(x => x.CenterX).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();
}
private static string StableId(string sheetId, string viewName)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
return "vp-" + Convert.ToHexString(hash)[..20];
}
}
internal sealed class PlantObjectReader : IPlantObjectReader
{
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
{
var document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null) return Array.Empty<PlantObjectSnapshot>();
DataLinksManager? dataLinks;
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
catch { return Array.Empty<PlantObjectSnapshot>(); }
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
var snapshots = new List<PlantObjectSnapshot>();
foreach (var rowId in dataLinks.SelectAcPpRowIds(document.Database))
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var className = dataLinks.GetObjectClassname(rowId);
var properties = dataLinks.GetAllProperties(rowId, true)
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
snapshots.Add(new PlantObjectSnapshot
{
SnapshotId = StableId(Path.GetFileName(document.Name), rowId, className),
ObjectClass = className ?? string.Empty,
Tag = Value("Tag", "TAG"),
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
SupportNumber = Value("Support Number", "SupportNumber"),
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
Geometry = new GeometrySnapshot(),
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
});
}
catch
{
// Keep the read path best-effort; no write operation is attempted.
}
}
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
}
private static string StableId(string drawingName, int rowId, string? className)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
return "obj-" + Convert.ToHexString(hash)[..24];
}
}
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
{
public void EnsureWriteAllowed(string operation)
{
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
throw new InvalidOperationException("SMOKE_WRITE_BLOCKED");
}
}
@@ -0,0 +1,79 @@
using System.Diagnostics;
using System.Text.Json;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.ProcessPower.PlantInstance;
namespace PRecAnnot.Plant3D2026;
internal static class RuntimeDiagnostics
{
private static readonly object Gate = new();
private static readonly string LogDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Promhimproekt", "PRecAnnot2026", "Logs");
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
public static void Write(
string eventType,
string level = "INFO",
string? operationId = null,
string? commandName = null,
string? resultStatus = null,
System.Exception? exception = null,
Document? document = null,
IReadOnlyDictionary<string, object?>? details = null)
{
try
{
var payload = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["Timestamp"] = DateTimeOffset.UtcNow,
["Level"] = level,
["OperationId"] = operationId,
["EventType"] = eventType,
["AssemblyVersion"] = typeof(EntryPoint).Assembly.GetName().Version?.ToString(),
["AutoCADVersion"] = typeof(Document).Assembly.GetName().Version?.ToString(),
["PlantVersion"] = typeof(PlantApplication).Assembly.GetName().Version?.ToString(),
["ProcessId"] = Environment.ProcessId,
["ThreadId"] = Environment.CurrentManagedThreadId,
["ApartmentState"] = Thread.CurrentThread.GetApartmentState().ToString(),
["ActiveDocumentName"] = SafeDocumentName(document),
["CommandName"] = commandName,
["ResultStatus"] = resultStatus,
["ExceptionType"] = exception?.GetType().FullName,
["ExceptionMessage"] = exception?.Message,
["StackTrace"] = level.Equals("ERROR", StringComparison.OrdinalIgnoreCase) ? exception?.StackTrace : null,
["RuntimeMode"] = RuntimeMode
};
if (details is not null)
foreach (var item in details) payload[item.Key] = item.Value;
var line = JsonSerializer.Serialize(payload) + Environment.NewLine;
lock (Gate)
{
Directory.CreateDirectory(LogDirectory);
File.AppendAllText(Path.Combine(LogDirectory, "runtime.jsonl"), line);
}
}
catch
{
// Runtime diagnostics must never prevent AutoCAD command execution.
}
}
private static string? SafeDocumentName(Document? document)
{
try { return document is null ? null : Path.GetFileName(document.Name); }
catch { return null; }
}
}
internal sealed class SmokeWriteBlockedException() : InvalidOperationException("SMOKE_WRITE_BLOCKED");
internal static class SmokeGuard
{
public static bool IsSmoke => string.Equals(RuntimeDiagnostics.RuntimeMode, "Smoke", StringComparison.OrdinalIgnoreCase);
public static void EnsureReadOnly() { if (IsSmoke) throw new SmokeWriteBlockedException(); }
}
@@ -0,0 +1,14 @@
using System.Text.Json;
using PRecAnnot.Abstractions;
namespace PRecAnnot.Telemetry;
public sealed class JsonlUsageSink(string path) : ICommandUsageSink
{
private readonly object _gate = new();
public void Record(CommandUsageEvent usageEvent)
{
var json = JsonSerializer.Serialize(usageEvent);
lock (_gate) File.AppendAllText(path, json + Environment.NewLine);
}
}
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
<ItemGroup><ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" /></ItemGroup>
</Project>
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><UseWindowsForms>true</UseWindowsForms></PropertyGroup>
<ItemGroup><ProjectReference Include="..\PRecAnnot.Application\PRecAnnot.Application.csproj" /></ItemGroup>
</Project>
@@ -0,0 +1,17 @@
using PRecAnnot.Application;
namespace PRecAnnot.UI;
public sealed class RibbonRegistration
{
private readonly HashSet<string> _registered = new(StringComparer.OrdinalIgnoreCase);
public bool Register(CommandRegistry registry)
{
var changed = false;
foreach (var item in registry.Commands)
changed |= _registered.Add($"{item.RibbonPanel}:{item.CommandName}");
return changed;
}
public void Unregister(CommandRegistry registry) { foreach (var item in registry.Commands) _registered.Remove($"{item.RibbonPanel}:{item.CommandName}"); }
public int RegisteredCount => _registered.Count;
}
@@ -0,0 +1,146 @@
using PRecAnnot.Abstractions;
using PRecAnnot.Application;
using PRecAnnot.Core;
using PRecAnnot.UI;
namespace PRecAnnot.Core.Tests;
public sealed class FoundationTests
{
private static string ProjectRoot
{
get
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "PRecAnnot2026.sln"))) directory = directory.Parent;
return directory?.FullName ?? throw new DirectoryNotFoundException("PRecAnnot2026.sln was not found.");
}
}
[Fact] public void Plan_IsDeterministic()
{
var engine = new AnnotationRuleEngine();
var request = new AnnotationRequest { Viewport = new("d", "v"), Objects = new[] { new PlantObjectData { ObjectId = "2", ClassName = "Pipe", LineNumber = "L-2", Position = new(2, 2) }, new PlantObjectData { ObjectId = "1", ClassName = "Pipe", LineNumber = "L-1", Position = new(1, 1) } }, Existing = Array.Empty<ExistingAnnotation>() };
var plan = engine.BuildPlan(request);
Assert.Equal(new[] { "1", "2" }, plan.Create.Select(x => x.StableKey.Split(':').Last()));
}
[Fact] public void ExistingAnnotation_IsUpdatedNotDuplicated()
{
var item = new PlantObjectData { ObjectId = "1", ClassName = "Valve", Tag = "V-1", Position = new(0, 0) };
var plan = new AnnotationRuleEngine().BuildPlan(new AnnotationRequest { Viewport = new("d", "v"), Objects = new[] { item }, Existing = new[] { new ExistingAnnotation("d:v:1", "old", new(0, 0)) } });
Assert.Empty(plan.Create); Assert.Single(plan.Update);
}
[Fact] public void CoreContracts_DoNotExposeAutodeskTypes()
{
var names = typeof(AnnotationRequest).Assembly.GetReferencedAssemblies().Select(x => x.Name).ToArray();
Assert.DoesNotContain(names, x => x is not null && x.StartsWith("Ac", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(names, x => x is not null && x.StartsWith("Autodesk", StringComparison.OrdinalIgnoreCase));
}
[Fact] public void RibbonRegistration_IsIdempotent()
{
var ribbon = new RibbonRegistration(); var registry = CommandRegistry.Foundation();
Assert.True(ribbon.Register(registry)); Assert.False(ribbon.Register(registry)); Assert.Equal(3, ribbon.RegisteredCount);
}
[Fact] public void PlantAdapter_LoadsAgainst2026Assemblies()
{
var adapter = Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "bin", "Release", "net8.0-windows", "PRecAnnot.Plant3D2026.dll");
var assembly = System.Reflection.AssemblyName.GetAssemblyName(adapter);
var project = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PRecAnnot.Plant3D2026.csproj"));
Assert.NotNull(assembly);
Assert.Contains("AcMgd", project);
Assert.Contains("PnPProjectManagerMgd", project);
}
[Fact] public void CommandClass_HasCommandMethodAttributes()
{
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs"));
Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", source);
Assert.Contains("[CommandMethod(\"PXP-AAV\")]", source);
Assert.Contains("[CommandMethod(\"PXP-OA\")]", source);
}
[Fact] public void PrimaryCommand_IsRegistered() => Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
[Fact] public void LegacyAliases_AreRegistered() => Assert.True(File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-AAV") && File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-OA"));
[Fact] public void RibbonCommand_ReferencesRegisteredCommand() => Assert.Contains("PHP_REC_ANNOT ", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
[Fact] public void AutodeskReferences_AreNotCopyLocal()
{
var project = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PRecAnnot.Plant3D2026.csproj"));
Assert.DoesNotContain("<Private>true</Private>", project);
Assert.DoesNotContain("<CopyLocal>true</CopyLocal>", project);
}
[Fact] public void Bundle_DoesNotContainAutodeskAssemblies()
{
var root = Path.Combine(ProjectRoot, "artifacts", "P_Rec_Annot.bundle");
if (!Directory.Exists(root)) return;
Assert.DoesNotContain(Directory.EnumerateFiles(root, "*.dll", SearchOption.AllDirectories), path => Path.GetFileName(path).StartsWith("PnP", StringComparison.OrdinalIgnoreCase) || new[] { "AcMgd.dll", "AcDbMgd.dll", "AcCoreMgd.dll", "AdWindows.dll" }.Contains(Path.GetFileName(path), StringComparer.OrdinalIgnoreCase));
}
[Fact] public void Core_HasNoAutodeskReferences()
{
var references = typeof(AnnotationRequest).Assembly.GetReferencedAssemblies();
Assert.DoesNotContain(references, x => x.Name?.StartsWith("Autodesk", StringComparison.OrdinalIgnoreCase) == true || x.Name?.StartsWith("Ac", StringComparison.OrdinalIgnoreCase) == true);
}
[Fact] public void OrthoDetection_NoPlantProject_ReturnsNotOrtho() => Assert.Equal(OrthoDetectionStatus.NotOrtho, OrthoDetectionClassifier.Classify(false, false, false).Status);
[Fact] public void OrthoDetection_NormalDwg_ReturnsNotOrtho() => Assert.Equal(OrthoDetectionStatus.NotOrtho, OrthoDetectionClassifier.Classify(true, true, false).Status);
[Fact] public void OrthoDetection_ConfirmedOrtho_ReturnsConfirmed() => Assert.Equal(OrthoDetectionStatus.Confirmed, OrthoDetectionClassifier.Classify(true, true, true).Status);
[Fact] public void OrthoDetection_AmbiguousMetadata_ReturnsAmbiguous() => Assert.Equal(OrthoDetectionStatus.Ambiguous, OrthoDetectionClassifier.Classify(true, true, false, true).Status);
[Fact] public void AnnotationPlan_InputOrderDoesNotChangeResult()
{
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" };
var objects = new[] { Snapshot("b", "Pipe", "L-2"), Snapshot("a", "Valve", "V-1") };
var builder = new AnnotationPlanBuilder();
var first = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = objects });
var second = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = objects.Reverse().ToArray() });
Assert.Equal(first.PlanId, second.PlanId);
Assert.Equal(first.Summary, second.Summary);
Assert.Equal(first.Operations.Select(x => x.SourceSnapshotId), second.Operations.Select(x => x.SourceSnapshotId));
}
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
{
var plan = Build(Snapshot("a", "Pipe", null, null));
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
}
[Fact] public void AnnotationPlan_UnsupportedObject_IsMarkedUnsupported()
{
var plan = Build(Snapshot("a", "", "L-1"));
Assert.Equal(AnnotationOperationKind.Unsupported, Assert.Single(plan.Operations).Kind);
}
[Fact] public void AnnotationPlan_ExistingAnnotation_BecomesUpdate()
{
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" };
var item = Snapshot("a", "Pipe", "L-1");
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = new[] { item }, Existing = new[] { new ExistingAnnotation("a", "old", new(0, 0)) } });
Assert.Equal(AnnotationOperationKind.Update, Assert.Single(plan.Operations).Kind);
}
[Fact] public void AnnotationPlan_DuplicateCandidate_BecomesConflict()
{
var plan = Build(Snapshot("a", "Pipe", "L-1"), Snapshot("a", "Pipe", "L-1"));
Assert.Equal(AnnotationOperationKind.Conflict, plan.Operations.Last().Kind);
}
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
[Fact] public void DryRunPath_HasNoForWriteOperations()
{
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
Assert.DoesNotContain("ForWrite", source);
Assert.DoesNotContain("AppendEntity", source);
Assert.DoesNotContain("AcceptChanges", source);
Assert.DoesNotContain("SetProperties", source);
}
private static AnnotationPlan Build(params PlantObjectSnapshot[] objects) => new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" }, Objects = objects });
private static PlantObjectSnapshot Snapshot(string id, string type, string? line, string? tag = "TAG") => new() { SnapshotId = id, ObjectClass = type, LineNumber = line, Tag = tag, Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(1, 2) } };
}
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><IsPackable>false</IsPackable><IsTestProject>true</IsTestProject></PropertyGroup>
<ItemGroup><ProjectReference Include="..\..\src\PRecAnnot.Core\PRecAnnot.Core.csproj" /><ProjectReference Include="..\..\src\PRecAnnot.Application\PRecAnnot.Application.csproj" /><ProjectReference Include="..\..\src\PRecAnnot.UI\PRecAnnot.UI.csproj" /></ItemGroup>
<ItemGroup><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,11 @@
param([ValidateSet('Debug','Release')][string]$Configuration='Release',[Parameter(Mandatory=$true)][string]$AutoCAD2026ManagedDir,[Parameter(Mandatory=$true)][string]$Plant3D2026ManagedDir)
$ErrorActionPreference='Stop'
dotnet build (Join-Path $PSScriptRoot '..\PRecAnnot2026.sln') --configuration $Configuration /p:AutoCAD2026ManagedDir=$AutoCAD2026ManagedDir /p:Plant3D2026ManagedDir=$Plant3D2026ManagedDir
if($LASTEXITCODE -ne 0){ throw "Build failed: $LASTEXITCODE" }
$bundleRoot=Join-Path $PSScriptRoot '..\artifacts\P_Rec_Annot.bundle'
$contents=Join-Path $bundleRoot 'Contents\Windows'
New-Item -ItemType Directory -Force -Path $contents | Out-Null
$output=Join-Path $PSScriptRoot "..\src\PRecAnnot.Plant3D2026\bin\$Configuration\net8.0-windows"
Get-ChildItem $output -File -Filter '*.dll' | Copy-Item -Destination $contents -Force
Copy-Item (Join-Path $PSScriptRoot '..\bundle\P_Rec_Annot.bundle\PackageContents.xml') $bundleRoot -Force
Write-Host 'Bundle assembly build completed; deployment is intentionally not performed.'
@@ -0,0 +1,13 @@
param([string]$BundleRoot=(Join-Path $PSScriptRoot '..\artifacts\P_Rec_Annot.bundle'))
$ErrorActionPreference='Stop'
$package=Join-Path $BundleRoot 'PackageContents.xml'
[xml]$xml=Get-Content -LiteralPath $package
$entry=$xml.ApplicationPackage.Components.ComponentEntry
if(@($entry).Count -ne 1){throw 'Bundle must have exactly one entry assembly.'}
if($entry.ModuleName -match '_netload|OrthoLoader|Settings.xml'){throw 'Legacy loading chain is forbidden.'}
if($entry.ModuleName -notmatch 'PRecAnnot\.Plant3D2026\.dll'){throw 'Unexpected entry assembly.'}
if(-not (Test-Path (Join-Path $BundleRoot 'Contents\Windows\PRecAnnot.Plant3D2026.dll'))){throw 'Entry assembly is missing.'}
$forbidden='^(AcMgd|AcDbMgd|AcCoreMgd|AdWindows|PnP.*)\.dll$'
$forbiddenFiles=Get-ChildItem $BundleRoot -Recurse -File | Where-Object { $_.Name -match $forbidden -or $_.Extension -in '.pdb','.cs' -or $_.FullName -match '\\(bin|obj)\\' }
if($forbiddenFiles){throw "Bundle contains forbidden files: $($forbiddenFiles.FullName -join ', ')"}
Write-Host 'Bundle manifest is structurally valid.'
@@ -0,0 +1,11 @@
# Launcher source architecture audit
Static, read-only audit of three independent nested Git repositories under `C:\dev\launcher\git` and the linked bootstrap repository `C:\dev\p_serv_starter`. No launcher executable, helper, build, installer, registry action, profile change, or AutoCAD process was run.
The three projects are all independent WinForms `WinExe` applications, not a three-project solution: `All_Serv_Install`, `All_Serv_InstallUpdater`, and `All_Serv_Load`. Each solution contains exactly one project and has Debug/Release AnyCPU `Build.0` enabled. There are no project references between them.
The current primary installer is `All_Serv_Install.exe` (v1.1.0.0). It presents a Form1 UI, reads server-side settings, copies selected corporate application directories to local targets, then starts a configured external executable. `All_Serv_InstallUpdater.exe` is a visible but short-lived self-updater: it updates the local installer with MD5 checks and starts `All_Serv_Install.exe`. `All_Serv_Load.exe` is an earlier separate WinForms loader (v1.0.0.0); source evidence does not establish that it is part of the current primary workflow.
No static source evidence shows a direct reference to `app_netload.lsp`, `OrthoLoader.dll`, or `P_Rec_Annot` in the updater. The installer configuration does establish `P_Rec_Annot` deployment: catalog `P_Rec_Annot`, install file `OrthoLoader.dll`, source DriveA, target `C:\AutoCAD User Support\Applications`.
`P_Serv_Starter.exe` is the confirmed external bootstrap selected by the Plant 3D installer pattern. Its source creates `_app_netload.lsp` from the installer `Settings.xml`, copies `launcher_starter.lsp` to the local Applications directory, and launches the configured local `start_plant_3d.bat`. The LISP renames the generated file to `app_netload.lsp`, adds the local Applications directory to SupportPath, and loads it. The source comment attributes LISP auto-start to `acaddoc.lsp`, but that file and the expected local installation were absent on this workstation during the audit; automatic startup remains `UNKNOWN`.
@@ -0,0 +1,16 @@
# Application synchronization analysis
`All_Serv_Install` is a synchronizing installer, not merely a launcher. It reads server `Settings.xml`, selects products/plugins, and recursively copies application directories from configured network roots to local targets. Its copy path includes directory deletion and file deletion (`Form1.cs:396-645`), so it was not executed for this audit.
For Plant 3D_2019, the configuration specifies DriveA and DriveQ network sources and local target `C:\AutoCAD User Support\Applications`. The catalog `P_Rec_Annot` is selected with install entry `OrthoLoader.dll`; `download_list_plant.xml` maps the P_Rec_Annot plugin ID to a real area. This proves the intended local deployment source and target, but not the state of a particular workstation installation.
`All_Serv_InstallUpdater` is a separate local updater. Its `Form1_Load` computes MD5 checksums, compares a server control-sums manifest, deletes stale local files, copies changed files, writes a local control-sums file and starts `All_Serv_Install.exe`. It references `N:\BIM\save\All_Serv_Install` and `N:\BIM\data_write\All_Serv_Install`; it can write both local and server test/control files. It was not executed.
| Component | Source | Target | Version check | Destructive operations | Role | Confidence |
|---|---|---|---|---|---|---|
| All_Serv_Install | configured network DriveA/DriveQ | configured local target | configuration-driven selection; no content hash established | recursive copy, delete files/directories | application synchronizer | CONFIRMED |
| All_Serv_InstallUpdater | server installer directories | updater own local directory | MD5 control sums | deletes stale local files; writes control sums | installer updater | CONFIRMED |
| P_Serv_Starter | local installed executable + installer Settings.xml | local Applications | none | overwrites `_app_netload.lsp` and `launcher_starter.lsp` | per-launch bootstrap | CONFIRMED |
Offline behavior is `UNKNOWN`: both installer and updater require configured paths at the point they run, but source evidence does not show an explicit offline fallback.
@@ -0,0 +1,47 @@
# Architecture summary
## Project architecture
```text
All_Serv_InstallUpdater.exe (independent repository / updater)
-> starts local All_Serv_Install.exe
All_Serv_Install.exe (independent repository / visible installer and synchronizer)
-> starts configured P_Serv_Starter.exe
P_Serv_Starter.exe (separate repository / Plant bootstrap)
-> generates local _app_netload.lsp
-> copies launcher_starter.lsp
-> starts configured local start_plant_3d.bat
All_Serv_Load.exe (independent repository)
-> separate historical/alternate loader; no source dependency edge to the above
```
There are no ProjectReference edges between the three projects beneath `C:\dev\launcher\git`; each is its own one-project solution. `P_Serv_Starter` is a fourth independent repository selected by configuration, not a project reference. This explains why a user can experience one product despite multiple executables: the updater can hand off to the visible installer, which synchronizes selected applications and delegates Plant startup to a hidden bootstrap. Actual deployed shortcut/entrypoint evidence was not present, so the updater's use in a specific installation remains `INFERRED`.
## Roles and status
| Project | Output | Role | Status | Evidence |
|---|---|---|---|---|
| All_Serv_Install | WinExe | visible catalog UI, synchronizer and configured process launcher | ACTIVE_PRIMARY | Form1 startup and copy/Process.Start code |
| All_Serv_InstallUpdater | WinExe | installer self-updater then handoff | ACTIVE_UPDATER | Form1_Load MD5/copy/start code |
| All_Serv_Load | WinExe | independent alternate/historical loader | UNKNOWN | independent Form1 and 2019 batch script; no current deployment proof |
| P_Serv_Starter | Exe | Plant session bootstrap | ACTIVE_BOOTSTRAP | selected in installer XML; writes loader and starts batch |
## Runtime chain
The confirmed chain ends at execution of the local Plant start batch. The gap between that batch and `launcher_starter.lsp` automatic loading is `UNKNOWN`: no local batch, `acaddoc.lsp`, or Applications files were found. Once `launcher_starter.lsp` runs, all following load steps are source-confirmed.
## Answers to the requested questions
1. The three launcher projects are independent WinForms executables: installer, updater and alternate loader.
2. `All_Serv_Install` creates the confirmed visible catalog UI.
3. Three EXE artifacts are declared by the launcher repositories; P_Serv_Starter adds one external bootstrap EXE.
4. No launcher project is a class library; `launcher_starter.lsp` is a copied runtime content artifact.
5. Updater/helper/bootstrap behavior is confirmed in source; actual installed-process count is unknown.
6. Plant launch is delegated by `All_Serv_Install` to configured `P_Serv_Starter.exe`; the bootstrap starts a local batch. A current Plant executable/profile/argument string is not proven.
7. `app_netload.lsp` and `OrthoLoader.dll` are connected through P_Serv_Starter generated LISP and installer plugin metadata.
8. The installer synchronizes corporate application folders to a local Applications cache; it does not load P_Rec_Annot directly.
9. No source proof warrants modifying LAUNCHER for the new plugin yet. First validate one local entry assembly through the established catalog/bootstrap contract in a non-production pilot.
@@ -0,0 +1,38 @@
# AutoCAD/Plant launch chain
## Confirmed static chain
```text
User
-> All_Serv_Install.exe Form1
-> copy selected Plant 3D application folders to local Applications target
-> Process.Start(P_Serv_Starter.exe, five configured arguments)
-> P_Serv_Starter creates local _app_netload.lsp and copies launcher_starter.lsp
-> P_Serv_Starter starts C:\AutoCAD User Support\p3d-support\start_plant_3d.bat
-> [UNKNOWN: installed batch contents and auto-load registration]
-> Plant 3D startup
-> launcher_starter.lsp (when loaded)
-> local app_netload.lsp
-> _netload local Applications\P_Rec_Annot\OrthoLoader.dll
-> OrthoLoader Settings.xml -> sequential ._netload configured modules
```
The final two steps are confirmed by the prior P_Rec_Annot legacy audit. The activation of `launcher_starter.lsp` is not directly implemented in `P_Serv_Starter`: `Program.cs:181` comments that `acaddoc.lsp` renames the generated file, while the copied LISP itself performs the rename and `load`. No matching installed `acaddoc.lsp`, batch file, or Applications directory existed on this workstation at audit time. Therefore automatic execution is `UNKNOWN`, not asserted.
## Source evidence
| Claim | Evidence | Confidence |
|---|---|---|
| User-visible installer starts configured object | `All_Serv_Install/Form1.cs:195-230` | CONFIRMED |
| Plant pattern object is `P_Serv_Starter.exe` | `All_Serv_Install/Settings.xml`, Plant 3D pattern | CONFIRMED |
| Five arguments include product, Applications target, settings, project and working directory | `Settings.xml` Plant pattern and `P_Serv_Starter/Program.cs:12-23` | CONFIRMED |
| Bootstrap generates `_app_netload.lsp` | `P_Serv_Starter/Program.cs:58-182` | CONFIRMED |
| Generated DLL modules use AutoLISP `_netload` | `P_Serv_Starter/Program.cs:146-151` | CONFIRMED |
| Bootstrap copies LISP and starts local batch file | `P_Serv_Starter/Program.cs:202-216,49-54` | CONFIRMED |
| Generated LISP is renamed, SupportPath updated and loaded | `P_Serv_Starter/launcher_starter.lsp:74-96` | CONFIRMED |
| Actual installed batch launches a specific Plant version | no installed batch was present | UNKNOWN |
## Historical source-only alternative
`All_Serv_Load/Addins/start_plant_3d.bat` contains a direct AutoCAD 2019 command using `/product PLNT3D /language "ru-RU"`, preceded by PlantCleaner and PnPPersistentCache cleanup. It is evidence only of that project's historical script; it is not evidence that current users start Plant 3D 2026 this way. No `/ld`, profile, LISP or script argument is present in that script.
@@ -0,0 +1,7 @@
Artifact,SourceProject,Type,AssemblyName,AssemblyVersion,TargetFramework,Architecture,ReferencedBy,VisibleToUser,RequiredAtRuntime,LikelyRole,Confidence
All_Serv_Install.exe,All_Serv_Install,WinExe,All_Serv_Install,1.1.0.0,.NETFramework v4.7,AnyCPU,None,Yes,Yes,PRIMARY_INSTALLER,CONFIRMED
All_Serv_InstallUpdater.exe,All_Serv_InstallUpdater,WinExe,All_Serv_InstallUpdater,3.0.0.0,.NETFramework v4.7,AnyCPU,None,Briefly,Only when updater is launched,UPDATER_HELPER,CONFIRMED
All_Serv_Load.exe,All_Serv_Load,WinExe,All_Serv_Load,1.0.0.0,.NETFramework v4.8,AnyCPU,None,Possible,Unknown,SEPARATE_LEGACY_LOADER,INFERRED
P_Serv_Starter.exe,P_Serv_Starter,Exe,P_Serv_Starter,3.0.1.0,.NETFramework v4.7,AnyCPU,Configured external process from All_Serv_Install,No,Yes for Plant launch bootstrap,BOOTSTRAP,CONFIRMED
launcher_starter.lsp,P_Serv_Starter,Content,LISP script,N/A,N/A,N/A,Copied by P_Serv_Starter,No,Yes for generated loader activation,LISP_BOOTSTRAP,CONFIRMED
1 Artifact SourceProject Type AssemblyName AssemblyVersion TargetFramework Architecture ReferencedBy VisibleToUser RequiredAtRuntime LikelyRole Confidence
2 All_Serv_Install.exe All_Serv_Install WinExe All_Serv_Install 1.1.0.0 .NETFramework v4.7 AnyCPU None Yes Yes PRIMARY_INSTALLER CONFIRMED
3 All_Serv_InstallUpdater.exe All_Serv_InstallUpdater WinExe All_Serv_InstallUpdater 3.0.0.0 .NETFramework v4.7 AnyCPU None Briefly Only when updater is launched UPDATER_HELPER CONFIRMED
4 All_Serv_Load.exe All_Serv_Load WinExe All_Serv_Load 1.0.0.0 .NETFramework v4.8 AnyCPU None Possible Unknown SEPARATE_LEGACY_LOADER INFERRED
5 P_Serv_Starter.exe P_Serv_Starter Exe P_Serv_Starter 3.0.1.0 .NETFramework v4.7 AnyCPU Configured external process from All_Serv_Install No Yes for Plant launch bootstrap BOOTSTRAP CONFIRMED
6 launcher_starter.lsp P_Serv_Starter Content LISP script N/A N/A N/A Copied by P_Serv_Starter No Yes for generated loader activation LISP_BOOTSTRAP CONFIRMED
@@ -0,0 +1,7 @@
Solution,Configuration,Platform,Project,BuildEnabled,Notes
All_Serv_Install.sln,Debug,Any CPU,All_Serv_Install,yes,Only project in solution
All_Serv_Install.sln,Release,Any CPU,All_Serv_Install,yes,Only project in solution
All_Serv_InstallUpdater.sln,Debug,Any CPU,All_Serv_InstallUpdater,yes,Only project in solution
All_Serv_InstallUpdater.sln,Release,Any CPU,All_Serv_InstallUpdater,yes,Only project in solution
All_Serv_Load.sln,Debug,Any CPU,All_Serv_Load,yes,Only project in solution
All_Serv_Load.sln,Release,Any CPU,All_Serv_Load,yes,Only project in solution
1 Solution Configuration Platform Project BuildEnabled Notes
2 All_Serv_Install.sln Debug Any CPU All_Serv_Install yes Only project in solution
3 All_Serv_Install.sln Release Any CPU All_Serv_Install yes Only project in solution
4 All_Serv_InstallUpdater.sln Debug Any CPU All_Serv_InstallUpdater yes Only project in solution
5 All_Serv_InstallUpdater.sln Release Any CPU All_Serv_InstallUpdater yes Only project in solution
6 All_Serv_Load.sln Debug Any CPU All_Serv_Load yes Only project in solution
7 All_Serv_Load.sln Release Any CPU All_Serv_Load yes Only project in solution
@@ -0,0 +1,10 @@
Setting,OwnerProject,Source,DefaultValue,RuntimeValueSource,Purpose,Sensitive,Required,Fallback,UsedBy,Confidence
InstallerSettings,All_Serv_Install,N:\BIM\save\All_Serv_Install\Settings.xml,None,hard-coded source path,product/plugin catalog and process pattern,No,Yes,None,Form1,CONFIRMED
PlantApplicationTarget,All_Serv_Install,Settings.xml Plant pattern,C:\AutoCAD User Support\Applications,XML target,local corporate applications directory,No,Yes,None,copy and P_Serv_Starter arguments,CONFIRMED
PlantBootstrapExecutable,All_Serv_Install,Settings.xml Plant pattern,C:\CAD User Support\All_Serv_Install\P_Serv_Starter.exe,XML object,external bootstrap process,No,Yes,None,Form1 Process.Start,CONFIRMED
PluginCatalog,All_Serv_Install,Settings.xml plugin entry,P_Rec_Annot,XML plugin metadata,subdirectory to deploy,No,Optional by selection,skip if unselected,copy and generated loader,CONFIRMED
PluginInstall,All_Serv_Install,Settings.xml plugin entry,OrthoLoader.dll,XML plugin metadata,first P_Rec_Annot assembly to load,No,Optional by selection,skip if catalog unavailable,P_Serv_Starter generated LISP,CONFIRMED
GeneratedLoader,P_Serv_Starter,arguments plus installer Settings.xml,_app_netload.lsp,computed at runtime,per-session module loading commands,Contains project path in first comment,Yes,abort on error,generated LISP,CONFIRMED
PlantStartScript,P_Serv_Starter,Program.cs,C:\AutoCAD User Support\p3d-support\start_plant_3d.bat,hard-coded source constant,launch Plant process,No,Yes,none,Process.Start,CONFIRMED
UpdaterServer,All_Serv_InstallUpdater,Form1.cs,N:\BIM\data_write\All_Serv_Install,hard-coded source constant,hash/copy source,No,Yes,none,Form1_Load,CONFIRMED
1 Setting OwnerProject Source DefaultValue RuntimeValueSource Purpose Sensitive Required Fallback UsedBy Confidence
2 InstallerSettings All_Serv_Install N:\BIM\save\All_Serv_Install\Settings.xml None hard-coded source path product/plugin catalog and process pattern No Yes None Form1 CONFIRMED
3 PlantApplicationTarget All_Serv_Install Settings.xml Plant pattern C:\AutoCAD User Support\Applications XML target local corporate applications directory No Yes None copy and P_Serv_Starter arguments CONFIRMED
4 PlantBootstrapExecutable All_Serv_Install Settings.xml Plant pattern C:\CAD User Support\All_Serv_Install\P_Serv_Starter.exe XML object external bootstrap process No Yes None Form1 Process.Start CONFIRMED
5 PluginCatalog All_Serv_Install Settings.xml plugin entry P_Rec_Annot XML plugin metadata subdirectory to deploy No Optional by selection skip if unselected copy and generated loader CONFIRMED
6 PluginInstall All_Serv_Install Settings.xml plugin entry OrthoLoader.dll XML plugin metadata first P_Rec_Annot assembly to load No Optional by selection skip if catalog unavailable P_Serv_Starter generated LISP CONFIRMED
7 GeneratedLoader P_Serv_Starter arguments plus installer Settings.xml _app_netload.lsp computed at runtime per-session module loading commands Contains project path in first comment Yes abort on error generated LISP CONFIRMED
8 PlantStartScript P_Serv_Starter Program.cs C:\AutoCAD User Support\p3d-support\start_plant_3d.bat hard-coded source constant launch Plant process No Yes none Process.Start CONFIRMED
9 UpdaterServer All_Serv_InstallUpdater Form1.cs N:\BIM\data_write\All_Serv_Install hard-coded source constant hash/copy source No Yes none Form1_Load CONFIRMED
@@ -0,0 +1,7 @@
Artifact,ExpectedInstalledLocation,SourceProject,SourceAssemblyVersion,InstalledBinaryFound,InstalledMetadataMatch,MatchStatus,Notes
All_Serv_Install.exe,C:\CAD User Support\All_Serv_Install\All_Serv_Install.exe,All_Serv_Install,1.1.0.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,Expected location came from Settings.xml object directory
P_Serv_Starter.exe,C:\CAD User Support\All_Serv_Install\P_Serv_Starter.exe,P_Serv_Starter,3.0.1.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,Expected location came from Settings.xml object
All_Serv_InstallUpdater.exe,UNKNOWN,All_Serv_InstallUpdater,3.0.0.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,No installed location in inspected source
All_Serv_Load.exe,UNKNOWN,All_Serv_Load,1.0.0.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,No installed location in inspected source
start_plant_3d.bat,C:\AutoCAD User Support\p3d-support\start_plant_3d.bat,P_Serv_Starter dependency,N/A,False,N/A,NO_DEPLOYED_FILE_FOUND,Not present at expected path during audit
1 Artifact ExpectedInstalledLocation SourceProject SourceAssemblyVersion InstalledBinaryFound InstalledMetadataMatch MatchStatus Notes
2 All_Serv_Install.exe C:\CAD User Support\All_Serv_Install\All_Serv_Install.exe All_Serv_Install 1.1.0.0 False N/A NO_DEPLOYED_BINARY_FOUND Expected location came from Settings.xml object directory
3 P_Serv_Starter.exe C:\CAD User Support\All_Serv_Install\P_Serv_Starter.exe P_Serv_Starter 3.0.1.0 False N/A NO_DEPLOYED_BINARY_FOUND Expected location came from Settings.xml object
4 All_Serv_InstallUpdater.exe UNKNOWN All_Serv_InstallUpdater 3.0.0.0 False N/A NO_DEPLOYED_BINARY_FOUND No installed location in inspected source
5 All_Serv_Load.exe UNKNOWN All_Serv_Load 1.0.0.0 False N/A NO_DEPLOYED_BINARY_FOUND No installed location in inspected source
6 start_plant_3d.bat C:\AutoCAD User Support\p3d-support\start_plant_3d.bat P_Serv_Starter dependency N/A False N/A NO_DEPLOYED_FILE_FOUND Not present at expected path during audit
@@ -0,0 +1,4 @@
EntryPoint,Project,File,Lines,ApplicationType,CommandLineArguments,CreatesVisibleWindow,CanRunHeadless,StartsOtherProcesses,SingleInstanceMechanism,ExitBehavior,Confidence
Program.Main,All_Serv_Install,All_Serv_Install/Program.cs,13-18,WinForms,none parsed,yes Form1,no,yes configured object,none,Application exits after launching configured object,CONFIRMED
Program.Main,All_Serv_InstallUpdater,All_Serv_InstallUpdater/Program.cs,13-18,WinForms,none parsed,yes Form1,no,yes All_Serv_Install.exe,none,exits after updater starts installer,CONFIRMED
Program.Main,All_Serv_Load,All_Serv_Load/All_Serv_Load/Program.cs,12-17,WinForms,none parsed,yes Form1,no,yes configured object,none,Application exits after launching configured object,CONFIRMED
1 EntryPoint Project File Lines ApplicationType CommandLineArguments CreatesVisibleWindow CanRunHeadless StartsOtherProcesses SingleInstanceMechanism ExitBehavior Confidence
2 Program.Main All_Serv_Install All_Serv_Install/Program.cs 13-18 WinForms none parsed yes Form1 no yes configured object none Application exits after launching configured object CONFIRMED
3 Program.Main All_Serv_InstallUpdater All_Serv_InstallUpdater/Program.cs 13-18 WinForms none parsed yes Form1 no yes All_Serv_Install.exe none exits after updater starts installer CONFIRMED
4 Program.Main All_Serv_Load All_Serv_Load/All_Serv_Load/Program.cs 12-17 WinForms none parsed yes Form1 no yes configured object none Application exits after launching configured object CONFIRMED
@@ -0,0 +1,11 @@
Generated: 2026-07-31
Method: static read-only source and metadata inspection; no build or executable launch.
Launcher sources:
- C:\dev\launcher\git\All_Serv_Install @ 29d163322a2dc01983e15bc26f62f787fb402a04 (main; clean)
- C:\dev\launcher\git\All_Serv_InstallUpdater @ 1e6f2e78a1ca0005ee704dbeb464e73a478c3390 (main; clean)
- C:\dev\launcher\git\All_Serv_Load @ bd38b87f903b02533dbb659d9776cd32dd9e2a3e (main; clean)
- C:\dev\p_serv_starter @ 8219520aad1a5ccc2cd96611f2a71fef61cbf9b1 (main; clean)
Local deployment probes were metadata-only. Expected launcher/bootstrap files and the relevant AutoCAD User Support scripts were absent at the configured locations at audit time.
No source, binaries, user configuration content, registry export, DWG, DCF, Project.xml or network files are included in this audit.
@@ -0,0 +1,24 @@
# LAUNCHER to P_Rec_Annot
## Proven loading contract
| Stage | Evidence | Finding | Confidence |
|---|---|---|---|
| Deployment selection | `All_Serv_Install/Settings.xml`; `download_lists/download_list_plant.xml` | `P_Rec_Annot` is an installer plugin catalog; its configured install file is `OrthoLoader.dll`. | CONFIRMED |
| Local target | Plant configuration target | P_Rec_Annot is intended to be copied under `C:\AutoCAD User Support\Applications\P_Rec_Annot`. | CONFIRMED |
| Bootstrap selection | installer Plant pattern object/arguments | `All_Serv_Install` invokes `P_Serv_Starter.exe` after install selection. | CONFIRMED |
| Generated first load | `P_Serv_Starter/Program.cs:140-174` | For each installed DLL catalog, generated LISP emits `(command "_netload" attribute)`. Therefore `OrthoLoader.dll` is loaded from the local Applications path when its catalog exists. | CONFIRMED |
| Generated LISP activation | `launcher_starter.lsp:74-96` | It renames `_app_netload.lsp`, adds Applications to SupportPath and `(load ...)`s `app_netload.lsp`. | CONFIRMED |
| Automatic LISP invocation | source comment in `Program.cs:181`; no installed startup file | `acaddoc.lsp` is named by a comment, but its installed content and registration were not available. | UNKNOWN |
| Secondary module load | legacy audit, `OrthoLoader.Ribbon.Initialize` / `OrthoLoader.Initialize` | OrthoLoader reads adjacent Settings.xml and sequentially calls `._netload` for 20 configured modules. | CONFIRMED |
## P_Rec_Annot-specific implication
The historic contract is not a direct network `NETLOAD`. It is a local synchronized application directory followed by a generated, per-session AutoLISP loader. A generated LISP includes the Plant project path in its first comment and conditionally emits loads only for existing local plugin catalogs. New code must not reproduce that behavior before deciding whether it is compatible, necessary and safe for Plant 3D 2026.
The prior legacy audit established that `OrthoLoader.dll` is the initial assembly for P_Rec_Annot. This launcher audit establishes how the installer/bootstrap is intended to make that assembly loadable. Neither audit proves the deployed local files existed or auto-loaded on this workstation.
## Integration recommendation
Use one versioned, locally synchronized entry artifact only after a compatibility decision. The minimal pilot should retain the existing installer catalog/target contract, make the bootstrap load exactly one new entry assembly, and use an explicitly reviewed startup mechanism. Do not rely on an unproven AppLoader bundle or add new global `TRUSTEDPATHS`/profile mutations. The generated loader and any project-path comment are legacy behavior, not requirements for the new P_Rec_Annot until runtime validation establishes a need.
@@ -0,0 +1,6 @@
ParentProject,ParentFile,ParentMethod,Executable,Arguments,WorkingDirectory,UseShellExecute,WaitForExit,Conditions,ErrorHandling,Confidence
All_Serv_Install,All_Serv_Install/Form1.cs,button launch handler around 195-230,configured Pattern.Object,Predictable quoted Pattern.Attrib values including project path,not set,default,not set,after successful plugin copy,none visible around Process.Start,CONFIRMED
All_Serv_InstallUpdater,All_Serv_InstallUpdater/Form1.cs,Form1_Load 28-48,local All_Serv_Install.exe,none,not set,default,not set,after successful MD5 update,closes on update failure,CONFIRMED
All_Serv_Load,All_Serv_Load/Form1.cs,launch handler around 180-205,configured Pattern.Object,configured attributes plus selected project path,not set,default,not set,after successful plugin copy,none visible around Process.Start,CONFIRMED
P_Serv_Starter,P_Serv_Starter/Program.cs,Main 15-54,C:\AutoCAD User Support\p3d-support\start_plant_3d.bat,none,changed to configured project directory before start,default,not set,after local loader generation and LISP copy,shows message and returns for loader/copy errors,CONFIRMED
start_plant_3d.bat,All_Serv_Load/Addins/start_plant_3d.bat,script,C:\Program Files\Autodesk\AutoCAD 2019\acad.exe,/product PLNT3D /language ru-RU,not set,START default,no,after PlantCleaner,script has no error checks,CONFIRMED
1 ParentProject ParentFile ParentMethod Executable Arguments WorkingDirectory UseShellExecute WaitForExit Conditions ErrorHandling Confidence
2 All_Serv_Install All_Serv_Install/Form1.cs button launch handler around 195-230 configured Pattern.Object Predictable quoted Pattern.Attrib values including project path not set default not set after successful plugin copy none visible around Process.Start CONFIRMED
3 All_Serv_InstallUpdater All_Serv_InstallUpdater/Form1.cs Form1_Load 28-48 local All_Serv_Install.exe none not set default not set after successful MD5 update closes on update failure CONFIRMED
4 All_Serv_Load All_Serv_Load/Form1.cs launch handler around 180-205 configured Pattern.Object configured attributes plus selected project path not set default not set after successful plugin copy none visible around Process.Start CONFIRMED
5 P_Serv_Starter P_Serv_Starter/Program.cs Main 15-54 C:\AutoCAD User Support\p3d-support\start_plant_3d.bat none changed to configured project directory before start default not set after local loader generation and LISP copy shows message and returns for loader/copy errors CONFIRMED
6 start_plant_3d.bat All_Serv_Load/Addins/start_plant_3d.bat script C:\Program Files\Autodesk\AutoCAD 2019\acad.exe /product PLNT3D /language ru-RU not set START default no after PlantCleaner script has no error checks CONFIRMED
@@ -0,0 +1,15 @@
# Project dependency graph
```text
All_Serv_InstallUpdater.exe
-> local All_Serv_Install.exe (Process.Start)
All_Serv_Install.exe
-> configured external object: P_Serv_Starter.exe for Plant 3D 2019
-> copies selected application catalogs from configured network sources
All_Serv_Load.exe
-> configured external object (separate legacy workflow)
```
No `ProjectReference` exists in any of the three projects. Each solution has a single buildable project. `P_Serv_Starter.exe` is referenced only as a configured external object, not supplied by any audited launcher project.
@@ -0,0 +1,10 @@
# Repositories
| Repository | HEAD | Branch | Dirty | Remote | Submodules | Confidence |
|---|---|---|---|---|---|---|
| All_Serv_Install | `29d163322a2dc01983e15bc26f62f787fb402a04` | main | no | GitHub `pxpgitorg/All_Serv_Install` | none | CONFIRMED |
| All_Serv_InstallUpdater | `1e6f2e78a1ca0005ee704dbeb464e73a478c3390` | main | no | GitHub `pxpgitorg/All_Serv_InstallUpdater` | none | CONFIRMED |
| All_Serv_Load | `bd38b87f903b02533dbb659d9776cd32dd9e2a3e` | main | no | GitHub `pxpgitorg/All_Serv_Load` | none | CONFIRMED |
| P_Serv_Starter | `8219520aad1a5ccc2cd96611f2a71fef61cbf9b1` | main | no | GitHub `pxpgitorg/P_Serv_Starter` | none | CONFIRMED |
The launcher parent directory is not a Git repository; it contains three nested repositories. `P_Serv_Starter` is a fourth, separately supplied repository outside that parent and is referenced by installer configuration. No launcher or bootstrap source was modified.
@@ -0,0 +1,5 @@
Project,ProjectPath,ProjectGuid,ProjectType,Sdk,OutputType,TargetFramework,PlatformTarget,AssemblyName,RootNamespace,StartupObject,EntryPoint,ApplicationIcon,ApplicationManifest,ProjectReferences,PackageReferences,DirectDllReferences,BuildOutput,PublishOutput,IsStartupProject,HasMainMethod,HasAppXaml,HasFormsEntryPoint,HasPostBuildActions,LikelyRole,Status
All_Serv_Install,All_Serv_Install/All_Serv_Install.csproj,{FE649634-187B-410E-9A53-A8C41134F53D},C# classic MSBuild,,WinExe,v4.7,AnyCPU,All_Serv_Install,All_Serv_Install,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,yes,no,primary corporate application installer/synchronizer,ACTIVE_PRIMARY
All_Serv_InstallUpdater,All_Serv_InstallUpdater/All_Serv_InstallUpdater.csproj,{6E075C69-20B5-4613-8BE1-5C41A615C70F},C# classic MSBuild,,WinExe,v4.7,AnyCPU,All_Serv_InstallUpdater,All_Serv_InstallUpdater,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,yes,no,self-updater for All_Serv_Install,ACTIVE_UPDATER
All_Serv_Load,All_Serv_Load/All_Serv_Load/All_Serv_Load.csproj,{CCEF88B4-9157-4F50-AA7F-E0A6F3A665E3},C# classic MSBuild,,WinExe,v4.8,AnyCPU,All_Serv_Load,All_Serv_Load,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,yes,no,earlier standalone loader/install UI,UNKNOWN
P_Serv_Starter,P_Serv_Starter/P_Serv_Starter.csproj,{DBA449EE-5DEC-454B-9EF1-E71BF949D880},C# classic MSBuild,,Exe,v4.7,AnyCPU,P_Serv_Starter,P_Serv_Starter,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,no,no,external Plant bootstrap selected by installer configuration,ACTIVE_BOOTSTRAP
1 Project ProjectPath ProjectGuid ProjectType Sdk OutputType TargetFramework PlatformTarget AssemblyName RootNamespace StartupObject EntryPoint ApplicationIcon ApplicationManifest ProjectReferences PackageReferences DirectDllReferences BuildOutput PublishOutput IsStartupProject HasMainMethod HasAppXaml HasFormsEntryPoint HasPostBuildActions LikelyRole Status
2 All_Serv_Install All_Serv_Install/All_Serv_Install.csproj {FE649634-187B-410E-9A53-A8C41134F53D} C# classic MSBuild WinExe v4.7 AnyCPU All_Serv_Install All_Serv_Install Program.Main none none System assemblies bin/Debug or bin/Release none only solution project yes no yes no primary corporate application installer/synchronizer ACTIVE_PRIMARY
3 All_Serv_InstallUpdater All_Serv_InstallUpdater/All_Serv_InstallUpdater.csproj {6E075C69-20B5-4613-8BE1-5C41A615C70F} C# classic MSBuild WinExe v4.7 AnyCPU All_Serv_InstallUpdater All_Serv_InstallUpdater Program.Main none none System assemblies bin/Debug or bin/Release none only solution project yes no yes no self-updater for All_Serv_Install ACTIVE_UPDATER
4 All_Serv_Load All_Serv_Load/All_Serv_Load/All_Serv_Load.csproj {CCEF88B4-9157-4F50-AA7F-E0A6F3A665E3} C# classic MSBuild WinExe v4.8 AnyCPU All_Serv_Load All_Serv_Load Program.Main none none System assemblies bin/Debug or bin/Release none only solution project yes no yes no earlier standalone loader/install UI UNKNOWN
5 P_Serv_Starter P_Serv_Starter/P_Serv_Starter.csproj {DBA449EE-5DEC-454B-9EF1-E71BF949D880} C# classic MSBuild Exe v4.7 AnyCPU P_Serv_Starter P_Serv_Starter Program.Main none none System assemblies bin/Debug or bin/Release none only solution project yes no no no external Plant bootstrap selected by installer configuration ACTIVE_BOOTSTRAP
@@ -0,0 +1,84 @@
param(
[string]$DeployedRoot = '\\phpsrv03\NBIM\save\Apps\Plant3D\DriveA\P_Rec_Annot',
[string]$SourceRoot = '\\phpsrv03\NBIM\code\CS\P_Rec_Annot\v1.0.4\repo',
[string]$OutputRoot = (Split-Path -Parent $PSCommandPath)
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-PeArchitecture([string]$Path) {
if ([IO.Path]::GetExtension($Path) -notin '.dll', '.exe') { return $null }
try {
$stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
try {
$reader = [IO.BinaryReader]::new($stream)
$stream.Position = 0x3c; $offset = $reader.ReadInt32(); $stream.Position = $offset + 4
$machine = $reader.ReadUInt16()
switch ($machine) { 0x14c { 'x86' } 0x8664 { 'x64' } 0x1c4 { 'ARM' } default { 'Unknown' } }
} finally { $stream.Dispose() }
} catch { 'Unreadable' }
}
function Get-AssemblyMetadata([string]$Path) {
$result = [ordered]@{ AssemblyName = $null; AssemblyVersion = $null; TargetFramework = $null }
if ([IO.Path]::GetExtension($Path) -ne '.dll') { return [pscustomobject]$result }
try {
$name = [Reflection.AssemblyName]::GetAssemblyName($Path)
$result.AssemblyName = $name.Name; $result.AssemblyVersion = $name.Version.ToString()
try {
$assembly = [Reflection.Assembly]::ReflectionOnlyLoadFrom($Path)
$attribute = $assembly.GetCustomAttributesData() | Where-Object { $_.AttributeType.FullName -eq 'System.Runtime.Versioning.TargetFrameworkAttribute' } | Select-Object -First 1
if ($attribute) { $result.TargetFramework = [string]$attribute.ConstructorArguments[0].Value }
} catch { }
} catch { }
[pscustomobject]$result
}
$inventory = foreach ($file in Get-ChildItem -LiteralPath $DeployedRoot -Recurse -File) {
$hash = Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256
$version = try { [Diagnostics.FileVersionInfo]::GetVersionInfo($file.FullName) } catch { $null }
$meta = Get-AssemblyMetadata $file.FullName
[pscustomobject]@{
RelativePath = $file.FullName.Substring($DeployedRoot.Length).TrimStart('\')
Name = $file.Name; Extension = $file.Extension; Length = $file.Length; LastWriteTimeUtc = $file.LastWriteTimeUtc.ToString('o')
Sha256 = $hash.Hash; FileVersion = $version.FileVersion; ProductVersion = $version.ProductVersion
AssemblyName = $meta.AssemblyName; AssemblyVersion = $meta.AssemblyVersion; TargetFramework = $meta.TargetFramework
PeArchitecture = Get-PeArchitecture $file.FullName
}
}
$inventory | Sort-Object RelativePath | Export-Csv -LiteralPath (Join-Path $OutputRoot 'deployed-files.csv') -NoTypeInformation -Encoding utf8
$settings = [xml](Get-Content -LiteralPath (Join-Path $DeployedRoot 'Settings.xml'))
$sequence = 0
$configuration = foreach ($item in $settings.Main.LoadAssemblies.LoadAssembly) {
$sequence++
$configured = [string]$item.assemblyname; $resolved = Join-Path $DeployedRoot $configured
$entry = $inventory | Where-Object Name -eq $configured | Select-Object -First 1
[pscustomobject]@{
Sequence = $sequence; ConfigFile = 'Settings.xml'; ConfiguredPath = $configured; ResolvedPath = ('<deployed-root>\' + $configured)
Assembly = $entry.AssemblyName; Exists = Test-Path -LiteralPath $resolved; Sha256 = $entry.Sha256
LoadMethod = 'acDoc.Editor.Command("._netload", fullPath)'; Required = $true
FailureBehavior = 'Outer loader catch stops remaining sequence and shows MessageBox'; Notes = 'Path is combined with OrthoLoader assembly directory.'
}
}
$configuration | Export-Csv -LiteralPath (Join-Path $OutputRoot 'configuration-map.csv') -NoTypeInformation -Encoding utf8
$projects = Get-ChildItem -LiteralPath $SourceRoot -Recurse -File -Filter *.csproj | ForEach-Object {
$content = Get-Content -LiteralPath $_.FullName -Raw
$assemblyName = if ($content -match '<AssemblyName>([^<]+)</AssemblyName>') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($_.Name) }
$framework = if ($content -match '<TargetFrameworkVersion>([^<]+)</TargetFrameworkVersion>') { $Matches[1] } else { $null }
$platform = if ($content -match '<PlatformTarget>([^<]+)</PlatformTarget>') { $Matches[1] } else { $null }
[pscustomobject]@{ AssemblyName = $assemblyName; Csproj = $_.FullName.Substring($SourceRoot.Length).TrimStart('\'); TargetFramework = $framework; PlatformTarget = $platform }
}
$sourceMap = foreach ($dll in $inventory | Where-Object Extension -eq '.dll') {
$matches = @($projects | Where-Object { $_.AssemblyName -eq $dll.AssemblyName -or $_.AssemblyName -eq [IO.Path]::GetFileNameWithoutExtension($dll.Name) })
$releaseCandidate = Join-Path (Join-Path $SourceRoot 'FilesRelease') $dll.Name
$exactReleaseMatch = (Test-Path -LiteralPath $releaseCandidate) -and ((Get-FileHash -LiteralPath $releaseCandidate -Algorithm SHA256).Hash -eq $dll.Sha256)
$status = if ($exactReleaseMatch) { 'EXACT_SOURCE_MATCH' } elseif ($matches.Count -eq 1) { 'LIKELY_SOURCE_MATCH' } elseif ($matches.Count -gt 1) { 'MULTIPLE_CANDIDATES' } elseif ($dll.Name -match '^PnP|^Ac') { 'AUTODESK_DEPENDENCY' } else { 'NO_SOURCE_FOUND' }
$project = $matches | Select-Object -First 1
[pscustomobject]@{ DeployedDll=$dll.Name; DeployedSha256=$dll.Sha256; FileVersion=$dll.FileVersion; AssemblyVersion=$dll.AssemblyVersion; LegacyProject=if($project){[IO.Path]::GetFileNameWithoutExtension($project.Csproj)}else{$null}; LegacyCsproj=if($project){$project.Csproj}else{$null}; LegacyOutputName=if($project){$project.AssemblyName}else{$null}; TargetFramework=if($project){$project.TargetFramework}else{$null}; PlatformTarget=if($project){$project.PlatformTarget}else{$null}; CommandCount=$null; RibbonRole=if($dll.AssemblyName -eq 'OrthoLoader'){'Central ribbon/loader'}else{$null}; ConfigurationSource=if($configuration.ConfiguredPath -contains $dll.Name){'Settings.xml'}else{$null}; MatchStatus=$status }
}
$sourceMap | Sort-Object DeployedDll | Export-Csv -LiteralPath (Join-Path $OutputRoot 'deployed-to-source-map.csv') -NoTypeInformation -Encoding utf8
"GeneratedUtc=$([DateTime]::UtcNow.ToString('o'))`nDeployedRoot=<network root redacted to report context>`nSourceRoot=<network source root redacted to report context>`nFiles=$($inventory.Count)" | Set-Content -LiteralPath (Join-Path $OutputRoot 'evidence\generation.txt') -Encoding utf8
@@ -0,0 +1,19 @@
# Legacy P_Rec_Annot loading audit
Scope: read-only inspection of the deployed network directory and the v1.0.4 source checkout. No legacy DLL was executed, copied, or deployed.
The deployed root has 52 files, including 22 managed DLLs. `Settings.xml` enumerates 20 application modules. The inventory and its SHA-256 values are in `deployed-files.csv`; generated report paths use `<deployed-root>` rather than embedding a new runtime configuration path.
High-confidence conclusion: the central loader is `OrthoLoader.dll`. `OrthoLoader.Ribbon.Initialize()` invokes `OrthoLoader.Initialize()`. The latter reads adjacent `Settings.xml` and calls `acDoc.Editor.Command("._netload", fullPath)` sequentially for the configured modules. The bootstrap that initially loads `OrthoLoader.dll` is not present in either audited directory; it remains unknown. Therefore the audit proves same-directory loading, but cannot prove whether an external launcher first NETLOADed the UNC copy or a local copy.
Source-to-binary mapping is `EXACT_SOURCE_MATCH` for 15 deployed DLLs by SHA-256 against `FilesRelease`. Six newer deployed DLLs are `LIKELY_SOURCE_MATCH` by assembly/project identity and metadata but differ byte-for-byte from the checked-in release output. `InsertElevation.dll` is `MULTIPLE_CANDIDATES` because archival projects share the assembly identity.
## Potential test data
| RelativePath | Type | LikelyPurpose | ContainsProjectData | SafeForTest | Reason |
|---|---|---|---|---|---|
| `Template.dwg` | DWG | legacy command template | Unknown | No | Located beside production deployment; no authorization to open or copy it was established. |
| `test.txt` | text | unknown/empty marker | No content | Not useful | Zero bytes. |
| `Справка*.pdf/docx` | documentation | user instructions | Unknown | Review only | Not a CAD fixture. |
No `Project.xml`, `.dcf`, or explicitly labelled sample/demo Ortho DWG was found in the deployed root.
@@ -0,0 +1,21 @@
"Sequence","ConfigFile","ConfiguredPath","ResolvedPath","Assembly","Exists","Sha256","LoadMethod","Required","FailureBehavior","Notes"
"1","Settings.xml","AnnotateLineNumbers.dll","<deployed-root>\AnnotateLineNumbers.dll","AnnotateLineNumbers","True","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"2","Settings.xml","AutoAnnotateView.dll","<deployed-root>\AutoAnnotateView.dll","AutoAnnotateView","True","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"3","Settings.xml","AutoLinenumberConnections.dll","<deployed-root>\AutoLinenumberConnections.dll","AutoLinenumberConnections","True","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"4","Settings.xml","CheckOrthoView.dll","<deployed-root>\CheckOrthoView.dll","CheckOrthoView","True","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"5","Settings.xml","EnablerLoader.dll","<deployed-root>\EnablerLoader.dll","EnablerLoader","True","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"6","Settings.xml","ExtentsIntersect.dll","<deployed-root>\ExtentsIntersect.dll","ExtentsIntersect","True","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"7","Settings.xml","GetComponentGeometry.dll","<deployed-root>\GetComponentGeometry.dll","GetComponentGeometry","True","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"8","Settings.xml","GetLinenumberAnnotationText.dll","<deployed-root>\GetLinenumberAnnotationText.dll","GetLinenumberAnnotationText","True","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"9","Settings.xml","GetPlantScriptFolderPath.dll","<deployed-root>\GetPlantScriptFolderPath.dll","GetPlantScriptFolderPath","True","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"10","Settings.xml","GetScriptParameters.dll","<deployed-root>\GetScriptParameters.dll","GetScriptParameters","True","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"11","Settings.xml","InsertElevation.dll","<deployed-root>\InsertElevation.dll","InsertElevation","True","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"12","Settings.xml","LoadedAssemblies.dll","<deployed-root>\LoadedAssemblies.dll","LoadedAssemblies","True","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"13","Settings.xml","OrthoAutoAnnotate.dll","<deployed-root>\OrthoAutoAnnotate.dll","OrthoAutoAnnotate","True","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"14","Settings.xml","P3DCopyMissingObjectsToOrthoDrawing.dll","<deployed-root>\P3DCopyMissingObjectsToOrthoDrawing.dll","P3DCopyMissingObjectsToOrthoDrawing","True","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"15","Settings.xml","P3DNewMa.dll","<deployed-root>\P3DNewMa.dll","P3DNewMa","True","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"16","Settings.xml","P3DSetLayerForViewport.dll","<deployed-root>\P3DSetLayerForViewport.dll","P3DSetLayerForViewport","True","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"17","Settings.xml","PHPAnnotations.dll","<deployed-root>\PHPAnnotations.dll","PHPAnnotations","True","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"18","Settings.xml","SwapAttributesMenuItem.dll","<deployed-root>\SwapAttributesMenuItem.dll","SwapAttributesMenuItem","True","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"19","Settings.xml","UpdateOrthoDrawings.dll","<deployed-root>\UpdateOrthoDrawings.dll","UpdateOrthoDrawings","True","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
"20","Settings.xml","UpdateOrthoViewBlocks.dll","<deployed-root>\UpdateOrthoViewBlocks.dll","UpdateOrthoViewBlocks","True","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
1 Sequence ConfigFile ConfiguredPath ResolvedPath Assembly Exists Sha256 LoadMethod Required FailureBehavior Notes
2 1 Settings.xml AnnotateLineNumbers.dll <deployed-root>\AnnotateLineNumbers.dll AnnotateLineNumbers True 799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
3 2 Settings.xml AutoAnnotateView.dll <deployed-root>\AutoAnnotateView.dll AutoAnnotateView True EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
4 3 Settings.xml AutoLinenumberConnections.dll <deployed-root>\AutoLinenumberConnections.dll AutoLinenumberConnections True 0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
5 4 Settings.xml CheckOrthoView.dll <deployed-root>\CheckOrthoView.dll CheckOrthoView True 89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
6 5 Settings.xml EnablerLoader.dll <deployed-root>\EnablerLoader.dll EnablerLoader True 745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
7 6 Settings.xml ExtentsIntersect.dll <deployed-root>\ExtentsIntersect.dll ExtentsIntersect True 59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
8 7 Settings.xml GetComponentGeometry.dll <deployed-root>\GetComponentGeometry.dll GetComponentGeometry True 9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
9 8 Settings.xml GetLinenumberAnnotationText.dll <deployed-root>\GetLinenumberAnnotationText.dll GetLinenumberAnnotationText True 047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
10 9 Settings.xml GetPlantScriptFolderPath.dll <deployed-root>\GetPlantScriptFolderPath.dll GetPlantScriptFolderPath True 5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
11 10 Settings.xml GetScriptParameters.dll <deployed-root>\GetScriptParameters.dll GetScriptParameters True 7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
12 11 Settings.xml InsertElevation.dll <deployed-root>\InsertElevation.dll InsertElevation True B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
13 12 Settings.xml LoadedAssemblies.dll <deployed-root>\LoadedAssemblies.dll LoadedAssemblies True 277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
14 13 Settings.xml OrthoAutoAnnotate.dll <deployed-root>\OrthoAutoAnnotate.dll OrthoAutoAnnotate True 2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
15 14 Settings.xml P3DCopyMissingObjectsToOrthoDrawing.dll <deployed-root>\P3DCopyMissingObjectsToOrthoDrawing.dll P3DCopyMissingObjectsToOrthoDrawing True 6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
16 15 Settings.xml P3DNewMa.dll <deployed-root>\P3DNewMa.dll P3DNewMa True 97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
17 16 Settings.xml P3DSetLayerForViewport.dll <deployed-root>\P3DSetLayerForViewport.dll P3DSetLayerForViewport True A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
18 17 Settings.xml PHPAnnotations.dll <deployed-root>\PHPAnnotations.dll PHPAnnotations True 816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
19 18 Settings.xml SwapAttributesMenuItem.dll <deployed-root>\SwapAttributesMenuItem.dll SwapAttributesMenuItem True 674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
20 19 Settings.xml UpdateOrthoDrawings.dll <deployed-root>\UpdateOrthoDrawings.dll UpdateOrthoDrawings True 1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
21 20 Settings.xml UpdateOrthoViewBlocks.dll <deployed-root>\UpdateOrthoViewBlocks.dll UpdateOrthoViewBlocks True 5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9 acDoc.Editor.Command("._netload", fullPath) True Outer loader catch stops remaining sequence and shows MessageBox Path is combined with OrthoLoader assembly directory.
@@ -0,0 +1,53 @@
"RelativePath","Name","Extension","Length","LastWriteTimeUtc","Sha256","FileVersion","ProductVersion","AssemblyName","AssemblyVersion","TargetFramework","PeArchitecture"
"AnnotateLineNumbers.dll","AnnotateLineNumbers.dll",".dll","126976","2024-12-13T07:41:34.0000000Z","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","1.0.4.1","v1.0.4","AnnotateLineNumbers","1.0.4.0",".NETFramework,Version=v4.7","x64"
"AutoAnnotateView.dll","AutoAnnotateView.dll",".dll","49664","2025-02-10T07:47:43.4844455Z","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","1.0.4.6","v1.0.4","AutoAnnotateView","1.0.4.0",".NETFramework,Version=v4.7","x64"
"AutoElevation.dll","AutoElevation.dll",".dll","28160","2025-01-16T08:13:32.2554341Z","A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885","1.0.4.5","v1.0.4","AutoElevation","1.0.4.0",".NETFramework,Version=v4.7","x64"
"AutoLinenumberConnections.dll","AutoLinenumberConnections.dll",".dll","41472","2024-12-13T07:41:34.0000000Z","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","1.0.4.1","v1.0.4","AutoLinenumberConnections","1.0.4.0",".NETFramework,Version=v4.7","x64"
"b455ee6eaed79d76f084846a60ce3284","b455ee6eaed79d76f084846a60ce3284","","0","2023-12-29T11:13:44.6409171Z","E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",,,,,,
"CheckOrthoView.dll","CheckOrthoView.dll",".dll","14848","2024-12-13T07:41:34.0000000Z","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","1.0.4.1","v1.0.4","CheckOrthoView","1.0.4.0",".NETFramework,Version=v4.7","x64"
"EnablerLoader.dll","EnablerLoader.dll",".dll","6144","2024-12-13T07:41:34.0000000Z","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","1.0.4.1","v1.0.4","EnablerLoader","1.0.4.0",".NETFramework,Version=v4.7","x64"
"ExtentsIntersect.dll","ExtentsIntersect.dll",".dll","6656","2024-12-13T07:41:34.0000000Z","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","1.0.4.1","v1.0.4","ExtentsIntersect","1.0.4.0",".NETFramework,Version=v4.7","x64"
"GetComponentGeometry.dll","GetComponentGeometry.dll",".dll","10240","2024-12-13T07:41:34.0000000Z","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","1.0.4.1","v1.0.4","GetComponentGeometry","1.0.4.0",".NETFramework,Version=v4.7","x64"
"GetComponentGeometry.xml","GetComponentGeometry.xml",".xml","1239","2025-01-14T11:26:24.5054138Z","70169C3A3AAAFE5A2282E01D5B9B7C99F2F4FF9B7514553434302674ABE46C98",,,,,,
"GetLinenumberAnnotationText.dll","GetLinenumberAnnotationText.dll",".dll","23040","2024-12-13T07:41:34.0000000Z","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","1.0.4.1","v1.0.4","GetLinenumberAnnotationText","1.0.4.0",".NETFramework,Version=v4.7","x64"
"GetPlantScriptFolderPath.dll","GetPlantScriptFolderPath.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","1.0.4.1","v1.0.4","GetPlantScriptFolderPath","1.0.4.0",".NETFramework,Version=v4.7","x64"
"GetScriptParameters.dll","GetScriptParameters.dll",".dll","6656","2024-12-13T07:41:34.0000000Z","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","1.0.4.1","v1.0.4","GetScriptParameters","1.0.4.0",".NETFramework,Version=v4.7","x64"
"ico\button_BJ.png","button_BJ.png",".png","1235","2024-12-13T07:41:34.0000000Z","414F0A23E917D41A82C697B0E9CDDDF9BFCACD1627310BD5717AEA172418AA51",,,,,,
"ico\button_CAC.png","button_CAC.png",".png","1943","2024-12-13T07:41:34.0000000Z","FA2B8877F8B41C70BA62C737C40374394D6840A53DFE2D0B0F83463C4817C138",,,,,,
"ico\button_PXP-AAV.png","button_PXP-AAV.png",".png","1905","2024-12-13T07:41:34.0000000Z","6AB171963B553B434B5AA4BFBE91993B1782606AAF9B486FA23BB27BC1F0E4AA",,,,,,
"ico\button_PXP-AC.png","button_PXP-AC.png",".png","1751","2024-12-13T07:41:34.0000000Z","60A6BD62EF3E50BDDCD00D9C281F2399E6C9D42E0C2DAA1742273C0DC48FE9C3",,,,,,
"ico\button_PXP-AE.png","button_PXP-AE.png",".png","2001","2024-12-13T07:41:34.0000000Z","F1DCF0E908F5AF38CCD13ABA58B824E5B186430414499C0F623C1D5EEAD8DAD7",,,,,,
"ico\button_PXP-ALC.png","button_PXP-ALC.png",".png","1740","2024-12-13T07:41:34.0000000Z","19DB2449B66B95965155E6E3BC313BD52000FE38C95B0FAD90EF07DF53EA9CC9",,,,,,
"ico\button_PXP-COV.png","button_PXP-COV.png",".png","2007","2024-12-13T07:41:34.0000000Z","529BDD045961C244FBD0DE177B899AF9E7CE0F4506B3463C56D2704B3BF207B5",,,,,,
"ico\button_PXP-DAV.png","button_PXP-DAV.png",".png","1249","2025-01-28T06:29:03.3724433Z","A7932F2ABDAC652EA3A901A221AB3BE2BC80FCB76FB203D44FC63AF3681F51E3",,,,,,
"ico\button_PXP-DJ.png","button_PXP-DJ.png",".png","1470","2024-12-13T07:41:34.0000000Z","127957752DC2B6573CD2FD9A2791914BA22802E07E9B7E23CD3AE1FFF9E4FFC4",,,,,,
"ico\button_PXP-ELN.png","button_PXP-ELN.png",".png","1927","2024-12-13T07:41:34.0000000Z","DF32E6A465D5C196A17F3F96473315590DF2D71995F63D2D241D7599B62C99BB",,,,,,
"ico\button_PXP-ESN.png","button_PXP-ESN.png",".png","1636","2024-12-13T07:41:34.0000000Z","0F089184CFD428DA3620F9C0AD2DCBFC21B31ED34FA301E8D5D45923783C0CC7",,,,,,
"ico\button_PXP-IE.png","button_PXP-IE.png",".png","1885","2024-12-13T07:41:34.0000000Z","F1C6373BFAF82E1908AF371D5F21133D27E0B84AFC706A703530855A2046FE43",,,,,,
"ico\button_PXP-IOD.png","button_PXP-IOD.png",".png","1942","2024-12-13T07:41:34.0000000Z","494E90C899A0BB5BB365B0E1452E92A68359610C5A38A37D09F520A39BDB3899",,,,,,
"ico\button_PXP-LN.png","button_PXP-LN.png",".png","2024","2024-12-13T07:41:34.0000000Z","F3305ED6D266E29FA475D4C8A7DF38D2D2A1776A4CE379B876B6CFA278B2FE5D",,,,,,
"ico\button_PXP-MA.png","button_PXP-MA.png",".png","1907","2024-12-13T07:41:34.0000000Z","9386249121644DCF4B9C70491F561E8B6E5BAAE87EBB2C76A84E46B10A60C8C3",,,,,,
"ico\button_PXP-OA.png","button_PXP-OA.png",".png","1999","2024-12-13T07:41:34.0000000Z","EAA2AD7EE5C6B7F3716AF1D7690C76AE59229AE9B6BA02009F5009CD9307F4E8",,,,,,
"ico\button_PXP-OSI.png","button_PXP-OSI.png",".png","1508","2024-12-13T07:41:34.0000000Z","422D10B6EBDD76085CF9C5985391CCA060464B200D6792D604658AAE47F12CEE",,,,,,
"ico\button_PXP-RA.png","button_PXP-RA.png",".png","1826","2024-12-13T07:41:34.0000000Z","1DB3F39C6CCA4EABE79D81DE6DCF9900649CDC9064F2EBF5E45F9AD849B8317F",,,,,,
"ico\button_PXP-RMP.png","button_PXP-RMP.png",".png","1759","2024-12-13T07:41:34.0000000Z","156026EC6DEB7BDBE720047409E9A25615D650D8634C2F0BAF49A71E27135258",,,,,,
"ico\button_PXP-ULN.png","button_PXP-ULN.png",".png","1928","2024-12-13T07:41:34.0000000Z","B37EBB460691EAA7BC3A91DF05C27FE7CC1F0E9F9D69BDA7703956EAA7AA785C",,,,,,
"ico\button_PXP-UOB.png","button_PXP-UOB.png",".png","1569","2024-12-13T07:41:34.0000000Z","3BC16B9923CCD34CF10F403CD65732B217E255E89539174841E7F30D9DFD2DCD",,,,,,
"InsertElevation.dll","InsertElevation.dll",".dll","24576","2025-01-16T08:13:32.2504470Z","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","1.0.4.5","v1.0.4","InsertElevation","1.0.4.0",".NETFramework,Version=v4.7","x64"
"LoadedAssemblies.dll","LoadedAssemblies.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","1.0.4.1","v1.0.4","LoadedAssemblies","1.0.4.0",".NETFramework,Version=v4.7","x64"
"OrthoAutoAnnotate.dll","OrthoAutoAnnotate.dll",".dll","108544","2025-02-10T07:45:33.3280516Z","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","1.0.4.6","v1.0.4","OrthoAutoAnnotate","1.0.4.0",".NETFramework,Version=v4.7","x64"
"OrthoLoader.dll","OrthoLoader.dll",".dll","18944","2025-02-24T06:59:01.9094468Z","D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D","1.0.4.7","v1.0.4","OrthoLoader","1.0.4.0",".NETFramework,Version=v4.7","x64"
"P3DCopyMissingObjectsToOrthoDrawing.dll","P3DCopyMissingObjectsToOrthoDrawing.dll",".dll","14336","2024-12-13T07:41:34.0000000Z","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","1.0.4.1","v1.0.4","P3DCopyMissingObjectsToOrthoDrawing","1.0.4.0",".NETFramework,Version=v4.7","x64"
"P3DNewMa.dll","P3DNewMa.dll",".dll","103936","2025-02-24T06:59:03.1350320Z","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","1.0.4.7","v1.0.4","P3DNewMa","1.0.4.0",".NETFramework,Version=v4.7","x64"
"P3DSetLayerForViewport.dll","P3DSetLayerForViewport.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","1.0.4.1","v1.0.4","P3DSetLayerForViewport","1.0.4.0",".NETFramework,Version=v4.7","x64"
"PHPAnnotations.dll","PHPAnnotations.dll",".dll","69120","2024-12-13T07:41:34.0000000Z","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","1.0.4.1","v1.0.4","PHPAnnotations","1.0.4.0",".NETFramework,Version=v4.7","x64"
"Settings.xml","Settings.xml",".xml","136576","2025-01-31T07:28:24.1891507Z","AFEDC7526923ACE1DC60374F98F171809B36992262A37E1F11828F2B8D9799CB",,,,,,
"SwapAttributesMenuItem.dll","SwapAttributesMenuItem.dll",".dll","11264","2024-12-13T07:41:34.0000000Z","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","1.0.4.1","v1.0.4","SwapAttributesMenuItem","1.0.4.0",".NETFramework,Version=v4.7","x64"
"Template.dwg","Template.dwg",".dwg","148438","2025-01-30T13:16:13.0000000Z","EA279A30F113660CF53585451083BAEB0A01CA58C8F9A767C4B7C2A5E66ECCA3",,,,,,
"test.txt","test.txt",".txt","0","2025-01-16T09:56:52.0597137Z","E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",,,,,,
"UpdateOrthoDrawings.dll","UpdateOrthoDrawings.dll",".dll","45568","2025-02-24T06:59:02.4829428Z","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","1.0.4.2","v1.0.4","UpdateOrthoDrawings","1.0.4.0",".NETFramework,Version=v4.7","x64"
"UpdateOrthoViewBlocks.dll","UpdateOrthoViewBlocks.dll",".dll","15360","2024-12-13T07:41:34.0000000Z","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","1.0.4.1","v1.0.4","UpdateOrthoViewBlocks","1.0.4.0",".NETFramework,Version=v4.7","x64"
"Справка по работе с палитрой.docx","Справка по работе с палитрой.docx",".docx","855301","2025-02-05T09:53:08.1186958Z","B6DC65BA17C4EA79DBCCC501909BEDBF56EB6A9F1E24BFAD1B01158B6EC639D0",,,,,,
"Справка по работе с палитрой.pdf","Справка по работе с палитрой.pdf",".pdf","744035","2025-02-05T09:53:21.3752687Z","332CA70D3264C28BC712CA790157D0CD03BE7E07F42603DAF9247600271B7B75",,,,,,
"Справка.docx","Справка.docx",".docx","87889","2025-02-06T05:13:36.4991443Z","14BA9BD252FB6DF37C4D151787E8799AB1703847C1DEC24F3EFE4D6805B0EB12",,,,,,
"Справка.pdf","Справка.pdf",".pdf","154074","2025-02-06T05:14:24.2809131Z","EB840514755A9F56ABC8351B8EC41233FE2512118DB2D4BA0ACD46E74D2C7DC9",,,,,,
1 RelativePath Name Extension Length LastWriteTimeUtc Sha256 FileVersion ProductVersion AssemblyName AssemblyVersion TargetFramework PeArchitecture
2 AnnotateLineNumbers.dll AnnotateLineNumbers.dll .dll 126976 2024-12-13T07:41:34.0000000Z 799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4 1.0.4.1 v1.0.4 AnnotateLineNumbers 1.0.4.0 .NETFramework,Version=v4.7 x64
3 AutoAnnotateView.dll AutoAnnotateView.dll .dll 49664 2025-02-10T07:47:43.4844455Z EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611 1.0.4.6 v1.0.4 AutoAnnotateView 1.0.4.0 .NETFramework,Version=v4.7 x64
4 AutoElevation.dll AutoElevation.dll .dll 28160 2025-01-16T08:13:32.2554341Z A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885 1.0.4.5 v1.0.4 AutoElevation 1.0.4.0 .NETFramework,Version=v4.7 x64
5 AutoLinenumberConnections.dll AutoLinenumberConnections.dll .dll 41472 2024-12-13T07:41:34.0000000Z 0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35 1.0.4.1 v1.0.4 AutoLinenumberConnections 1.0.4.0 .NETFramework,Version=v4.7 x64
6 b455ee6eaed79d76f084846a60ce3284 b455ee6eaed79d76f084846a60ce3284 0 2023-12-29T11:13:44.6409171Z E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
7 CheckOrthoView.dll CheckOrthoView.dll .dll 14848 2024-12-13T07:41:34.0000000Z 89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0 1.0.4.1 v1.0.4 CheckOrthoView 1.0.4.0 .NETFramework,Version=v4.7 x64
8 EnablerLoader.dll EnablerLoader.dll .dll 6144 2024-12-13T07:41:34.0000000Z 745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51 1.0.4.1 v1.0.4 EnablerLoader 1.0.4.0 .NETFramework,Version=v4.7 x64
9 ExtentsIntersect.dll ExtentsIntersect.dll .dll 6656 2024-12-13T07:41:34.0000000Z 59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9 1.0.4.1 v1.0.4 ExtentsIntersect 1.0.4.0 .NETFramework,Version=v4.7 x64
10 GetComponentGeometry.dll GetComponentGeometry.dll .dll 10240 2024-12-13T07:41:34.0000000Z 9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142 1.0.4.1 v1.0.4 GetComponentGeometry 1.0.4.0 .NETFramework,Version=v4.7 x64
11 GetComponentGeometry.xml GetComponentGeometry.xml .xml 1239 2025-01-14T11:26:24.5054138Z 70169C3A3AAAFE5A2282E01D5B9B7C99F2F4FF9B7514553434302674ABE46C98
12 GetLinenumberAnnotationText.dll GetLinenumberAnnotationText.dll .dll 23040 2024-12-13T07:41:34.0000000Z 047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E 1.0.4.1 v1.0.4 GetLinenumberAnnotationText 1.0.4.0 .NETFramework,Version=v4.7 x64
13 GetPlantScriptFolderPath.dll GetPlantScriptFolderPath.dll .dll 4608 2024-12-13T07:41:34.0000000Z 5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65 1.0.4.1 v1.0.4 GetPlantScriptFolderPath 1.0.4.0 .NETFramework,Version=v4.7 x64
14 GetScriptParameters.dll GetScriptParameters.dll .dll 6656 2024-12-13T07:41:34.0000000Z 7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87 1.0.4.1 v1.0.4 GetScriptParameters 1.0.4.0 .NETFramework,Version=v4.7 x64
15 ico\button_BJ.png button_BJ.png .png 1235 2024-12-13T07:41:34.0000000Z 414F0A23E917D41A82C697B0E9CDDDF9BFCACD1627310BD5717AEA172418AA51
16 ico\button_CAC.png button_CAC.png .png 1943 2024-12-13T07:41:34.0000000Z FA2B8877F8B41C70BA62C737C40374394D6840A53DFE2D0B0F83463C4817C138
17 ico\button_PXP-AAV.png button_PXP-AAV.png .png 1905 2024-12-13T07:41:34.0000000Z 6AB171963B553B434B5AA4BFBE91993B1782606AAF9B486FA23BB27BC1F0E4AA
18 ico\button_PXP-AC.png button_PXP-AC.png .png 1751 2024-12-13T07:41:34.0000000Z 60A6BD62EF3E50BDDCD00D9C281F2399E6C9D42E0C2DAA1742273C0DC48FE9C3
19 ico\button_PXP-AE.png button_PXP-AE.png .png 2001 2024-12-13T07:41:34.0000000Z F1DCF0E908F5AF38CCD13ABA58B824E5B186430414499C0F623C1D5EEAD8DAD7
20 ico\button_PXP-ALC.png button_PXP-ALC.png .png 1740 2024-12-13T07:41:34.0000000Z 19DB2449B66B95965155E6E3BC313BD52000FE38C95B0FAD90EF07DF53EA9CC9
21 ico\button_PXP-COV.png button_PXP-COV.png .png 2007 2024-12-13T07:41:34.0000000Z 529BDD045961C244FBD0DE177B899AF9E7CE0F4506B3463C56D2704B3BF207B5
22 ico\button_PXP-DAV.png button_PXP-DAV.png .png 1249 2025-01-28T06:29:03.3724433Z A7932F2ABDAC652EA3A901A221AB3BE2BC80FCB76FB203D44FC63AF3681F51E3
23 ico\button_PXP-DJ.png button_PXP-DJ.png .png 1470 2024-12-13T07:41:34.0000000Z 127957752DC2B6573CD2FD9A2791914BA22802E07E9B7E23CD3AE1FFF9E4FFC4
24 ico\button_PXP-ELN.png button_PXP-ELN.png .png 1927 2024-12-13T07:41:34.0000000Z DF32E6A465D5C196A17F3F96473315590DF2D71995F63D2D241D7599B62C99BB
25 ico\button_PXP-ESN.png button_PXP-ESN.png .png 1636 2024-12-13T07:41:34.0000000Z 0F089184CFD428DA3620F9C0AD2DCBFC21B31ED34FA301E8D5D45923783C0CC7
26 ico\button_PXP-IE.png button_PXP-IE.png .png 1885 2024-12-13T07:41:34.0000000Z F1C6373BFAF82E1908AF371D5F21133D27E0B84AFC706A703530855A2046FE43
27 ico\button_PXP-IOD.png button_PXP-IOD.png .png 1942 2024-12-13T07:41:34.0000000Z 494E90C899A0BB5BB365B0E1452E92A68359610C5A38A37D09F520A39BDB3899
28 ico\button_PXP-LN.png button_PXP-LN.png .png 2024 2024-12-13T07:41:34.0000000Z F3305ED6D266E29FA475D4C8A7DF38D2D2A1776A4CE379B876B6CFA278B2FE5D
29 ico\button_PXP-MA.png button_PXP-MA.png .png 1907 2024-12-13T07:41:34.0000000Z 9386249121644DCF4B9C70491F561E8B6E5BAAE87EBB2C76A84E46B10A60C8C3
30 ico\button_PXP-OA.png button_PXP-OA.png .png 1999 2024-12-13T07:41:34.0000000Z EAA2AD7EE5C6B7F3716AF1D7690C76AE59229AE9B6BA02009F5009CD9307F4E8
31 ico\button_PXP-OSI.png button_PXP-OSI.png .png 1508 2024-12-13T07:41:34.0000000Z 422D10B6EBDD76085CF9C5985391CCA060464B200D6792D604658AAE47F12CEE
32 ico\button_PXP-RA.png button_PXP-RA.png .png 1826 2024-12-13T07:41:34.0000000Z 1DB3F39C6CCA4EABE79D81DE6DCF9900649CDC9064F2EBF5E45F9AD849B8317F
33 ico\button_PXP-RMP.png button_PXP-RMP.png .png 1759 2024-12-13T07:41:34.0000000Z 156026EC6DEB7BDBE720047409E9A25615D650D8634C2F0BAF49A71E27135258
34 ico\button_PXP-ULN.png button_PXP-ULN.png .png 1928 2024-12-13T07:41:34.0000000Z B37EBB460691EAA7BC3A91DF05C27FE7CC1F0E9F9D69BDA7703956EAA7AA785C
35 ico\button_PXP-UOB.png button_PXP-UOB.png .png 1569 2024-12-13T07:41:34.0000000Z 3BC16B9923CCD34CF10F403CD65732B217E255E89539174841E7F30D9DFD2DCD
36 InsertElevation.dll InsertElevation.dll .dll 24576 2025-01-16T08:13:32.2504470Z B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82 1.0.4.5 v1.0.4 InsertElevation 1.0.4.0 .NETFramework,Version=v4.7 x64
37 LoadedAssemblies.dll LoadedAssemblies.dll .dll 4608 2024-12-13T07:41:34.0000000Z 277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E 1.0.4.1 v1.0.4 LoadedAssemblies 1.0.4.0 .NETFramework,Version=v4.7 x64
38 OrthoAutoAnnotate.dll OrthoAutoAnnotate.dll .dll 108544 2025-02-10T07:45:33.3280516Z 2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6 1.0.4.6 v1.0.4 OrthoAutoAnnotate 1.0.4.0 .NETFramework,Version=v4.7 x64
39 OrthoLoader.dll OrthoLoader.dll .dll 18944 2025-02-24T06:59:01.9094468Z D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D 1.0.4.7 v1.0.4 OrthoLoader 1.0.4.0 .NETFramework,Version=v4.7 x64
40 P3DCopyMissingObjectsToOrthoDrawing.dll P3DCopyMissingObjectsToOrthoDrawing.dll .dll 14336 2024-12-13T07:41:34.0000000Z 6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2 1.0.4.1 v1.0.4 P3DCopyMissingObjectsToOrthoDrawing 1.0.4.0 .NETFramework,Version=v4.7 x64
41 P3DNewMa.dll P3DNewMa.dll .dll 103936 2025-02-24T06:59:03.1350320Z 97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8 1.0.4.7 v1.0.4 P3DNewMa 1.0.4.0 .NETFramework,Version=v4.7 x64
42 P3DSetLayerForViewport.dll P3DSetLayerForViewport.dll .dll 4608 2024-12-13T07:41:34.0000000Z A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B 1.0.4.1 v1.0.4 P3DSetLayerForViewport 1.0.4.0 .NETFramework,Version=v4.7 x64
43 PHPAnnotations.dll PHPAnnotations.dll .dll 69120 2024-12-13T07:41:34.0000000Z 816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73 1.0.4.1 v1.0.4 PHPAnnotations 1.0.4.0 .NETFramework,Version=v4.7 x64
44 Settings.xml Settings.xml .xml 136576 2025-01-31T07:28:24.1891507Z AFEDC7526923ACE1DC60374F98F171809B36992262A37E1F11828F2B8D9799CB
45 SwapAttributesMenuItem.dll SwapAttributesMenuItem.dll .dll 11264 2024-12-13T07:41:34.0000000Z 674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460 1.0.4.1 v1.0.4 SwapAttributesMenuItem 1.0.4.0 .NETFramework,Version=v4.7 x64
46 Template.dwg Template.dwg .dwg 148438 2025-01-30T13:16:13.0000000Z EA279A30F113660CF53585451083BAEB0A01CA58C8F9A767C4B7C2A5E66ECCA3
47 test.txt test.txt .txt 0 2025-01-16T09:56:52.0597137Z E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855
48 UpdateOrthoDrawings.dll UpdateOrthoDrawings.dll .dll 45568 2025-02-24T06:59:02.4829428Z 1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA 1.0.4.2 v1.0.4 UpdateOrthoDrawings 1.0.4.0 .NETFramework,Version=v4.7 x64
49 UpdateOrthoViewBlocks.dll UpdateOrthoViewBlocks.dll .dll 15360 2024-12-13T07:41:34.0000000Z 5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9 1.0.4.1 v1.0.4 UpdateOrthoViewBlocks 1.0.4.0 .NETFramework,Version=v4.7 x64
50 Справка по работе с палитрой.docx Справка по работе с палитрой.docx .docx 855301 2025-02-05T09:53:08.1186958Z B6DC65BA17C4EA79DBCCC501909BEDBF56EB6A9F1E24BFAD1B01158B6EC639D0
51 Справка по работе с палитрой.pdf Справка по работе с палитрой.pdf .pdf 744035 2025-02-05T09:53:21.3752687Z 332CA70D3264C28BC712CA790157D0CD03BE7E07F42603DAF9247600271B7B75
52 Справка.docx Справка.docx .docx 87889 2025-02-06T05:13:36.4991443Z 14BA9BD252FB6DF37C4D151787E8799AB1703847C1DEC24F3EFE4D6805B0EB12
53 Справка.pdf Справка.pdf .pdf 154074 2025-02-06T05:14:24.2809131Z EB840514755A9F56ABC8351B8EC41233FE2512118DB2D4BA0ACD46E74D2C7DC9
@@ -0,0 +1,23 @@
"DeployedDll","DeployedSha256","FileVersion","AssemblyVersion","LegacyProject","LegacyCsproj","LegacyOutputName","TargetFramework","PlatformTarget","CommandCount","RibbonRole","ConfigurationSource","MatchStatus"
"AnnotateLineNumbers.dll","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","1.0.4.1","1.0.4.0","AnnotateLineNumbers","AnnotateLineNumbers\AnnotateLineNumbers.csproj","AnnotateLineNumbers","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"AutoAnnotateView.dll","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","1.0.4.6","1.0.4.0","AutoAnnotateView","AutoAnnotateView\AutoAnnotateView.csproj","AutoAnnotateView","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
"AutoElevation.dll","A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885","1.0.4.5","1.0.4.0","AutoElevation","AutoElevation\AutoElevation.csproj","AutoElevation","v4.7","x64",,,,"LIKELY_SOURCE_MATCH"
"AutoLinenumberConnections.dll","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","1.0.4.1","1.0.4.0","AutoLinenumberConnections","AutoLinenumberConnections\AutoLinenumberConnections.csproj","AutoLinenumberConnections","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"CheckOrthoView.dll","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","1.0.4.1","1.0.4.0","CheckOrthoView","CheckOrthoView\CheckOrthoView.csproj","CheckOrthoView","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"EnablerLoader.dll","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","1.0.4.1","1.0.4.0","EnablerLoader","EnablerLoader\EnablerLoader.csproj","EnablerLoader","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"ExtentsIntersect.dll","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","1.0.4.1","1.0.4.0","ExtentsIntersect","ExtentsIntersect\ExtentsIntersect.csproj","ExtentsIntersect","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"GetComponentGeometry.dll","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","1.0.4.1","1.0.4.0","GetComponentGeometry","GetComponentGeometry\GetComponentGeometry.csproj","GetComponentGeometry","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"GetLinenumberAnnotationText.dll","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","1.0.4.1","1.0.4.0","GetLinenumberAnnotationText","GetLinenumberAnnotationText\GetLinenumberAnnotationText.csproj","GetLinenumberAnnotationText","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"GetPlantScriptFolderPath.dll","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","1.0.4.1","1.0.4.0","GetPlantScriptFolderPath","GetPlantScriptFolderPath\GetPlantScriptFolderPath.csproj","GetPlantScriptFolderPath","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"GetScriptParameters.dll","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","1.0.4.1","1.0.4.0","GetScriptParameters","GetScriptParameters\GetScriptParameters.csproj","GetScriptParameters","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"InsertElevation.dll","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","1.0.4.5","1.0.4.0","InsertElevation","InsertElevation\InsertElevation.csproj","InsertElevation","v4.7","x64",,,"Settings.xml","MULTIPLE_CANDIDATES"
"LoadedAssemblies.dll","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","1.0.4.1","1.0.4.0","LoadedAssemblies","LoadedAssemblies\LoadedAssemblies.csproj","LoadedAssemblies","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"OrthoAutoAnnotate.dll","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","1.0.4.6","1.0.4.0","OrthoAutoAnnotate","OrthoAutoAnnotate\OrthoAutoAnnotate.csproj","OrthoAutoAnnotate","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
"OrthoLoader.dll","D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D","1.0.4.7","1.0.4.0","OrthoLoader","OrthoLoader\OrthoLoader.csproj","OrthoLoader","v4.7","x64",,"Central ribbon/loader",,"LIKELY_SOURCE_MATCH"
"P3DCopyMissingObjectsToOrthoDrawing.dll","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","1.0.4.1","1.0.4.0","P3DCopyMissingObjectsToOrthoDrawing","P3DCopyMissingObjectsToOrthoDrawing\P3DCopyMissingObjectsToOrthoDrawing.csproj","P3DCopyMissingObjectsToOrthoDrawing","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"P3DNewMa.dll","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","1.0.4.7","1.0.4.0","P3DNewMa","P3DNewMa\P3DNewMa.csproj","P3DNewMa","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
"P3DSetLayerForViewport.dll","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","1.0.4.1","1.0.4.0","P3DSetLayerForViewport","P3DSetLayerForViewport\P3DSetLayerForViewport.csproj","P3DSetLayerForViewport","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"PHPAnnotations.dll","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","1.0.4.1","1.0.4.0","PHPAnnotations","PHPAnnotations\PHPAnnotations.csproj","PHPAnnotations","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"SwapAttributesMenuItem.dll","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","1.0.4.1","1.0.4.0","SwapAttributesMenuItem","SwapAttributesMenuItem\SwapAttributesMenuItem.csproj","SwapAttributesMenuItem","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
"UpdateOrthoDrawings.dll","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","1.0.4.2","1.0.4.0","UpdateOrthoDrawings","UpdateOrthoDrawings\UpdateOrthoDrawings.csproj","UpdateOrthoDrawings","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
"UpdateOrthoViewBlocks.dll","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","1.0.4.1","1.0.4.0","UpdateOrthoViewBlocks","UpdateOrthoViewBlocks\UpdateOrthoViewBlocks.csproj","UpdateOrthoViewBlocks","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
1 DeployedDll DeployedSha256 FileVersion AssemblyVersion LegacyProject LegacyCsproj LegacyOutputName TargetFramework PlatformTarget CommandCount RibbonRole ConfigurationSource MatchStatus
2 AnnotateLineNumbers.dll 799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4 1.0.4.1 1.0.4.0 AnnotateLineNumbers AnnotateLineNumbers\AnnotateLineNumbers.csproj AnnotateLineNumbers v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
3 AutoAnnotateView.dll EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611 1.0.4.6 1.0.4.0 AutoAnnotateView AutoAnnotateView\AutoAnnotateView.csproj AutoAnnotateView v4.7 x64 Settings.xml LIKELY_SOURCE_MATCH
4 AutoElevation.dll A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885 1.0.4.5 1.0.4.0 AutoElevation AutoElevation\AutoElevation.csproj AutoElevation v4.7 x64 LIKELY_SOURCE_MATCH
5 AutoLinenumberConnections.dll 0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35 1.0.4.1 1.0.4.0 AutoLinenumberConnections AutoLinenumberConnections\AutoLinenumberConnections.csproj AutoLinenumberConnections v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
6 CheckOrthoView.dll 89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0 1.0.4.1 1.0.4.0 CheckOrthoView CheckOrthoView\CheckOrthoView.csproj CheckOrthoView v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
7 EnablerLoader.dll 745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51 1.0.4.1 1.0.4.0 EnablerLoader EnablerLoader\EnablerLoader.csproj EnablerLoader v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
8 ExtentsIntersect.dll 59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9 1.0.4.1 1.0.4.0 ExtentsIntersect ExtentsIntersect\ExtentsIntersect.csproj ExtentsIntersect v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
9 GetComponentGeometry.dll 9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142 1.0.4.1 1.0.4.0 GetComponentGeometry GetComponentGeometry\GetComponentGeometry.csproj GetComponentGeometry v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
10 GetLinenumberAnnotationText.dll 047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E 1.0.4.1 1.0.4.0 GetLinenumberAnnotationText GetLinenumberAnnotationText\GetLinenumberAnnotationText.csproj GetLinenumberAnnotationText v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
11 GetPlantScriptFolderPath.dll 5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65 1.0.4.1 1.0.4.0 GetPlantScriptFolderPath GetPlantScriptFolderPath\GetPlantScriptFolderPath.csproj GetPlantScriptFolderPath v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
12 GetScriptParameters.dll 7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87 1.0.4.1 1.0.4.0 GetScriptParameters GetScriptParameters\GetScriptParameters.csproj GetScriptParameters v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
13 InsertElevation.dll B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82 1.0.4.5 1.0.4.0 InsertElevation InsertElevation\InsertElevation.csproj InsertElevation v4.7 x64 Settings.xml MULTIPLE_CANDIDATES
14 LoadedAssemblies.dll 277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E 1.0.4.1 1.0.4.0 LoadedAssemblies LoadedAssemblies\LoadedAssemblies.csproj LoadedAssemblies v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
15 OrthoAutoAnnotate.dll 2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6 1.0.4.6 1.0.4.0 OrthoAutoAnnotate OrthoAutoAnnotate\OrthoAutoAnnotate.csproj OrthoAutoAnnotate v4.7 x64 Settings.xml LIKELY_SOURCE_MATCH
16 OrthoLoader.dll D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D 1.0.4.7 1.0.4.0 OrthoLoader OrthoLoader\OrthoLoader.csproj OrthoLoader v4.7 x64 Central ribbon/loader LIKELY_SOURCE_MATCH
17 P3DCopyMissingObjectsToOrthoDrawing.dll 6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2 1.0.4.1 1.0.4.0 P3DCopyMissingObjectsToOrthoDrawing P3DCopyMissingObjectsToOrthoDrawing\P3DCopyMissingObjectsToOrthoDrawing.csproj P3DCopyMissingObjectsToOrthoDrawing v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
18 P3DNewMa.dll 97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8 1.0.4.7 1.0.4.0 P3DNewMa P3DNewMa\P3DNewMa.csproj P3DNewMa v4.7 x64 Settings.xml LIKELY_SOURCE_MATCH
19 P3DSetLayerForViewport.dll A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B 1.0.4.1 1.0.4.0 P3DSetLayerForViewport P3DSetLayerForViewport\P3DSetLayerForViewport.csproj P3DSetLayerForViewport v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
20 PHPAnnotations.dll 816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73 1.0.4.1 1.0.4.0 PHPAnnotations PHPAnnotations\PHPAnnotations.csproj PHPAnnotations v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
21 SwapAttributesMenuItem.dll 674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460 1.0.4.1 1.0.4.0 SwapAttributesMenuItem SwapAttributesMenuItem\SwapAttributesMenuItem.csproj SwapAttributesMenuItem v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
22 UpdateOrthoDrawings.dll 1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA 1.0.4.2 1.0.4.0 UpdateOrthoDrawings UpdateOrthoDrawings\UpdateOrthoDrawings.csproj UpdateOrthoDrawings v4.7 x64 Settings.xml LIKELY_SOURCE_MATCH
23 UpdateOrthoViewBlocks.dll 5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9 1.0.4.1 1.0.4.0 UpdateOrthoViewBlocks UpdateOrthoViewBlocks\UpdateOrthoViewBlocks.csproj UpdateOrthoViewBlocks v4.7 x64 Settings.xml EXACT_SOURCE_MATCH
@@ -0,0 +1,4 @@
GeneratedUtc=2026-07-31T10:39:04.9899963Z
DeployedRoot=<network root redacted to report context>
SourceRoot=<network source root redacted to report context>
Files=52
@@ -0,0 +1,13 @@
# Legacy versus new loading model
| Legacy behavior | Legacy evidence | New implementation | Compatibility requirement | Decision | Risk |
|---|---|---|---|---|---|
| Unknown external bootstrap loads `OrthoLoader.dll` | `loading-chain.md` | Bundle bootstrap not selected yet | Identify the workstation/project startup owner first | Defer loading-model decision | High |
| 20 independent DLLs loaded in order | `Settings.xml`, `Loader.cs:63-81` | one entry assembly currently | Preserve required command availability, not the DLL count | Evaluate command/module migration individually | Medium |
| DLLs loaded from OrthoLoader's directory (possibly UNC) | `Loader.cs:49,65`; bootstrap source unknown | local smoke bundle only | Decide offline/update policy with operations owner | Do not hard-code UNC in new code | High |
| Settings.xml controls order | `Loader.cs:104-112` | MSBuild/bundle manifest currently | Preserve explicit dependency ordering where behavior requires it | Document, do not emulate blindly | Medium |
| central PluginsOrtho Ribbon | `Ribbon.cs` | new Promhimproekt tab | Preserve actual command mapping where adopted | Separate UI migration decision | Medium |
| project selection uses local app_netload.lsp | `Ribbon.cs:380-405` | no equivalent | Confirm security/project-selection policy | Do not reproduce hidden local control file | High |
| network icons and documents | `Ribbon.cs:330-357`, deployed files | bundle resources | Bundle only licensed, necessary resources | Replace network runtime dependency | Medium |
Recommendation: do not choose AppLoader or a manual-NETLOAD model yet. First identify the missing external bootstrap and confirm whether `app_netload.lsp` is the corporate launcher contract. Once confirmed, use a single versioned entry assembly with explicit manifest/dependency validation; avoid sequential programmatic NETLOAD and UNC execution because the legacy chain is non-atomic and fails the remaining modules after one error.
@@ -0,0 +1,29 @@
# Proven legacy loading chain
```text
Unknown external bootstrap
-> NETLOAD of deployed OrthoLoader.dll (not evidenced in audited directories)
-> OrthoLoader.Ribbon.Initialize()
-> subscribe ComponentManager.ItemInitialized
-> OrthoLoader.Initialize()
-> adjacent Settings.xml / Main/LoadAssemblies/LoadAssembly
-> sequential acDoc.Editor.Command("._netload", fullPath) for 20 DLLs
-> each DLL exposes CommandMethod commands
-> Ribbon ItemInitialized creates PluginsOrtho tab and command buttons
```
| Claim | Evidence | Confidence |
|---|---|---|
| Central loader is `OrthoLoader.dll` | `OrthoLoader/Ribbon.cs:151-156`; deployed DLL metadata and `Settings.xml` | High |
| Module list is read from adjacent `Settings.xml` | `OrthoLoader/Loader.cs:104-112` | High |
| Module loading is programmatic `._netload`, in XML order | `OrthoLoader/Loader.cs:63-81`; `configuration-map.csv` | High |
| Full DLL path is loader directory + configured filename | `OrthoLoader/Loader.cs:49, 65` | High |
| Modules load from the same directory as OrthoLoader | `Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)` in `Loader.cs:49,65` | High |
| Modules loaded directly from UNC | true only if external bootstrap NETLOADed the deployed UNC copy; bootstrap is outside audit scope | Unknown |
| Missing/failed module stops later loading and shows MessageBox | single outer try/catch around the loop, `Loader.cs:63-99` | High |
| Ribbon owner is OrthoLoader | `OrthoLoader/Ribbon.cs:16-19,151-172,193-321` | High |
| Initial bootstrap mechanism | no LISP/CUI/ARG/REG/PackageContents file found in either audited directory | Unknown |
`Assembly.LoadFrom` occurs inside several legacy command implementations for secondary dependencies. It is not the mechanism used by `OrthoLoader` to load its configured modules. `EnablerLoader` separately uses `Assembly.LoadFile` for the Plant enabler from the host base directory.
No `PackageContents.xml`, `*.bundle`, registry demand-load registration, LISP, CUI, MNL, script, batch, or PowerShell bootstrap was found in the audited locations. This proves absence only within audit scope; it does not prove the workstation bootstrap did not exist elsewhere.
@@ -0,0 +1,10 @@
Dependency,Category,Evidence,Requiredness,Notes
Settings.xml,CONFIGURATION,OrthoLoader/Loader.cs:104-112,Required,Adjacent to each loaded assembly directory
ico/*.png,RIBBON_RESOURCE,OrthoLoader/Ribbon.cs:330-357,Optional,Missing file yields null image
app_netload.lsp,CONFIGURATION,OrthoLoader/Ribbon.cs:380-405,Implicit,Local workstation file gates project-specific Ribbon enablement
annotation_settings.xml,REFERENCE_DATA,README.md and annotation modules,Project-dependent,Located in Plant project data area according to README
users_indexes_list.xml,REFERENCE_DATA,README.md and annotation modules,Project-dependent,Located in Plant project data area according to README
Template.dwg,TEMPLATE,Deployed inventory and legacy source references,Command-dependent,Do not treat deployed copy as approved test fixture
Plant project XML/DWG,COMMAND_TIME,UpdateOrthoDrawings and annotation source,Required by relevant command,Out of deployed plugin root
Update report CSV,LOGGING,UpdateOrthoDrawings/Commands.cs:1004,Optional,Written beside executing assembly by legacy command
Help PDFs,RIBBON_RESOURCE,P3DNewMa/NewMaControl.cs:6214,Optional,Opened from assembly directory
1 Dependency Category Evidence Requiredness Notes
2 Settings.xml CONFIGURATION OrthoLoader/Loader.cs:104-112 Required Adjacent to each loaded assembly directory
3 ico/*.png RIBBON_RESOURCE OrthoLoader/Ribbon.cs:330-357 Optional Missing file yields null image
4 app_netload.lsp CONFIGURATION OrthoLoader/Ribbon.cs:380-405 Implicit Local workstation file gates project-specific Ribbon enablement
5 annotation_settings.xml REFERENCE_DATA README.md and annotation modules Project-dependent Located in Plant project data area according to README
6 users_indexes_list.xml REFERENCE_DATA README.md and annotation modules Project-dependent Located in Plant project data area according to README
7 Template.dwg TEMPLATE Deployed inventory and legacy source references Command-dependent Do not treat deployed copy as approved test fixture
8 Plant project XML/DWG COMMAND_TIME UpdateOrthoDrawings and annotation source Required by relevant command Out of deployed plugin root
9 Update report CSV LOGGING UpdateOrthoDrawings/Commands.cs:1004 Optional Written beside executing assembly by legacy command
10 Help PDFs RIBBON_RESOURCE P3DNewMa/NewMaControl.cs:6214 Optional Opened from assembly directory
@@ -0,0 +1,9 @@
# Observed user workflow
After an external bootstrap loaded `OrthoLoader.dll`, the loader created a `PluginsOrtho` Ribbon tab only when `isProjectCorrect` accepted the current Plant project. That predicate reads the first line of the local file `C:\AutoCAD User Support\Applications\app_netload.lsp` and compares its encoded project root with the active Plant project (`OrthoLoader/Ribbon.cs:380-405`).
`PluginsOrtho` has eight panels. Button IDs are commands; clicks call `Document.SendStringToExecute(command + "\n", ...)` (`Ribbon.cs:193-321, 526-529`). Examples: `PXP-AAV` is registered by `AutoAnnotateView/Commands.cs:546`; `PXP-OA` by `OrthoAutoAnnotate/Commands.cs:6444`.
The deployed `ico/*.png` files are runtime Ribbon resources. They are resolved from the loader assembly directory; in this deployment that is the UNC directory. The code has a null-image fallback when an icon is absent.
The tab and panels use stable IDs and lookup before creation. Button collections are appended when a missing panel is created; repeated project-open events can enable existing UI or create absent UI. The event is detached after first Ribbon initialization, but `CurrentProjectOpenedEvent` is retained for the process lifetime.
@@ -0,0 +1,57 @@
# Аудит P_Rec_Annot v1.0.4
Статический read-only аудит каталога `C:\dev\autodesk\link\P_Rec_Annot\v1.0.4\repo`. Исходник не запускался, не собирался и не изменялся. `link` — symbolic link на `\\phpsrv03\NBIM\code\CS`; каталог `repo` внутри ссылки — обычный directory без собственного `.git`.
## Краткий вывод
P_Rec_Annot — не одна команда, а комплект из 25 legacy-проектов DLL в одном решении. Его ядро обслуживает орто-чертежи Plant 3D: пакетное обновление, размещение и обновление line-number annotations, автоматические осевые линии/опоры, высотные отметки, проверку отсутствующих объектов, слои viewport и ручные annotation blocks. README исходника прямо называет former `OrthoTools.2019` (`README.md:1-5`) и перечисляет пользовательские команды (`README.md:8-31`).
Наиболее употребляемым в агрегате XLSX является имя `P_Rec_Annot` с 218 796 единицами `Количество запусков`. В самом исходном комплекте не найден подтверждённый writer этого счётчика: найдено только использование `Environment.MachineName` для выбора пользовательского индекса (`GetLinenumberAnnotationText/LinenumberAnnotation.cs:225-250`). Поэтому 218 796 нельзя доказательно трактовать как число ручных запусков; это внешний агрегат, семантику которого нужно подтвердить по источнику telemetry.
## Факты инвентаризации
| Показатель | Значение |
|---|---:|
| Файлов | 271 |
| C# исходников вне bin/obj/lib | 120 |
| Проектов `.csproj` | 25 |
| DLL в исходном комплекте | 32 |
| AutoCAD-команд, включая архивные совпадения | 49 |
| Ribbon/even-handler/database/write evidence строк | 4 402+ |
| Target framework актуальных проектов | `.NET Framework 4.7` |
| Platform target | `x64` |
| Решение | `P_Rec_Annot.sln` |
### Подтверждённые точки входа
* `OrthoLoader` реализует загрузку библиотек: читает `Settings.xml`, строит путь от расположения текущей DLL и вызывает `Editor.Command("._netload", ...)` для каждой сборки (`OrthoLoader/Loader.cs:24-29`, `48-82`, `105-118`).
* `Ribbon` реализует `IExtensionApplication`, подписывается на `ComponentManager.ItemInitialized`, создаёт вкладку `PluginsOrtho`, восемь панелей и кнопки с командами (`OrthoLoader/Ribbon.cs:16-29`, `151-190`, `193-377`).
* Кнопка аннотаций содержит `PXP-AAV`, `MTO-MA` и `BJ`; отдельные команды актуальны для line numbers, elevations, supports, layers и package update (`OrthoLoader/Ribbon.cs:31-145`).
* `PXP-IOD` — session command пакетного обновления (`UpdateOrthoDrawings/Commands.cs:519`); `PXP-OA` и `PXP-AAV` запускают автоматическую обработку, но автоматического event-trigger для самих аннотаций не доказано (`OrthoAutoAnnotate/Commands.cs:6444-6463`, `AutoAnnotateView/Commands.cs:546`).
## Назначение и миграция
Основной переносимый контракт — правила получения Plant 3D данных, построения текста и создания/обновления орто-аннотаций. Переносить следует после выделения Core и Plant 3D 2026 adapter. Не переносить как есть загрузчик `_netload`, hard-coded ссылки на AutoCAD 2019, архивные копии и не подтверждённую telemetry.
Официальная документация Plant 3D 2026 подтверждает, что штатные annotations предназначены для orthographic drawings и поддерживают annotation styles/templates: [About Annotations](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-DAD70DC1-32F2-45E4-8685-0C574EF31299.htm). Штатная настройка формата выполняется через Project Setup/Class Definitions: [Annotation Format for Orthographic Drawings](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-6D5E3975-53AA-4495-B727-BBB2B599B464.htm). Это подтверждает область пересечения, но не доказывает замену корпоративных правил P_Rec_Annot. Autodesk также документирует создание и обновление Ortho, но не эквивалент всего пакетного поведения: [Create an Orthographic View](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-09E67A9E-45F7-4392-BFB6-EF1ADAB1D46B.htm), [About Updating Orthographic Drawings](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-EEDF2AA7-B50B-484B-B47F-B48986A51C3D.htm).
**Нужна ли новая кнопка:** да, на первом этапе миграции нужна одна явная корпоративная кнопка/команда для воспроизводимого сценария `Annotate`, а не набор скрытых вызовов. Предварительно: вкладка «Промхимпроект», панель «Аннотации», команда `PHP_REC_ANNOT`; состав операций должен выбираться режимом или отдельными безопасными командами после golden-теста.
## Ответы на ключевые вопросы
* **Автоматические вызовы:** обнаружены загрузочные и UI-события Ribbon, WinForms-события и временные PointMonitor; подписок `CommandWillStart/ObjectModified/Idle`, запускающих аннотацию, не подтверждено. Риск повторной подписки остаётся у `CurrentProjectOpenedEvent`.
* **DWG:** код массово использует AutoCAD transactions, `OpenMode.ForWrite`, `AppendEntity`, `BlockReference`, `MLeader`, attributes и удаление старых annotation blocks; изменения проекта через Plant DataLinks читаются, прямой SQL/SQLite writer в исходниках не найден.
* **DCF/Project.xml:** прямое изменение не доказано; `PnPDatabase`/`DataLinksManager` и `PnPDrawings/PnPDatalinks` создают косвенный риск через Plant API.
* **Повторный запуск:** есть явные удаления ранее размещённых blocks в `AutoAnnotateView/Commands.cs:476-500` и `AutoLinenumberConnections/Commands.cs:535-558`, но глобальная идемпотентность всего комплекта не доказана.
* **Undo:** операции заключены в AutoCAD transactions; гарантия единого undo для пакетного сценария не доказана, нужна runtime-проверка.
* **Build:** не выполнялся по ограничению задания.
## Файлы отчёта
Основные выводы находятся в Markdown-файлах этого каталога. `evidence/` содержит только агрегированные CSV/текстовые метаданные, без копий исходников и бинарников. Инструмент повторного read-only анализа: `tools/plugin-usage/Build-PRecAnnotAudit.ps1`.
## Ограничения
1. Исходный каталог содержит release DLL, DWG, DOCX, PDF и ZIP, но они не добавляются в Git и не открывались/запускались.
2. Без Plant 3D 2019 runtime нельзя подтвердить фактические визуальные результаты, число созданных объектов, undo и семантику внешнего usage-счётчика.
3. Сравнение 2026 ограничено официально опубликованными возможностями; полноценная замена требует runtime-сопоставления корпоративных annotation blocks и правил.
@@ -0,0 +1,7 @@
# Annotation algorithm
По исходникам функция `Annot` — создание/обновление орто-аннотаций и вспомогательных graphic blocks, а не универсальное 3D/P&ID/Iso annotator. Plant 3D 2026 официально также ограничивает понятие annotations orthographic drawings ([About Annotations](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-DAD70DC1-32F2-45E4-8685-0C574EF31299.htm)); поэтому область совпадает, но корпоративная логика не считается заменённой.
Подтверждённые операции: viewport/block selection, поиск DataLinks/PnP rows, чтение line groups и connected parts, transaction writes, block/attribute/MLeader/dimension operations. `AutoAnnotateView/Commands.cs:476-500` сначала удаляет ранее размещённые annotations, затем вызывает `InsertAnnotations()` и пишет count; `AutoLinenumberConnections/Commands.cs:535-558` аналогично удаляет blocks стыков. Это признаки частичной идемпотентности, но не доказательство для всего набора.
Нужно сохранять в golden test: тип блока, attribute tags/text, layer, scale, rotation, coordinates, XData/DataLinks, number of created/updated/skipped entities. Сырые DWG не включены.
@@ -0,0 +1,17 @@
# Architecture
```mermaid
flowchart LR
L[OrthoLoader.dll] --> S[Settings.xml]
L --> N[_netload each DLL]
N --> R[OrthoLoader.Ribbon]
R --> C[AutoCAD command]
C --> P[PlantApplication.CurrentProject]
P --> D[DataLinksManager/PnPDatabase]
D --> A[Ortho/model DB objects]
A --> T[Blocks, attributes, MLeader, dimensions, layers]
```
`P_Rec_Annot` — legacy монорешение с отдельными DLL-проектами, общими `Files`/`FilesRelease` и runtime-зависимостями между DLL. Основные функциональные модули: `AnnotateLineNumbers`, `AutoAnnotateView`, `AutoLinenumberConnections`, `AutoElevation`, `InsertElevation`, `OrthoAutoAnnotate`, `UpdateOrthoDrawings`, `UpdateOrthoViewBlocks`, `CheckOrthoView`, `P3DNewMa`, `PHPAnnotations`, `OrthoLoader`.
Целевая архитектура: `P_Rec_Annot.Core` (DTO, правила текста/позиционирования, duplicate detection), `P_Rec_Annot.Plant3D2026` (команды, DataLinks, transactions, selection), `P_Rec_Annot.UI`, `P_Rec_Annot.Telemetry`, тесты. Telemetry должна быть на уровне явного command scope, не внутри циклов объектов.
@@ -0,0 +1,7 @@
# Commands and UI
Актуальные команды, найденные в неархивных проектах: `PXP-LN`, `PXP-ELN`, `PXP-ALC`, `PXP-ULN`, `PXP-ESN`, `PXP-RMP`; `PXP-AAV`; `PXP-AE`; `PXP-IE`; `PXP-OSI`, `PXP-DJ`, `PXP-RA`, `PXP-OA`; `PXP-COV`; `mto-ma`, `CAC1`; `PXP-MA`, `CAC`, `BJ`; `PXP-AC`; `PXP-IOD`; `PXP-UOB`. Полные file/line записи — `evidence/commands.csv`; архивные одноимённые команды там сохранены отдельно.
Ribbon создаётся программно в `OrthoLoader.Ribbon`, вкладка `PluginsOrtho`, панели: package update, layers, lines/supports, pipe numbering, elevations, annotations, sizes, checks (`Ribbon.cs:18-29`). Кнопки строятся из массивов command id/text/description/icon (`Ribbon.cs:31-145`, `193-377`). UI включает WinForms формы выбора чертежей/аннотаций/операций и palette `P3DNewMa`; автоматическая кнопка `PXP-AAV` заявлена для арматуры, НКР и позиций КИП (`Ribbon.cs:109-129`).
Кнопки создаются после Ribbon initialization и при открытии проекта. `CurrentProjectOpenedEvent` подключается в `ComponentManager_ItemInitialized` (`Ribbon.cs:159-172`), а кнопки включаются/выключаются в `OpenProjectHandler` (`176-189`). Повторная загрузка и отсутствие отписки в `Terminate()` требуют runtime-проверки.
@@ -0,0 +1,12 @@
# Plant 3D 2019 → 2026
| Feature | 2019 implementation | 2026 official capability | Action |
|---|---|---|---|
| Ortho annotations | custom blocks/DataLinks | штатные orthographic annotations/styles documented | сравнить корпоративные blocks; не списывать плагин |
| Annotation formats | custom XML/settings + code | Project Setup/Class Definitions supports annotation styles | перенести правила в configuration where equivalent |
| Batch update | `PXP-IOD` session command | official update/create workflows documented, full batch equivalence not proven | KEEP_AFTER_ISOLATION + runtime |
| Locate 3D object | `PXP-OSI` custom traversal | official Locate in 3D Model workflow documented | verify native replacement |
| Layers/viewport | `PXP-UOB` custom writes | 2026 docs/API comparison required | adapter + golden test |
| Line-number connection annotations | `PXP-ALC` custom DataLinks/blocks | no full equivalence established | REIMPLEMENT_SAME_BEHAVIOR |
Официальные ссылки: [About Annotations](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-DAD70DC1-32F2-45E4-8685-0C574EF31299.htm), [Annotation Format](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-6D5E3975-53AA-4495-B727-BBB2B599B464.htm), [Create Ortho](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-09E67A9E-45F7-4392-BFB6-EF1ADAB1D46B.htm), [Locate 3D counterpart](https://help.autodesk.com/cloudhelp/2026/ENU/Plant3D-UserGuide/files/GUID-E49E4523-05AD-4F33-8535-EAC721B620EB.htm). Утверждение о полноценной замене запрещено до runtime-сопоставления.
@@ -0,0 +1,5 @@
# Configuration and paths
`Settings.xml` перечисляет DLL для загрузки. `annotation_settings.xml` и `users_indexes_list.xml` находятся в сетевом project support tree; README описывает `_System\Plant 3D_2019\Applications` и `_Data\Plant 3D_2019\AddAnnotationText` (`README.md:68-80`). `Template.dwg` и icons используются как deployment resources.
Абсолютные Autodesk 2019 HintPath и сетевые/локальные пути агрегированы в `evidence/external-paths.csv` с маскированием значений. Секретов в опубликованные evidence не включать; значения путей/реквизитов проектов не публиковать повторно.
@@ -0,0 +1,5 @@
# Dependencies
Проекты — legacy `.NET Framework 4.7`, `x64`, `OutputType=Library`. Autodesk references: `acCoreMgd`, `acDbMgd`, `acMgd`, `PnPProjectManagerMgd`, `PnPDataLinks`, `PnPDataObjects`, `PnP3dObjectsMgd`, `PnP3dPartsRepository`, `PnP3dOrtho*`, `PnPDwg2d*`, AutoCAD Interop. В HintPath присутствует `C:\Program Files\Autodesk\AutoCAD 2019\...`; подробности в `evidence/project-references.csv`.
Сторонние/внутренние DLL: deployment assemblies в `FilesRelease`, `UpdateOrthoDrawings/Libraries`, `lib/Excel.dll`, `lib/ICSharpCode.SharpZipLib.dll`. Office automation в актуальном исходном комплекте не доказана. Риск 2026 — API/binary binding, x64, assembly load order и внутренние Ortho assemblies.
@@ -0,0 +1,7 @@
# Drawing modifications
Исходник содержит многочисленные `StartTransaction`, `OpenMode.ForWrite`, `UpgradeOpen`, `AppendEntity`, `Erase`, `SetXData`, `BlockReference`, `AttributeReference`, `MLeader`, `MText`, `DBText`, `Table` и `Viewport`. Полный агрегированный список — `evidence/drawing-write-operations.csv`.
Из этого следует подтверждённая возможность записи DWG. Прямой записи DCF или `Project.xml` статическим поиском не доказано; DataLinks/PnP operations могут менять связанные данные как побочный эффект и требуют runtime/golden проверок. Для каждой операции transaction обычно используется, но единый undo scope пакетного действия не гарантирован.
Риск частичной записи: исключение после части transaction/между несколькими открытыми drawings, особенно в batch update. Следующий шаг — disposable test project, сравнение before/after и проверка восстановления/rollback.
@@ -0,0 +1,5 @@
# Error handling
Загрузчик использует MessageBox и `finally` для восстановления `FILEDIA`/`CMDECHO` (`OrthoLoader/Loader.cs:62-100`). Команды содержат многочисленные `try/catch`, включая пустые catch при получении drawing id (`OrthoAutoAnnotate/Commands.cs:360-366`, `6474-6480`). Это повышает риск silent failure и неполного результата.
Классы риска: WRONG_ANNOTATION, DUPLICATE_ANNOTATION, UI_FREEZE в batch, RESOURCE_LEAK при открытии нескольких drawings, SILENT_FAILURE из пустых catch, частичная запись DWG/связей. Нужны structured error/result, cancellation, progress и тесты исключений.
@@ -0,0 +1,5 @@
# Event handlers and automatic calls
Найдены `ComponentManager.ItemInitialized`, `PlantApplication.CurrentProjectOpenedEvent`, `AssemblyResolve`, WinForms UI events и временные `Editor.PointMonitor`. `Ribbon.cs:153-172` показывает subscribe/unsubscribe для Ribbon item, но `Ribbon.Terminate()` пуст (`Ribbon.cs:147-149`); project-open handler явно не отписывается. В `AnnotateLineNumbers/Commands.cs:1017-1044` PointMonitor отписывается в рамках операции.
Статически не подтверждены обработчики `CommandWillStart`, `ObjectModified`, `ObjectAppended`, `Idle`, которые автоматически аннотируют каждый объект. Поэтому нельзя объяснить 218 796 как event storm по этому исходнику. Риск повторной подписки на project-open event и UI re-entry сохраняется.
@@ -0,0 +1,5 @@
SourceRoot=C:\dev\autodesk\link\P_Rec_Annot\v1.0.4\repo
FileCount=271
ProjectCount=25
SourceCodeFileCount=120
ReadOnlyAnalysis=TRUE
@@ -0,0 +1,50 @@
"File","Line","Command","Flags","Class","Method","Purpose","Selection","PlantProject","ReadsDrawing","WritesDrawing","Telemetry"
"AnnotateLineNumbers\Commands.cs","705","PXP-ESN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"AnnotateLineNumbers\Commands.cs","845","PXP-ELN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"AnnotateLineNumbers\Commands.cs","1655","PXP-RMP",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"AnnotateLineNumbers\Commands.cs","1709","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"AnnotateLineNumbers\Commands.cs","2439","PXP-ULN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"AutoAnnotateView\Commands.cs","546","PXP-AAV",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"AutoElevation\Commands.cs","404","PXP-AE",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"AutoLinenumberConnections\Commands.cs","882","PXP-ALC",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"CheckOrthoView\Commands.cs","156","PXP-COV",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"ExtentsIntersect\Commands.cs","17","PXP-GIT",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"InsertElevation\Commands.cs","1040","PXP-IE",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"OrthoAutoAnnotate\Commands.cs","336","PXP-OSI","CommandFlags.Session","see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"OrthoAutoAnnotate\Commands.cs","795","PXP-DJ",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"OrthoAutoAnnotate\Commands.cs","1308","PXP-RA",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"OrthoAutoAnnotate\Commands.cs","6444","PXP-OA",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"P3DNewMa\Commands.cs","17","mto-ma",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"P3DNewMa\NewMaControl.cs","176","CAC1",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"P3DNewMa\NewMaControl.cs","1047","mto-ma1",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"PHPAnnotations\PHPAnnotations.cs","164","PXP-MA","CommandFlags.Redraw | CommandFlags.UsePickSet","see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"PHPAnnotations\PHPAnnotations.cs","287","CAC",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"PHPAnnotations\PHPAnnotations.cs","1701","CreateAnnotationCommand",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"PHPAnnotations\PHPBlockJig.cs","209","BJ",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"SwapAttributesMenuItem\Commands.cs","40","PXP-AC","CommandFlags.UsePickSet","see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"UpdateOrthoDrawings\Commands.cs","519","PXP-IOD","CommandFlags.Session","see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"UpdateOrthoViewBlocks\Commands.cs","585","PXP-UOB",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Class1.cs","139","TestEntityJigger15",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (2).cs","167","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (2).cs","783","PointMonitorSelection",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (2).cs","902","KA",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (3).cs","162","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (3).cs","679","PointMonitorSelection",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (3).cs","798","KA",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (4).cs","168","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (4).cs","679","PointMonitorSelection",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (4).cs","798","KA",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs","230","attbackgrhide","CommandFlags.UsePickSet","see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs","274","PXP-TEST",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs","340","PXP-RMP",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs","355","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия.cs","118","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands1.cs","162","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands1.cs","679","PointMonitorSelection",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands1.cs","798","KA",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands2.cs","168","PXP-LN",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands2.cs","679","PointMonitorSelection",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands2.cs","798","KA",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\LineNumberMarker - копия.cs","488","BJ",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_InsertElevation_ОЗХ\2019_InsertElevation\2019_InsertElevation\Commands.cs","1044","PXP-IE",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
"_archive\2019_PHPAnnotations\2019_PHPAnnotations\Class1.cs","20","MyCreateAnnotation",,"see source","see source","registered AutoCAD command","unknown","verify body","yes","unknown","not evidenced"
1 File Line Command Flags Class Method Purpose Selection PlantProject ReadsDrawing WritesDrawing Telemetry
2 AnnotateLineNumbers\Commands.cs 705 PXP-ESN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
3 AnnotateLineNumbers\Commands.cs 845 PXP-ELN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
4 AnnotateLineNumbers\Commands.cs 1655 PXP-RMP see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
5 AnnotateLineNumbers\Commands.cs 1709 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
6 AnnotateLineNumbers\Commands.cs 2439 PXP-ULN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
7 AutoAnnotateView\Commands.cs 546 PXP-AAV see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
8 AutoElevation\Commands.cs 404 PXP-AE see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
9 AutoLinenumberConnections\Commands.cs 882 PXP-ALC see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
10 CheckOrthoView\Commands.cs 156 PXP-COV see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
11 ExtentsIntersect\Commands.cs 17 PXP-GIT see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
12 InsertElevation\Commands.cs 1040 PXP-IE see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
13 OrthoAutoAnnotate\Commands.cs 336 PXP-OSI CommandFlags.Session see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
14 OrthoAutoAnnotate\Commands.cs 795 PXP-DJ see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
15 OrthoAutoAnnotate\Commands.cs 1308 PXP-RA see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
16 OrthoAutoAnnotate\Commands.cs 6444 PXP-OA see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
17 P3DNewMa\Commands.cs 17 mto-ma see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
18 P3DNewMa\NewMaControl.cs 176 CAC1 see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
19 P3DNewMa\NewMaControl.cs 1047 mto-ma1 see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
20 PHPAnnotations\PHPAnnotations.cs 164 PXP-MA CommandFlags.Redraw | CommandFlags.UsePickSet see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
21 PHPAnnotations\PHPAnnotations.cs 287 CAC see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
22 PHPAnnotations\PHPAnnotations.cs 1701 CreateAnnotationCommand see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
23 PHPAnnotations\PHPBlockJig.cs 209 BJ see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
24 SwapAttributesMenuItem\Commands.cs 40 PXP-AC CommandFlags.UsePickSet see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
25 UpdateOrthoDrawings\Commands.cs 519 PXP-IOD CommandFlags.Session see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
26 UpdateOrthoViewBlocks\Commands.cs 585 PXP-UOB see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
27 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Class1.cs 139 TestEntityJigger15 see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
28 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (2).cs 167 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
29 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (2).cs 783 PointMonitorSelection see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
30 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (2).cs 902 KA see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
31 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (3).cs 162 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
32 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (3).cs 679 PointMonitorSelection see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
33 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (3).cs 798 KA see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
34 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (4).cs 168 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
35 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (4).cs 679 PointMonitorSelection see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
36 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (4).cs 798 KA see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
37 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs 230 attbackgrhide CommandFlags.UsePickSet see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
38 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs 274 PXP-TEST see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
39 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs 340 PXP-RMP see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
40 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия (5).cs 355 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
41 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands - копия.cs 118 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
42 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands1.cs 162 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
43 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands1.cs 679 PointMonitorSelection see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
44 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands1.cs 798 KA see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
45 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands2.cs 168 PXP-LN see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
46 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands2.cs 679 PointMonitorSelection see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
47 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\Commands2.cs 798 KA see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
48 _archive\2019_AnnotateLineNumbers\2019_AnnotateLineNumbers\LineNumberMarker - копия.cs 488 BJ see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
49 _archive\2019_InsertElevation_ОЗХ\2019_InsertElevation\2019_InsertElevation\Commands.cs 1044 PXP-IE see source see source registered AutoCAD command unknown verify body yes unknown not evidenced
50 _archive\2019_PHPAnnotations\2019_PHPAnnotations\Class1.cs 20 MyCreateAnnotation see source see source registered AutoCAD command unknown verify body yes unknown not evidenced

Some files were not shown because too many files have changed in this diff Show More