@danmoisan/drm-copilot-mcp 1.0.18 → 1.0.20

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 (27) hide show
  1. package/out/mcp-server.js +22 -2
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +2 -2
  4. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +14 -6
  5. package/resources/claude-customizations/.claude/lib/model-routing/ModelRouting.psm1 +34 -14
  6. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +30 -18
  7. package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
  8. package/resources/claude-customizations/.claude/rules/general-unit-test.md +2 -2
  9. package/resources/claude-customizations/.claude/rules/typescript.md +5 -5
  10. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
  11. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +2 -2
  12. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  13. package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +9 -65
  14. package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +9 -65
  15. package/resources/codex-and-agents-customizations/.codex/hooks/codex-pretooluse-file-mapping.ps1 +474 -0
  16. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +20 -101
  17. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +16 -3
  18. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +14 -5
  19. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +23 -5
  20. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +30 -52
  21. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +28 -27
  22. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +19 -49
  23. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +19 -49
  24. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  25. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +21 -1
  26. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +19 -0
  27. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +0 -500
package/out/mcp-server.js CHANGED
@@ -25867,6 +25867,26 @@ var STEP_STATUS_KEYS = [
25867
25867
  "step9_status",
25868
25868
  "step10_status"
25869
25869
  ];
25870
+ var STEP_SPECIFIC_EXTRA_STATUS = /* @__PURE__ */ new Map([
25871
+ ["step6_status", /* @__PURE__ */ new Set(["blocked_remediation_loop_limit"])],
25872
+ [
25873
+ "step9_status",
25874
+ /* @__PURE__ */ new Set(["passed", "failed_remediation_required", "blocked_ci_loop_limit"])
25875
+ ]
25876
+ ]);
25877
+ var COMPLETION_BLOCKING_STEP_STATUS = /* @__PURE__ */ new Set([
25878
+ "pending",
25879
+ "blocked",
25880
+ "failed_remediation_required",
25881
+ "blocked_ci_loop_limit",
25882
+ "blocked_remediation_loop_limit"
25883
+ ]);
25884
+ function isValidStepStatus(key, value) {
25885
+ if (VALID_STEP_STATUS.has(value)) {
25886
+ return true;
25887
+ }
25888
+ return STEP_SPECIFIC_EXTRA_STATUS.get(key)?.has(value) === true;
25889
+ }
25870
25890
  function isObject13(value) {
25871
25891
  return typeof value === "object" && value !== null && !Array.isArray(value);
25872
25892
  }
@@ -25951,7 +25971,7 @@ function validateOrchestratorStateText(text, options = {}) {
25951
25971
  }
25952
25972
  for (const key of STEP_STATUS_KEYS) {
25953
25973
  const value = stateMap[key];
25954
- if (value !== void 0 && value !== null && !(typeof value === "string" && VALID_STEP_STATUS.has(value))) {
25974
+ if (value !== void 0 && value !== null && !(typeof value === "string" && isValidStepStatus(key, value))) {
25955
25975
  errors.push(`Checkpoint has invalid ${key}: ${String(value)}`);
25956
25976
  }
25957
25977
  }
@@ -25982,7 +26002,7 @@ function validateOrchestratorStateText(text, options = {}) {
25982
26002
  if (options.requireComplete === true) {
25983
26003
  for (const key of STEP_STATUS_KEYS) {
25984
26004
  const value = stateMap[key];
25985
- if (value === "pending" || value === "blocked") {
26005
+ if (COMPLETION_BLOCKING_STEP_STATUS.has(value)) {
25986
26006
  errors.push(
25987
26007
  `Checkpoint completion validation failed: ${key} is ${String(value)}.`
25988
26008
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -15,7 +15,7 @@ tools:
15
15
  - "Bash(npx prettier *)"
16
16
  - "Bash(npx eslint *)"
17
17
  - "Bash(npx tsc *)"
18
- - "Bash(npx vitest *)"
18
+ - "Bash(npx jest *)"
19
19
  - "Bash(pwsh *)"
20
20
  - "Bash(git *)"
21
21
  - "mcp__drm-copilot__run_poshqc_format"
@@ -76,7 +76,7 @@ For each task:
76
76
  Use the scoped tool patterns for quality gates:
77
77
 
78
78
  - **Python**: `poetry run black`, `poetry run ruff`, `poetry run pyright`, `poetry run pytest`
79
- - **TypeScript**: `npx prettier`, `npx eslint`, `npx tsc`, `npx vitest`
79
+ - **TypeScript**: `npx prettier`, `npx eslint`, `npx tsc`, `npx jest`
80
80
  - **PowerShell**: MCP server functions (`mcp__drm-copilot__run_poshqc_format`, `mcp__drm-copilot__run_poshqc_analyze`, `mcp__drm-copilot__run_poshqc_test`, `mcp__drm-copilot__run_poshqc_analyze_autofix`)
81
81
  - **Git**: `git diff`, `git status`, `git log`
82
82
 
@@ -165,9 +165,15 @@ function Invoke-RoutingContractValidation {
165
165
  string is unchanged for every existing caller of this hook.
166
166
 
167
167
  Returns a hashtable with keys:
168
- - HasErrors: $true when the validator reported a non-zero exit or
169
- produced any error text; $false when clean.
170
- - ErrorText: the validator's combined output text (empty on success).
168
+ - HasErrors: $true only when the validator reported a non-zero exit
169
+ code; $false when it exited 0. The exit code is the sole
170
+ discriminator, because the validator prints its success
171
+ line to stdout on a clean pass and the default Invoker
172
+ captures with 2>&1, so output text is present on success.
173
+ - ErrorText: the validator's combined captured output text, carried
174
+ through unchanged: the error lines on a failure, and the
175
+ success line (Python CLI) or empty (portable fallback)
176
+ on a clean pass.
171
177
  #>
172
178
  [CmdletBinding()]
173
179
  [OutputType([hashtable])]
@@ -219,9 +225,11 @@ function Invoke-RoutingContractValidation {
219
225
  $outputText = ([string]$result.Output).Trim()
220
226
  }
221
227
 
222
- # The validator signals a routing-contract failure either through a non-zero
223
- # exit code or through emitted error text; either condition blocks DONE.
224
- $hasErrors = ($exitCode -ne 0) -or (-not [string]::IsNullOrWhiteSpace($outputText))
228
+ # The exit code is the complete failure discriminator: the validator prints every
229
+ # error to stderr and returns non-zero, and prints its success line to stdout and
230
+ # returns 0. Because the default invoker captures with 2>&1, the success line lands
231
+ # in $outputText on a clean pass, so output text must not influence this decision.
232
+ $hasErrors = ($exitCode -ne 0)
225
233
  return @{ HasErrors = $hasErrors; ErrorText = $outputText }
226
234
  }
227
235
 
@@ -11,8 +11,9 @@
11
11
  - Resolve-DelegationModel port of scripts/dev_tools/resolve_delegation_model.py
12
12
 
13
13
  Both functions are pure and deterministic: they read no file at runtime and
14
- encode only the fixed band ordering, the base complexity-to-model table, the
15
- preferred overlay, and the disabled-mode clamp as module-scope constants.
14
+ encode only the fixed band ordering, the floor-signal name set, the base
15
+ complexity-to-model table, the preferred overlay, and the disabled-mode clamp
16
+ as module-scope constants.
16
17
  Those literals are pinned to config/orchestration-routing.json (model_policy /
17
18
  model_budget) by a static config-parity Pester test, and the Python modules
18
19
  remain the validator's authoritative reference. This module is one half of a
@@ -29,6 +30,20 @@ $script:BAND_ORDER = @('C1', 'C2', 'C3', 'C4')
29
30
  # The lowest band, returned when no floor signal is present (LOWEST_BAND).
30
31
  $script:LOWEST_BAND = 'C1'
31
32
 
33
+ # The catalog signal names flagged "floor": true in model_policy.complexity. Only
34
+ # a signal named here contributes a floor candidate; a "floor": false name and an
35
+ # unknown name each contribute nothing. Hard-coded (never read from disk) because
36
+ # this module is pushed down to consumer repositories that do not ship
37
+ # config/orchestration-routing.json; a static parity Pester test pins this set to
38
+ # the config's "floor": true entries. Mirrors FLOOR_SIGNAL_NAMES in
39
+ # scripts/dev_tools/compute_complexity_floor.py.
40
+ $script:FLOOR_SIGNAL_NAMES = @(
41
+ 'classifier_or_model_logic',
42
+ 'auth_or_token_handling',
43
+ 'concurrency_or_ordering',
44
+ 'cross_module_contract_change'
45
+ )
46
+
32
47
  # Every present floor signal contributes this uniform candidate band, per the
33
48
  # model_policy.complexity contract (each [floor] signal contributes C3).
34
49
  $script:FLOOR_CANDIDATE_BAND = 'C3'
@@ -78,21 +93,22 @@ function Get-ComplexityFloor {
78
93
  .DESCRIPTION
79
94
  Faithful PowerShell port of compute_complexity_floor
80
95
  (scripts/dev_tools/compute_complexity_floor.py). Returns the deterministic
81
- lower-bound complexity band implied by the set of present floor signals:
82
- each present floor signal contributes a candidate band of C3, the floor is
83
- the maximum triggered candidate band, and the floor never exceeds C3
84
- (C4 is never floor-forced). With no floor signal present the floor is the
85
- lowest band C1. The function is pure: it reads no file and does not mutate
86
- its input, and the result is independent of input ordering.
96
+ lower-bound complexity band implied by the recorded signal names: the
97
+ function intersects the input with FLOOR_SIGNAL_NAMES, each surviving
98
+ [floor] signal contributes a candidate band of C3, the floor is the maximum
99
+ triggered candidate band, and the floor never exceeds C3 (C4 is never
100
+ floor-forced). With no floor signal present the floor is the lowest band C1.
101
+ The function is pure: it reads no file and does not mutate its input, and
102
+ the result is independent of input ordering.
87
103
 
88
104
  .PARAMETER SignalsPresent
89
- The names of the present signals flagged [floor] in the
90
- model_policy.complexity catalog. Every element is treated as a triggered
91
- floor signal contributing the candidate band C3. An empty collection means
92
- no floor signal is present.
105
+ The full set of recorded signal names, as written to the checkpoint's
106
+ signals_present[] array. The caller does not pre-filter: names outside
107
+ FLOOR_SIGNAL_NAMES ("floor": false catalog names and unknown names alike)
108
+ contribute nothing. An empty collection means no floor signal is present.
93
109
 
94
110
  .OUTPUTS
95
- System.String. The floor band: C1 when no floor signal is present,
111
+ System.String. The floor band: C1 when no recorded name is a floor signal,
96
112
  otherwise the maximum triggered candidate band clamped to at most C3.
97
113
  C4 is never returned.
98
114
  #>
@@ -104,9 +120,13 @@ function Get-ComplexityFloor {
104
120
  [string[]] $SignalsPresent
105
121
  )
106
122
 
123
+ # Keep only the recorded names that are floor signals. A "floor": false catalog
124
+ # name and an unknown name both drop out here and contribute no candidate band.
125
+ $triggered = @($SignalsPresent | Where-Object { $script:FLOOR_SIGNAL_NAMES -contains $_ })
126
+
107
127
  # With no present floor signal there is no candidate band to raise the floor
108
128
  # above the lowest band, so the floor is C1 (mirrors the empty-input guard).
109
- if (-not $SignalsPresent -or $SignalsPresent.Count -eq 0) {
129
+ if ($triggered.Count -eq 0) {
110
130
  return $script:LOWEST_BAND
111
131
  }
112
132
 
@@ -14,7 +14,7 @@
14
14
  checkpoint-presence checks (required keys, step-status validity, blocked_reason
15
15
  validity) from `scripts/dev_tools/validate_orchestrator_state.py`. The base
16
16
  constants below are pinned to `REQUIRED_STATE_KEYS`, `STEP_STATUS_KEYS`,
17
- `VALID_STEP_STATUS`, and `VALID_BLOCKED_REASONS` in that validator.
17
+ `VALID_STEP_STATUS`, `VALID_BLOCKED_REASONS`, and `STEP_SPECIFIC_EXTRA_STATUS`.
18
18
 
19
19
  Every public function FAILS CLOSED: a missing checkpoint file, invalid JSON, a
20
20
  missing required key, an invalid step status, or an unmet readiness condition all
@@ -83,6 +83,15 @@ $script:VALID_STEP_STATUS = @(
83
83
  'completed'
84
84
  )
85
85
 
86
+ # Per-key additive step-status vocabulary layered on VALID_STEP_STATUS: each value
87
+ # below is valid only on its owning key and is still rejected on every other step
88
+ # key. Pinned to STEP_SPECIFIC_EXTRA_STATUS in
89
+ # scripts/dev_tools/_orchestrator_state_step_status.py.
90
+ $script:STEP_SPECIFIC_EXTRA_STATUS = @{
91
+ step6_status = @('blocked_remediation_loop_limit')
92
+ step9_status = @('passed', 'failed_remediation_required', 'blocked_ci_loop_limit')
93
+ }
94
+
86
95
  # The allowed blocked_reason vocabulary. Pinned to VALID_BLOCKED_REASONS in the
87
96
  # primary validator.
88
97
  $script:VALID_BLOCKED_REASONS = @(
@@ -226,11 +235,11 @@ function Get-OrchestratorStateBasePresenceError {
226
235
  Return the base checkpoint-presence errors, mirroring the primary validator.
227
236
  .DESCRIPTION
228
237
  Private base check. Emits one error string per missing required key, one per
229
- step5_status..step10_status value outside VALID_STEP_STATUS, and one when
230
- blocked_reason is present with a value outside VALID_BLOCKED_REASONS. This
231
- mirrors the base block of scripts/dev_tools/validate_orchestrator_state.py
232
- (required keys, step-status validity, blocked_reason validity) that runs
233
- before any mode-specific gate.
238
+ step5_status..step10_status value outside VALID_STEP_STATUS and that key's
239
+ STEP_SPECIFIC_EXTRA_STATUS set, and one when blocked_reason is present with a
240
+ value outside VALID_BLOCKED_REASONS. This mirrors the base block of
241
+ scripts/dev_tools/validate_orchestrator_state.py (required keys, step-status
242
+ validity, blocked_reason validity) that runs before any mode-specific gate.
234
243
  .PARAMETER State
235
244
  The parsed checkpoint PSCustomObject.
236
245
  .OUTPUTS
@@ -254,12 +263,16 @@ function Get-OrchestratorStateBasePresenceError {
254
263
  }
255
264
  }
256
265
 
257
- # Every present step status must be a member of the allowed vocabulary; an absent
258
- # step key contributes no error (mirrors the primary validator's None guard).
266
+ # Every present step status must be a member of the shared vocabulary or of that
267
+ # key's additive extra set; an absent step key contributes no error (mirrors the
268
+ # primary validator's None guard).
259
269
  foreach ($key in $script:STEP_STATUS_KEYS) {
260
270
  $field = Get-OrchestratorStateField -State $State -Name $key
271
+ $extra = @()
272
+ if ($script:STEP_SPECIFIC_EXTRA_STATUS.ContainsKey($key)) { $extra = @($script:STEP_SPECIFIC_EXTRA_STATUS[$key]) }
261
273
  if ($field.Present -and $null -ne $field.Value -and
262
- ($script:VALID_STEP_STATUS -notcontains [string]$field.Value)) {
274
+ ($script:VALID_STEP_STATUS -notcontains [string]$field.Value) -and
275
+ ($extra -notcontains [string]$field.Value)) {
263
276
  $errors.Add("Checkpoint has invalid $key`: $($field.Value)")
264
277
  }
265
278
  }
@@ -279,12 +292,11 @@ function Get-OrchestratorStatePrCreationReadinessError {
279
292
  .SYNOPSIS
280
293
  Return the PR-creation-readiness errors, parity with the Python reference.
281
294
  .DESCRIPTION
282
- Private readiness check mirroring
283
- validate_orchestrator_state_pr_creation_readiness in
284
- _orchestrator_state_pr_creation_readiness.py: steps 5-8 must not be
285
- pending/blocked; blocked_reason must be `none` or absent; and the
286
- local_execution_overrides / delegation_bypasses lists must be empty when
287
- present. It does not enforce completion, CI, PR, or routing-contract gates.
295
+ Private readiness check mirroring validate_orchestrator_state_pr_creation_readiness in
296
+ _orchestrator_state_pr_creation_readiness.py: steps 5-8 must not be pending, blocked, or
297
+ blocked_remediation_loop_limit; blocked_reason must be `none` or absent; and the
298
+ local_execution_overrides / delegation_bypasses lists must be empty when present. It does
299
+ not enforce completion, CI, PR, or routing-contract gates.
288
300
  .PARAMETER State
289
301
  The parsed checkpoint PSCustomObject.
290
302
  .OUTPUTS
@@ -299,11 +311,11 @@ function Get-OrchestratorStatePrCreationReadinessError {
299
311
 
300
312
  $errors = [System.Collections.Generic.List[string]]::new()
301
313
 
302
- # Reject an upstream step recorded as pending or blocked; steps 5-8 must have
303
- # finished before the first PR of a branch is created.
314
+ # Reject an upstream step recorded as pending, blocked, or blocked_remediation_loop_limit; steps
315
+ # 5-8 must have finished before the first PR of a branch is created.
304
316
  foreach ($key in $script:PR_CREATION_READY_STEP_KEYS) {
305
317
  $field = Get-OrchestratorStateField -State $State -Name $key
306
- if ($field.Present -and ($field.Value -eq 'pending' -or $field.Value -eq 'blocked')) {
318
+ if ($field.Present -and (@('pending', 'blocked', 'blocked_remediation_loop_limit') -contains $field.Value)) {
307
319
  $errors.Add("Checkpoint PR-creation readiness validation failed: $key is $($field.Value).")
308
320
  }
309
321
  }
@@ -36,7 +36,7 @@ Run the full seven-stage toolchain in this exact order and repeat until all stag
36
36
  2. **Linting** (e.g., Ruff, ESLint, PSScriptAnalyzer, .NET analyzers)
37
37
  3. **Type checking** (e.g., Pyright, TSC, nullable analysis; skip for PowerShell)
38
38
  4. **Architecture-boundary tests** (e.g., dependency-cruiser, NetArchTest.Rules)
39
- 5. **Unit tests** (e.g., Pytest, Vitest, MSTest, Pester) including property-based tests where applicable per `quality-tiers.md`
39
+ 5. **Unit tests** (e.g., Pytest, Jest, MSTest, Pester) including property-based tests where applicable per `quality-tiers.md`
40
40
  6. **Contract / schema compatibility checks** (e.g., oasdiff, schema-snapshot diff)
41
41
  7. **Integration tests**
42
42
 
@@ -37,7 +37,7 @@ The correct response to a file that contains untestable lines is to refactor it
37
37
  **Permitted `exclude` entries** (non-production paths only):
38
38
  - Build output directories: `dist/**`, `lib/**`, `lib-amd/**`.
39
39
  - Test files and test infrastructure: `**/*.test.ts`, `tests/**`, `src/test-support/**`.
40
- - Config files that are not production code: `vitest.config.ts`, `eslint.config.mjs`, `.dependency-cruiser.cjs`, `webpack.config.js`.
40
+ - Config files that are not production code: `jest.config.cjs`, `eslint.config.mjs`, `.dependency-cruiser.cjs`, `webpack.config.js`.
41
41
  - `node_modules/**`.
42
42
 
43
43
  **Prohibited `exclude` entries:**
@@ -102,4 +102,4 @@ All test code must be deterministic. The following infrastructure requirements a
102
102
  - **Controllable clock** — use a `Clock` interface (TypeScript) or `TimeProvider` (.NET) injected into code under test. Do not read wall-clock time directly in production code under test.
103
103
  - **Seeded RNG** — randomness must be supplied via a seedable interface; on test failure the seed must be printed so the failure is reproducible.
104
104
  - **Banned APIs in test code** — `setTimeout`, `Thread.Sleep`, `Task.Delay`, real wall-clock waits, and `Date.now()` outside the clock interface are prohibited in tests.
105
- - **Virtual scheduler / fake timers / `FakeTimeProvider`** — async tests must use the framework's fake-timer facility (`vi.useFakeTimers()` for Vitest, `FakeTimeProvider` for .NET) to advance simulated time deterministically.
105
+ - **Virtual scheduler / fake timers / `FakeTimeProvider`** — async tests must use the framework's fake-timer facility (`jest.useFakeTimers()` for Jest, `FakeTimeProvider` for .NET) to advance simulated time deterministically.
@@ -13,7 +13,7 @@ This rule file summarizes the TypeScript-specific policies for this repository.
13
13
  1. **Formatting — Prettier**: All TypeScript must be formatted with the repository Prettier configuration. Command: `npm run format`
14
14
  2. **Linting — ESLint**: TypeScript must pass ESLint using the repository configuration. Command: `npm run lint`
15
15
  3. **Type Checking — TSC**: TypeScript must pass the compiler type-check. Avoid `any`; prefer `unknown` plus narrowing. Command: `npm run typecheck`
16
- 4. **Testing — Vitest**: All TypeScript unit tests must use Vitest. Command: `npm run test`
16
+ 4. **Testing — Jest**: All TypeScript unit tests must use Jest. Command: `npm run test:unit`
17
17
 
18
18
  Run the toolchain in order: format → lint → type-check → test. Restart from step 1 if any step fails or changes files.
19
19
 
@@ -39,16 +39,16 @@ Run the toolchain in order: format → lint → type-check → test. Restart fro
39
39
 
40
40
  ## Testing Standards
41
41
 
42
- - Use **Vitest** as the test framework.
42
+ - Use **Jest** as the test framework.
43
43
  - Name test files `*.test.ts`.
44
44
  - Unit tests must not require the Outlook host runtime.
45
45
  - Follow Arrange–Act–Assert structure.
46
46
  - Each test targets one behavior.
47
- - Use `vi.spyOn` or `vi.mock` for targeted mocking; reset mocks with `afterEach(() => { vi.resetAllMocks(); })`.
47
+ - Use `jest.spyOn` or `jest.mock` for targeted mocking; reset mocks with `afterEach(() => { jest.resetAllMocks(); })`.
48
48
  - No external dependencies (network, filesystem temp files, external processes) in unit tests.
49
49
  - Avoid snapshot tests unless stable and intentional.
50
50
  - Coverage thresholds follow the uniform tier rule defined in `.claude/rules/quality-tiers.md`: line coverage >= 85% and branch coverage >= 75% across all tiers (T1–T4).
51
- - Coverage command: `npm run test:coverage` (the script is wired in Prompt B1 alongside the Vitest dependency).
51
+ - Coverage command: `npm run test:unit:coverage` (the root `package.json` script runs `node run-jest.cjs --coverage`).
52
52
  - Coverage regression on changed lines is a blocking finding.
53
53
  - Interface/type-only files with no executable behavior — files consisting solely of `interface` or `type` declarations — may be omitted from coverage measurement. Such files legitimately report 0% executable coverage. This is a clarification only; it does not lower any coverage threshold.
54
54
 
@@ -70,5 +70,5 @@ Layer rules and the No-COM architecture assertions are defined in `.claude/rules
70
70
  ## Runtime Determinism
71
71
 
72
72
  - `Date`, `Math.random`, and `setTimeout` access must flow through an injected `Clock` / `Random` interface.
73
- - Tests use Vitest fake timers (`vi.useFakeTimers()`).
73
+ - Tests use Jest fake timers (`jest.useFakeTimers()`).
74
74
  - Prefer `await flushPromises()` over `setTimeout(0)` for awaiting micro-tasks.
@@ -41,7 +41,7 @@ Run the full seven-stage toolchain in this exact order and repeat until all stag
41
41
  2. **Linting** (e.g., Ruff, ESLint, PSScriptAnalyzer, .NET analyzers)
42
42
  3. **Type checking** (e.g., Pyright, TSC, nullable analysis; skip for PowerShell)
43
43
  4. **Architecture-boundary tests** (e.g., dependency-cruiser, NetArchTest.Rules)
44
- 5. **Unit tests** (e.g., Pytest, Vitest, MSTest, Pester) including property-based tests where applicable per `quality-tiers.md`
44
+ 5. **Unit tests** (e.g., Pytest, Jest, MSTest, Pester) including property-based tests where applicable per `quality-tiers.md`
45
45
  6. **Contract / schema compatibility checks** (e.g., oasdiff, schema-snapshot diff)
46
46
  7. **Integration tests**
47
47
 
@@ -42,7 +42,7 @@ The correct response to a file that contains untestable lines is to refactor it
42
42
  **Permitted `exclude` entries** (non-production paths only):
43
43
  - Build output directories: `dist/**`, `lib/**`, `lib-amd/**`.
44
44
  - Test files and test infrastructure: `**/*.test.ts`, `tests/**`, `src/test-support/**`.
45
- - Config files that are not production code: `vitest.config.ts`, `eslint.config.mjs`, `.dependency-cruiser.cjs`, `webpack.config.js`.
45
+ - Config files that are not production code: `jest.config.cjs`, `eslint.config.mjs`, `.dependency-cruiser.cjs`, `webpack.config.js`.
46
46
  - `node_modules/**`.
47
47
 
48
48
  **Prohibited `exclude` entries:**
@@ -107,4 +107,4 @@ All test code must be deterministic. The following infrastructure requirements a
107
107
  - **Controllable clock** — use a `Clock` interface (TypeScript) or `TimeProvider` (.NET) injected into code under test. Do not read wall-clock time directly in production code under test.
108
108
  - **Seeded RNG** — randomness must be supplied via a seedable interface; on test failure the seed must be printed so the failure is reproducible.
109
109
  - **Banned APIs in test code** — `setTimeout`, `Thread.Sleep`, `Task.Delay`, real wall-clock waits, and `Date.now()` outside the clock interface are prohibited in tests.
110
- - **Virtual scheduler / fake timers / `FakeTimeProvider`** — async tests must use the framework's fake-timer facility (`vi.useFakeTimers()` for Vitest, `FakeTimeProvider` for .NET) to advance simulated time deterministically.
110
+ - **Virtual scheduler / fake timers / `FakeTimeProvider`** — async tests must use the framework's fake-timer facility (`jest.useFakeTimers()` for Jest, `FakeTimeProvider` for .NET) to advance simulated time deterministically.
@@ -2,7 +2,7 @@ default_permissions = ":danger-full-access"
2
2
 
3
3
  [mcp_servers.drm-copilot]
4
4
  command = "npx"
5
- args = ["-y", "@danmoisan/drm-copilot-mcp@1.0.18"]
5
+ args = ["-y", "@danmoisan/drm-copilot-mcp@1.0.20"]
6
6
  required = true
7
7
  enabled_tools = [
8
8
  "collect_commit_context",
@@ -33,6 +33,10 @@
33
33
  [CmdletBinding()]
34
34
  param()
35
35
 
36
+ # Shared Codex PreToolUse transport: stdin payload parsing and tool_input-to-file
37
+ # mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
38
+ . (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
39
+
36
40
  function Get-PowerShellTestPurityBlockDecision {
37
41
  [CmdletBinding()]
38
42
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -138,76 +142,16 @@ function Invoke-PowerShellTestPurityDecision {
138
142
  return Get-PowerShellTestPurityBlockDecision -Reason $reason
139
143
  }
140
144
 
141
- function ConvertFrom-CodexPowerShellPurityPayload {
142
- [CmdletBinding()]
143
- param([Parameter(Mandatory)][string] $PayloadRaw)
144
-
145
- if ([string]::IsNullOrWhiteSpace($PayloadRaw)) {
146
- throw 'check-powershell-test-purity hook input is empty.'
147
- }
148
- try {
149
- $payload = $PayloadRaw | ConvertFrom-Json -ErrorAction Stop
150
- } catch {
151
- throw "check-powershell-test-purity hook input is malformed JSON: $_"
152
- }
153
- if ($payload.PSObject.Properties.Name -notcontains 'tool_input' -or $null -eq $payload.tool_input) {
154
- throw 'check-powershell-test-purity hook input is missing tool_input.'
155
- }
156
- if ([string]$payload.hook_event_name -ne 'PreToolUse' -or [string]$payload.tool_name -ne 'apply_patch') {
157
- throw 'check-powershell-test-purity requires a PreToolUse apply_patch payload.'
158
- }
159
- return $payload
160
- }
161
-
162
- function ConvertTo-CodexPowerShellPurityInput {
163
- [CmdletBinding()]
164
- [OutputType([object[]])]
165
- param([Parameter(Mandatory)] $Payload)
166
-
167
- if ($Payload.tool_input.PSObject.Properties.Name -contains 'file_path') {
168
- return , $Payload.tool_input
169
- }
170
-
171
- $command = [string]$Payload.tool_input.command
172
- if ([string]::IsNullOrWhiteSpace($command)) {
173
- throw 'check-powershell-test-purity cannot map tool_input to a file edit.'
174
- }
175
-
176
- $fileMatches = [regex]::Matches(
177
- $command,
178
- '(?ms)^\*\*\* (?:Add|Update|Delete) File:\s*(?<path>.+?)\r?\n(?<body>.*?)(?=^\*\*\* (?:(?:Add|Update|Delete) File:|End Patch)\s*|\z)'
179
- )
180
- if ($fileMatches.Count -eq 0) {
181
- throw 'check-powershell-test-purity received an unrecognized apply_patch command.'
182
- }
183
-
184
- $inputs = [System.Collections.Generic.List[object]]::new()
185
- foreach ($match in $fileMatches) {
186
- $filePath = ([string]$match.Groups['path'].Value).Trim()
187
- $moveMatch = [regex]::Match([string]$match.Groups['body'].Value, '(?m)^\*\*\* Move to:\s*(?<path>.+?)\s*$')
188
- if ($moveMatch.Success) {
189
- $filePath = ([string]$moveMatch.Groups['path'].Value).Trim()
190
- }
191
- $addedLines = foreach ($line in ([string]$match.Groups['body'].Value -split '\r?\n')) {
192
- if ($line.StartsWith('+') -and -not $line.StartsWith('+++')) {
193
- $line.Substring(1)
194
- }
195
- }
196
- $inputs.Add([pscustomobject]@{
197
- file_path = $filePath
198
- content = $addedLines -join [Environment]::NewLine
199
- })
200
- }
201
- return $inputs.ToArray()
202
- }
203
-
204
145
  if ($MyInvocation.InvocationName -eq '.') {
205
146
  return
206
147
  }
207
148
 
208
149
  try {
209
- $payload = ConvertFrom-CodexPowerShellPurityPayload -PayloadRaw ([Console]::In.ReadToEnd())
210
- foreach ($toolInput in @(ConvertTo-CodexPowerShellPurityInput -Payload $payload)) {
150
+ # Transport and mapping come from the shared module. A well-formed payload
151
+ # that maps to no file edit produces an empty record set, so the loop body
152
+ # never runs and the hook allows silently.
153
+ $payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw ([Console]::In.ReadToEnd()) -HookName 'check-powershell-test-purity'
154
+ foreach ($toolInput in @(ConvertTo-CodexFileEditInput -Payload $payload)) {
211
155
  $toolInputRaw = $toolInput | ConvertTo-Json -Compress -Depth 20
212
156
  $decision = Invoke-PowerShellTestPurityDecision -ToolInputRaw $toolInputRaw
213
157
  if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {
@@ -30,6 +30,10 @@
30
30
  [CmdletBinding()]
31
31
  param()
32
32
 
33
+ # Shared Codex PreToolUse transport: stdin payload parsing and tool_input-to-file
34
+ # mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
35
+ . (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
36
+
33
37
  function Get-PythonTestPurityBlockDecision {
34
38
  [CmdletBinding()]
35
39
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -138,76 +142,16 @@ function Invoke-PythonTestPurityDecision {
138
142
  return Get-PythonTestPurityBlockDecision -Reason $reason
139
143
  }
140
144
 
141
- function ConvertFrom-CodexPythonPurityPayload {
142
- [CmdletBinding()]
143
- param([Parameter(Mandatory)][string] $PayloadRaw)
144
-
145
- if ([string]::IsNullOrWhiteSpace($PayloadRaw)) {
146
- throw 'check-python-test-purity hook input is empty.'
147
- }
148
- try {
149
- $payload = $PayloadRaw | ConvertFrom-Json -ErrorAction Stop
150
- } catch {
151
- throw "check-python-test-purity hook input is malformed JSON: $_"
152
- }
153
- if ($payload.PSObject.Properties.Name -notcontains 'tool_input' -or $null -eq $payload.tool_input) {
154
- throw 'check-python-test-purity hook input is missing tool_input.'
155
- }
156
- if ([string]$payload.hook_event_name -ne 'PreToolUse' -or [string]$payload.tool_name -ne 'apply_patch') {
157
- throw 'check-python-test-purity requires a PreToolUse apply_patch payload.'
158
- }
159
- return $payload
160
- }
161
-
162
- function ConvertTo-CodexPythonPurityInput {
163
- [CmdletBinding()]
164
- [OutputType([object[]])]
165
- param([Parameter(Mandatory)] $Payload)
166
-
167
- if ($Payload.tool_input.PSObject.Properties.Name -contains 'file_path') {
168
- return , $Payload.tool_input
169
- }
170
-
171
- $command = [string]$Payload.tool_input.command
172
- if ([string]::IsNullOrWhiteSpace($command)) {
173
- throw 'check-python-test-purity cannot map tool_input to a file edit.'
174
- }
175
-
176
- $fileMatches = [regex]::Matches(
177
- $command,
178
- '(?ms)^\*\*\* (?:Add|Update|Delete) File:\s*(?<path>.+?)\r?\n(?<body>.*?)(?=^\*\*\* (?:(?:Add|Update|Delete) File:|End Patch)\s*|\z)'
179
- )
180
- if ($fileMatches.Count -eq 0) {
181
- throw 'check-python-test-purity received an unrecognized apply_patch command.'
182
- }
183
-
184
- $inputs = [System.Collections.Generic.List[object]]::new()
185
- foreach ($match in $fileMatches) {
186
- $filePath = ([string]$match.Groups['path'].Value).Trim()
187
- $moveMatch = [regex]::Match([string]$match.Groups['body'].Value, '(?m)^\*\*\* Move to:\s*(?<path>.+?)\s*$')
188
- if ($moveMatch.Success) {
189
- $filePath = ([string]$moveMatch.Groups['path'].Value).Trim()
190
- }
191
- $addedLines = foreach ($line in ([string]$match.Groups['body'].Value -split '\r?\n')) {
192
- if ($line.StartsWith('+') -and -not $line.StartsWith('+++')) {
193
- $line.Substring(1)
194
- }
195
- }
196
- $inputs.Add([pscustomobject]@{
197
- file_path = $filePath
198
- content = $addedLines -join [Environment]::NewLine
199
- })
200
- }
201
- return $inputs.ToArray()
202
- }
203
-
204
145
  if ($MyInvocation.InvocationName -eq '.') {
205
146
  return
206
147
  }
207
148
 
208
149
  try {
209
- $payload = ConvertFrom-CodexPythonPurityPayload -PayloadRaw ([Console]::In.ReadToEnd())
210
- foreach ($toolInput in @(ConvertTo-CodexPythonPurityInput -Payload $payload)) {
150
+ # Transport and mapping come from the shared module. A well-formed payload
151
+ # that maps to no file edit produces an empty record set, so the loop body
152
+ # never runs and the hook allows silently.
153
+ $payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw ([Console]::In.ReadToEnd()) -HookName 'check-python-test-purity'
154
+ foreach ($toolInput in @(ConvertTo-CodexFileEditInput -Payload $payload)) {
211
155
  $toolInputRaw = $toolInput | ConvertTo-Json -Compress -Depth 20
212
156
  $decision = Invoke-PythonTestPurityDecision -ToolInputRaw $toolInputRaw
213
157
  if ($null -ne $decision -and $decision.hookSpecificOutput.permissionDecision -eq 'deny') {