@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.23

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 (43) hide show
  1. package/out/mcp-server.js +1730 -139
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
  4. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
  5. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
  6. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +257 -0
  8. package/resources/claude-customizations/.claude/agents/parallel-planner.md +183 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
  10. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
  15. package/resources/claude-customizations/.claude/lib/bash/compute-cohorts.sh +143 -0
  16. package/resources/claude-customizations/.claude/lib/bash/compute-concurrency-batches.sh +122 -0
  17. package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +330 -0
  18. package/resources/claude-customizations/.claude/lib/bash/parallel-common.sh +238 -0
  19. package/resources/claude-customizations/.claude/lib/bash/parallel-items-validate.sh +244 -0
  20. package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +187 -0
  21. package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-emit.sh +340 -0
  22. package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-scan.sh +335 -0
  23. package/resources/claude-customizations/.claude/lib/bash/validate-parallel-manifest.sh +134 -0
  24. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
  25. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
  26. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
  28. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
  29. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
  30. package/resources/claude-customizations/.claude/rules/shell.md +7 -2
  31. package/resources/claude-customizations/.claude/settings.json +28 -0
  32. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +152 -0
  33. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
  34. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +971 -0
  35. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +461 -0
  36. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
  37. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
  38. package/resources/claude-customizations/config/blast-radius.json +16 -0
  39. package/resources/claude-customizations/config/orchestration-routing.json +355 -0
  40. package/resources/claude-customizations/pack-manifests/core.json +32 -1
  41. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  42. package/resources/config/orchestration-routing.json +22 -0
  43. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env bash
2
+ # parallel-cohorts.sh: sourceable bash port of
3
+ # scripts/dev_tools/parallel_cohort_computation.py. Partitions an item-key set
4
+ # into execution cohorts by deterministic greedy graph coloring in Welsh-Powell
5
+ # order, and chunks one cohort into concurrency-capped batches.
6
+ #
7
+ # Determinism is the objective, not optimality. Every ordering decision is made
8
+ # by an explicit numeric sort under LC_ALL=C: the Welsh-Powell visit order comes
9
+ # from `sort -k1,1nr -k2,2n` over `<degree> <item_key>` lines, which is the
10
+ # composite key (-degree, item_key) ascending; cohort membership and batch
11
+ # contents come from `sort -n`. Associative arrays are used for adjacency
12
+ # membership, degree counting, and index assignment only, and their iteration
13
+ # order never reaches output.
14
+ #
15
+ # Error messages reproduce the Python module's four literal failure messages
16
+ # byte for byte, including the tuple repr `(a, b)` with its comma and space.
17
+ # The caller reads PCOH_ERROR after a non-zero return.
18
+ #
19
+ # The Python module remains the repository authority.
20
+ #
21
+ # shellcheck disable=SC2034
22
+ # SC2034 is disabled file-wide because PCOH_ERROR and PCOH_RESULT are
23
+ # written here and read by the entry points compute-cohorts.sh and
24
+ # compute-concurrency-batches.sh, which shellcheck analyses separately.
25
+
26
+ # Resolve this file's own directory so its dependencies source regardless of
27
+ # the caller's working directory.
28
+ PCOH_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
29
+ # shellcheck source=.claude/lib/bash/parallel-common.sh
30
+ # shellcheck disable=SC1091
31
+ source "$PCOH_LIB_DIR/parallel-common.sh"
32
+
33
+ # Adjacency membership flags keyed by `a,b`; never iterated into output.
34
+ declare -gA PCOH_ADJACENT=()
35
+
36
+ # Distinct-neighbor degree per item key; never iterated into output.
37
+ declare -gA PCOH_DEGREE=()
38
+
39
+ # Space-separated neighbor list per item key; never iterated into output.
40
+ declare -gA PCOH_NEIGHBORS=()
41
+
42
+ # Assigned cohort index per item key; never iterated into output.
43
+ declare -gA PCOH_INDEX_OF=()
44
+
45
+ # The failure message set by the most recent non-zero return.
46
+ PCOH_ERROR=""
47
+
48
+ # The compact JSON array-of-arrays produced by the most recent success.
49
+ PCOH_RESULT=""
50
+
51
+ # Scratch array populated by pcoh_split_words.
52
+ PCOH_WORDS=()
53
+
54
+ pcoh_split_words() {
55
+ # Populate PCOH_WORDS by splitting a space-separated string.
56
+ #
57
+ # Args: $1 = the string to split. An empty or whitespace-only string yields
58
+ # an empty array, which is the empty-graph and empty-cohort case. `read -ra`
59
+ # is used rather than an unquoted expansion so a token is never subjected to
60
+ # pathname expansion.
61
+ PCOH_WORDS=()
62
+ read -ra PCOH_WORDS <<<"${1-}"
63
+ }
64
+
65
+ pcoh_fail() {
66
+ # Record a failure message and return 1.
67
+ #
68
+ # Args: $1 = the complete, literal failure message.
69
+ PCOH_ERROR="$1"
70
+ return 1
71
+ }
72
+
73
+ pcoh_validate_item_keys() {
74
+ # Reject duplicate item keys, walking the supplied order.
75
+ #
76
+ # Duplicates break the uniqueness assumption behind the total-order sort
77
+ # key (-degree, item_key), so they are rejected rather than deduplicated.
78
+ #
79
+ # Args: $1 = space-separated item keys.
80
+ # Returns 0 when every key is distinct, 1 with PCOH_ERROR otherwise.
81
+ local key seen=""
82
+ pcoh_split_words "$1"
83
+ local -a key_items=("${PCOH_WORDS[@]}")
84
+ # Walk in the supplied order so the first repeat encountered is the key
85
+ # reported, which keeps the message stable for a given input.
86
+ for key in "${key_items[@]}"; do
87
+ if pc_contains_word "$seen" "$key"; then
88
+ pcoh_fail "Duplicate item key $key in item_keys; item keys must be unique because cohort ordering relies on key uniqueness."
89
+ return 1
90
+ fi
91
+ seen="$seen $key"
92
+ done
93
+ return 0
94
+ }
95
+
96
+ pcoh_record_edge() {
97
+ # Record one validated edge on both endpoints, ignoring repeats.
98
+ #
99
+ # Storing a membership flag per ordered pair normalizes the edge list by
100
+ # construction: an edge supplied as (a, b), as (b, a), or supplied
101
+ # repeatedly collapses to the same single neighbor entry on both sides.
102
+ #
103
+ # Args: $1 = first endpoint, $2 = second endpoint.
104
+ local first="$1" second="$2"
105
+ [[ -z ${PCOH_ADJACENT["$first,$second"]-} ]] || return 0
106
+ PCOH_ADJACENT["$first,$second"]=1
107
+ PCOH_ADJACENT["$second,$first"]=1
108
+ PCOH_NEIGHBORS["$first"]="${PCOH_NEIGHBORS["$first"]} $second"
109
+ PCOH_NEIGHBORS["$second"]="${PCOH_NEIGHBORS["$second"]} $first"
110
+ PCOH_DEGREE["$first"]=$((PCOH_DEGREE["$first"] + 1))
111
+ PCOH_DEGREE["$second"]=$((PCOH_DEGREE["$second"] + 1))
112
+ }
113
+
114
+ pcoh_build_adjacency() {
115
+ # Build symmetric adjacency, normalizing edge direction and duplicates.
116
+ #
117
+ # Args: $1 = space-separated item keys, $2 = space-separated `a:b` edges.
118
+ # Populates PCOH_DEGREE and PCOH_NEIGHBORS. Returns 0 on success, 1 with
119
+ # PCOH_ERROR when an edge is a self-loop or names an undeclared endpoint.
120
+ local keys="$1" key edge first second
121
+ PCOH_DEGREE=()
122
+ PCOH_NEIGHBORS=()
123
+ PCOH_ADJACENT=()
124
+ pcoh_split_words "$keys"
125
+ local -a key_list=("${PCOH_WORDS[@]}")
126
+ # Seed every declared key so an isolated vertex survives into the coloring
127
+ # step; inferring vertices from the edge list alone would drop it.
128
+ for key in "${key_list[@]}"; do
129
+ PCOH_DEGREE["$key"]=0
130
+ PCOH_NEIGHBORS["$key"]=""
131
+ done
132
+
133
+ pcoh_split_words "${2-}"
134
+ local -a edge_list=("${PCOH_WORDS[@]}")
135
+ # Validate then record each conflict. Validation order is part of the
136
+ # contract: the self-loop check precedes the endpoint-membership check, and
137
+ # the first endpoint is checked before the second.
138
+ for edge in "${edge_list[@]}"; do
139
+ first="${edge%%:*}"
140
+ second="${edge#*:}"
141
+ if [[ $first == "$second" ]]; then
142
+ pcoh_fail "Self-loop edge on item key $first; the conflict relation is defined over distinct items, so an item cannot conflict with itself."
143
+ return 1
144
+ fi
145
+ if ! pc_contains_word "$keys" "$first"; then
146
+ pcoh_fail "Conflict edge ($first, $second) names item key $first, which is not a member of item_keys; every edge endpoint must be a declared item key."
147
+ return 1
148
+ fi
149
+ if ! pc_contains_word "$keys" "$second"; then
150
+ pcoh_fail "Conflict edge ($first, $second) names item key $second, which is not a member of item_keys; every edge endpoint must be a declared item key."
151
+ return 1
152
+ fi
153
+ pcoh_record_edge "$first" "$second"
154
+ done
155
+ return 0
156
+ }
157
+
158
+ pcoh_welsh_powell_order() {
159
+ # Echo the item keys in Welsh-Powell visit order, one per line.
160
+ #
161
+ # The composite key (-degree, item_key) is a total order because item keys
162
+ # are unique. That is the single load-bearing determinism guard: the visit
163
+ # order depends on the graph alone, never on the caller's input order. The
164
+ # ordering is delegated to sort so no bash-side comparison logic can drift
165
+ # from the Python sort key.
166
+ #
167
+ # Args: $1 = space-separated item keys.
168
+ local key
169
+ pcoh_split_words "$1"
170
+ local -a key_items=("${PCOH_WORDS[@]}")
171
+ for key in "${key_items[@]}"; do
172
+ printf '%s %s\n' "${PCOH_DEGREE["$key"]}" "$key"
173
+ done | LC_ALL=C sort -k1,1nr -k2,2n | cut -d' ' -f2
174
+ }
175
+
176
+ pcoh_lowest_free_index() {
177
+ # Echo the smallest cohort index not held by any assigned neighbor.
178
+ #
179
+ # Args: $1 = the item key being placed.
180
+ local key="$1" neighbor taken="" candidate=0
181
+ pcoh_split_words "${PCOH_NEIGHBORS["$key"]}"
182
+ local -a neighbors=("${PCOH_WORDS[@]}")
183
+ # Collecting neighbor indices into a membership string is order
184
+ # insensitive, so reading the neighbor list here cannot affect the outcome.
185
+ for neighbor in "${neighbors[@]}"; do
186
+ if [[ -n ${PCOH_INDEX_OF["$neighbor"]-} ]]; then
187
+ taken="$taken ${PCOH_INDEX_OF["$neighbor"]}"
188
+ fi
189
+ done
190
+ # Scan upward from zero for the first free index. An isolated vertex and
191
+ # the first-visited vertex both land in cohort 0.
192
+ while pc_contains_word "$taken" "$candidate"; do
193
+ candidate=$((candidate + 1))
194
+ done
195
+ printf '%s' "$candidate"
196
+ }
197
+
198
+ pcoh_assign_cohort_indices() {
199
+ # Assign each vertex the lowest cohort index free among its neighbors.
200
+ #
201
+ # Args: $1 = space-separated item keys. Populates PCOH_INDEX_OF. A vertex
202
+ # never takes an index already held by a neighbor, so each index class is
203
+ # an independent set of the conflict graph.
204
+ local ordered key
205
+ PCOH_INDEX_OF=()
206
+ ordered=$(pcoh_welsh_powell_order "$1")
207
+ [[ -n $ordered ]] || return 0
208
+ # Visit vertices in Welsh-Powell order; each takes the smallest index its
209
+ # already-assigned neighbors have not taken.
210
+ while IFS= read -r key; do
211
+ [[ -n $key ]] || continue
212
+ PCOH_INDEX_OF["$key"]=$(pcoh_lowest_free_index "$key")
213
+ done <<<"$ordered"
214
+ return 0
215
+ }
216
+
217
+ pcoh_render_cohorts() {
218
+ # Echo the compact JSON array-of-arrays for the current assignment.
219
+ #
220
+ # Args: $1 = space-separated item keys. List position is the cohort index
221
+ # and each inner list holds that cohort's keys sorted ascending.
222
+ local key index highest=-1 ordered
223
+ pcoh_split_words "$1"
224
+ local -a key_items=("${PCOH_WORDS[@]}")
225
+ for key in "${key_items[@]}"; do
226
+ index="${PCOH_INDEX_OF["$key"]}"
227
+ ((index > highest)) && highest=$index
228
+ done
229
+ if ((highest < 0)); then
230
+ printf '[]'
231
+ return 0
232
+ fi
233
+
234
+ local -a buckets=()
235
+ for ((index = 0; index <= highest; index++)); do
236
+ buckets+=("")
237
+ done
238
+ ordered=$(printf '%s\n' "${key_items[@]}" | LC_ALL=C sort -n)
239
+ # Walk the ascending key order rather than any hash order, so each cohort's
240
+ # keys come out ascending regardless of how the caller ordered its input.
241
+ while IFS= read -r key; do
242
+ [[ -n $key ]] || continue
243
+ index="${PCOH_INDEX_OF["$key"]}"
244
+ if [[ -z ${buckets[index]} ]]; then
245
+ buckets[index]="$key"
246
+ else
247
+ buckets[index]="${buckets[index]},$key"
248
+ fi
249
+ done <<<"$ordered"
250
+
251
+ local rendered=""
252
+ for ((index = 0; index <= highest; index++)); do
253
+ [[ -z $rendered ]] || rendered="$rendered,"
254
+ rendered="${rendered}[${buckets[index]}]"
255
+ done
256
+ printf '[%s]' "$rendered"
257
+ }
258
+
259
+ pcoh_compute_cohorts() {
260
+ # Partition item keys into cohorts by deterministic greedy graph coloring.
261
+ #
262
+ # Args: $1 = space-separated item keys, $2 = space-separated `a:b` edges.
263
+ # On success returns 0 with PCOH_RESULT holding the compact JSON output.
264
+ # On malformed input returns 1 with PCOH_ERROR holding the exact Python
265
+ # message. All validation runs before any coloring work.
266
+ local keys="$1" edges="${2-}"
267
+ PCOH_ERROR=""
268
+ PCOH_RESULT=""
269
+ pcoh_validate_item_keys "$keys" || return 1
270
+ pcoh_build_adjacency "$keys" "$edges" || return 1
271
+ pcoh_assign_cohort_indices "$keys"
272
+ PCOH_RESULT=$(pcoh_render_cohorts "$keys")
273
+ return 0
274
+ }
275
+
276
+ pcoh_compute_concurrency_batches() {
277
+ # Chunk one cohort into concurrency-capped batches in ascending key order.
278
+ #
279
+ # The cohort's keys are sorted inside this function rather than trusting
280
+ # the caller's ordering, so determinism does not depend on caller
281
+ # discipline. Every batch is exactly max_concurrency long except a possibly
282
+ # smaller final batch.
283
+ #
284
+ # Args: $1 = space-separated cohort item keys, $2 = the fan-out cap.
285
+ # On success returns 0 with PCOH_RESULT holding the compact JSON output;
286
+ # on a cap below 1 returns 1 with PCOH_ERROR holding the exact message.
287
+ local cap="$2" key sorted
288
+ PCOH_ERROR=""
289
+ PCOH_RESULT=""
290
+ # A cap below 1 would admit no items into any batch, so the cohort could
291
+ # never drain; reject it rather than returning an unusable schedule.
292
+ if ((cap < 1)); then
293
+ pcoh_fail "max_concurrency must be >= 1; received $cap."
294
+ return 1
295
+ fi
296
+
297
+ pcoh_split_words "$1"
298
+ local -a supplied=("${PCOH_WORDS[@]}")
299
+ local -a ordered=()
300
+ if ((${#supplied[@]} > 0)); then
301
+ sorted=$(printf '%s\n' "${supplied[@]}" | LC_ALL=C sort -n)
302
+ while IFS= read -r key; do
303
+ [[ -n $key ]] || continue
304
+ ordered+=("$key")
305
+ done <<<"$sorted"
306
+ fi
307
+
308
+ local total=${#ordered[@]}
309
+ if ((total == 0)); then
310
+ PCOH_RESULT="[]"
311
+ return 0
312
+ fi
313
+
314
+ local start end position rendered="" batch
315
+ # Walk the sorted keys in fixed-size strides so slot filling follows
316
+ # ascending key order and the batch boundaries are reproducible.
317
+ for ((start = 0; start < total; start += cap)); do
318
+ end=$((start + cap))
319
+ ((end > total)) && end=$total
320
+ batch=""
321
+ for ((position = start; position < end; position++)); do
322
+ [[ -z $batch ]] || batch="$batch,"
323
+ batch="$batch${ordered[position]}"
324
+ done
325
+ [[ -z $rendered ]] || rendered="$rendered,"
326
+ rendered="${rendered}[$batch]"
327
+ done
328
+ PCOH_RESULT="[$rendered]"
329
+ return 0
330
+ }
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bash
2
+ # parallel-common.sh: sourceable bash port of the shared predicates, enum
3
+ # vocabularies, and error-string builders that back the parallel-surface
4
+ # validators. This is the bash counterpart of
5
+ # scripts/dev_tools/_parallel_state_common.py; the Python module remains the
6
+ # repository authority and this file must reproduce its output byte for byte.
7
+ #
8
+ # Scope. Pure string and arithmetic helpers only: nothing here reads a file,
9
+ # starts a process, reads the clock, or mutates global state other than the
10
+ # PC_ERRORS accumulator its own reset/add functions own. The enum tuples are
11
+ # consumed from .claude/rules/parallel-orchestration.md and are never extended
12
+ # here; member order is load-bearing because pc_enum_error renders it.
13
+ #
14
+ # Locale. Every entry point that sources this file calls pc_enforce_c_locale
15
+ # before doing any work, so sorting and character classification are byte
16
+ # ordered and independent of the destination workspace's environment.
17
+ #
18
+ # shellcheck disable=SC2034
19
+ # SC2034 is disabled file-wide because every constant below is consumed by a
20
+ # sibling file of this library -- parallel-items-validate.sh,
21
+ # parallel-manifest-validate.sh, and parallel-cohorts.sh -- which shellcheck
22
+ # analyses separately and therefore cannot see the use.
23
+
24
+ # Item lifecycle states, in canonical order (schema S4).
25
+ PC_VALID_ITEM_STATES="proposed, admitted, prepared, scheduled, in_flight, merged, withdrawn, blocked"
26
+
27
+ # Per-item merge lifecycle, in canonical order (schema S4).
28
+ PC_VALID_MERGE_STATUS="not_started, worktree_created, pr_open, ci_green, merged, worktree_removed, blocked_drift, blocked_ci_loop_limit"
29
+
30
+ # Blast-radius confidence sources, in canonical order (schema S4).
31
+ PC_VALID_SOURCES="derived, declared, observed"
32
+
33
+ # Work-item kinds carried by the manifest (schema S4).
34
+ PC_VALID_KINDS="feature, bug"
35
+
36
+ # Run modes; closed is the documented default (schema S4).
37
+ PC_VALID_MODES="closed, open"
38
+
39
+ # Merge-status values meaning the item reached a terminal merged outcome.
40
+ PC_MERGED_MERGE_STATUSES="merged worktree_removed"
41
+
42
+ # Merge-status values meaning the item is blocked.
43
+ PC_BLOCKED_MERGE_STATUSES="blocked_drift blocked_ci_loop_limit"
44
+
45
+ # The four blast_radius collection fields, in serialization order.
46
+ PC_BLAST_RADIUS_LIST_FIELDS="paths modules shared_surfaces contracts"
47
+
48
+ # Path label for the document root in prohibited-key error strings.
49
+ PC_ROOT_PATH="<root>"
50
+
51
+ # Accumulated validation errors, in emission order.
52
+ PC_ERRORS=()
53
+
54
+ pc_enforce_c_locale() {
55
+ # Pin the byte-ordered C locale for the remainder of the process.
56
+ #
57
+ # Determinism countermeasure R9: every numeric sort, glob expansion, and
58
+ # character class in this library must behave identically regardless of the
59
+ # destination workspace's locale settings.
60
+ export LC_ALL=C
61
+ }
62
+
63
+ pc_errors_reset() {
64
+ # Discard any accumulated errors so a fresh validation starts empty.
65
+ PC_ERRORS=()
66
+ }
67
+
68
+ pc_error_add() {
69
+ # Append one error string to the accumulator.
70
+ #
71
+ # Args: $1 = the complete error string, already fully rendered.
72
+ PC_ERRORS+=("$1")
73
+ }
74
+
75
+ pc_errors_count() {
76
+ # Echo the number of accumulated errors.
77
+ printf '%s' "${#PC_ERRORS[@]}"
78
+ }
79
+
80
+ pc_errors_print() {
81
+ # Print every accumulated error, one per line, in emission order.
82
+ #
83
+ # Prints nothing for an empty accumulator, which is the valid-artifact case.
84
+ ((${#PC_ERRORS[@]} > 0)) || return 0
85
+ local entry
86
+ # Emit in accumulation order: message sequence is part of the parity
87
+ # contract, so the accumulator is never sorted or de-duplicated.
88
+ for entry in "${PC_ERRORS[@]}"; do
89
+ printf '%s\n' "$entry"
90
+ done
91
+ }
92
+
93
+ pc_repr_string() {
94
+ # Echo the Python repr of a string, including Python's quote selection.
95
+ #
96
+ # Python prefers single quotes and switches to double quotes only when the
97
+ # string contains a single quote and no double quote. Backslashes are
98
+ # escaped first so a later escape's own backslash is not doubled, then the
99
+ # three printable control escapes, then the active quote character.
100
+ #
101
+ # Args: $1 = the raw string value.
102
+ local value="$1"
103
+ local quote="'"
104
+ # Quote selection: only a string carrying an apostrophe but no double quote
105
+ # switches Python to double quotes.
106
+ if [[ $value == *"'"* && $value != *'"'* ]]; then
107
+ quote='"'
108
+ fi
109
+ local body=${value//\\/\\\\}
110
+ body=${body//$'\n'/\\n}
111
+ body=${body//$'\r'/\\r}
112
+ body=${body//$'\t'/\\t}
113
+ # Escape whichever quote is active. Under double quoting the escape is a
114
+ # no-op by the selection rule above; under single quoting it fires only for
115
+ # a string that carries both quote characters.
116
+ if [[ $quote == "'" ]]; then
117
+ body=${body//\'/\\\'}
118
+ else
119
+ body=${body//\"/\\\"}
120
+ fi
121
+ printf '%s%s%s' "$quote" "$body" "$quote"
122
+ }
123
+
124
+ pc_repr() {
125
+ # Echo the Python repr of a lexically typed value.
126
+ #
127
+ # Args: $1 = type tag (absent, null, bool, int, float, str), $2 = raw value.
128
+ # An absent key reads as None, matching Python's mapping.get default.
129
+ local type="$1"
130
+ local value="${2-}"
131
+ # Routing table: the five scalar types the manifest subset can carry, plus
132
+ # the absent-key case that Python renders as None.
133
+ case "$type" in
134
+ absent | null)
135
+ printf 'None'
136
+ ;;
137
+ bool)
138
+ if [[ $value == true ]]; then
139
+ printf 'True'
140
+ else
141
+ printf 'False'
142
+ fi
143
+ ;;
144
+ int | float)
145
+ printf '%s' "$value"
146
+ ;;
147
+ str)
148
+ pc_repr_string "$value"
149
+ ;;
150
+ *)
151
+ # A container reaching repr would be a caller defect; render the tag so
152
+ # the failure is visible rather than silently producing a bare value.
153
+ printf '<%s>' "$type"
154
+ ;;
155
+ esac
156
+ }
157
+
158
+ pc_is_non_empty_string() {
159
+ # Return 0 when a typed value is a string carrying a non-space character.
160
+ #
161
+ # Args: $1 = type tag, $2 = raw value. Mirrors is_non_empty_string.
162
+ [[ $1 == str ]] || return 1
163
+ local stripped="${2-}"
164
+ # A left strip is sufficient: a value that is entirely whitespace becomes
165
+ # empty, which is exactly what Python's two-sided strip would report.
166
+ stripped=${stripped#"${stripped%%[![:space:]]*}"}
167
+ [[ -n $stripped ]]
168
+ }
169
+
170
+ pc_is_integer() {
171
+ # Return 0 when a typed value is a genuine integer rather than a boolean.
172
+ #
173
+ # Args: $1 = type tag. The parser classifies booleans as `bool`, never as
174
+ # `int`, so the boolean exclusion that Python needs is structural here.
175
+ [[ $1 == int ]]
176
+ }
177
+
178
+ pc_is_positive_integer() {
179
+ # Return 0 when a typed value is an integer greater than zero.
180
+ #
181
+ # Args: $1 = type tag, $2 = raw value.
182
+ [[ $1 == int ]] || return 1
183
+ local number="$2"
184
+ ((number > 0))
185
+ }
186
+
187
+ pc_is_non_negative_integer() {
188
+ # Return 0 when a typed value is an integer of zero or more.
189
+ #
190
+ # Args: $1 = type tag, $2 = raw value.
191
+ [[ $1 == int ]] || return 1
192
+ local number="$2"
193
+ ((number >= 0))
194
+ }
195
+
196
+ pc_in_bounded_range() {
197
+ # Return 0 when a typed value is an integer inside an inclusive range.
198
+ #
199
+ # Args: $1 = type tag, $2 = raw value, $3 = minimum, $4 = maximum.
200
+ [[ $1 == int ]] || return 1
201
+ local number="$2" minimum="$3" maximum="$4"
202
+ ((number >= minimum && number <= maximum))
203
+ }
204
+
205
+ pc_enum_error() {
206
+ # Echo the standard out-of-enum error string for a field.
207
+ #
208
+ # Args: $1 = context prefix, $2 = dotted field name, $3 = comma-and-space
209
+ # joined member list in canonical order, $4 = the already-rendered repr of
210
+ # the offending value. Mirrors enum_error so the wording cannot drift.
211
+ printf '%s %s must be one of %s; found: %s.' "$1" "$2" "$3" "$4"
212
+ }
213
+
214
+ pc_item_context() {
215
+ # Echo the context prefix for one items[] entry.
216
+ #
217
+ # Args: $1 = surface prefix, $2 = zero-based entry index. The positional
218
+ # index is used rather than issue_num because it exists for every entry,
219
+ # including one whose issue_num is missing.
220
+ printf '%s items[%s]' "$1" "$2"
221
+ }
222
+
223
+ pc_contains_word() {
224
+ # Return 0 when a space-separated list contains an exact word.
225
+ #
226
+ # Args: $1 = space-separated haystack, $2 = needle. Used for the enum
227
+ # membership tests whose member lists are stored space separated.
228
+ local haystack=" $1 "
229
+ [[ $haystack == *" $2 "* ]]
230
+ }
231
+
232
+ pc_enum_members_contains() {
233
+ # Return 0 when a comma-and-space joined enum list contains an exact member.
234
+ #
235
+ # Args: $1 = the rendered member list, $2 = candidate member.
236
+ local haystack="${1//, / }"
237
+ pc_contains_word "$haystack" "$2"
238
+ }