diff --git a/Plant3D2026/P_Rec_Annot/.gitignore b/Plant3D2026/P_Rec_Annot/.gitignore
new file mode 100644
index 0000000..42fcf36
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/.gitignore
@@ -0,0 +1,4 @@
+artifacts/
+bin/
+obj/
+.vs/
diff --git a/Plant3D2026/P_Rec_Annot/Directory.Build.props b/Plant3D2026/P_Rec_Annot/Directory.Build.props
new file mode 100644
index 0000000..fa78c7b
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+ enable
+ enable
+ latest
+ true
+ true
+ portable
+ $(ProgramFiles)\Autodesk\AutoCAD 2026
+
+
diff --git a/Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln b/Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln
new file mode 100644
index 0000000..a958ea3
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln
@@ -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
diff --git a/Plant3D2026/P_Rec_Annot/README.md b/Plant3D2026/P_Rec_Annot/README.md
new file mode 100644
index 0000000..11fb38b
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/README.md
@@ -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.
diff --git a/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml b/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml
new file mode 100644
index 0000000..2bd6de8
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/Plant3D2026/P_Rec_Annot/docs/architecture.md b/Plant3D2026/P_Rec_Annot/docs/architecture.md
new file mode 100644
index 0000000..9c2acb4
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/architecture.md
@@ -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.
diff --git a/Plant3D2026/P_Rec_Annot/docs/build.md b/Plant3D2026/P_Rec_Annot/docs/build.md
new file mode 100644
index 0000000..d7b0829
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/build.md
@@ -0,0 +1,3 @@
+# Build
+
+SDK 8.0.423 is installed. Plant 3D 2026 is not installed on the audit machine, so Autodesk references are currently absent and the adapter is a deliberate blocker. Build Core/tests independently first; after installation pass `Plant3D2026InstallDir` and validate exact assembly/member compatibility. No Autodesk DLL is copied to Git or bundle. `SeriesMin`, `SeriesMax` and the exact platform token are intentionally absent from the manifest until verified against the installed product and official package contract.
diff --git a/Plant3D2026/P_Rec_Annot/docs/command-migration-matrix.md b/Plant3D2026/P_Rec_Annot/docs/command-migration-matrix.md
new file mode 100644
index 0000000..8596f76
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/command-migration-matrix.md
@@ -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.
diff --git a/Plant3D2026/P_Rec_Annot/docs/golden-test-plan.md b/Plant3D2026/P_Rec_Annot/docs/golden-test-plan.md
new file mode 100644
index 0000000..2d721ae
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/golden-test-plan.md
@@ -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.
diff --git a/Plant3D2026/P_Rec_Annot/docs/legacy-rule-provenance.csv b/Plant3D2026/P_Rec_Annot/docs/legacy-rule-provenance.csv
new file mode 100644
index 0000000..592e568
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/legacy-rule-provenance.csv
@@ -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
diff --git a/Plant3D2026/P_Rec_Annot/docs/migration-dashboard.md b/Plant3D2026/P_Rec_Annot/docs/migration-dashboard.md
new file mode 100644
index 0000000..48bbdca
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/migration-dashboard.md
@@ -0,0 +1,3 @@
+# Migration dashboard
+
+The active command set is mapped in `command-migration-matrix.md`; all currently published target descriptors are `BLOCKED_WITH_REASON` because Plant 3D 2026 assemblies/runtime are unavailable. Coverage is not reported as a percentage until every active use case has a runtime result. Current status: mapped 3 foundation descriptors, implemented Core planning/registry/telemetry contracts, runtime verified 0, native replacements approved 0, retirements approved 0, blockers 3.
diff --git a/Plant3D2026/P_Rec_Annot/docs/module-migration-matrix.md b/Plant3D2026/P_Rec_Annot/docs/module-migration-matrix.md
new file mode 100644
index 0000000..62e2a63
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/module-migration-matrix.md
@@ -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 |
diff --git a/Plant3D2026/P_Rec_Annot/docs/runtime-test-plan.md b/Plant3D2026/P_Rec_Annot/docs/runtime-test-plan.md
new file mode 100644
index 0000000..b8a19f5
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/docs/runtime-test-plan.md
@@ -0,0 +1,3 @@
+# Runtime test plan
+
+On a controlled Plant 3D 2026 test machine: install the bundle outside production, start Plant, verify exactly one Ribbon tab/panel, open a test project and Ortho DWG, run `PHP_REC_ANNOT`, inspect dry-run counts, confirm one transaction, save/reopen/audit, repeat command, test no duplicates and Undo, then compare entities/text/layers/positions/links with the 2019 golden sample. This plan is prepared but not executed.
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs
new file mode 100644
index 0000000..8a194e3
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs
@@ -0,0 +1,95 @@
+namespace PRecAnnot.Abstractions;
+
+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 Objects { get; init; }
+ public required IReadOnlyList 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 required IReadOnlyList Create { get; init; }
+ public required IReadOnlyList Update { get; init; }
+ public required IReadOnlyList Delete { get; init; }
+ public required IReadOnlyList 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 GetViewports();
+}
+
+public interface IPlantObjectReader
+{
+ IReadOnlyList ReadObjects(OrthoViewportRef viewport, CancellationToken cancellationToken);
+}
+
+public interface IAnnotationRepository
+{
+ IReadOnlyList 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);
+}
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/PRecAnnot.Abstractions.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/PRecAnnot.Abstractions.csproj
new file mode 100644
index 0000000..c3c3554
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/PRecAnnot.Abstractions.csproj
@@ -0,0 +1,3 @@
+
+ net8.0
+
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/CommandRegistry.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/CommandRegistry.cs
new file mode 100644
index 0000000..9581daa
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/CommandRegistry.cs
@@ -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 _commands;
+
+ public CommandRegistry(IEnumerable 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 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" }
+ });
+}
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/PRecAnnot.Application.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/PRecAnnot.Application.csproj
new file mode 100644
index 0000000..c156ee7
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/PRecAnnot.Application.csproj
@@ -0,0 +1,4 @@
+
+ net8.0
+
+
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs
new file mode 100644
index 0000000..0c935e8
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs
@@ -0,0 +1,48 @@
+using PRecAnnot.Abstractions;
+
+namespace PRecAnnot.Core;
+
+public sealed class AnnotationRuleEngine : IAnnotationRuleEngine
+{
+ public AnnotationPlan BuildPlan(AnnotationRequest request)
+ {
+ var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase);
+ var create = new List();
+ var update = new List();
+ var skipped = new List();
+
+ foreach (var item in request.Objects.OrderBy(x => x.ObjectId, StringComparer.Ordinal))
+ {
+ if (string.IsNullOrWhiteSpace(item.LineNumber) && string.IsNullOrWhiteSpace(item.Tag))
+ {
+ skipped.Add($"{item.ObjectId}:missing-line-number-or-tag");
+ continue;
+ }
+
+ var text = BuildText(item);
+ var intent = new AnnotationIntent
+ {
+ StableKey = $"{request.Viewport.DrawingId}:{request.Viewport.ViewportId}:{item.ObjectId}",
+ Text = text,
+ Position = item.Position,
+ Rotation = item.Rotation,
+ ClassName = item.ClassName,
+ Layer = "PXP_ANNOTATION"
+ };
+
+ if (existing.ContainsKey(intent.StableKey)) update.Add(intent); else create.Add(intent);
+ }
+
+ return new AnnotationPlan
+ {
+ Create = create,
+ Update = update,
+ Delete = Array.Empty(),
+ SkipReasons = skipped,
+ ExaminedCount = request.Objects.Count
+ };
+ }
+
+ private static string BuildText(PlantObjectData item) =>
+ string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag!.Trim() : item.LineNumber.Trim();
+}
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/PRecAnnot.Core.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/PRecAnnot.Core.csproj
new file mode 100644
index 0000000..c156ee7
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/PRecAnnot.Core.csproj
@@ -0,0 +1,4 @@
+
+ net8.0
+
+
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPointLifecycle.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPointLifecycle.cs
new file mode 100644
index 0000000..3f3b2ca
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPointLifecycle.cs
@@ -0,0 +1,15 @@
+namespace PRecAnnot.Plant3D2026;
+
+///
+/// Autodesk-facing composition-root lifecycle. The concrete IExtensionApplication
+/// implementation is enabled only after the Plant 3D 2026 SDK reference contract
+/// is verified on the target installation.
+///
+public sealed class EntryPointLifecycle : IDisposable
+{
+ private int _initialized;
+ public bool IsInitialized => Volatile.Read(ref _initialized) == 1;
+ public bool Initialize() => Interlocked.Exchange(ref _initialized, 1) == 0;
+ public void Terminate() => Interlocked.Exchange(ref _initialized, 0);
+ public void Dispose() => Terminate();
+}
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj
new file mode 100644
index 0000000..c5d408b
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj
@@ -0,0 +1,15 @@
+
+ net8.0-windowsx64true
+
+
+
+
+
+
+ $(Plant3D2026InstallDir)\AcMgd.dllfalse
+ $(Plant3D2026InstallDir)\AcDbMgd.dllfalse
+ $(Plant3D2026InstallDir)\AcCoreMgd.dllfalse
+ $(Plant3D2026InstallDir)\PLNT3D\PnPProjectManagerMgd.dllfalse
+ $(Plant3D2026InstallDir)\PLNT3D\PnPDataLinks.dllfalse
+
+
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/Plant3D2026Availability.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/Plant3D2026Availability.cs
new file mode 100644
index 0000000..541907c
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/Plant3D2026Availability.cs
@@ -0,0 +1,14 @@
+namespace PRecAnnot.Plant3D2026;
+
+public sealed record Plant3D2026Availability(bool IsAvailable, string InstallDirectory, IReadOnlyList MissingAssemblies)
+{
+ public static Plant3D2026Availability Detect(string installDirectory)
+ {
+ var expected = new[] { "AcMgd.dll", "AcDbMgd.dll", "AcCoreMgd.dll", Path.Combine("PLNT3D", "PnPProjectManagerMgd.dll"), Path.Combine("PLNT3D", "PnPDataLinks.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)}") { }
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/JsonlUsageSink.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/JsonlUsageSink.cs
new file mode 100644
index 0000000..bfcaea9
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/JsonlUsageSink.cs
@@ -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);
+ }
+}
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/PRecAnnot.Telemetry.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/PRecAnnot.Telemetry.csproj
new file mode 100644
index 0000000..c156ee7
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/PRecAnnot.Telemetry.csproj
@@ -0,0 +1,4 @@
+
+ net8.0
+
+
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/PRecAnnot.UI.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/PRecAnnot.UI.csproj
new file mode 100644
index 0000000..960f619
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/PRecAnnot.UI.csproj
@@ -0,0 +1,4 @@
+
+ net8.0-windowstrue
+
+
diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/RibbonRegistration.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/RibbonRegistration.cs
new file mode 100644
index 0000000..1149009
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/RibbonRegistration.cs
@@ -0,0 +1,17 @@
+using PRecAnnot.Application;
+
+namespace PRecAnnot.UI;
+
+public sealed class RibbonRegistration
+{
+ private readonly HashSet _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;
+}
diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs
new file mode 100644
index 0000000..2a2106b
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs
@@ -0,0 +1,37 @@
+using PRecAnnot.Abstractions;
+using PRecAnnot.Application;
+using PRecAnnot.Core;
+using PRecAnnot.UI;
+
+namespace PRecAnnot.Core.Tests;
+
+public sealed class FoundationTests
+{
+ [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() };
+ 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);
+ }
+}
diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PRecAnnot.Core.Tests.csproj b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PRecAnnot.Core.Tests.csproj
new file mode 100644
index 0000000..8f61c3b
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PRecAnnot.Core.Tests.csproj
@@ -0,0 +1,6 @@
+
+ net8.0-windowsfalsetrue
+
+
+
+
diff --git a/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 b/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1
new file mode 100644
index 0000000..68d002a
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1
@@ -0,0 +1,4 @@
+param([ValidateSet('Debug','Release')][string]$Configuration='Release',[string]$Plant3D2026InstallDir)
+$ErrorActionPreference='Stop'
+dotnet build (Join-Path $PSScriptRoot '..\PRecAnnot2026.sln') --configuration $Configuration /p:Plant3D2026InstallDir=$Plant3D2026InstallDir
+Write-Host 'Bundle assembly build completed; deployment is intentionally not performed.'
diff --git a/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 b/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1
new file mode 100644
index 0000000..8eb2f25
--- /dev/null
+++ b/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1
@@ -0,0 +1,8 @@
+$ErrorActionPreference='Stop'
+$package=Join-Path $PSScriptRoot '..\bundle\P_Rec_Annot.bundle\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.'}
+Write-Host 'Bundle manifest is structurally valid.'