@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,340 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parallel-yaml-emit.sh: structural half of the restricted block-YAML parser.
|
|
3
|
+
# Walks the indentation of the frontmatter body lines produced by
|
|
4
|
+
# parallel-yaml-scan.sh and populates that module's node table in document
|
|
5
|
+
# order. Sourcing this file also sources the scanning half, so a consumer needs
|
|
6
|
+
# only this one source line.
|
|
7
|
+
#
|
|
8
|
+
# Algorithm. A stack of open containers is kept, each carrying the indent
|
|
9
|
+
# column its children sit at, its rendered path, whether it is a mapping or a
|
|
10
|
+
# sequence, and how many children it has taken. A `key:` line with no inline
|
|
11
|
+
# value opens a pending node whose kind is not yet known: the next content line
|
|
12
|
+
# decides it, becoming a mapping or sequence when it is indented deeper, and
|
|
13
|
+
# leaving the pending node a null scalar when it is not. That one-line lookahead
|
|
14
|
+
# is the whole of the parser's ambiguity handling.
|
|
15
|
+
#
|
|
16
|
+
# Failure modes are the two the scanning half defines. A malformed line sets
|
|
17
|
+
# YP_STATUS=yaml_error and is rendered by the caller through the M1 message; a
|
|
18
|
+
# construct that is valid YAML but outside the subset sets
|
|
19
|
+
# YP_STATUS=out_of_subset and makes the caller refuse to answer rather than
|
|
20
|
+
# risk disagreeing with the Python authority.
|
|
21
|
+
#
|
|
22
|
+
# shellcheck disable=SC2034
|
|
23
|
+
# SC2034 is disabled file-wide because the node-table and status globals this
|
|
24
|
+
# module writes -- YP_TYPE, YP_COUNT, YP_STATUS -- are declared in
|
|
25
|
+
# parallel-yaml-scan.sh and read by parallel-items-validate.sh and
|
|
26
|
+
# parallel-manifest-validate.sh, which shellcheck analyses as separate files.
|
|
27
|
+
|
|
28
|
+
# Resolve this file's own directory so the scanning half sources regardless of
|
|
29
|
+
# the caller's working directory.
|
|
30
|
+
YP_EMIT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
|
31
|
+
# shellcheck source=.claude/lib/bash/parallel-yaml-scan.sh
|
|
32
|
+
# shellcheck disable=SC1091
|
|
33
|
+
source "$YP_EMIT_DIR/parallel-yaml-scan.sh"
|
|
34
|
+
|
|
35
|
+
# Path label for the document root in key-scoped errors.
|
|
36
|
+
YP_ROOT_LABEL="<root>"
|
|
37
|
+
|
|
38
|
+
# Open-container stack, parallel arrays indexed by depth.
|
|
39
|
+
YP_STACK_INDENT=()
|
|
40
|
+
YP_STACK_PATH=()
|
|
41
|
+
YP_STACK_KIND=()
|
|
42
|
+
YP_STACK_COUNT=()
|
|
43
|
+
|
|
44
|
+
# The pending node opened by a `key:` or bare `-` line, if any.
|
|
45
|
+
YP_PENDING_PATH=""
|
|
46
|
+
YP_PENDING_INDENT=-1
|
|
47
|
+
YP_PENDING_ACTIVE=0
|
|
48
|
+
|
|
49
|
+
yp_stack_reset() {
|
|
50
|
+
# Clear the container stack and the pending-node slot.
|
|
51
|
+
YP_STACK_INDENT=()
|
|
52
|
+
YP_STACK_PATH=()
|
|
53
|
+
YP_STACK_KIND=()
|
|
54
|
+
YP_STACK_COUNT=()
|
|
55
|
+
YP_PENDING_PATH=""
|
|
56
|
+
YP_PENDING_INDENT=-1
|
|
57
|
+
YP_PENDING_ACTIVE=0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
yp_stack_push() {
|
|
61
|
+
# Push one open container onto the stack.
|
|
62
|
+
#
|
|
63
|
+
# Args: $1 = indent column of the container's children, $2 = rendered path,
|
|
64
|
+
# $3 = kind (map or seq).
|
|
65
|
+
YP_STACK_INDENT+=("$1")
|
|
66
|
+
YP_STACK_PATH+=("$2")
|
|
67
|
+
YP_STACK_KIND+=("$3")
|
|
68
|
+
YP_STACK_COUNT+=(0)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
yp_stack_depth() {
|
|
72
|
+
# Echo the number of open containers.
|
|
73
|
+
printf '%s' "${#YP_STACK_INDENT[@]}"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
yp_stack_pop() {
|
|
77
|
+
# Discard the innermost open container by truncating every stack array.
|
|
78
|
+
local keep=$((${#YP_STACK_INDENT[@]} - 1))
|
|
79
|
+
((keep >= 0)) || return 0
|
|
80
|
+
YP_STACK_INDENT=("${YP_STACK_INDENT[@]:0:keep}")
|
|
81
|
+
YP_STACK_PATH=("${YP_STACK_PATH[@]:0:keep}")
|
|
82
|
+
YP_STACK_KIND=("${YP_STACK_KIND[@]:0:keep}")
|
|
83
|
+
YP_STACK_COUNT=("${YP_STACK_COUNT[@]:0:keep}")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
yp_stack_bump() {
|
|
87
|
+
# Increment the innermost container's child count and mirror it into the
|
|
88
|
+
# node table so yp_count_of reports sequence lengths and key counts.
|
|
89
|
+
local last=$((${#YP_STACK_INDENT[@]} - 1))
|
|
90
|
+
((last >= 0)) || return 0
|
|
91
|
+
YP_STACK_COUNT[last]=$((YP_STACK_COUNT[last] + 1))
|
|
92
|
+
local path="${YP_STACK_PATH[last]}"
|
|
93
|
+
if [[ -n $path ]]; then
|
|
94
|
+
YP_COUNT["$path"]=${YP_STACK_COUNT[last]}
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
yp_render_child_path() {
|
|
99
|
+
# Echo the path of a mapping key inside a parent container.
|
|
100
|
+
#
|
|
101
|
+
# Args: $1 = parent path (empty at the document root), $2 = key name.
|
|
102
|
+
if [[ -z $1 ]]; then
|
|
103
|
+
printf '%s' "$2"
|
|
104
|
+
else
|
|
105
|
+
printf '%s.%s' "$1" "$2"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
yp_render_parent_label() {
|
|
110
|
+
# Echo the path label Python uses for the mapping that owns a key.
|
|
111
|
+
#
|
|
112
|
+
# Args: $1 = parent path. The document root renders as `<root>` so
|
|
113
|
+
# prohibited-key errors reproduce the Python wording exactly.
|
|
114
|
+
if [[ -z $1 ]]; then
|
|
115
|
+
printf '%s' "$YP_ROOT_LABEL"
|
|
116
|
+
else
|
|
117
|
+
printf '%s' "$1"
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
yp_open_pending() {
|
|
122
|
+
# Mark a node as awaiting the next line to decide its kind.
|
|
123
|
+
#
|
|
124
|
+
# Args: $1 = node path, $2 = indent column of the line that opened it.
|
|
125
|
+
YP_PENDING_PATH="$1"
|
|
126
|
+
YP_PENDING_INDENT="$2"
|
|
127
|
+
YP_PENDING_ACTIVE=1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
yp_resolve_pending() {
|
|
131
|
+
# Decide the kind of a pending node using the next content line.
|
|
132
|
+
#
|
|
133
|
+
# Args: $1 = indent of the next content line, $2 = 1 when that line is a
|
|
134
|
+
# sequence entry. Returns 0 always; the pending slot is cleared.
|
|
135
|
+
local indent="$1" is_seq="$2"
|
|
136
|
+
((YP_PENDING_ACTIVE == 1)) || return 0
|
|
137
|
+
# A deeper line, or a sequence entry at the same column, opens a container;
|
|
138
|
+
# anything else means the key carried no value and reads as null.
|
|
139
|
+
if ((indent > YP_PENDING_INDENT)) || ((indent == YP_PENDING_INDENT && is_seq == 1)); then
|
|
140
|
+
local kind="map"
|
|
141
|
+
((is_seq == 1)) && kind="seq"
|
|
142
|
+
YP_TYPE["$YP_PENDING_PATH"]="$kind"
|
|
143
|
+
YP_COUNT["$YP_PENDING_PATH"]=0
|
|
144
|
+
yp_stack_push "$indent" "$YP_PENDING_PATH" "$kind"
|
|
145
|
+
else
|
|
146
|
+
YP_TYPE["$YP_PENDING_PATH"]="null"
|
|
147
|
+
fi
|
|
148
|
+
YP_PENDING_ACTIVE=0
|
|
149
|
+
YP_PENDING_PATH=""
|
|
150
|
+
YP_PENDING_INDENT=-1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
yp_emit_map_entry() {
|
|
154
|
+
# Parse one `key: value` or `key:` line into the node table.
|
|
155
|
+
#
|
|
156
|
+
# Args: $1 = owning mapping path (empty at the root), $2 = line content
|
|
157
|
+
# with indentation stripped, $3 = indent column of the line.
|
|
158
|
+
# Returns 0 on success, 1 with YP_STATUS set on a malformed or refused line.
|
|
159
|
+
local parent="$1" content="$2" indent="$3"
|
|
160
|
+
local key rest
|
|
161
|
+
if [[ $content == *": "* ]]; then
|
|
162
|
+
key=${content%%": "*}
|
|
163
|
+
rest=$(yp_trim "${content#*": "}")
|
|
164
|
+
elif [[ $content == *":" ]]; then
|
|
165
|
+
key=${content%":"}
|
|
166
|
+
rest=""
|
|
167
|
+
else
|
|
168
|
+
yp_yaml_error "line is neither a mapping entry nor a sequence entry: $content"
|
|
169
|
+
return 1
|
|
170
|
+
fi
|
|
171
|
+
# Keys in a machine-authored manifest are bare identifiers; a quoted or
|
|
172
|
+
# otherwise exotic key is refused rather than guessed at.
|
|
173
|
+
if [[ ! $key =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
|
|
174
|
+
yp_reject "mapping keys must be bare identifiers; found: $key"
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
local path parent_label
|
|
178
|
+
path=$(yp_render_child_path "$parent" "$key")
|
|
179
|
+
parent_label=$(yp_render_parent_label "$parent")
|
|
180
|
+
|
|
181
|
+
# An inline empty flow collection is a complete container; an empty value
|
|
182
|
+
# defers the decision to the next line; anything else is a scalar.
|
|
183
|
+
if [[ $rest == "[]" ]]; then
|
|
184
|
+
yp_node_add "$path" "seq" "" "$parent_label" "$key"
|
|
185
|
+
elif [[ $rest == "{}" ]]; then
|
|
186
|
+
yp_node_add "$path" "map" "" "$parent_label" "$key"
|
|
187
|
+
elif [[ -z $rest && $content == *":" ]]; then
|
|
188
|
+
yp_node_add "$path" "null" "" "$parent_label" "$key"
|
|
189
|
+
yp_open_pending "$path" "$indent"
|
|
190
|
+
else
|
|
191
|
+
yp_classify_scalar "$rest" || return 1
|
|
192
|
+
yp_node_add "$path" "$YP_SCALAR_TYPE" "$YP_SCALAR_VALUE" "$parent_label" "$key"
|
|
193
|
+
fi
|
|
194
|
+
yp_stack_bump
|
|
195
|
+
return 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
yp_emit_seq_entry() {
|
|
199
|
+
# Parse one `- ...` line into the node table.
|
|
200
|
+
#
|
|
201
|
+
# Args: $1 = owning sequence path, $2 = the sequence index to use, $3 = the
|
|
202
|
+
# text after the dash with surrounding whitespace trimmed, $4 = indent
|
|
203
|
+
# column of the dash. Returns 0 on success, 1 with YP_STATUS set otherwise.
|
|
204
|
+
local parent="$1" index="$2" rest="$3" indent="$4"
|
|
205
|
+
local path="${parent}[${index}]"
|
|
206
|
+
|
|
207
|
+
# A bare dash defers to the next line; a `key: value` payload opens a
|
|
208
|
+
# compact mapping whose entries sit two columns right of the dash; anything
|
|
209
|
+
# else is a scalar element.
|
|
210
|
+
if [[ -z $rest ]]; then
|
|
211
|
+
yp_node_add "$path" "null" "" "" ""
|
|
212
|
+
yp_open_pending "$path" "$indent"
|
|
213
|
+
return 0
|
|
214
|
+
fi
|
|
215
|
+
if [[ $rest == *": "* || $rest == *":" ]]; then
|
|
216
|
+
yp_node_add "$path" "map" "" "" ""
|
|
217
|
+
yp_stack_push $((indent + 2)) "$path" "map"
|
|
218
|
+
yp_emit_map_entry "$path" "$rest" $((indent + 2)) || return 1
|
|
219
|
+
return 0
|
|
220
|
+
fi
|
|
221
|
+
yp_classify_scalar "$rest" || return 1
|
|
222
|
+
yp_node_add "$path" "$YP_SCALAR_TYPE" "$YP_SCALAR_VALUE" "" ""
|
|
223
|
+
return 0
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
yp_scan_line_guards() {
|
|
227
|
+
# Reject the line-level constructs the subset does not model.
|
|
228
|
+
#
|
|
229
|
+
# Args: $1 = the raw line, $2 = the line with indentation stripped.
|
|
230
|
+
# Returns 0 when the line may be parsed, 1 with YP_STATUS set otherwise.
|
|
231
|
+
local line="$1" content="$2"
|
|
232
|
+
if [[ $line == *$'\t'* ]]; then
|
|
233
|
+
yp_yaml_error "tab characters are not permitted in YAML indentation"
|
|
234
|
+
return 1
|
|
235
|
+
fi
|
|
236
|
+
if [[ $content == *' #'* ]]; then
|
|
237
|
+
yp_reject "trailing comments are outside the subset: $content"
|
|
238
|
+
return 1
|
|
239
|
+
fi
|
|
240
|
+
if [[ $content == '---' || $content == '...' ]]; then
|
|
241
|
+
yp_reject "multi-document streams are outside the subset"
|
|
242
|
+
return 1
|
|
243
|
+
fi
|
|
244
|
+
return 0
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
yp_close_deeper_containers() {
|
|
248
|
+
# Pop every open container whose children sit right of the given column.
|
|
249
|
+
#
|
|
250
|
+
# Args: $1 = the current line's indent column.
|
|
251
|
+
local indent="$1" top
|
|
252
|
+
# Unwinding stops at the first container whose child column is at or left
|
|
253
|
+
# of this line, which is the container the line belongs to.
|
|
254
|
+
while ((${#YP_STACK_INDENT[@]} > 0)); do
|
|
255
|
+
top=$((${#YP_STACK_INDENT[@]} - 1))
|
|
256
|
+
((indent < YP_STACK_INDENT[top])) || break
|
|
257
|
+
yp_stack_pop
|
|
258
|
+
done
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
yp_parse_body() {
|
|
262
|
+
# Parse YP_BODY_LINES into the node table.
|
|
263
|
+
#
|
|
264
|
+
# Returns 0 when YP_STATUS is ok. Sets YP_STATUS to not_a_mapping when the
|
|
265
|
+
# body is empty or its root is a sequence, and to yaml_error or
|
|
266
|
+
# out_of_subset for the failure modes the module header documents.
|
|
267
|
+
yp_reset
|
|
268
|
+
yp_stack_reset
|
|
269
|
+
local root_seen=0
|
|
270
|
+
local total=${#YP_BODY_LINES[@]}
|
|
271
|
+
local index line content indent is_seq rest depth seq_index
|
|
272
|
+
|
|
273
|
+
# Walk every body line, resolving the pending node from the previous line
|
|
274
|
+
# before deciding what the current line contributes.
|
|
275
|
+
for ((index = 0; index < total; index++)); do
|
|
276
|
+
line="${YP_BODY_LINES[index]}"
|
|
277
|
+
[[ -n $(yp_trim "$line") ]] || continue
|
|
278
|
+
content=${line#"${line%%[![:space:]]*}"}
|
|
279
|
+
[[ $content == '#'* ]] && continue
|
|
280
|
+
yp_scan_line_guards "$line" "$content" || return 1
|
|
281
|
+
indent=$((${#line} - ${#content}))
|
|
282
|
+
is_seq=0
|
|
283
|
+
[[ $content == "-" || $content == "- "* ]] && is_seq=1
|
|
284
|
+
|
|
285
|
+
yp_resolve_pending "$indent" "$is_seq"
|
|
286
|
+
|
|
287
|
+
# The first content line fixes the document's root kind; a sequence
|
|
288
|
+
# root is reported as not-a-mapping rather than parsed.
|
|
289
|
+
if ((root_seen == 0)); then
|
|
290
|
+
root_seen=1
|
|
291
|
+
if ((is_seq == 1)); then
|
|
292
|
+
YP_STATUS="not_a_mapping"
|
|
293
|
+
return 1
|
|
294
|
+
fi
|
|
295
|
+
((${#YP_STACK_INDENT[@]} > 0)) || yp_stack_push "$indent" "" "map"
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
yp_close_deeper_containers "$indent"
|
|
299
|
+
if ((${#YP_STACK_INDENT[@]} == 0)); then
|
|
300
|
+
yp_yaml_error "indentation closes the document root: $content"
|
|
301
|
+
return 1
|
|
302
|
+
fi
|
|
303
|
+
depth=$((${#YP_STACK_INDENT[@]} - 1))
|
|
304
|
+
if ((indent != YP_STACK_INDENT[depth])); then
|
|
305
|
+
yp_yaml_error "inconsistent indentation at column $indent: $content"
|
|
306
|
+
return 1
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# Route the line against the container it belongs to; a mismatch means
|
|
310
|
+
# the document mixes a sequence entry into a mapping or the reverse.
|
|
311
|
+
if ((is_seq == 1)); then
|
|
312
|
+
if [[ ${YP_STACK_KIND[depth]} != seq ]]; then
|
|
313
|
+
yp_yaml_error "sequence entry inside a mapping: $content"
|
|
314
|
+
return 1
|
|
315
|
+
fi
|
|
316
|
+
rest=$(yp_trim "${content#-}")
|
|
317
|
+
seq_index="${YP_STACK_COUNT[depth]}"
|
|
318
|
+
yp_stack_bump
|
|
319
|
+
yp_emit_seq_entry "${YP_STACK_PATH[depth]}" "$seq_index" "$rest" "$indent" || return 1
|
|
320
|
+
else
|
|
321
|
+
if [[ ${YP_STACK_KIND[depth]} != map ]]; then
|
|
322
|
+
yp_yaml_error "mapping entry inside a sequence: $content"
|
|
323
|
+
return 1
|
|
324
|
+
fi
|
|
325
|
+
yp_emit_map_entry "${YP_STACK_PATH[depth]}" "$content" "$indent" || return 1
|
|
326
|
+
fi
|
|
327
|
+
done
|
|
328
|
+
|
|
329
|
+
# A pending node left open at end of input carried no value.
|
|
330
|
+
if ((YP_PENDING_ACTIVE == 1)); then
|
|
331
|
+
YP_TYPE["$YP_PENDING_PATH"]="null"
|
|
332
|
+
YP_PENDING_ACTIVE=0
|
|
333
|
+
fi
|
|
334
|
+
if ((root_seen == 0)); then
|
|
335
|
+
YP_STATUS="not_a_mapping"
|
|
336
|
+
return 1
|
|
337
|
+
fi
|
|
338
|
+
YP_STATUS="ok"
|
|
339
|
+
return 0
|
|
340
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# parallel-yaml-scan.sh: scanning half of the hand-written parser for the
|
|
3
|
+
# restricted block-YAML subset that a machine-authored parallel-run manifest is
|
|
4
|
+
# written in. It owns line splitting, frontmatter fence extraction, scalar
|
|
5
|
+
# lexing, and the node-table storage and accessors; the structural half that
|
|
6
|
+
# walks indentation and populates the table lives in parallel-yaml-emit.sh,
|
|
7
|
+
# which sources this file. The pair exists so the manifest validator can run in
|
|
8
|
+
# a destination workspace that has no Python, no yq, and no Node; it is
|
|
9
|
+
# deliberately NOT a general YAML implementation and fails closed on every
|
|
10
|
+
# construct outside the subset.
|
|
11
|
+
#
|
|
12
|
+
# The module is split across two files because the combined implementation
|
|
13
|
+
# exceeds the 500-line ceiling in .claude/rules/general-code-change.md.
|
|
14
|
+
#
|
|
15
|
+
# Accepted subset:
|
|
16
|
+
# - block mappings `key: value` and `key:` followed by an indented block,
|
|
17
|
+
# - block sequences `- scalar`, `- key: value` (compact mapping), and `-`
|
|
18
|
+
# followed by an indented block,
|
|
19
|
+
# - two-space indentation steps, spaces only,
|
|
20
|
+
# - empty flow collections `[]` and `{}`,
|
|
21
|
+
# - scalars: null (`~`, `null`, empty), the YAML 1.1 boolean words, decimal
|
|
22
|
+
# integers matching `[-+]?(0|[1-9][0-9]*)`, single-quoted and double-quoted
|
|
23
|
+
# strings without escape sequences, and plain strings,
|
|
24
|
+
# - full-line `#` comments and blank lines.
|
|
25
|
+
#
|
|
26
|
+
# Rejected fail-closed (YP_STATUS=out_of_subset): non-empty flow collections,
|
|
27
|
+
# anchors, aliases, tags, block scalars (`|`, `>`), floats, unquoted dates and
|
|
28
|
+
# timestamps, `.inf`/`.nan`, escape sequences inside double-quoted scalars,
|
|
29
|
+
# trailing comments, and multi-document streams. Rejecting these is the point:
|
|
30
|
+
# silently mis-parsing one would produce a validator verdict that disagrees with
|
|
31
|
+
# the Python authority, which is worse than refusing to answer.
|
|
32
|
+
#
|
|
33
|
+
# Reported as a YAML error (YP_STATUS=yaml_error): unterminated quotes, tab
|
|
34
|
+
# indentation, misaligned indentation, and lines that are neither a mapping
|
|
35
|
+
# entry nor a sequence entry. The manifest validator renders these through the
|
|
36
|
+
# M1 "frontmatter is not valid YAML" message, whose text is a declared
|
|
37
|
+
# divergence class scoped to its prefix.
|
|
38
|
+
#
|
|
39
|
+
# Output model. A successful parse populates a node table addressed by path:
|
|
40
|
+
# `parallel`, `items`, `items[0]`, `items[0].blast_radius.paths[1]`. Document
|
|
41
|
+
# order is preserved in the indexed array YP_ORDER; the associative arrays are
|
|
42
|
+
# used for lookup only and are never iterated, so no hash order reaches output.
|
|
43
|
+
#
|
|
44
|
+
# shellcheck disable=SC2034
|
|
45
|
+
# SC2034 is disabled file-wide because this module is the storage half of a
|
|
46
|
+
# two-file library: the node table and the parse-status globals declared below
|
|
47
|
+
# are written here and read by parallel-yaml-emit.sh, parallel-items-validate.sh,
|
|
48
|
+
# and parallel-manifest-validate.sh, which shellcheck analyses as separate
|
|
49
|
+
# files and therefore cannot see the use.
|
|
50
|
+
|
|
51
|
+
# Node paths in document order. Every ordered traversal walks this array.
|
|
52
|
+
YP_ORDER=()
|
|
53
|
+
|
|
54
|
+
# Lexical type per path: map, seq, null, bool, int, float, or str.
|
|
55
|
+
declare -gA YP_TYPE=()
|
|
56
|
+
|
|
57
|
+
# Decoded scalar value per path; empty string for container nodes.
|
|
58
|
+
declare -gA YP_VALUE=()
|
|
59
|
+
|
|
60
|
+
# Mapping-key name per path, for nodes that came from a `key:` entry.
|
|
61
|
+
declare -gA YP_KEY=()
|
|
62
|
+
|
|
63
|
+
# Rendered path of the mapping that owns each key node; `<root>` at the top.
|
|
64
|
+
declare -gA YP_PARENT=()
|
|
65
|
+
|
|
66
|
+
# Child count per container path: sequence length or mapping key count.
|
|
67
|
+
declare -gA YP_COUNT=()
|
|
68
|
+
|
|
69
|
+
# Parse outcome: ok, not_a_mapping, yaml_error, or out_of_subset.
|
|
70
|
+
YP_STATUS="ok"
|
|
71
|
+
|
|
72
|
+
# Human-readable detail for a yaml_error or out_of_subset outcome.
|
|
73
|
+
YP_DETAIL=""
|
|
74
|
+
|
|
75
|
+
# Source lines produced by yp_split_lines.
|
|
76
|
+
YP_LINES=()
|
|
77
|
+
|
|
78
|
+
# Frontmatter body lines produced by yp_extract_frontmatter_body.
|
|
79
|
+
YP_BODY_LINES=()
|
|
80
|
+
|
|
81
|
+
# The single M1 error produced by a failed fence extraction.
|
|
82
|
+
YP_FENCE_ERROR=""
|
|
83
|
+
|
|
84
|
+
# Scratch outputs of yp_classify_scalar.
|
|
85
|
+
YP_SCALAR_TYPE=""
|
|
86
|
+
YP_SCALAR_VALUE=""
|
|
87
|
+
|
|
88
|
+
# The fence line that opens and closes a frontmatter block.
|
|
89
|
+
YP_FENCE="---"
|
|
90
|
+
|
|
91
|
+
yp_trim() {
|
|
92
|
+
# Echo a string with leading and trailing whitespace removed.
|
|
93
|
+
#
|
|
94
|
+
# Args: $1 = the raw string. Mirrors Python's str.strip() for the ASCII
|
|
95
|
+
# whitespace the manifest subset can contain.
|
|
96
|
+
local value="$1"
|
|
97
|
+
value=${value#"${value%%[![:space:]]*}"}
|
|
98
|
+
value=${value%"${value##*[![:space:]]}"}
|
|
99
|
+
printf '%s' "$value"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
yp_split_lines() {
|
|
103
|
+
# Populate YP_LINES by splitting text on any of the three line terminators.
|
|
104
|
+
#
|
|
105
|
+
# The CRLF replacement runs first so a carriage-return/line-feed pair is
|
|
106
|
+
# consumed as one terminator rather than as a CR followed by an empty line,
|
|
107
|
+
# matching the ordered alternation in the Python module's regex.
|
|
108
|
+
#
|
|
109
|
+
# Args: $1 = raw document text.
|
|
110
|
+
local text="$1"
|
|
111
|
+
text=${text//$'\r\n'/$'\n'}
|
|
112
|
+
text=${text//$'\r'/$'\n'}
|
|
113
|
+
YP_LINES=()
|
|
114
|
+
local line
|
|
115
|
+
# A here-string appends one newline, so a text that already ends in a
|
|
116
|
+
# terminator yields the same trailing empty element Python's split does.
|
|
117
|
+
while IFS= read -r line; do
|
|
118
|
+
YP_LINES+=("$line")
|
|
119
|
+
done <<<"$text"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
yp_extract_frontmatter_body() {
|
|
123
|
+
# Populate YP_BODY_LINES with the leading frontmatter block (invariant M1).
|
|
124
|
+
#
|
|
125
|
+
# Args: $1 = raw document text.
|
|
126
|
+
# Returns 0 on success. On failure returns 1 and sets YP_FENCE_ERROR to the
|
|
127
|
+
# single M1 error string, using the caller-supplied context prefix in $2.
|
|
128
|
+
local text="$1" context="$2"
|
|
129
|
+
yp_split_lines "$text"
|
|
130
|
+
YP_BODY_LINES=()
|
|
131
|
+
YP_FENCE_ERROR=""
|
|
132
|
+
if ((${#YP_LINES[@]} == 0)) || [[ $(yp_trim "${YP_LINES[0]}") != "$YP_FENCE" ]]; then
|
|
133
|
+
YP_FENCE_ERROR="$context must open with a '$YP_FENCE' frontmatter fence."
|
|
134
|
+
return 1
|
|
135
|
+
fi
|
|
136
|
+
local index
|
|
137
|
+
# Scan forward for the first closing fence: everything between the two
|
|
138
|
+
# fences is the body, and no second fence means the block is unterminated.
|
|
139
|
+
for ((index = 1; index < ${#YP_LINES[@]}; index++)); do
|
|
140
|
+
if [[ $(yp_trim "${YP_LINES[index]}") == "$YP_FENCE" ]]; then
|
|
141
|
+
local body_index
|
|
142
|
+
for ((body_index = 1; body_index < index; body_index++)); do
|
|
143
|
+
YP_BODY_LINES+=("${YP_LINES[body_index]}")
|
|
144
|
+
done
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
YP_FENCE_ERROR="$context frontmatter block is not terminated by '$YP_FENCE'."
|
|
149
|
+
return 1
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
yp_reject() {
|
|
153
|
+
# Record an out-of-subset rejection and return 1.
|
|
154
|
+
#
|
|
155
|
+
# Args: $1 = detail describing the refused construct.
|
|
156
|
+
YP_STATUS="out_of_subset"
|
|
157
|
+
YP_DETAIL="$1"
|
|
158
|
+
return 1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
yp_yaml_error() {
|
|
162
|
+
# Record a YAML-level parse failure and return 1.
|
|
163
|
+
#
|
|
164
|
+
# Args: $1 = detail describing the malformed input.
|
|
165
|
+
YP_STATUS="yaml_error"
|
|
166
|
+
YP_DETAIL="$1"
|
|
167
|
+
return 1
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
yp_classify_scalar() {
|
|
171
|
+
# Classify one scalar token, setting YP_SCALAR_TYPE and YP_SCALAR_VALUE.
|
|
172
|
+
#
|
|
173
|
+
# Args: $1 = the raw token with surrounding whitespace already trimmed.
|
|
174
|
+
# Returns 0 when the token is inside the subset, 1 otherwise, with
|
|
175
|
+
# YP_STATUS and YP_DETAIL set by yp_reject or yp_yaml_error.
|
|
176
|
+
local raw="$1"
|
|
177
|
+
YP_SCALAR_TYPE=""
|
|
178
|
+
YP_SCALAR_VALUE=""
|
|
179
|
+
|
|
180
|
+
# Quoted forms are decided first: a quote character makes every later
|
|
181
|
+
# lexical rule inapplicable, and an unterminated quote is a YAML error
|
|
182
|
+
# rather than an out-of-subset construct.
|
|
183
|
+
if [[ $raw == '"'* ]]; then
|
|
184
|
+
if [[ ${#raw} -lt 2 || $raw != *'"' ]]; then
|
|
185
|
+
yp_yaml_error "unterminated double-quoted scalar: $raw"
|
|
186
|
+
return 1
|
|
187
|
+
fi
|
|
188
|
+
local body=${raw:1:${#raw}-2}
|
|
189
|
+
local backslash=$'\\'
|
|
190
|
+
if [[ $body == *"$backslash"* || $body == *'"'* ]]; then
|
|
191
|
+
yp_reject "escape sequences inside double-quoted scalars are outside the subset"
|
|
192
|
+
return 1
|
|
193
|
+
fi
|
|
194
|
+
YP_SCALAR_TYPE="str"
|
|
195
|
+
YP_SCALAR_VALUE="$body"
|
|
196
|
+
return 0
|
|
197
|
+
fi
|
|
198
|
+
if [[ $raw == "'"* ]]; then
|
|
199
|
+
if [[ ${#raw} -lt 2 || $raw != *"'" ]]; then
|
|
200
|
+
yp_yaml_error "unterminated single-quoted scalar: $raw"
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
local single_body=${raw:1:${#raw}-2}
|
|
204
|
+
YP_SCALAR_TYPE="str"
|
|
205
|
+
YP_SCALAR_VALUE="${single_body//\'\'/\'}"
|
|
206
|
+
return 0
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Structural sigils that introduce constructs this parser does not model.
|
|
210
|
+
case "$raw" in
|
|
211
|
+
'&'* | '*'* | '!'* | '|'* | '>'* | '?'* | '%'* | '@'* | '\`'*)
|
|
212
|
+
yp_reject "value begins with the unsupported YAML indicator: $raw"
|
|
213
|
+
return 1
|
|
214
|
+
;;
|
|
215
|
+
'[]' | '{}') ;;
|
|
216
|
+
'['* | '{'*)
|
|
217
|
+
yp_reject "non-empty flow collections are outside the subset: $raw"
|
|
218
|
+
return 1
|
|
219
|
+
;;
|
|
220
|
+
esac
|
|
221
|
+
|
|
222
|
+
# Null, then boolean, then integer: the resolver order PyYAML applies to a
|
|
223
|
+
# plain scalar, restricted to the members the manifest subset admits.
|
|
224
|
+
case "$raw" in
|
|
225
|
+
'' | '~' | null | Null | NULL)
|
|
226
|
+
YP_SCALAR_TYPE="null"
|
|
227
|
+
YP_SCALAR_VALUE=""
|
|
228
|
+
return 0
|
|
229
|
+
;;
|
|
230
|
+
true | True | TRUE | yes | Yes | YES | on | On | ON)
|
|
231
|
+
YP_SCALAR_TYPE="bool"
|
|
232
|
+
YP_SCALAR_VALUE="true"
|
|
233
|
+
return 0
|
|
234
|
+
;;
|
|
235
|
+
false | False | FALSE | no | No | NO | off | Off | OFF)
|
|
236
|
+
YP_SCALAR_TYPE="bool"
|
|
237
|
+
YP_SCALAR_VALUE="false"
|
|
238
|
+
return 0
|
|
239
|
+
;;
|
|
240
|
+
.inf | .Inf | .INF | -.inf | -.Inf | -.INF | .nan | .NaN | .NAN)
|
|
241
|
+
yp_reject "special float values are outside the subset: $raw"
|
|
242
|
+
return 1
|
|
243
|
+
;;
|
|
244
|
+
esac
|
|
245
|
+
|
|
246
|
+
if [[ $raw =~ ^[-+]?(0|[1-9][0-9]*)$ ]]; then
|
|
247
|
+
YP_SCALAR_TYPE="int"
|
|
248
|
+
YP_SCALAR_VALUE="${raw#+}"
|
|
249
|
+
return 0
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# A token that looks numeric but is not a clean decimal integer is refused
|
|
253
|
+
# rather than silently demoted to a string, because PyYAML would resolve
|
|
254
|
+
# many of these to int, float, or datetime.
|
|
255
|
+
if [[ $raw =~ ^[-+]?[0-9][0-9_]*$ ]] ||
|
|
256
|
+
[[ $raw =~ ^[-+]?0[xXoObB] ]] ||
|
|
257
|
+
[[ $raw =~ ^[-+]?[0-9]*\.[0-9]* ]] ||
|
|
258
|
+
[[ $raw =~ ^[-+]?[0-9]+[eE][-+]?[0-9]+$ ]] ||
|
|
259
|
+
[[ $raw =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2} ]]; then
|
|
260
|
+
yp_reject "numeric, float, or timestamp scalar outside the subset: $raw"
|
|
261
|
+
return 1
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
YP_SCALAR_TYPE="str"
|
|
265
|
+
YP_SCALAR_VALUE="$raw"
|
|
266
|
+
return 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
yp_node_add() {
|
|
270
|
+
# Register one node in the table and append it to the document order.
|
|
271
|
+
#
|
|
272
|
+
# Args: $1 = path, $2 = type, $3 = value, $4 = owning-map path or empty,
|
|
273
|
+
# $5 = mapping key name or empty.
|
|
274
|
+
local path="$1" type="$2" value="$3" parent="$4" key="$5"
|
|
275
|
+
YP_ORDER+=("$path")
|
|
276
|
+
YP_TYPE["$path"]="$type"
|
|
277
|
+
YP_VALUE["$path"]="$value"
|
|
278
|
+
if [[ -n $key ]]; then
|
|
279
|
+
YP_KEY["$path"]="$key"
|
|
280
|
+
YP_PARENT["$path"]="$parent"
|
|
281
|
+
fi
|
|
282
|
+
if [[ $type == map || $type == seq ]]; then
|
|
283
|
+
YP_COUNT["$path"]=0
|
|
284
|
+
fi
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
yp_reset() {
|
|
288
|
+
# Clear the node table so a fresh parse starts from an empty state.
|
|
289
|
+
YP_ORDER=()
|
|
290
|
+
YP_TYPE=()
|
|
291
|
+
YP_VALUE=()
|
|
292
|
+
YP_KEY=()
|
|
293
|
+
YP_PARENT=()
|
|
294
|
+
YP_COUNT=()
|
|
295
|
+
YP_STATUS="ok"
|
|
296
|
+
YP_DETAIL=""
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
yp_has() {
|
|
300
|
+
# Return 0 when a path exists in the node table.
|
|
301
|
+
#
|
|
302
|
+
# Args: $1 = path.
|
|
303
|
+
[[ -n ${YP_TYPE["$1"]+set} ]]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
yp_type_of() {
|
|
307
|
+
# Echo the lexical type at a path, or `absent` when the path is unknown.
|
|
308
|
+
#
|
|
309
|
+
# Args: $1 = path.
|
|
310
|
+
if yp_has "$1"; then
|
|
311
|
+
printf '%s' "${YP_TYPE["$1"]}"
|
|
312
|
+
else
|
|
313
|
+
printf 'absent'
|
|
314
|
+
fi
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
yp_value_of() {
|
|
318
|
+
# Echo the decoded scalar value at a path, or the empty string when absent.
|
|
319
|
+
#
|
|
320
|
+
# Args: $1 = path.
|
|
321
|
+
if yp_has "$1"; then
|
|
322
|
+
printf '%s' "${YP_VALUE["$1"]}"
|
|
323
|
+
fi
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
yp_count_of() {
|
|
327
|
+
# Echo the child count at a container path, or 0 when the path is absent.
|
|
328
|
+
#
|
|
329
|
+
# Args: $1 = path.
|
|
330
|
+
if [[ -n ${YP_COUNT["$1"]+set} ]]; then
|
|
331
|
+
printf '%s' "${YP_COUNT["$1"]}"
|
|
332
|
+
else
|
|
333
|
+
printf '0'
|
|
334
|
+
fi
|
|
335
|
+
}
|