@danmoisan/drm-copilot-mcp 1.1.23 → 1.1.24

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 (199) hide show
  1. package/out/mcp-server.js +13347 -7926
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/discovery-capability-sentinel.md +34 -0
  4. package/resources/claude-customizations/.claude/agents/legacy-parity-analyst.md +101 -43
  5. package/resources/claude-customizations/.claude/agents/migration-coverage-reviewer.md +107 -43
  6. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +14 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-planner.md +21 -1
  8. package/resources/claude-customizations/.claude/agents/pr-author.md +17 -1
  9. package/resources/claude-customizations/.claude/agents/requirements-reconciler.md +117 -42
  10. package/resources/claude-customizations/.claude/agents/runtime-characterization-analyst.md +103 -43
  11. package/resources/claude-customizations/.claude/hooks/discovery-capability-probe.ps1 +102 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +69 -194
  13. package/resources/claude-customizations/.claude/hooks/enforce-discovery-planning-gate.ps1 +87 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +7 -1
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +6 -3
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +168 -48
  17. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +1 -1
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +59 -20
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +44 -3
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +60 -4
  21. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +4 -2
  22. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +59 -20
  23. package/resources/claude-customizations/.claude/hooks/record-discovery-agent-start.ps1 +132 -0
  24. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +56 -212
  25. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-rejection.ps1 +172 -0
  26. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +9 -1
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +9 -3
  28. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusAggregate.psm1 +154 -0
  29. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusCombination.psm1 +228 -0
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +1 -0
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +4 -2
  32. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +63 -16
  33. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +56 -1
  34. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +4 -1
  35. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +5 -1
  36. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
  37. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +25 -2
  38. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
  39. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
  40. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
  41. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
  42. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
  43. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
  44. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
  45. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
  46. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
  47. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
  48. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
  49. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
  50. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
  51. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
  52. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +188 -341
  53. package/resources/claude-customizations/.claude/lib/worktree-resolution/HookTargetWorktree.psm1 +391 -0
  54. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +1 -0
  55. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -18
  56. package/resources/claude-customizations/.claude/settings.json +32 -3
  57. package/resources/claude-customizations/.claude/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  58. package/resources/claude-customizations/.claude/skills/characterize-runtime-behavior/SKILL.md +134 -0
  59. package/resources/claude-customizations/.claude/skills/discovery-behavior-reconciliation/SKILL.md +28 -47
  60. package/resources/claude-customizations/.claude/skills/discovery-coverage-ledger/SKILL.md +32 -48
  61. package/resources/claude-customizations/.claude/skills/discovery-parity-matrix/SKILL.md +28 -45
  62. package/resources/claude-customizations/.claude/skills/discovery-repo-inventory/SKILL.md +28 -62
  63. package/resources/claude-customizations/.claude/skills/discovery-runtime-characterization/SKILL.md +28 -45
  64. package/resources/claude-customizations/.claude/skills/discovery-validate-artifacts/SKILL.md +28 -61
  65. package/resources/claude-customizations/.claude/skills/discovery-workflow/SKILL.md +55 -139
  66. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +24 -9
  67. package/resources/claude-customizations/.claude/skills/extract-feature-contract/SKILL.md +133 -0
  68. package/resources/claude-customizations/.claude/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  69. package/resources/claude-customizations/.claude/skills/initialize-discovery-workspace/SKILL.md +131 -0
  70. package/resources/claude-customizations/.claude/skills/inventory-legacy-system/SKILL.md +131 -0
  71. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +5 -4
  72. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +21 -1
  73. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +11 -1
  74. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -0
  75. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +69 -6
  76. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +15 -0
  77. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +6 -0
  78. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +21 -4
  79. package/resources/claude-customizations/.claude/skills/reconcile-parity-matrix/SKILL.md +131 -0
  80. package/resources/claude-customizations/.claude/skills/review-discovery-coverage/SKILL.md +129 -0
  81. package/resources/claude-customizations/.claude/skills/validate-discovery-artifacts/SKILL.md +127 -0
  82. package/resources/claude-customizations/config/blast-radius.json +6 -5
  83. package/resources/claude-customizations/config/orchestration-routing.json +61 -1
  84. package/resources/claude-customizations/pack-manifests/core.json +33 -1
  85. package/resources/codex-and-agents-customizations/.agents/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  86. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +7 -0
  87. package/resources/codex-and-agents-customizations/.agents/skills/characterize-runtime-behavior/SKILL.md +134 -0
  88. package/resources/codex-and-agents-customizations/.agents/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
  89. package/resources/codex-and-agents-customizations/.agents/skills/discovery-coverage-ledger/SKILL.md +50 -0
  90. package/resources/codex-and-agents-customizations/.agents/skills/discovery-parity-matrix/SKILL.md +46 -0
  91. package/resources/codex-and-agents-customizations/.agents/skills/discovery-repo-inventory/SKILL.md +46 -0
  92. package/resources/codex-and-agents-customizations/.agents/skills/discovery-runtime-characterization/SKILL.md +46 -0
  93. package/resources/codex-and-agents-customizations/.agents/skills/discovery-validate-artifacts/SKILL.md +46 -0
  94. package/resources/codex-and-agents-customizations/.agents/skills/discovery-workflow/SKILL.md +62 -0
  95. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +24 -9
  96. package/resources/codex-and-agents-customizations/.agents/skills/extract-feature-contract/SKILL.md +133 -0
  97. package/resources/codex-and-agents-customizations/.agents/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  98. package/resources/codex-and-agents-customizations/.agents/skills/initialize-discovery-workspace/SKILL.md +131 -0
  99. package/resources/codex-and-agents-customizations/.agents/skills/inventory-legacy-system/SKILL.md +131 -0
  100. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +8 -0
  101. package/resources/codex-and-agents-customizations/.agents/skills/reconcile-parity-matrix/SKILL.md +131 -0
  102. package/resources/codex-and-agents-customizations/.agents/skills/review-discovery-coverage/SKILL.md +129 -0
  103. package/resources/codex-and-agents-customizations/.agents/skills/validate-discovery-artifacts/SKILL.md +127 -0
  104. package/resources/codex-and-agents-customizations/.codex/agents/discovery-capability-sentinel.toml +51 -0
  105. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c1.toml +148 -0
  106. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c2.toml +148 -0
  107. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3-elevated.toml +148 -0
  108. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3.toml +148 -0
  109. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c4.toml +148 -0
  110. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst.toml +148 -0
  111. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c1.toml +154 -0
  112. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c2.toml +154 -0
  113. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3-elevated.toml +154 -0
  114. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3.toml +154 -0
  115. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c4.toml +154 -0
  116. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer.toml +154 -0
  117. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c1.toml +164 -0
  118. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c2.toml +164 -0
  119. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3-elevated.toml +164 -0
  120. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3.toml +164 -0
  121. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c4.toml +164 -0
  122. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler.toml +164 -0
  123. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c1.toml +150 -0
  124. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c2.toml +150 -0
  125. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3-elevated.toml +150 -0
  126. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3.toml +150 -0
  127. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c4.toml +150 -0
  128. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst.toml +150 -0
  129. package/resources/codex-and-agents-customizations/.codex/config.toml +33 -3
  130. package/resources/codex-and-agents-customizations/.codex/hooks/codex-agent-profile-attestation.ps1 +22 -0
  131. package/resources/codex-and-agents-customizations/.codex/hooks/discovery-capability-probe.ps1 +97 -0
  132. package/resources/codex-and-agents-customizations/.codex/hooks/discovery-start-attestation.ps1 +171 -0
  133. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +4 -1
  134. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-artifact-gate.ps1 +187 -0
  135. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-planning-gate.ps1 +149 -0
  136. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +239 -2
  137. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -15
  138. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +54 -15
  139. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +54 -15
  140. package/resources/codex-and-agents-customizations/.codex/hooks/hook-target-worktree.ps1 +369 -0
  141. package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +8 -0
  142. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +22 -1
  143. package/resources/codex-and-agents-customizations/.codex/hooks/validate-discovery-agent-output.ps1 +178 -0
  144. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +11 -1
  145. package/resources/codex-and-agents-customizations/pack-manifests/core.json +63 -1
  146. package/resources/config/orchestration-routing.json +61 -1
  147. package/resources/customizations/.github/agents/discovery-capability-sentinel.agent.md +34 -0
  148. package/resources/customizations/.github/agents/legacy-parity-analyst.agent.md +122 -0
  149. package/resources/customizations/.github/agents/migration-coverage-reviewer.agent.md +128 -0
  150. package/resources/customizations/.github/agents/requirements-reconciler.agent.md +138 -0
  151. package/resources/customizations/.github/agents/runtime-characterization-analyst.agent.md +124 -0
  152. package/resources/customizations/.github/hooks/discovery-agent-start.ps1 +139 -0
  153. package/resources/customizations/.github/hooks/discovery-agent-stop.ps1 +123 -0
  154. package/resources/customizations/.github/hooks/discovery-capability-probe.ps1 +98 -0
  155. package/resources/customizations/.github/hooks/discovery-planning.ps1 +91 -0
  156. package/resources/customizations/.github/hooks/discovery-pre-tool.ps1 +123 -0
  157. package/resources/customizations/.github/hooks/discovery.json +35 -0
  158. package/resources/customizations/.github/instructions/mermaid.instructions.md +85 -0
  159. package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +105 -0
  160. package/resources/customizations/.github/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  161. package/resources/customizations/.github/skills/characterize-runtime-behavior/SKILL.md +134 -0
  162. package/resources/customizations/.github/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
  163. package/resources/customizations/.github/skills/discovery-coverage-ledger/SKILL.md +50 -0
  164. package/resources/customizations/.github/skills/discovery-parity-matrix/SKILL.md +46 -0
  165. package/resources/customizations/.github/skills/discovery-repo-inventory/SKILL.md +46 -0
  166. package/resources/customizations/.github/skills/discovery-runtime-characterization/SKILL.md +46 -0
  167. package/resources/customizations/.github/skills/discovery-validate-artifacts/SKILL.md +46 -0
  168. package/resources/customizations/.github/skills/discovery-workflow/SKILL.md +62 -0
  169. package/resources/customizations/.github/skills/execution-environment/SKILL.md +24 -9
  170. package/resources/customizations/.github/skills/extract-feature-contract/SKILL.md +133 -0
  171. package/resources/customizations/.github/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  172. package/resources/customizations/.github/skills/initialize-discovery-workspace/SKILL.md +131 -0
  173. package/resources/customizations/.github/skills/inventory-legacy-system/SKILL.md +131 -0
  174. package/resources/customizations/.github/skills/reconcile-parity-matrix/SKILL.md +131 -0
  175. package/resources/customizations/.github/skills/review-discovery-coverage/SKILL.md +129 -0
  176. package/resources/customizations/.github/skills/validate-discovery-artifacts/SKILL.md +127 -0
  177. package/resources/lib/codex-routing/CodexDeployment.psm1 +5 -1
  178. package/resources/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
  179. package/resources/lib/codex-routing/CodexTopology.psm1 +25 -2
  180. package/resources/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
  181. package/resources/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
  182. package/resources/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
  183. package/resources/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
  184. package/resources/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
  185. package/resources/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
  186. package/resources/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
  187. package/resources/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
  188. package/resources/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
  189. package/resources/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
  190. package/resources/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
  191. package/resources/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
  192. package/resources/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
  193. package/resources/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
  194. package/resources/powershell/PoshQC/PoshQC.Parallel.psm1 +452 -0
  195. package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +106 -8
  196. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -0
  197. package/resources/powershell/PoshQC/PoshQC.psd1 +6 -1
  198. package/resources/powershell/PoshQC/PoshQC.psm1 +7 -1
  199. package/resources/powershell/PoshQC/README.md +20 -1
@@ -0,0 +1,172 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Parallel mutation rejection helpers for validate-orchestrator-output.ps1 (issue #1012).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by validate-orchestrator-output.ps1. A parallel skill that rejects a mutation before
7
+ any run checkpoint exists stops without writing state and ends its final output with the line
8
+ PARALLEL_MUTATION_REJECTED: <code>. These helpers parse that signal, scan the live worktrees for
9
+ parallel checkpoint text that would contradict the claim that no state was written, decide
10
+ whether the unresolved parallel stop may be allowed, and compose the hint appended to the block
11
+ message when it may not.
12
+
13
+ The signal grammar, the closed code set, and the hook behavior are defined once in the section
14
+ `## Mutation Rejection Signal (issue #1012)` of .claude/rules/parallel-orchestration.md.
15
+
16
+ Imports no module, invokes no Python, and starts no process. Relies on
17
+ Find-WorktreeRunIdentitySignal, Get-WorktreeItemLiveRoot, Get-WorktreeRunCheckpointText, and
18
+ Get-WorktreeRunCheckpointPath, which the hook imports before it dot-sources this file. Reads no
19
+ file except through Get-WorktreeRunCheckpointText.
20
+ #>
21
+
22
+ # The closed rejection code set, in rule order. It mirrors the table of
23
+ # `## Mutation Rejection Signal (issue #1012)` in .claude/rules/parallel-orchestration.md, which owns
24
+ # the set; a Pester parity row pins this array to that table.
25
+ $script:ParallelMutationRejectionCodes = @('kickoff_artifact_missing', 'run_not_started', 'items_in_flight', 'unknown_item')
26
+ $script:ParallelMutationRejectionPrefix = 'PARALLEL_MUTATION_REJECTED:'
27
+ $script:ParallelMutationRejectionPattern = '^PARALLEL_MUTATION_REJECTED: (?<code>[a-z_]+)$'
28
+ $script:ParallelMutationRejectionHeading = '## Mutation Rejection Signal (issue #1012)'
29
+ $script:ParallelMutationRejectionArtifactType = 'parallel-orchestrator-state'
30
+
31
+ function Get-ParallelMutationRejectionCode {
32
+ <#
33
+ .SYNOPSIS
34
+ Return the code of a valid final-line rejection signal, or $null.
35
+ .DESCRIPTION
36
+ Pure. Only the last non-blank line of the output is considered, after trailing whitespace
37
+ (including a carriage return) is trimmed. The line must match
38
+ ^PARALLEL_MUTATION_REJECTED: (?<code>[a-z_]+)$ case-sensitively, and the code must be an
39
+ ordinal member of the closed code set. Any other form, including formatting or trailing
40
+ text, returns $null.
41
+ .PARAMETER AgentOutput
42
+ The agent's final output text.
43
+ #>
44
+ [CmdletBinding()]
45
+ [OutputType([string])]
46
+ param(
47
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput
48
+ )
49
+
50
+ $trimmed = ([string]$AgentOutput).TrimEnd()
51
+ if ($trimmed.Length -eq 0) { return $null }
52
+ $lastLine = $trimmed.Substring($trimmed.LastIndexOf("`n") + 1)
53
+ $match = [regex]::Match($lastLine, $script:ParallelMutationRejectionPattern)
54
+ if (-not $match.Success) { return $null }
55
+ $code = $match.Groups['code'].Value
56
+ foreach ($member in $script:ParallelMutationRejectionCodes) {
57
+ if ([string]::Equals($member, $code, [System.StringComparison]::Ordinal)) { return $member }
58
+ }
59
+ return $null
60
+ }
61
+
62
+ function Test-ParallelRunStateAbsent {
63
+ <#
64
+ .SYNOPSIS
65
+ Return $true when no live worktree holds parallel checkpoint text that contradicts a no-state stop.
66
+ .DESCRIPTION
67
+ For every live root, the text at the canonical parallel checkpoint path must be null or blank,
68
+ or parse as a JSON object with route_id 'parallel' and a non-blank string parallel_slug that
69
+ differs (ordinally) from the parallel_slug: value the output names, when it names one. Any
70
+ other non-blank text (unparseable, a non-object root, off-route, or without a slug) and a
71
+ checkpoint recording the named slug return $false.
72
+ .PARAMETER AgentOutput
73
+ The agent's final output text, scanned for a parallel_slug: literal.
74
+ .PARAMETER SessionRoot
75
+ The calling process's path, used only to find the repository.
76
+ #>
77
+ [CmdletBinding()]
78
+ [OutputType([bool])]
79
+ param(
80
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput,
81
+ [Parameter(Mandatory = $true)] [string] $SessionRoot
82
+ )
83
+
84
+ $namedSlug = (Find-WorktreeRunIdentitySignal -Text ([string]$AgentOutput)).ParallelSlug
85
+ # The seam returns its array as one object, so it is assigned before it is iterated.
86
+ $liveRoots = Get-WorktreeItemLiveRoot -SessionRoot $SessionRoot
87
+ foreach ($root in $liveRoots) {
88
+ if ([string]::IsNullOrWhiteSpace($root)) { continue }
89
+ $text = Get-WorktreeRunCheckpointText -Path (Get-WorktreeRunCheckpointPath -Kind 'parallel' -WorktreeRoot $root)
90
+ if ([string]::IsNullOrWhiteSpace($text)) { continue }
91
+ try { $parsed = ConvertFrom-Json -InputObject $text -NoEnumerate -ErrorAction Stop } catch { return $false }
92
+ if ($parsed -isnot [System.Management.Automation.PSCustomObject]) { return $false }
93
+ $names = @($parsed.PSObject.Properties.Name)
94
+ if (-not ($names -ccontains 'route_id') -or ([string]$parsed.route_id -cne 'parallel')) { return $false }
95
+ if (-not ($names -ccontains 'parallel_slug')) { return $false }
96
+ $slug = $parsed.parallel_slug
97
+ if ($slug -isnot [string] -or [string]::IsNullOrWhiteSpace($slug)) { return $false }
98
+ if (-not [string]::IsNullOrWhiteSpace($namedSlug) -and ($slug -ceq $namedSlug)) { return $false }
99
+ }
100
+ return $true
101
+ }
102
+
103
+ function Get-OrchestratorOutputRejectionDecision {
104
+ <#
105
+ .SYNOPSIS
106
+ Return the allow decision for an unresolved parallel stop that carries a valid rejection signal, or $null.
107
+ .DESCRIPTION
108
+ Returns @{ Ok = $true; Message = $null } only when every gate holds: the artifact type is
109
+ parallel-orchestrator-state, the resolution status is NoTarget, the final line is a valid
110
+ rejection signal, and the state-absence scan passes. Otherwise returns $null, and the caller
111
+ applies its existing block.
112
+ .PARAMETER ArtifactType
113
+ The artifact type the hook validates.
114
+ .PARAMETER Resolution
115
+ The unresolved result of Resolve-OrchestratorOutputCheckpointPath.
116
+ .PARAMETER AgentOutput
117
+ The agent's final output text.
118
+ .PARAMETER SessionRoot
119
+ The calling process's path, used only to find the repository.
120
+ #>
121
+ [CmdletBinding()]
122
+ [OutputType([hashtable])]
123
+ param(
124
+ [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ArtifactType,
125
+ [Parameter(Mandatory = $true)] [AllowNull()] [object] $Resolution,
126
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput,
127
+ [Parameter(Mandatory = $true)] [string] $SessionRoot
128
+ )
129
+
130
+ if ($ArtifactType -cne $script:ParallelMutationRejectionArtifactType) { return $null }
131
+ if ($null -eq $Resolution -or [string]$Resolution.Status -cne 'NoTarget') { return $null }
132
+ if ($null -eq (Get-ParallelMutationRejectionCode -AgentOutput $AgentOutput)) { return $null }
133
+ if (-not (Test-ParallelRunStateAbsent -AgentOutput $AgentOutput -SessionRoot $SessionRoot)) { return $null }
134
+ return @{ Ok = $true; Message = $null }
135
+ }
136
+
137
+ function Get-ParallelMutationRejectionHint {
138
+ <#
139
+ .SYNOPSIS
140
+ Return the text appended to an unresolved block message.
141
+ .DESCRIPTION
142
+ Pure. Returns an empty string unless the artifact type is parallel-orchestrator-state and the
143
+ status is NoTarget. Otherwise returns one newline-prefixed hint line naming the signal form
144
+ and the rule heading and, when the last non-blank line begins with the signal prefix but
145
+ carries no valid code, a second line listing the allowed codes.
146
+ .PARAMETER ArtifactType
147
+ The artifact type the hook validates.
148
+ .PARAMETER Status
149
+ The resolution status of the block.
150
+ .PARAMETER AgentOutput
151
+ The agent's final output text.
152
+ #>
153
+ [CmdletBinding()]
154
+ [OutputType([string])]
155
+ param(
156
+ [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ArtifactType,
157
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Status,
158
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput
159
+ )
160
+
161
+ if ($ArtifactType -cne $script:ParallelMutationRejectionArtifactType -or $Status -cne 'NoTarget') { return '' }
162
+ $hint = ("`norchestrator hook: a parallel skill that stopped without writing state ends its final output with the line 'PARALLEL_MUTATION_REJECTED: <code>' as the last line, with no formatting; see '{0}' in .claude/rules/parallel-orchestration.md." -f
163
+ $script:ParallelMutationRejectionHeading)
164
+ $trimmed = ([string]$AgentOutput).TrimEnd()
165
+ $lastLine = $trimmed.Substring($trimmed.LastIndexOf("`n") + 1)
166
+ if ($lastLine.StartsWith($script:ParallelMutationRejectionPrefix, [System.StringComparison]::Ordinal) -and
167
+ $null -eq (Get-ParallelMutationRejectionCode -AgentOutput $AgentOutput)) {
168
+ $hint += ("`norchestrator hook: the final line carries the {0} prefix with an invalid code; allowed codes: {1}." -f
169
+ $script:ParallelMutationRejectionPrefix, ($script:ParallelMutationRejectionCodes -join ', '))
170
+ }
171
+ return $hint
172
+ }
@@ -57,6 +57,7 @@ Set-StrictMode -Version Latest
57
57
 
58
58
  try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-resolution.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-resolution.ps1' -ErrorRecord $_ }
59
59
  try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-routing.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-routing.ps1' -ErrorRecord $_ }
60
+ try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-rejection.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-rejection.ps1' -ErrorRecord $_ }
60
61
  try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeItemResolution.psm1' -ErrorRecord $_ }
61
62
  try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunResolution.psm1' -ErrorRecord $_ }
62
63
  try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateEpicWaveBarrier.psm1' -ErrorRecord $_ }
@@ -235,6 +236,10 @@ function Get-OrchestratorOutputBlockDecision {
235
236
  the condition (message content, its checkpoint, or its runbook) and $false when it cannot
236
237
  (a registration argument, an internal evaluation failure, or a state it may not clear).
237
238
  The caller applies the re-entry policy to that flag.
239
+ An unresolved parallel-orchestrator-state checkpoint (status NoTarget) is allowed when the
240
+ output ends with a valid PARALLEL_MUTATION_REJECTED: <code> line and no live worktree holds
241
+ contradicting parallel checkpoint text (Get-OrchestratorOutputRejectionDecision, issue #1012);
242
+ otherwise the unresolved block carries the hint from Get-ParallelMutationRejectionHint.
238
243
  #>
239
244
  [CmdletBinding()]
240
245
  [OutputType([hashtable])]
@@ -264,9 +269,12 @@ function Get-OrchestratorOutputBlockDecision {
264
269
  return @{ Ok = $false; Repairable = $false; Message = ('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: {0}: NoTarget (RESOLVER_FAILED): {1}' -f $ArtifactType, $_.Exception.Message) }
265
270
  }
266
271
  if (-not $resolution.Resolved) {
272
+ # Issue #1012: a parallel stop that wrote no state and ends with a valid rejection signal is allowed.
273
+ $rejectionDecision = Get-OrchestratorOutputRejectionDecision -ArtifactType $ArtifactType -Resolution $resolution -AgentOutput ([string]$agentOutput) -SessionRoot $SessionRoot
274
+ if ($null -ne $rejectionDecision) { return $rejectionDecision }
267
275
  $resolutionRepairable = ($resolution.Status -in @('NoTarget', 'Ambiguous')) -and ($ArtifactType -in @('epic-orchestrator-state', 'parallel-orchestrator-state'))
268
276
  return @{ Ok = $false; Repairable = $resolutionRepairable; Message = ('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: {0}: {1} ({2}): {3}' -f
269
- $ArtifactType, $resolution.Status, $resolution.ReasonCode, $resolution.Detail) }
277
+ $ArtifactType, $resolution.Status, $resolution.ReasonCode, $resolution.Detail) + (Get-ParallelMutationRejectionHint -ArtifactType $ArtifactType -Status ([string]$resolution.Status) -AgentOutput ([string]$agentOutput)) }
270
278
  }
271
279
 
272
280
  $file = Get-CheckpointFileContent -Path $resolution.CheckpointPath
@@ -73,6 +73,7 @@ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusValidation.p
73
73
  Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConflict.psm1') -Force -ErrorAction Stop
74
74
  Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusScheduling.psm1') -Force -ErrorAction Stop
75
75
  Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusWriteIntent.psm1') -Force -ErrorAction Stop
76
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusCombination.psm1') -Force -ErrorAction Stop
76
77
 
77
78
  # Feature-folder handling. Every radius contains its own feature folder, and a
78
79
  # caller may pass either a bare folder name or an already-qualified path.
@@ -246,7 +247,8 @@ function Get-NormalizedDeclaredRadius {
246
247
 
247
248
  .OUTPUTS
248
249
  System.Collections.Hashtable. A new radius carrying the surviving paths,
249
- contracts re-filtered by the ASCII-letter rule, and modules and shared
250
+ contracts re-filtered by the ASCII-letter rule and the generic sheet-tab
251
+ rule of Test-GenericContractToken (issue #1006), and modules and shared
250
252
  surfaces re-resolved from the surviving paths. Throws when the radius
251
253
  source is observed: an observed radius records a diff listing rather than
252
254
  a plan-text harvest, so the plan-text acceptance rules do not apply to it
@@ -297,9 +299,12 @@ function Get-NormalizedDeclaredRadius {
297
299
  -MandateRead ([string[]]@(Get-ConfigMandateRead -Config $Config)))
298
300
  $concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
299
301
 
302
+ # Keep a recorded contract only when it carries an ASCII letter and is not a
303
+ # default spreadsheet sheet-tab name (issue #1006).
300
304
  $contract = [System.Collections.Generic.List[string]]::new()
301
305
  foreach ($identifier in @($normalized['contracts'])) {
302
- if ($script:ContractLetterPattern.IsMatch([string]$identifier)) {
306
+ if ($script:ContractLetterPattern.IsMatch([string]$identifier) -and
307
+ -not (Test-GenericContractToken -Token ([string]$identifier))) {
303
308
  $contract.Add([string]$identifier)
304
309
  }
305
310
  }
@@ -472,4 +477,5 @@ Export-ModuleMember -Function `
472
477
  Get-WriteIntentPlanPath, `
473
478
  Get-WriteIntentSpecContract, `
474
479
  Select-WriteIntentPathEntry, `
475
- Get-PlanPathForConfig
480
+ Get-PlanPathForConfig, `
481
+ Get-BlastRadiusCombination
@@ -0,0 +1,154 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Aggregate integration-cost cap over tolerated overlaps (issue #1013, D-2).
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell mirror of
7
+ scripts/dev_tools/_blast_radius_aggregate.py. The pairwise edge rule judges
8
+ each pair alone, so one item can accumulate many tolerated overlaps whose
9
+ summed cost exceeds what running it serially would cost. Get-AggregateCappedPair
10
+ caps each item's summed tolerated cost and returns the pairs to promote to
11
+ scheduling edges; Get-BlastRadiusConflictEdge applies the result.
12
+
13
+ Cap rule: total(i) is the summed cost of the remaining tolerated overlaps
14
+ that include item i. Item i is over its cap when
15
+ total(i) * 100 > band_duration(i) * tolerance_percent, evaluated in
16
+ integers; a missing band reads as default_band. While an item is over its
17
+ cap, the smallest-key such item promotes its overlap with the minimum key
18
+ (benefit ascending, cost descending, a ascending, b ascending).
19
+
20
+ Parity notes for maintainers:
21
+ - The Python module is the reference implementation; the committed
22
+ aggregate fixtures bind the two runtimes together.
23
+ - Every function is pure: no filesystem, subprocess, network, or clock
24
+ access, and no input is mutated. Integers only.
25
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
26
+ #>
27
+
28
+ Set-StrictMode -Version Latest
29
+ $ErrorActionPreference = 'Stop'
30
+
31
+ # tolerance_percent is a percentage of the band duration.
32
+ $script:Percent = 100
33
+
34
+ # Resolve an item's band duration, reading a missing band as default_band.
35
+ function Get-AggregateBandDuration {
36
+ [CmdletBinding()]
37
+ [OutputType([long])]
38
+ param(
39
+ [AllowNull()]
40
+ [object] $Band,
41
+ [Parameter(Mandatory = $true)]
42
+ [hashtable] $Tolerance
43
+ )
44
+
45
+ $resolved = if ($null -eq $Band) { $Tolerance['default_band'] } else { $Band }
46
+ $duration = $Tolerance['band_durations']
47
+ if ($resolved -isnot [string] -or -not $duration.ContainsKey($resolved)) {
48
+ throw "band '$resolved' must be one of $(@($duration.Keys | Sort-Object) -join ', ')."
49
+ }
50
+ return [long]$duration[$resolved]
51
+ }
52
+
53
+ # Return the smallest item key whose tolerated total exceeds its cap, or $null.
54
+ function Get-FirstOverCapItem {
55
+ [CmdletBinding()]
56
+ [OutputType([long])]
57
+ param(
58
+ [Parameter(Mandatory = $true)]
59
+ [AllowEmptyCollection()]
60
+ [object[]] $Remaining,
61
+ [Parameter(Mandatory = $true)]
62
+ [hashtable] $BandByKey,
63
+ [Parameter(Mandatory = $true)]
64
+ [hashtable] $Tolerance
65
+ )
66
+
67
+ # Each overlap's cost counts toward both of its items.
68
+ $total = @{}
69
+ foreach ($overlap in $Remaining) {
70
+ foreach ($key in @($overlap['a'], $overlap['b'])) {
71
+ $current = if ($total.ContainsKey($key)) { $total[$key] } else { [long]0 }
72
+ $total[$key] = $current + $overlap['cost']
73
+ }
74
+ }
75
+ $percent = [long]$Tolerance['tolerance_percent']
76
+ foreach ($key in @($total.Keys | Sort-Object)) {
77
+ $band = if ($BandByKey.ContainsKey($key)) { $BandByKey[$key] } else { $null }
78
+ $cap = (Get-AggregateBandDuration -Band $band -Tolerance $Tolerance) * $percent
79
+ if ($total[$key] * $script:Percent -gt $cap) {
80
+ return $key
81
+ }
82
+ }
83
+ return $null
84
+ }
85
+
86
+ function Get-AggregateCappedPair {
87
+ <#
88
+ .SYNOPSIS
89
+ Return the tolerated pairs to promote so every item is within its cap.
90
+
91
+ .DESCRIPTION
92
+ Port of select_capped_pairs. Repeatedly takes the smallest-key item that
93
+ is over its cap, promotes its cheapest-to-serialize overlap, and
94
+ recomputes the totals, until no item is over its cap.
95
+
96
+ .PARAMETER ToleratedOverlap
97
+ The pairwise tolerated overlaps, in any order: records carrying a, b,
98
+ cost, and benefit.
99
+
100
+ .PARAMETER Band
101
+ Item key to complexity band; a key that is absent or maps to null reads
102
+ as default_band.
103
+
104
+ .PARAMETER Tolerance
105
+ A value returned by Get-ConfigConflictTolerance.
106
+
107
+ .OUTPUTS
108
+ System.Collections.Specialized.OrderedDictionary. The promoted pairs in
109
+ promotion order, each an ordered record with keys a and b, written to
110
+ the pipeline; nothing when no item is over its cap. Wrap the call in
111
+ @() to read the result as an array. Throws when a band is not a
112
+ configured band.
113
+ #>
114
+ [CmdletBinding()]
115
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
116
+ param(
117
+ [Parameter(Mandatory = $true)]
118
+ [AllowEmptyCollection()]
119
+ [object[]] $ToleratedOverlap,
120
+ [Parameter(Mandatory = $true)]
121
+ [hashtable] $Band,
122
+ [Parameter(Mandatory = $true)]
123
+ [hashtable] $Tolerance
124
+ )
125
+
126
+ # Copy every overlap and band key into Int64 records, so integer keys of any
127
+ # width compare equal and no input is mutated.
128
+ $bandByKey = @{}
129
+ foreach ($key in $Band.Keys) { $bandByKey[[long]$key] = $Band[$key] }
130
+ $copied = foreach ($overlap in $ToleratedOverlap) {
131
+ [ordered]@{ a = [long]$overlap['a']; b = [long]$overlap['b']; cost = [long]$overlap['cost']; benefit = [long]$overlap['benefit'] }
132
+ }
133
+ $remaining = [System.Collections.Generic.List[object]]::new()
134
+ foreach ($overlap in @($copied | Sort-Object -Property { $_['a'] }, { $_['b'] })) {
135
+ if ($null -ne $overlap) { $remaining.Add($overlap) }
136
+ }
137
+
138
+ $promoted = [System.Collections.Generic.List[object]]::new()
139
+ while ($true) {
140
+ $item = Get-FirstOverCapItem -Remaining @($remaining.ToArray()) -BandByKey $bandByKey -Tolerance $Tolerance
141
+ if ($null -eq $item) {
142
+ # Callers wrap the enumerated records with @() so an empty result is
143
+ # an empty array.
144
+ return $promoted.ToArray()
145
+ }
146
+ # Cheapest to serialize: least benefit, then most cost, then pair keys.
147
+ $cheapest = @($remaining | Where-Object { $_['a'] -eq $item -or $_['b'] -eq $item } |
148
+ Sort-Object -Property { $_['benefit'] }, { - $_['cost'] }, { $_['a'] }, { $_['b'] })[0]
149
+ [void]$remaining.Remove($cheapest)
150
+ $promoted.Add([ordered]@{ a = $cheapest['a']; b = $cheapest['b'] })
151
+ }
152
+ }
153
+
154
+ Export-ModuleMember -Function Get-AggregateCappedPair
@@ -0,0 +1,228 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Planning-time issue combination over the scheduling pair data (issue #1013).
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell mirror of
7
+ scripts/dev_tools/_blast_radius_combination.py (spec D-5). Finds the pairs
8
+ of work items whose integration cost is high enough that delivering them as
9
+ one item costs less than merging them, and groups them into combinations.
10
+
11
+ Rules:
12
+ 1. Disabled when conflict_tolerance is absent or tolerance_percent is 0.
13
+ 2. Get-BlastRadiusPairDecision supplies each conflicting pair's reasons,
14
+ cost (including the D-7 contract term), and benefit; edges and
15
+ tolerated pairs are both evaluated.
16
+ 3. A pair is a candidate when its full reasons contain
17
+ contract_dependency or it has a same-file overlap: an overlapping entry
18
+ pair, after the mergeable exclusion, of two equal concrete entries that
19
+ do not match append_only_paths.
20
+ 4. A candidate qualifies when
21
+ cost * 100 >= benefit * combination_threshold_percent.
22
+ 5. Qualifying pairs are grouped into connected components (union-find);
23
+ each becomes one combination with ascending members, primary equal to
24
+ the smallest member, pairs sorted by (a, b), and estimated_savings equal
25
+ to the sum of its pair costs.
26
+
27
+ Parity notes for maintainers:
28
+ - The Python module is the reference implementation; the committed
29
+ combination fixtures bind the two runtimes together.
30
+ - Every function is pure: no filesystem, subprocess, network, or clock
31
+ access, and no input is mutated.
32
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
33
+ #>
34
+
35
+ Set-StrictMode -Version Latest
36
+ $ErrorActionPreference = 'Stop'
37
+
38
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusGlob.psm1') -Force -ErrorAction Stop
39
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConflict.psm1') -Force -ErrorAction Stop
40
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusValidation.psm1') -Force -ErrorAction Stop
41
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusScheduling.psm1') -Force -ErrorAction Stop
42
+
43
+ # The threshold is a percentage of the pair benefit.
44
+ $script:Percent = 100
45
+ $script:ContractDependency = 'contract_dependency'
46
+
47
+ # Port of has_same_file_overlap: two equal, non-mergeable, non-append-only
48
+ # concrete entries among the overlapping pairs the relation enumerates.
49
+ function Test-SameFileOverlap {
50
+ [CmdletBinding()]
51
+ [OutputType([bool])]
52
+ param(
53
+ [Parameter(Mandatory = $true)]
54
+ [AllowNull()]
55
+ [object] $RadiusA,
56
+ [Parameter(Mandatory = $true)]
57
+ [AllowNull()]
58
+ [object] $RadiusB,
59
+ [Parameter(Mandatory = $true)]
60
+ [AllowNull()]
61
+ [object] $Config,
62
+ [Parameter(Mandatory = $true)]
63
+ [hashtable] $Tolerance
64
+ )
65
+
66
+ $left = ConvertTo-NormalizedBlastRadius -Radius $RadiusA
67
+ $right = ConvertTo-NormalizedBlastRadius -Radius $RadiusB
68
+ $mergeable = [string[]]@(Get-ConfigMergeablePath -Config $Config)
69
+ $pathsA = @(Get-NonMergeablePathEntry -Entry ([string[]]@($left['paths'])) -MergeablePath $mergeable)
70
+ $pathsB = @(Get-NonMergeablePathEntry -Entry ([string[]]@($right['paths'])) -MergeablePath $mergeable)
71
+ $appendOnly = [string[]]@($Tolerance['append_only_paths'])
72
+ foreach ($pair in @(Get-OverlappingPathPair -PathA $pathsA -PathB $pathsB)) {
73
+ $entry = [string]$pair['EntryA']
74
+ $isEqual = [string]::Equals($entry, [string]$pair['EntryB'], [System.StringComparison]::Ordinal)
75
+ if ($isEqual -and -not (Test-GlobEntry -Entry $entry) -and -not (Test-MergeablePath -Entry $entry -MergeablePath $appendOnly)) {
76
+ return $true
77
+ }
78
+ }
79
+ return $false
80
+ }
81
+
82
+ # Validate item keys (distinct integers) and return records sorted by key.
83
+ function Get-CombinationOrderedItem {
84
+ [CmdletBinding()]
85
+ [OutputType([System.Object[]])]
86
+ param(
87
+ [Parameter(Mandatory = $true)]
88
+ [AllowEmptyCollection()]
89
+ [object[]] $Item
90
+ )
91
+
92
+ $seen = [System.Collections.Generic.HashSet[long]]::new()
93
+ $record = foreach ($entry in $Item) {
94
+ $key = [long]$entry['key']
95
+ if (-not $seen.Add($key)) {
96
+ throw "combination item keys must be distinct: $key."
97
+ }
98
+ $band = if ($entry.Contains('band')) { $entry['band'] } else { $null }
99
+ @{ key = $key; radius = $entry['radius']; band = $band }
100
+ }
101
+ return @(@($record) | Where-Object { $null -ne $_ } | Sort-Object -Property { $_['key'] })
102
+ }
103
+
104
+ # Return the union-find root of Key, registering Key as its own root when new.
105
+ # Parent is the component's working map, owned by Get-CombinationComponent.
106
+ function Get-UnionFindRoot {
107
+ [CmdletBinding()]
108
+ [OutputType([long])]
109
+ param(
110
+ [Parameter(Mandatory = $true)]
111
+ [hashtable] $Parent,
112
+ [Parameter(Mandatory = $true)]
113
+ [long] $Key
114
+ )
115
+
116
+ if (-not $Parent.ContainsKey($Key)) { $Parent[$Key] = $Key }
117
+ $root = $Key
118
+ while ($Parent[$root] -ne $root) { $root = $Parent[$root] }
119
+ return $root
120
+ }
121
+
122
+ # Group qualifying pairs into connected components with union-find.
123
+ function Get-CombinationComponent {
124
+ [CmdletBinding()]
125
+ [OutputType([System.Object[]])]
126
+ param(
127
+ [Parameter(Mandatory = $true)]
128
+ [AllowEmptyCollection()]
129
+ [object[]] $Pair
130
+ )
131
+
132
+ $parent = @{}
133
+ foreach ($entry in $Pair) {
134
+ $rootA = Get-UnionFindRoot -Parent $parent -Key $entry['a']
135
+ $rootB = Get-UnionFindRoot -Parent $parent -Key $entry['b']
136
+ if ($rootA -ne $rootB) {
137
+ $parent[[System.Math]::Max($rootA, $rootB)] = [System.Math]::Min($rootA, $rootB)
138
+ }
139
+ }
140
+
141
+ $grouped = @{}
142
+ foreach ($entry in $Pair) {
143
+ $root = Get-UnionFindRoot -Parent $parent -Key $entry['a']
144
+ if (-not $grouped.ContainsKey($root)) { $grouped[$root] = [System.Collections.Generic.List[object]]::new() }
145
+ $grouped[$root].Add($entry)
146
+ }
147
+ $combination = foreach ($root in @($grouped.Keys | Sort-Object)) {
148
+ $component = @($grouped[$root].ToArray() | Sort-Object -Property { $_['a'] }, { $_['b'] })
149
+ [long[]]$member = @($component | ForEach-Object { $_['a']; $_['b'] } | Sort-Object -Unique)
150
+ [long]$savings = 0
151
+ foreach ($entry in $component) { $savings += $entry['cost'] }
152
+ [ordered]@{ members = $member; primary = $member[0]; pairs = $component; estimated_savings = $savings }
153
+ }
154
+ return @($combination)
155
+ }
156
+
157
+ function Get-BlastRadiusCombination {
158
+ <#
159
+ .SYNOPSIS
160
+ Find the qualifying pairs of a set of items and group them.
161
+
162
+ .DESCRIPTION
163
+ Port of find_combinations. Visits every unordered pair once in ascending
164
+ key order and applies D-5 rules 1 to 5.
165
+
166
+ .PARAMETER Item
167
+ The run's items: records carrying key (an integer issue number), radius,
168
+ and an optional band.
169
+
170
+ .PARAMETER Config
171
+ Parsed truth table.
172
+
173
+ .PARAMETER Relation
174
+ The detection relation passed to every pair decision; pass
175
+ ${function:Test-BlastRadiusConflict}.
176
+
177
+ .OUTPUTS
178
+ System.Collections.Hashtable. Keys pairs (ordered records a, b, cost,
179
+ benefit, sorted by (a, b)) and combinations (ordered records members,
180
+ primary, pairs, estimated_savings, sorted by primary). Both are empty
181
+ when combination is disabled.
182
+ #>
183
+ [CmdletBinding()]
184
+ [OutputType([hashtable])]
185
+ param(
186
+ [Parameter(Mandatory = $true)]
187
+ [AllowEmptyCollection()]
188
+ [object[]] $Item,
189
+ [Parameter(Mandatory = $true)]
190
+ [AllowNull()]
191
+ [object] $Config,
192
+ [scriptblock] $Relation
193
+ )
194
+
195
+ if ($null -eq $Relation) {
196
+ throw '-Relation is required: pass ${function:Test-BlastRadiusConflict} from the facade module BlastRadius.psm1.'
197
+ }
198
+ $tolerance = Get-ConfigConflictTolerance -Config $Config
199
+ $ordered = @(Get-CombinationOrderedItem -Item $Item)
200
+ # The absent key reads as the strict tolerance, whose percent is 0.
201
+ if ([long]$tolerance['tolerance_percent'] -eq 0) {
202
+ return @{ pairs = @(); combinations = @() }
203
+ }
204
+
205
+ $threshold = [long]$tolerance['combination_threshold_percent']
206
+ $qualifying = [System.Collections.Generic.List[object]]::new()
207
+ for ($first = 0; $first -lt $ordered.Count; $first++) {
208
+ for ($second = $first + 1; $second -lt $ordered.Count; $second++) {
209
+ $a = $ordered[$first]
210
+ $b = $ordered[$second]
211
+ $decision = Get-BlastRadiusPairDecision -RadiusA $a['radius'] -RadiusB $b['radius'] -Config $Config -BandA $a['band'] -BandB $b['band'] -Relation $Relation
212
+ if (-not $decision['conflict']) { continue }
213
+ # The full reasons are read, because contract_dependency is never first.
214
+ $isCandidate = @($decision['reasons']) -ccontains $script:ContractDependency
215
+ if (-not $isCandidate) {
216
+ $isCandidate = Test-SameFileOverlap -RadiusA $a['radius'] -RadiusB $b['radius'] -Config $Config -Tolerance $tolerance
217
+ }
218
+ if ($isCandidate -and ($decision['cost'] * $script:Percent -ge $decision['benefit'] * $threshold)) {
219
+ $qualifying.Add([ordered]@{ a = $a['key']; b = $b['key']; cost = [long]$decision['cost']; benefit = [long]$decision['benefit'] })
220
+ }
221
+ }
222
+ }
223
+
224
+ $pair = @($qualifying.ToArray())
225
+ return @{ pairs = $pair; combinations = @(Get-CombinationComponent -Pair $pair) }
226
+ }
227
+
228
+ Export-ModuleMember -Function Get-BlastRadiusCombination
@@ -459,6 +459,7 @@ Export-ModuleMember -Function `
459
459
  Test-PlaceholderMarker, `
460
460
  Test-MultipleFeatureFolderSpan, `
461
461
  Test-FileShapedComponent, `
462
+ Test-GenericContractToken, `
462
463
  Get-PathTokenKind, `
463
464
  Get-PathFromLine, `
464
465
  Get-PlanPaths
@@ -120,10 +120,12 @@ function Get-ContractIdentifier {
120
120
  # Inside a qualifying section an inline-code token without a separator is
121
121
  # a contract identifier; a token with one is a path reference and is
122
122
  # recorded at the paths level instead. A token carrying no ASCII letter
123
- # is notation, not an identifier, and is dropped (issue #489).
123
+ # is notation, not an identifier, and is dropped (issue #489), as is a
124
+ # default spreadsheet sheet-tab name such as Sheet1 (issue #1006).
124
125
  foreach ($token in @(Get-InlineCodeToken -Line $line)) {
125
126
  if ($token.IndexOf('/') -lt 0 -and
126
- $script:ContractLetterPattern.IsMatch($token)) {
127
+ $script:ContractLetterPattern.IsMatch($token) -and
128
+ -not (Test-GenericContractToken -Token $token)) {
127
129
  $identifier.Add($token)
128
130
  }
129
131
  }