@danmoisan/drm-copilot-mcp 0.0.1 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/out/mcp-server.js +98 -21
  2. package/package.json +21 -5
  3. package/resources/claude-customizations/.claude/agent-memory/orchestrator/MEMORY.md +15 -3
  4. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_branch_base_check_unmerged_pr_deps.md +16 -0
  5. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_every_change_through_lifecycle.md +15 -0
  6. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_policy_compliance_not_optional.md +18 -0
  7. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_potential_to_issue_creates_github_issue.md +13 -0
  8. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_remediation_plan_em_dash_required.md +13 -0
  9. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_small_bug_uses_minor_audit.md +13 -0
  10. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_test_files_count_against_500_cap.md +13 -0
  11. package/resources/claude-customizations/.claude/agents/atomic-executor.md +7 -7
  12. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +4 -5
  13. package/resources/claude-customizations/.claude/agents/feature-review.md +7 -3
  14. package/resources/claude-customizations/.claude/agents/orchestrator.md +22 -3
  15. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -1
  16. package/resources/claude-customizations/.claude/agents/pr-author.md +78 -0
  17. package/resources/claude-customizations/.claude/agents/task-researcher.md +10 -6
  18. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +300 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
  21. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +148 -0
  22. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +210 -0
  23. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +333 -0
  24. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +216 -0
  25. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +84 -15
  26. package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +1 -1
  27. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage.ps1 +75 -5
  28. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +93 -0
  29. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
  30. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +90 -6
  31. package/resources/claude-customizations/.claude/rules/architecture-boundaries.md +46 -0
  32. package/resources/claude-customizations/.claude/rules/benchmark-baselines.md +35 -0
  33. package/resources/claude-customizations/.claude/rules/ci-workflows.md +36 -0
  34. package/resources/claude-customizations/.claude/rules/csharp.md +62 -16
  35. package/resources/claude-customizations/.claude/rules/general-code-change.md +12 -3
  36. package/resources/claude-customizations/.claude/rules/general-unit-test.md +47 -2
  37. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +39 -0
  38. package/resources/claude-customizations/.claude/rules/powershell.md +5 -5
  39. package/resources/claude-customizations/.claude/rules/python.md +4 -3
  40. package/resources/claude-customizations/.claude/rules/quality-tiers.md +51 -0
  41. package/resources/claude-customizations/.claude/rules/typescript.md +37 -8
  42. package/resources/claude-customizations/.claude/settings.json +59 -12
  43. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +2 -2
  44. package/resources/claude-customizations/.claude/skills/csharp-qa-gate/SKILL.md +25 -10
  45. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
  46. package/resources/claude-customizations/.claude/skills/execute-hard-lock/SKILL.md +6 -6
  47. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +8 -8
  48. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +17 -6
  49. package/resources/claude-customizations/.claude/skills/human-exception-runbook/SKILL.md +52 -0
  50. package/resources/claude-customizations/.claude/skills/human-exception-runbook/example.runbook.md +36 -0
  51. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -4
  52. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +172 -5
  53. package/resources/claude-customizations/.claude/skills/policy-audit-template-usage/SKILL.md +3 -3
  54. package/resources/claude-customizations/.claude/skills/powershell-qa-gate/SKILL.md +4 -4
  55. package/resources/claude-customizations/.claude/skills/pr-base-branch-merge-base/SKILL.md +3 -3
  56. package/resources/claude-customizations/.claude/skills/python-qa-gate/SKILL.md +1 -1
  57. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +90 -17
  58. package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
  59. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
  60. package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
  61. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
  62. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
  63. package/resources/claude-customizations/pack-manifests/core.json +65 -0
  64. package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
  65. package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
  66. package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
  67. package/resources/claude-customizations/pack-manifests/python.json +14 -0
  68. package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
  69. package/resources/claude-dir-customizations/.mcp.json +3 -3
  70. package/resources/codex-and-agents-customizations/.agents/README.md +1 -1
  71. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +0 -5
  72. package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +52 -0
  73. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +16 -8
  74. package/resources/codex-and-agents-customizations/.agents/skills/benchmark-baselines/SKILL.md +44 -0
  75. package/resources/codex-and-agents-customizations/.agents/skills/ci-workflows/SKILL.md +45 -0
  76. package/resources/codex-and-agents-customizations/.agents/skills/commit-message/SKILL.md +3 -11
  77. package/resources/codex-and-agents-customizations/.agents/skills/csharp/SKILL.md +1 -5
  78. package/resources/codex-and-agents-customizations/.agents/skills/csharp-change-budget-router/SKILL.md +1 -6
  79. package/resources/codex-and-agents-customizations/.agents/skills/csharp-orchestration-state-machine/SKILL.md +0 -5
  80. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +0 -5
  81. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +0 -5
  82. package/resources/codex-and-agents-customizations/.agents/skills/execute-hard-lock/SKILL.md +8 -17
  83. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +41 -25
  84. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +1 -6
  85. package/resources/codex-and-agents-customizations/.agents/skills/fill-feature-docs/SKILL.md +0 -5
  86. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +86 -0
  87. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +111 -0
  88. package/resources/codex-and-agents-customizations/.agents/skills/human-exception-runbook/SKILL.md +57 -0
  89. package/resources/codex-and-agents-customizations/.agents/skills/human-exception-runbook/example.runbook.md +36 -0
  90. package/resources/codex-and-agents-customizations/.agents/skills/invoke-csharp-engineer/SKILL.md +0 -9
  91. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -9
  92. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +0 -9
  93. package/resources/codex-and-agents-customizations/.agents/skills/make-skill-template/SKILL.md +0 -5
  94. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +206 -18
  95. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +48 -0
  96. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +94 -10
  97. package/resources/codex-and-agents-customizations/.agents/skills/policy-audit-template-usage/SKILL.md +3 -8
  98. package/resources/codex-and-agents-customizations/.agents/skills/policy-compliance-order/SKILL.md +0 -10
  99. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +4 -8
  100. package/resources/codex-and-agents-customizations/.agents/skills/powershell-change-budget-router/SKILL.md +1 -6
  101. package/resources/codex-and-agents-customizations/.agents/skills/powershell-orchestration-state-machine/SKILL.md +0 -5
  102. package/resources/codex-and-agents-customizations/.agents/skills/powershell-qa-gate/SKILL.md +3 -9
  103. package/resources/codex-and-agents-customizations/.agents/skills/pr-author/SKILL.md +1 -9
  104. package/resources/codex-and-agents-customizations/.agents/skills/pr-base-branch-merge-base/SKILL.md +4 -9
  105. package/resources/codex-and-agents-customizations/.agents/skills/pr-context-artifacts/SKILL.md +0 -5
  106. package/resources/codex-and-agents-customizations/.agents/skills/python/SKILL.md +1 -5
  107. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +1 -6
  108. package/resources/codex-and-agents-customizations/.agents/skills/python-qa-gate/SKILL.md +0 -5
  109. package/resources/codex-and-agents-customizations/.agents/skills/python-suppressions/SKILL.md +2 -6
  110. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +57 -0
  111. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +0 -5
  112. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +96 -72
  113. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/agents/openai.yaml +1 -1
  114. package/resources/codex-and-agents-customizations/.agents/skills/research-issue/SKILL.md +0 -10
  115. package/resources/codex-and-agents-customizations/.agents/skills/review-epic/SKILL.md +0 -5
  116. package/resources/codex-and-agents-customizations/.agents/skills/review-feature/SKILL.md +0 -5
  117. package/resources/codex-and-agents-customizations/.agents/skills/review-staged/SKILL.md +0 -5
  118. package/resources/codex-and-agents-customizations/.agents/skills/self-explanatory-code-commenting/SKILL.md +2 -6
  119. package/resources/codex-and-agents-customizations/.agents/skills/skill-canonical-location-audit/SKILL.md +0 -5
  120. package/resources/codex-and-agents-customizations/.agents/skills/tonality/SKILL.md +86 -0
  121. package/resources/codex-and-agents-customizations/.agents/skills/translate-claude-to-codex/SKILL.md +297 -0
  122. package/resources/codex-and-agents-customizations/.agents/skills/translate-copilot-to-claude/SKILL.md +0 -22
  123. package/resources/codex-and-agents-customizations/.agents/skills/typescript/SKILL.md +1 -5
  124. package/resources/codex-and-agents-customizations/.agents/skills/typescript-suppressions/SKILL.md +2 -6
  125. package/resources/codex-and-agents-customizations/.agents/skills/update-status/SKILL.md +0 -5
  126. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +5 -5
  127. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +91 -63
  128. package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-executor.toml +1 -1
  129. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
  130. package/resources/codex-and-agents-customizations/.codex/config.toml +111 -137
  131. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
  132. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
  133. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
  134. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
  135. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +84 -18
  136. package/resources/codex-and-agents-customizations/.codex/prompts/orchestrate-work.md +4 -3
  137. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +5 -0
  138. package/resources/codex-and-agents-customizations/.github/workflows/_validate-orchestrator-state.yml +68 -0
  139. package/resources/codex-and-agents-customizations/.github/workflows/validate-orchestrator-state.yml +15 -0
  140. package/resources/config/orchestration-routing.json +76 -0
  141. package/resources/customizations/.github/agents/Powershell DI Unit Test Engineer.agent.md +1 -1
  142. package/resources/customizations/.github/agents/atomic_executor.agent.md +1 -1
  143. package/resources/customizations/.github/agents/atomic_planning.agent.md +10 -10
  144. package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +6 -2
  145. package/resources/customizations/.github/agents/feature-review.agent.md +2 -2
  146. package/resources/customizations/.github/agents/orchestrator.agent.md +6 -2
  147. package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +4 -4
  148. package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +10 -10
  149. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +6 -2
  150. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +2 -2
  151. package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
  152. package/resources/customizations/.github/agents/python-orchestrator.agent.md +6 -2
  153. package/resources/customizations/.github/agents/staged-review.agent.md +1 -1
  154. package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
  155. package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +6 -6
  156. package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
  157. package/resources/customizations/.github/prompts/generate-commit-message-repo.prompt.md +1 -1
  158. package/resources/customizations/.github/prompts/orchestrate-csharp-work.prompt.md +5 -3
  159. package/resources/customizations/.github/prompts/orchestrate-work.prompt.md +5 -3
  160. package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
  161. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +14 -1
  162. package/resources/customizations/.github/skills/feature-promotion-lifecycle/SKILL.md +11 -7
  163. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +10 -1
  164. package/resources/customizations/.github/skills/pr-base-branch-merge-base/SKILL.md +2 -2
  165. package/resources/customizations/.github/skills/remediation-handoff-atomic-planner/SKILL.md +5 -0
  166. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +7 -0
  167. package/resources/scripts/dev_tools/_orchestrator_state_human_interaction.py +127 -0
  168. package/resources/scripts/dev_tools/_orchestrator_state_routing.py +216 -0
  169. package/resources/scripts/dev_tools/push_down_claude_customizations.py +276 -61
  170. package/resources/scripts/dev_tools/push_down_claude_filesystem.py +472 -0
  171. package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +401 -0
  172. package/resources/scripts/dev_tools/validate_orchestration_artifacts.py +103 -411
  173. package/resources/scripts/dev_tools/validate_orchestration_review_artifacts.py +107 -0
  174. package/resources/scripts/dev_tools/validate_orchestrator_state.py +505 -0
  175. package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +472 -0
  176. package/resources/templates/push_down_claude_customizations.py +162 -117
  177. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_repo_root_is_source_of_truth.md +0 -11
  178. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_vsce_verify_package_location.md +0 -19
  179. package/resources/claude-customizations/.claude/agent-memory/orchestrator/project_extension_location.md +0 -11
  180. package/resources/claude-customizations/.claude/agent-memory/prd-feature/MEMORY.md +0 -1
  181. package/resources/claude-customizations/.claude/agent-memory/prd-feature/project_push_down_pattern.md +0 -13
  182. package/resources/claude-customizations/.claude/agent-memory/task-researcher/MEMORY.md +0 -3
  183. package/resources/claude-customizations/.claude/agent-memory/task-researcher/project_push_down_claude_dir.md +0 -11
@@ -0,0 +1,401 @@
1
+ """Pack selection, variant routing, and memory-mode helpers for `.claude` push-down.
2
+
3
+ Purpose:
4
+ Provide the pure logic that the Claude customization push-down entry point
5
+ uses to decide which `.claude`-relative files to publish, which C# variant
6
+ source to read for each canonical destination path, and how to treat
7
+ agent-memory files for the selected memory mode. The entry point
8
+ (`push_down_claude_customizations`) composes these helpers with a filesystem
9
+ wrapper and delegates the actual copy to the shared publisher engine.
10
+
11
+ Responsibilities:
12
+ - Load and validate pack-manifest JSON files via an injected filesystem
13
+ adapter (no direct disk I/O here).
14
+ - Compute the set of `.claude`-relative destination paths to publish for a
15
+ given pack selection, always including `core`.
16
+ - Resolve the source-relative path to read for a destination path given the
17
+ selected C# variant (`modern` reads from `.claude`, `legacy` reads from
18
+ the bundle-only `.claude-variants/csharp-legacy/` subtree).
19
+ - Assert C# mutual exclusion so the four canonical C# destination paths are
20
+ written at most once each and a single run never selects both variants.
21
+
22
+ Usage:
23
+ These functions are stateless and are called by
24
+ `scripts.dev_tools.push_down_claude_customizations`. They never touch the
25
+ real filesystem directly; all reads go through the `PushDownFileSystem`
26
+ adapter supplied by the caller.
27
+
28
+ Invariants / Constraints:
29
+ - All manifest paths are `.claude`-relative POSIX strings.
30
+ - `core` is always part of the published set when any explicit pack
31
+ selection is provided.
32
+ - The four canonical C# destination paths are fixed and shared by the
33
+ modern and legacy packs; only one variant may be selected per run.
34
+
35
+ Side Effects:
36
+ None beyond reads performed through the injected adapter.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ from dataclasses import dataclass
43
+ from typing import TYPE_CHECKING, Literal, cast
44
+
45
+ if TYPE_CHECKING:
46
+ from pathlib import Path
47
+
48
+ from scripts.dev_tools.push_down_copilot_customizations_filesystem import (
49
+ PushDownFileSystem,
50
+ )
51
+
52
+ # Pack name reserved for the always-included non-language customization set.
53
+ CORE_PACK_NAME = "core"
54
+
55
+ # The four canonical `.claude`-relative C# destination paths shared by the
56
+ # modern and legacy C# packs. Exactly one variant's content may land at these
57
+ # destinations in a single push-down run.
58
+ CSHARP_CANONICAL_PATHS: tuple[str, ...] = (
59
+ ".claude/rules/csharp.md",
60
+ ".claude/agents/csharp-typed-engineer.md",
61
+ ".claude/skills/csharp-qa-gate/SKILL.md",
62
+ ".claude/skills/invoke-csharp-engineer/SKILL.md",
63
+ )
64
+
65
+ # Pack names whose paths are the canonical C# toolchain. Selecting either pack
66
+ # contributes the same destination paths; the variant flag chooses the source.
67
+ CSHARP_PACK_NAMES: frozenset[str] = frozenset({"csharp-modern", "csharp-legacy"})
68
+
69
+ # Bundle-only source prefix for the legacy C# variant subtree. A legacy
70
+ # destination path's `.claude/` head is replaced with this prefix to locate the
71
+ # source file that holds the legacy profile content.
72
+ LEGACY_VARIANT_SOURCE_PREFIX = ".claude-variants/csharp-legacy"
73
+
74
+ CSharpVariant = Literal["modern", "legacy"]
75
+ MemoryMode = Literal["overwrite", "merge", "skip"]
76
+
77
+
78
+ class ManifestError(ValueError):
79
+ """Raised when a pack manifest is missing or structurally invalid.
80
+
81
+ Purpose:
82
+ Signal a fail-fast condition when the push-down cannot trust the pack
83
+ manifest inputs (a missing manifest file or a manifest that lacks the
84
+ required keys or uses the wrong types). Raising a specific subclass of
85
+ ``ValueError`` lets callers and tests distinguish manifest problems from
86
+ unrelated value errors.
87
+
88
+ Usage:
89
+ Raised by ``load_pack_manifests`` only; not caught internally.
90
+
91
+ Side Effects:
92
+ None.
93
+ """
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class PackManifest:
98
+ """Describe one loaded pack manifest.
99
+
100
+ Purpose:
101
+ Hold the validated contents of a single pack-manifest JSON file in a
102
+ typed, immutable structure so downstream selection logic does not work
103
+ with raw dictionaries.
104
+
105
+ Usage:
106
+ Produced by ``load_pack_manifests`` and consumed by
107
+ ``compute_published_paths``.
108
+
109
+ Invariants / Constraints:
110
+ - ``name`` and ``label`` are non-empty strings.
111
+ - ``paths`` is a tuple of `.claude`-relative POSIX path strings.
112
+ - ``source_prefix`` is ``None`` for packs that read from `.claude`, or a
113
+ bundle-relative prefix string for variant packs that read elsewhere.
114
+
115
+ Attributes:
116
+ name (str): Stable pack identifier (for example ``"python"``).
117
+ label (str): Human-readable label used by the command UI.
118
+ paths (tuple[str, ...]): The `.claude`-relative destination paths this
119
+ pack contributes.
120
+ source_prefix (str | None): Optional bundle-relative source prefix used
121
+ by variant packs; ``None`` for normal packs.
122
+ """
123
+
124
+ name: str
125
+ label: str
126
+ paths: tuple[str, ...]
127
+ source_prefix: str | None
128
+
129
+
130
+ def load_pack_manifests(
131
+ manifest_dir: Path,
132
+ selected_pack_names: frozenset[str],
133
+ fs: PushDownFileSystem,
134
+ ) -> dict[str, PackManifest]:
135
+ """Load and validate the selected pack manifests from the bundle.
136
+
137
+ Purpose:
138
+ Read each selected manifest JSON through the injected filesystem
139
+ adapter, validate its required structure, and return a mapping from pack
140
+ name to its typed :class:`PackManifest`.
141
+
142
+ Args:
143
+ manifest_dir (Path): Directory holding ``<pack>.json`` manifest files,
144
+ typically ``resources/claude-customizations/pack-manifests``.
145
+ selected_pack_names (frozenset[str]): Pack names to load. ``core`` is
146
+ loaded automatically even when it is not present in this set so the
147
+ always-included contract holds.
148
+ fs (PushDownFileSystem): Filesystem adapter used for existence checks
149
+ and text reads. No direct disk I/O is performed here.
150
+
151
+ Returns:
152
+ dict[str, PackManifest]: Mapping of pack name to its validated manifest,
153
+ including the ``core`` manifest.
154
+
155
+ Raises:
156
+ ManifestError: When a required manifest file is missing, is not valid
157
+ JSON, is not a JSON object, or lacks valid ``name``/``label``/
158
+ ``paths`` values.
159
+
160
+ Side Effects:
161
+ Reads manifest files through the injected adapter.
162
+ """
163
+
164
+ # Always load core in addition to the explicitly selected packs so the
165
+ # always-included contract is satisfied at the loading stage.
166
+ names_to_load = set(selected_pack_names) | {CORE_PACK_NAME}
167
+
168
+ manifests: dict[str, PackManifest] = {}
169
+ # Load each selected manifest deterministically (sorted) so any error order
170
+ # is stable and reproducible across runs.
171
+ for name in sorted(names_to_load):
172
+ manifest_path = manifest_dir / f"{name}.json"
173
+ if not fs.is_file(manifest_path):
174
+ raise ManifestError(
175
+ f"Pack manifest is missing for pack '{name}': {manifest_path}"
176
+ )
177
+ raw_text = fs.read_text(manifest_path)
178
+ manifests[name] = _parse_manifest(name, manifest_path, raw_text)
179
+ return manifests
180
+
181
+
182
+ def _parse_manifest(name: str, manifest_path: Path, raw_text: str) -> PackManifest:
183
+ """Parse and validate one manifest's JSON text into a :class:`PackManifest`.
184
+
185
+ Purpose:
186
+ Centralize the structural validation of a single manifest so the loader
187
+ stays focused on filesystem access and iteration.
188
+
189
+ Args:
190
+ name (str): The pack name expected for this manifest (the file stem).
191
+ manifest_path (Path): The manifest path, used only for error messages.
192
+ raw_text (str): The raw JSON text read from the manifest file.
193
+
194
+ Returns:
195
+ PackManifest: The validated, immutable manifest structure.
196
+
197
+ Raises:
198
+ ManifestError: When the text is not valid JSON, is not an object, or has
199
+ missing or wrongly typed ``name``/``label``/``paths``/
200
+ ``source_prefix`` values.
201
+
202
+ Side Effects:
203
+ None.
204
+ """
205
+
206
+ try:
207
+ loaded: object = json.loads(raw_text)
208
+ except json.JSONDecodeError as error:
209
+ raise ManifestError(
210
+ f"Pack manifest is not valid JSON for pack '{name}': {manifest_path}"
211
+ ) from error
212
+
213
+ if not isinstance(loaded, dict):
214
+ raise ManifestError(
215
+ f"Pack manifest must be a JSON object for pack '{name}': {manifest_path}"
216
+ )
217
+
218
+ # Treat the decoded mapping as untyped JSON values; each leaf is narrowed
219
+ # explicitly below so the constructed PackManifest is fully typed.
220
+ parsed = cast("dict[str, object]", loaded)
221
+ manifest_name = parsed.get("name")
222
+ manifest_label = parsed.get("label")
223
+ manifest_paths = parsed.get("paths")
224
+ source_prefix = parsed.get("source_prefix")
225
+
226
+ # Validate the required string keys; both must be non-empty so the command
227
+ # UI and selection logic always have a usable identifier and label.
228
+ if not isinstance(manifest_name, str) or not manifest_name:
229
+ raise ManifestError(
230
+ f"Pack manifest 'name' must be a non-empty string: {manifest_path}"
231
+ )
232
+ if not isinstance(manifest_label, str) or not manifest_label:
233
+ raise ManifestError(
234
+ f"Pack manifest 'label' must be a non-empty string: {manifest_path}"
235
+ )
236
+
237
+ # Validate the paths array is a list of strings, building a typed list so the
238
+ # PackManifest 'paths' tuple is str-typed for downstream consumers.
239
+ if not isinstance(manifest_paths, list):
240
+ raise ManifestError(
241
+ f"Pack manifest 'paths' must be a list of strings: {manifest_path}"
242
+ )
243
+ raw_paths = cast("list[object]", manifest_paths)
244
+ typed_paths: list[str] = []
245
+ # Narrow each entry to str so a non-string element is rejected explicitly.
246
+ for entry in raw_paths:
247
+ if not isinstance(entry, str):
248
+ raise ManifestError(
249
+ f"Pack manifest 'paths' must be a list of strings: {manifest_path}"
250
+ )
251
+ typed_paths.append(entry)
252
+
253
+ # source_prefix is optional; when present it must be a string so variant
254
+ # source resolution can join it with the destination tail.
255
+ if source_prefix is not None and not isinstance(source_prefix, str):
256
+ raise ManifestError(
257
+ f"Pack manifest 'source_prefix' must be a string when present: "
258
+ f"{manifest_path}"
259
+ )
260
+
261
+ return PackManifest(
262
+ name=manifest_name,
263
+ label=manifest_label,
264
+ paths=tuple(typed_paths),
265
+ source_prefix=source_prefix,
266
+ )
267
+
268
+
269
+ def compute_published_paths(
270
+ selected_pack_names: frozenset[str] | None,
271
+ manifests: dict[str, PackManifest],
272
+ ) -> frozenset[str] | None:
273
+ """Compute the `.claude`-relative destination paths to publish.
274
+
275
+ Purpose:
276
+ Translate a pack selection into the concrete set of destination paths
277
+ the push-down should write, always unioning ``core`` into the result so
278
+ the non-language customization set is never dropped.
279
+
280
+ Args:
281
+ selected_pack_names (frozenset[str] | None): The explicitly selected
282
+ pack names, or ``None``/empty to signal the backward-compatible
283
+ "publish everything" default.
284
+ manifests (dict[str, PackManifest]): Loaded manifests for at least the
285
+ selected packs plus ``core``.
286
+
287
+ Returns:
288
+ frozenset[str] | None: The union of the selected packs' paths plus
289
+ ``core`` paths, or ``None`` when no explicit selection was made (the
290
+ caller then publishes the full tree without manifest filtering).
291
+
292
+ Raises:
293
+ ManifestError: When a selected pack has no loaded manifest.
294
+
295
+ Side Effects:
296
+ None.
297
+ """
298
+
299
+ # An empty or None selection means the caller did not pass --packs, so the
300
+ # backward-compatible default is to publish the full tree (signalled by
301
+ # returning None rather than an empty set).
302
+ if not selected_pack_names:
303
+ return None
304
+
305
+ # Always include core so the non-language baseline is published regardless
306
+ # of whether the caller named it explicitly.
307
+ effective_names = set(selected_pack_names) | {CORE_PACK_NAME}
308
+
309
+ published: set[str] = set()
310
+ # Union every selected pack's destination paths into the published set.
311
+ for name in effective_names:
312
+ manifest = manifests.get(name)
313
+ if manifest is None:
314
+ raise ManifestError(f"No loaded manifest for selected pack '{name}'.")
315
+ published.update(manifest.paths)
316
+ return frozenset(published)
317
+
318
+
319
+ def resolve_variant_source_path(
320
+ destination_relative_path: str,
321
+ csharp_variant: CSharpVariant,
322
+ ) -> str:
323
+ """Resolve the source-relative path to read for a destination path.
324
+
325
+ Purpose:
326
+ Route C# canonical destination paths to the correct source: the modern
327
+ variant reads from `.claude`, while the legacy variant reads from the
328
+ bundle-only `.claude-variants/csharp-legacy/` subtree. Non-C# paths and
329
+ the modern variant always resolve to the destination path unchanged.
330
+
331
+ Args:
332
+ destination_relative_path (str): A `.claude`-relative destination path
333
+ (for example ``.claude/rules/csharp.md``).
334
+ csharp_variant (CSharpVariant): ``"modern"`` to read from `.claude`, or
335
+ ``"legacy"`` to read the legacy variant source.
336
+
337
+ Returns:
338
+ str: The source-relative path to read. For ``modern`` or any non-C#
339
+ path this equals ``destination_relative_path``. For ``legacy`` and a C#
340
+ canonical path it is the path under
341
+ ``.claude-variants/csharp-legacy/`` (the leading ``.claude/`` is
342
+ replaced with the legacy prefix).
343
+
344
+ Raises:
345
+ None.
346
+
347
+ Side Effects:
348
+ None.
349
+ """
350
+
351
+ # Only the four canonical C# destinations are variant-routed; everything
352
+ # else (and the modern variant) reads from its own destination path.
353
+ if (
354
+ csharp_variant == "legacy"
355
+ and destination_relative_path in CSHARP_CANONICAL_PATHS
356
+ ):
357
+ # Replace the leading `.claude/` with the legacy variant prefix so the
358
+ # source read targets the bundle-only legacy profile.
359
+ tail = destination_relative_path[len(".claude/") :]
360
+ return f"{LEGACY_VARIANT_SOURCE_PREFIX}/{tail}"
361
+ return destination_relative_path
362
+
363
+
364
+ def assert_single_csharp_toolchain(
365
+ published_paths: frozenset[str],
366
+ selected_pack_names: frozenset[str],
367
+ ) -> None:
368
+ """Assert the C# selection yields exactly one toolchain at the destination.
369
+
370
+ Purpose:
371
+ Enforce C# mutual exclusion: the four canonical C# destination paths may
372
+ be written at most once each, and a single run must not select both the
373
+ modern and legacy C# packs (which would attempt to write two profiles to
374
+ the same destination paths).
375
+
376
+ Args:
377
+ published_paths (frozenset[str]): The computed destination paths for the
378
+ run. A set already deduplicates, so this is checked for the presence
379
+ of the canonical paths rather than for literal duplicates.
380
+ selected_pack_names (frozenset[str]): The selected pack names, used to
381
+ detect selection of both C# variants in one run.
382
+
383
+ Returns:
384
+ None.
385
+
386
+ Raises:
387
+ ManifestError: When both ``csharp-modern`` and ``csharp-legacy`` are
388
+ selected in the same run.
389
+
390
+ Side Effects:
391
+ None.
392
+ """
393
+
394
+ # Selecting both C# packs would route two different sources to the same four
395
+ # canonical destinations; reject it so only one toolchain is ever written.
396
+ selected_csharp = selected_pack_names & CSHARP_PACK_NAMES
397
+ if len(selected_csharp) > 1:
398
+ raise ManifestError(
399
+ "C# mutual exclusion violated: both modern and legacy C# packs were "
400
+ f"selected ({sorted(selected_csharp)}); select exactly one C# variant."
401
+ )