@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.
- package/out/mcp-server.js +1730 -139
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
- package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +257 -0
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +183 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
- package/resources/claude-customizations/.claude/lib/bash/compute-cohorts.sh +143 -0
- package/resources/claude-customizations/.claude/lib/bash/compute-concurrency-batches.sh +122 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +330 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-common.sh +238 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-items-validate.sh +244 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +187 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-emit.sh +340 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-scan.sh +335 -0
- package/resources/claude-customizations/.claude/lib/bash/validate-parallel-manifest.sh +134 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
- package/resources/claude-customizations/.claude/rules/shell.md +7 -2
- package/resources/claude-customizations/.claude/settings.json +28 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +152 -0
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +971 -0
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +461 -0
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
- package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
- package/resources/claude-customizations/config/blast-radius.json +16 -0
- package/resources/claude-customizations/config/orchestration-routing.json +355 -0
- package/resources/claude-customizations/pack-manifests/core.json +32 -1
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/config/orchestration-routing.json +22 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parallel-items-validate.sh: sourceable bash port of the shared work-item
|
|
3
|
+
# validators in scripts/dev_tools/_parallel_state_common.py -- validate_items,
|
|
4
|
+
# validate_item_record, _validate_merge_status, and validate_blast_radius_block.
|
|
5
|
+
# It reads the node table populated by parallel-yaml-emit.sh and appends errors
|
|
6
|
+
# to the PC_ERRORS accumulator owned by parallel-common.sh.
|
|
7
|
+
#
|
|
8
|
+
# Message order is part of the contract, not an implementation detail. Within
|
|
9
|
+
# one item the order is issue_num, feature_folder, state, kind (when required),
|
|
10
|
+
# merge_status, then the blast-radius block in field order. Across items the
|
|
11
|
+
# order is positional, and every duplicate issue_num is reported after the
|
|
12
|
+
# whole per-entry pass, in ascending key order, so the sequence is reproducible
|
|
13
|
+
# regardless of the order the entries appeared in.
|
|
14
|
+
#
|
|
15
|
+
# The Python module remains the repository authority; this file must reproduce
|
|
16
|
+
# its output byte for byte for every fixture in tests/fixtures/parallel_manifest_bash.
|
|
17
|
+
|
|
18
|
+
# Resolve this file's own directory so its dependencies source regardless of
|
|
19
|
+
# the caller's working directory.
|
|
20
|
+
PI_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
21
|
+
# shellcheck source=.claude/lib/bash/parallel-common.sh
|
|
22
|
+
# shellcheck disable=SC1091
|
|
23
|
+
source "$PI_LIB_DIR/parallel-common.sh"
|
|
24
|
+
# shellcheck source=.claude/lib/bash/parallel-yaml-emit.sh
|
|
25
|
+
# shellcheck disable=SC1091
|
|
26
|
+
source "$PI_LIB_DIR/parallel-yaml-emit.sh"
|
|
27
|
+
|
|
28
|
+
pi_repr_at() {
|
|
29
|
+
# Echo the Python repr of the value stored at a node path.
|
|
30
|
+
#
|
|
31
|
+
# Args: $1 = node path. An unknown path renders as None, matching the
|
|
32
|
+
# Python code's mapping.get default.
|
|
33
|
+
local path="$1" type value
|
|
34
|
+
type=$(yp_type_of "$path")
|
|
35
|
+
value=$(yp_value_of "$path")
|
|
36
|
+
pc_repr "$type" "$value"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pi_is_string_list() {
|
|
40
|
+
# Return 0 when a node is a sequence whose every entry is a non-empty string.
|
|
41
|
+
#
|
|
42
|
+
# An empty sequence satisfies the predicate: every blast-radius collection
|
|
43
|
+
# other than paths is legitimately empty. Mirrors is_string_list.
|
|
44
|
+
#
|
|
45
|
+
# Args: $1 = node path.
|
|
46
|
+
local path="$1"
|
|
47
|
+
[[ $(yp_type_of "$path") == seq ]] || return 1
|
|
48
|
+
local count index entry_type entry_value
|
|
49
|
+
count=$(yp_count_of "$path")
|
|
50
|
+
# A single blank entry fails the whole list, because it would silently
|
|
51
|
+
# widen a radius and under-report contention.
|
|
52
|
+
for ((index = 0; index < count; index++)); do
|
|
53
|
+
entry_type=$(yp_type_of "${path}[${index}]")
|
|
54
|
+
entry_value=$(yp_value_of "${path}[${index}]")
|
|
55
|
+
pc_is_non_empty_string "$entry_type" "$entry_value" || return 1
|
|
56
|
+
done
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pi_validate_blast_radius_block() {
|
|
61
|
+
# Validate one blast_radius block against spec invariant 9.
|
|
62
|
+
#
|
|
63
|
+
# Args: $1 = node path of the block, $2 = item-scoped context prefix.
|
|
64
|
+
local path="$1" context="$2"
|
|
65
|
+
if [[ $(yp_type_of "$path") != map ]]; then
|
|
66
|
+
pc_error_add "$context blast_radius must be an object."
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
local field
|
|
70
|
+
local -a radius_fields=()
|
|
71
|
+
read -ra radius_fields <<<"$PC_BLAST_RADIUS_LIST_FIELDS"
|
|
72
|
+
# Report every malformed collection field rather than stopping at the
|
|
73
|
+
# first, so one validation pass tells the author everything to fix.
|
|
74
|
+
for field in "${radius_fields[@]}"; do
|
|
75
|
+
if ! pi_is_string_list "${path}.${field}"; then
|
|
76
|
+
pc_error_add "$context blast_radius.$field must be a list of non-empty strings."
|
|
77
|
+
fi
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
local source_type source_value
|
|
81
|
+
source_type=$(yp_type_of "${path}.source")
|
|
82
|
+
source_value=$(yp_value_of "${path}.source")
|
|
83
|
+
if [[ $source_type != str ]] || ! pc_enum_members_contains "$PC_VALID_SOURCES" "$source_value"; then
|
|
84
|
+
pc_error_add "$(pc_enum_error "$context" "blast_radius.source" "$PC_VALID_SOURCES" "$(pi_repr_at "${path}.source")")"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
local computed_type computed_value
|
|
88
|
+
computed_type=$(yp_type_of "${path}.computed_at")
|
|
89
|
+
computed_value=$(yp_value_of "${path}.computed_at")
|
|
90
|
+
if ! pc_is_non_empty_string "$computed_type" "$computed_value"; then
|
|
91
|
+
pc_error_add "$context blast_radius.computed_at must be a non-empty string."
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pi_validate_merge_status() {
|
|
96
|
+
# Validate merge_status membership and its agreement with item state.
|
|
97
|
+
#
|
|
98
|
+
# Absence is the backward-compatible case: an item with no merge_status is
|
|
99
|
+
# treated as not_started and yields no error, so the check is presence
|
|
100
|
+
# gated rather than requirement gated.
|
|
101
|
+
#
|
|
102
|
+
# Args: $1 = item node path, $2 = item-scoped context, $3 = state repr,
|
|
103
|
+
# $4 = raw state value.
|
|
104
|
+
local path="$1" context="$2" state_repr="$3" state_value="$4"
|
|
105
|
+
yp_has "${path}.merge_status" || return 0
|
|
106
|
+
local status_type status_value
|
|
107
|
+
status_type=$(yp_type_of "${path}.merge_status")
|
|
108
|
+
status_value=$(yp_value_of "${path}.merge_status")
|
|
109
|
+
# An out-of-enum value short-circuits the consistency rule, because
|
|
110
|
+
# agreement with state is meaningless for a value that is not a status.
|
|
111
|
+
if [[ $status_type != str ]] || ! pc_enum_members_contains "$PC_VALID_MERGE_STATUS" "$status_value"; then
|
|
112
|
+
pc_error_add "$(pc_enum_error "$context" "merge_status" "$PC_VALID_MERGE_STATUS" "$(pi_repr_at "${path}.merge_status")")"
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Invariant 8 pins the two terminal families to their item states; every
|
|
117
|
+
# other status places no constraint on state.
|
|
118
|
+
if pc_contains_word "$PC_MERGED_MERGE_STATUSES" "$status_value" && [[ $state_value != merged ]]; then
|
|
119
|
+
pc_error_add "$context merge_status $(pc_repr_string "$status_value") requires state 'merged'; found: $state_repr."
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
if pc_contains_word "$PC_BLOCKED_MERGE_STATUSES" "$status_value" && [[ $state_value != blocked ]]; then
|
|
123
|
+
pc_error_add "$context merge_status $(pc_repr_string "$status_value") requires state 'blocked'; found: $state_repr."
|
|
124
|
+
fi
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pi_validate_item_record() {
|
|
128
|
+
# Validate one work-item record against spec invariants 5 through 9.
|
|
129
|
+
#
|
|
130
|
+
# Args: $1 = item node path, $2 = item-scoped context prefix, $3 = 1 when
|
|
131
|
+
# kind is required (the manifest and planner surfaces carry it).
|
|
132
|
+
local path="$1" context="$2" require_kind="$3"
|
|
133
|
+
if [[ $(yp_type_of "$path") != map ]]; then
|
|
134
|
+
pc_error_add "$context must be an object."
|
|
135
|
+
return 0
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
local issue_type issue_value
|
|
139
|
+
issue_type=$(yp_type_of "${path}.issue_num")
|
|
140
|
+
issue_value=$(yp_value_of "${path}.issue_num")
|
|
141
|
+
if ! pc_is_positive_integer "$issue_type" "$issue_value"; then
|
|
142
|
+
pc_error_add "$context issue_num must be a positive integer; found: $(pi_repr_at "${path}.issue_num")."
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
local folder_type folder_value
|
|
146
|
+
folder_type=$(yp_type_of "${path}.feature_folder")
|
|
147
|
+
folder_value=$(yp_value_of "${path}.feature_folder")
|
|
148
|
+
if ! pc_is_non_empty_string "$folder_type" "$folder_value"; then
|
|
149
|
+
pc_error_add "$context feature_folder must be a non-empty string."
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
local state_type state_value state_repr
|
|
153
|
+
state_type=$(yp_type_of "${path}.state")
|
|
154
|
+
state_value=$(yp_value_of "${path}.state")
|
|
155
|
+
state_repr=$(pi_repr_at "${path}.state")
|
|
156
|
+
if [[ $state_type != str ]] || ! pc_enum_members_contains "$PC_VALID_ITEM_STATES" "$state_value"; then
|
|
157
|
+
pc_error_add "$(pc_enum_error "$context" "state" "$PC_VALID_ITEM_STATES" "$state_repr")"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if ((require_kind == 1)); then
|
|
161
|
+
local kind_type kind_value
|
|
162
|
+
kind_type=$(yp_type_of "${path}.kind")
|
|
163
|
+
kind_value=$(yp_value_of "${path}.kind")
|
|
164
|
+
if [[ $kind_type != str ]] || ! pc_enum_members_contains "$PC_VALID_KINDS" "$kind_value"; then
|
|
165
|
+
pc_error_add "$(pc_enum_error "$context" "kind" "$PC_VALID_KINDS" "$(pi_repr_at "${path}.kind")")"
|
|
166
|
+
fi
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
pi_validate_merge_status "$path" "$context" "$state_repr" "$state_value"
|
|
170
|
+
pi_validate_blast_radius_block "${path}.blast_radius" "$context"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pi_validate_items() {
|
|
174
|
+
# Validate the items collection, including issue_num uniqueness.
|
|
175
|
+
#
|
|
176
|
+
# Args: $1 = node path of the items collection, $2 = surface prefix,
|
|
177
|
+
# $3 = 1 when kind is required on every entry.
|
|
178
|
+
local path="$1" context="$2" require_kind="$3"
|
|
179
|
+
if [[ $(yp_type_of "$path") != seq ]]; then
|
|
180
|
+
pc_error_add "$context items must be a list."
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
local count index entry_path item_ctx issue_type issue_value
|
|
185
|
+
local seen=""
|
|
186
|
+
local -a duplicates=()
|
|
187
|
+
count=$(yp_count_of "$path")
|
|
188
|
+
# Validate each entry in place, and in the same pass accumulate the primary
|
|
189
|
+
# keys so uniqueness is decided without a second traversal.
|
|
190
|
+
for ((index = 0; index < count; index++)); do
|
|
191
|
+
entry_path="${path}[${index}]"
|
|
192
|
+
item_ctx=$(pc_item_context "$context" "$index")
|
|
193
|
+
pi_validate_item_record "$entry_path" "$item_ctx" "$require_kind"
|
|
194
|
+
[[ $(yp_type_of "$entry_path") == map ]] || continue
|
|
195
|
+
issue_type=$(yp_type_of "${entry_path}.issue_num")
|
|
196
|
+
issue_value=$(yp_value_of "${entry_path}.issue_num")
|
|
197
|
+
[[ $issue_type == int ]] || continue
|
|
198
|
+
if pc_contains_word "$seen" "$issue_value"; then
|
|
199
|
+
duplicates+=("$issue_value")
|
|
200
|
+
fi
|
|
201
|
+
seen="$seen $issue_value"
|
|
202
|
+
done
|
|
203
|
+
|
|
204
|
+
((${#duplicates[@]} > 0)) || return 0
|
|
205
|
+
# Report duplicates in ascending numeric order, de-duplicated, so a key
|
|
206
|
+
# repeated three times still yields exactly one message.
|
|
207
|
+
local ordered duplicate
|
|
208
|
+
ordered=$(printf '%s\n' "${duplicates[@]}" | sort -n -u)
|
|
209
|
+
while IFS= read -r duplicate; do
|
|
210
|
+
[[ -n $duplicate ]] || continue
|
|
211
|
+
pc_error_add "$context has duplicate items[].issue_num: $duplicate."
|
|
212
|
+
done <<<"$ordered"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pi_scan_prohibited_keys() {
|
|
216
|
+
# Reject prohibited keys per manifest invariant M7.
|
|
217
|
+
#
|
|
218
|
+
# Args: $1 = surface prefix, $2 = space-separated keys rejected at any
|
|
219
|
+
# nesting level, $3 = space-separated keys rejected at the document root
|
|
220
|
+
# only. Deep results come first, in document order, then the top-level
|
|
221
|
+
# results in the order the third argument lists them.
|
|
222
|
+
local context="$1" deep_keys="$2" top_keys="$3"
|
|
223
|
+
local index path key total=${#YP_ORDER[@]}
|
|
224
|
+
# Walk the node table in document order so the message sequence reproduces
|
|
225
|
+
# Python's depth-first, insertion-ordered traversal exactly.
|
|
226
|
+
for ((index = 0; index < total; index++)); do
|
|
227
|
+
path="${YP_ORDER[index]}"
|
|
228
|
+
key="${YP_KEY[$path]-}"
|
|
229
|
+
[[ -n $key ]] || continue
|
|
230
|
+
if pc_contains_word "$deep_keys" "$key"; then
|
|
231
|
+
pc_error_add "$context carries prohibited key '$key' at ${YP_PARENT[$path]}."
|
|
232
|
+
fi
|
|
233
|
+
done
|
|
234
|
+
|
|
235
|
+
# The shallow pass exists because manifest M7 bans integration_branch at
|
|
236
|
+
# the top level only, where a nested occurrence is legitimate child data.
|
|
237
|
+
local -a top_key_list=()
|
|
238
|
+
read -ra top_key_list <<<"$top_keys"
|
|
239
|
+
for key in "${top_key_list[@]}"; do
|
|
240
|
+
if yp_has "$key"; then
|
|
241
|
+
pc_error_add "$context carries prohibited key '$key' at $PC_ROOT_PATH."
|
|
242
|
+
fi
|
|
243
|
+
done
|
|
244
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parallel-manifest-validate.sh: sourceable bash port of
|
|
3
|
+
# scripts/dev_tools/parallel_manifest_contract.py. Validates a parallel-run
|
|
4
|
+
# manifest document against invariants M1 through M7 and exposes the two
|
|
5
|
+
# default-resolving accessors that every consumer uses instead of reading
|
|
6
|
+
# `mode` and `max_concurrency` directly.
|
|
7
|
+
#
|
|
8
|
+
# Section order is load-bearing and mirrors the Python module exactly: the M1
|
|
9
|
+
# frontmatter checks short-circuit with a single error; then run identity
|
|
10
|
+
# (M2 parallel, M3 mode, M4 max_concurrency, M5 created_at) in schema field
|
|
11
|
+
# order; then the M7 prohibited-key scan, deep results in document order
|
|
12
|
+
# followed by the top-level results; then the M6 items collection.
|
|
13
|
+
#
|
|
14
|
+
# Every error string begins with the literal prefix `Parallel manifest` and
|
|
15
|
+
# ends with a period. The Python module remains the repository authority.
|
|
16
|
+
#
|
|
17
|
+
# Out-of-subset input. When the YAML scanner refuses a construct it does not
|
|
18
|
+
# model, this module returns exit status 2 with the refusal detail in
|
|
19
|
+
# PM_SUBSET_DETAIL and emits no validation errors. Refusing to answer is
|
|
20
|
+
# deliberate: a guessed parse could disagree with the Python authority
|
|
21
|
+
# silently, whereas an explicit refusal is visible to the caller.
|
|
22
|
+
#
|
|
23
|
+
# shellcheck disable=SC2034
|
|
24
|
+
# SC2034 is disabled file-wide because PM_SUBSET_DETAIL is written here and
|
|
25
|
+
# read by the entry point validate-parallel-manifest.sh, which shellcheck
|
|
26
|
+
# analyses as a separate file and therefore cannot see the use.
|
|
27
|
+
|
|
28
|
+
# Resolve this file's own directory so its dependencies source regardless of
|
|
29
|
+
# the caller's working directory.
|
|
30
|
+
PM_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
31
|
+
# shellcheck source=.claude/lib/bash/parallel-items-validate.sh
|
|
32
|
+
# shellcheck disable=SC1091
|
|
33
|
+
source "$PM_LIB_DIR/parallel-items-validate.sh"
|
|
34
|
+
|
|
35
|
+
# Literal context prefix for every error this module and its helpers emit.
|
|
36
|
+
PM_CONTEXT="Parallel manifest"
|
|
37
|
+
|
|
38
|
+
# Documented default run mode when the manifest omits `mode` (invariant M3).
|
|
39
|
+
PM_DEFAULT_MODE="closed"
|
|
40
|
+
|
|
41
|
+
# Documented default fan-out when the manifest omits `max_concurrency` (M4).
|
|
42
|
+
PM_DEFAULT_MAX_CONCURRENCY=4
|
|
43
|
+
|
|
44
|
+
# Inclusive bounds on a present `max_concurrency` (invariant M4, A7).
|
|
45
|
+
PM_MIN_CONCURRENCY=1
|
|
46
|
+
PM_MAX_CONCURRENCY=8
|
|
47
|
+
|
|
48
|
+
# Keys the manifest rejects at any nesting level (invariant M7).
|
|
49
|
+
PM_DEEP_PROHIBITED_KEYS="depends_on"
|
|
50
|
+
|
|
51
|
+
# Keys the manifest rejects at the document root only (invariant M7).
|
|
52
|
+
PM_TOP_LEVEL_PROHIBITED_KEYS="integration_branch"
|
|
53
|
+
|
|
54
|
+
# Detail recorded when the parser refuses an out-of-subset construct.
|
|
55
|
+
PM_SUBSET_DETAIL=""
|
|
56
|
+
|
|
57
|
+
pm_parse_manifest() {
|
|
58
|
+
# Parse manifest text into the shared node table (invariant M1).
|
|
59
|
+
#
|
|
60
|
+
# Args: $1 = raw manifest document text.
|
|
61
|
+
# Returns 0 when the frontmatter parsed into a mapping. Returns 1 after
|
|
62
|
+
# appending the single M1 error to PC_ERRORS for a missing, unterminated,
|
|
63
|
+
# unparseable, or non-mapping block. Returns 2 without appending anything
|
|
64
|
+
# when the document uses a construct outside the supported subset.
|
|
65
|
+
local text="$1"
|
|
66
|
+
if ! yp_extract_frontmatter_body "$text" "$PM_CONTEXT"; then
|
|
67
|
+
pc_error_add "$YP_FENCE_ERROR"
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
if yp_parse_body; then
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
# Routing table for the three parser failure modes: a sequence or empty
|
|
74
|
+
# root is M1's non-mapping case, a malformed line is M1's YAML-error case,
|
|
75
|
+
# and an unsupported construct makes the module refuse to answer.
|
|
76
|
+
case "$YP_STATUS" in
|
|
77
|
+
not_a_mapping)
|
|
78
|
+
pc_error_add "$PM_CONTEXT frontmatter must be a mapping."
|
|
79
|
+
return 1
|
|
80
|
+
;;
|
|
81
|
+
yaml_error)
|
|
82
|
+
pc_error_add "$PM_CONTEXT frontmatter is not valid YAML: $YP_DETAIL."
|
|
83
|
+
return 1
|
|
84
|
+
;;
|
|
85
|
+
*)
|
|
86
|
+
PM_SUBSET_DETAIL="$YP_DETAIL"
|
|
87
|
+
return 2
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pm_validate_identity() {
|
|
93
|
+
# Validate run identity fields against invariants M2 through M5.
|
|
94
|
+
#
|
|
95
|
+
# The mode and max_concurrency checks are presence gated because both keys
|
|
96
|
+
# are optional: absence is the documented authoring shape that the
|
|
97
|
+
# accessors resolve, so it must contribute no error.
|
|
98
|
+
local slug_type slug_value
|
|
99
|
+
slug_type=$(yp_type_of "parallel")
|
|
100
|
+
slug_value=$(yp_value_of "parallel")
|
|
101
|
+
if ! pc_is_non_empty_string "$slug_type" "$slug_value"; then
|
|
102
|
+
pc_error_add "$PM_CONTEXT parallel must be a non-empty string."
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if yp_has "mode"; then
|
|
106
|
+
local mode_type mode_value
|
|
107
|
+
mode_type=$(yp_type_of "mode")
|
|
108
|
+
mode_value=$(yp_value_of "mode")
|
|
109
|
+
if [[ $mode_type != str ]] || ! pc_enum_members_contains "$PC_VALID_MODES" "$mode_value"; then
|
|
110
|
+
pc_error_add "$(pc_enum_error "$PM_CONTEXT" "mode" "$PC_VALID_MODES" "$(pi_repr_at "mode")")"
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if yp_has "max_concurrency"; then
|
|
115
|
+
local cap_type cap_value
|
|
116
|
+
cap_type=$(yp_type_of "max_concurrency")
|
|
117
|
+
cap_value=$(yp_value_of "max_concurrency")
|
|
118
|
+
if ! pc_in_bounded_range "$cap_type" "$cap_value" "$PM_MIN_CONCURRENCY" "$PM_MAX_CONCURRENCY"; then
|
|
119
|
+
pc_error_add "$PM_CONTEXT max_concurrency must be an integer from $PM_MIN_CONCURRENCY through $PM_MAX_CONCURRENCY; found: $(pi_repr_at "max_concurrency")."
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
local created_type created_value
|
|
124
|
+
created_type=$(yp_type_of "created_at")
|
|
125
|
+
created_value=$(yp_value_of "created_at")
|
|
126
|
+
if ! pc_is_non_empty_string "$created_type" "$created_value"; then
|
|
127
|
+
pc_error_add "$PM_CONTEXT created_at must be a non-empty string."
|
|
128
|
+
fi
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pm_validate_text() {
|
|
132
|
+
# Validate a manifest document against invariants M1 to M7.
|
|
133
|
+
#
|
|
134
|
+
# Args: $1 = raw manifest document text, authored with LF, CRLF, or CR
|
|
135
|
+
# line endings.
|
|
136
|
+
# Returns 0 after populating PC_ERRORS with the complete error list, which
|
|
137
|
+
# is empty for a valid manifest. Returns 2 without populating PC_ERRORS
|
|
138
|
+
# when the document uses a construct outside the supported subset.
|
|
139
|
+
local text="$1" parse_status=0
|
|
140
|
+
pc_errors_reset
|
|
141
|
+
PM_SUBSET_DETAIL=""
|
|
142
|
+
pm_parse_manifest "$text" || parse_status=$?
|
|
143
|
+
# An out-of-subset refusal propagates so the caller can distinguish it from
|
|
144
|
+
# a validation verdict; an M1 failure returns the single-element error list
|
|
145
|
+
# already accumulated, because no field check is meaningful without a
|
|
146
|
+
# mapping to read fields from.
|
|
147
|
+
if ((parse_status == 2)); then
|
|
148
|
+
return 2
|
|
149
|
+
fi
|
|
150
|
+
if ((parse_status != 0)); then
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
pm_validate_identity
|
|
155
|
+
pi_scan_prohibited_keys "$PM_CONTEXT" "$PM_DEEP_PROHIBITED_KEYS" "$PM_TOP_LEVEL_PROHIBITED_KEYS"
|
|
156
|
+
# The manifest carries `kind` on every item, unlike the orchestrator
|
|
157
|
+
# checkpoint, so the shared item validator is asked to require it (S1).
|
|
158
|
+
pi_validate_items "items" "$PM_CONTEXT" 1
|
|
159
|
+
return 0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pm_manifest_mode() {
|
|
163
|
+
# Echo the run mode declared by the parsed manifest (invariant M3).
|
|
164
|
+
#
|
|
165
|
+
# A present but non-string value resolves to the default so this accessor
|
|
166
|
+
# always returns a mode; the malformed value is reported separately by
|
|
167
|
+
# pm_validate_text.
|
|
168
|
+
if [[ $(yp_type_of "mode") == str ]]; then
|
|
169
|
+
yp_value_of "mode"
|
|
170
|
+
else
|
|
171
|
+
printf '%s' "$PM_DEFAULT_MODE"
|
|
172
|
+
fi
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pm_manifest_max_concurrency() {
|
|
176
|
+
# Echo the fan-out cap declared by the parsed manifest (invariant M4).
|
|
177
|
+
#
|
|
178
|
+
# A present but non-integer value resolves to the default so this accessor
|
|
179
|
+
# always returns an integer; the malformed value is reported separately by
|
|
180
|
+
# pm_validate_text. Booleans are classified as `bool` by the scanner and
|
|
181
|
+
# therefore never satisfy the integer test.
|
|
182
|
+
if [[ $(yp_type_of "max_concurrency") == int ]]; then
|
|
183
|
+
yp_value_of "max_concurrency"
|
|
184
|
+
else
|
|
185
|
+
printf '%s' "$PM_DEFAULT_MAX_CONCURRENCY"
|
|
186
|
+
fi
|
|
187
|
+
}
|