@jenga-ai/agent 3.6.0 → 4.1.0
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/README.md +32 -1
- package/lib/generate-skill-allow-list.js +42 -5
- package/lib/skill-allow-list.json +1 -1
- package/package.json +1 -2
- package/project/app/api/lib/resolve-project-root.js +1 -1
- package/project/app/api/scripts/capture-snapshot.js +12 -7
- package/project/app/package.json +4 -0
- package/project/app/ui/dist/assets/index-BADc5mmH.css +1 -0
- package/project/app/ui/dist/assets/index-DX2pfTAW.js +104 -0
- package/project/app/ui/dist/index.html +2 -2
- package/scripts/acquire-concurrency-slot.sh +34 -4
- package/scripts/apply-j-prefix.sh +1 -1
- package/scripts/delete-bare-skill-dirs.sh +1 -2
- package/scripts/idea_manager.sh +16 -1
- package/scripts/release-concurrency-slot.sh +34 -4
- package/scripts/render-ranked-list.sh +270 -0
- package/scripts/repoint-dead-bare-path-prose.py +81 -0
- package/scripts/rewrite-stale-skill-preambles.py +188 -0
- package/scripts/strip-polyfill-frontmatter.py +166 -0
- package/scripts/todo_manager.sh +16 -1
- package/scripts/validate-typed-object.sh +750 -0
- package/scripts/verify-postinstall-reconcile.sh +33 -7
- package/skills/index/scripts/board_index.py +29 -1
- package/skills/j-brainstorm/SKILL.md +3 -4
- package/skills/j-btw/SKILL.md +3 -4
- package/skills/j-clearify/SKILL.md +3 -4
- package/skills/j-close-story/SKILL.md +11 -12
- package/skills/j-close-story/scripts/check-story-closeable.sh +11 -4
- package/skills/j-commit/SKILL.md +3 -4
- package/skills/j-continue/SKILL.md +5 -6
- package/skills/j-deep-dive/SKILL.md +3 -4
- package/skills/j-distribute/SKILL.md +3 -4
- package/skills/j-do/SKILL.md +13 -15
- package/skills/j-doc/SKILL.md +3 -4
- package/skills/j-doc/scripts/resolve_last_update.py +27 -1
- package/skills/j-doc-sync/SKILL.md +3 -4
- package/skills/j-dooo/SKILL.md +6 -15
- package/skills/j-error/SKILL.md +3 -4
- package/skills/j-evaluate/SKILL.md +3 -4
- package/skills/j-examplify/SKILL.md +3 -4
- package/skills/j-help/SKILL.md +3 -4
- package/skills/j-idea/SKILL.md +3 -4
- package/skills/j-improve/SKILL.md +3 -4
- package/skills/j-init/SKILL.md +20 -11
- package/skills/j-init/scripts/apply-scaffold-visibility.sh +8 -6
- package/skills/j-jbp/SKILL.md +3 -4
- package/skills/j-lgtm/SKILL.md +3 -4
- package/skills/j-pi-plan/SKILL.md +3 -4
- package/skills/j-proceed/SKILL.md +3 -4
- package/skills/j-publish/SKILL.md +3 -4
- package/skills/j-publish/scripts/run_gates.sh +1 -1
- package/skills/j-reconcile/SKILL.md +38 -5
- package/skills/j-reconcile/assets/report_format.md +11 -0
- package/skills/j-reconcile/scripts/detect-unlinked-code.sh +2 -2
- package/skills/j-reconcile-origin/SKILL.md +3 -4
- package/skills/j-redo/SKILL.md +3 -4
- package/skills/j-skillify/SKILL.md +3 -4
- package/skills/j-spinoff/SKILL.md +3 -4
- package/skills/j-status/SKILL.md +4 -5
- package/skills/j-todo/SKILL.md +42 -5
- package/skills/j-todo/scripts/add_trivial_task.sh +12 -1
- package/skills/j-todo/scripts/argument-is-not-ranked-list.sh +92 -0
- package/skills/j-todo/scripts/argument-is-ranked-list.sh +78 -0
- package/skills/j-uncharted/SKILL.md +251 -12
- package/skills/j-uncharted/scripts/detect-dependencies.sh +79 -21
- package/skills/j-uncharted/scripts/diff-since-baseline.sh +600 -0
- package/skills/j-uncharted/scripts/find-scan-baseline.sh +545 -0
- package/skills/j-uncharted/scripts/run-engine.sh +36 -2
- package/skills/j-uncharted/scripts/write-scan-record.sh +361 -0
- package/skills/j-wtf/SKILL.md +3 -4
- package/skills/jenga/SKILL.md +106 -10
- package/skills/jenga/playbooks/schema.json +4 -4
- package/skills/jenga/scripts/enrich-nl-prompt.sh +225 -0
- package/skills/jenga/scripts/load-nl-catalog.js +6 -3
- package/skills/jenga/scripts/load-playbooks.sh +289 -4
- package/skills/jenga/scripts/match-playbook.sh +6 -5
- package/skills/jenga/scripts/run-playbook-step.sh +270 -3
- package/templates/permission-levels/level-1-locked.json +1 -1
- package/templates/permission-levels/level-2-guarded.json +1 -1
- package/templates/permission-levels/level-3-standard.json +1 -1
- package/templates/permission-levels/level-4-elevated.json +1 -1
- package/templates/permission-levels/level-5-unrestricted.json +1 -1
- package/templates/playbook-types.json +6 -0
- package/project/app/ui/dist/assets/index-BVR_7Owg.css +0 -1
- package/project/app/ui/dist/assets/index-CtU2xLQm.js +0 -104
- package/scripts/audit-twin-divergence.sh +0 -693
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ---------------------------------------------------------------------------
|
|
3
|
+
# scripts/validate-typed-object.sh
|
|
4
|
+
#
|
|
5
|
+
# The single deterministic answer to "does this value conform to this type?"
|
|
6
|
+
# for Epic E62's playbook type descriptors (E62_S01_T02). Consumed by
|
|
7
|
+
# E62_S01_T04 at playbook LOAD time and by E62_S02 at RUNTIME, which is why it
|
|
8
|
+
# lives in shared scripts/ rather than under skills/jenga/scripts/.
|
|
9
|
+
#
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# NOTHING ABOUT THE TYPE SYSTEM IS HARDCODED HERE
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Every type name, every `verify` rule kind, and every `normalize` transform
|
|
14
|
+
# name is read out of templates/playbook-types.json on each invocation. Grep
|
|
15
|
+
# this file for `id_list`, `file_list` or `text` and you will find them only in
|
|
16
|
+
# comments. That is the whole point: the registry's own header asserts that
|
|
17
|
+
# "adding a TYPE is a DATA-ONLY edit and NEVER a code change", and a validator
|
|
18
|
+
# carrying its own copy of the type list would silently make that claim false.
|
|
19
|
+
#
|
|
20
|
+
# The registry's header is equally precise about the one exception, and this
|
|
21
|
+
# script implements exactly that line:
|
|
22
|
+
#
|
|
23
|
+
# adding a TYPE -> data only, works here with zero code change
|
|
24
|
+
# adding a RULE -> requires code, because something has to implement it
|
|
25
|
+
# adding a TRANSFORM -> requires code, for the same reason
|
|
26
|
+
#
|
|
27
|
+
# So there are two distinct authorities, and they are deliberately not the same
|
|
28
|
+
# thing:
|
|
29
|
+
#
|
|
30
|
+
# * the REGISTRY (`verify_rules`, `normalize_vocabulary`) is the authority on
|
|
31
|
+
# what is ALLOWED to appear in a descriptor;
|
|
32
|
+
# * the IMPLEMENTED_* constants below are the authority on what this script
|
|
33
|
+
# can EXECUTE.
|
|
34
|
+
#
|
|
35
|
+
# A name present in one but not the other is a hard error in whichever
|
|
36
|
+
# direction it points, never a silent skip:
|
|
37
|
+
#
|
|
38
|
+
# in a descriptor but not in the registry enumeration -> exit 6
|
|
39
|
+
# (a hand-edit smuggled in something the closed vocabulary never admitted)
|
|
40
|
+
# in the registry enumeration but not implemented here -> exit 6
|
|
41
|
+
# (the registry is ahead of the validator; the code half of the extension
|
|
42
|
+
# was not written)
|
|
43
|
+
#
|
|
44
|
+
# A silent skip on either path would turn "the format cannot express semantic
|
|
45
|
+
# extraction" back into a soft guideline, which is precisely what E62 rejected
|
|
46
|
+
# at epic level.
|
|
47
|
+
#
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# OUT OF SCOPE — a hard boundary, not a preference
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# No repair. No conversion between types. No LLM call. No network call. A value
|
|
52
|
+
# that still fails after `normalize` is simply NON-CONFORMING; producing "what
|
|
53
|
+
# the skill probably meant" is semantic extraction, rejected at epic level (see
|
|
54
|
+
# E62's "What was rejected, and why it matters"). The closed transform
|
|
55
|
+
# vocabulary exists so that this cannot creep back in later.
|
|
56
|
+
#
|
|
57
|
+
# Fully deterministic: identical inputs produce byte-identical output on every
|
|
58
|
+
# run. There is no clock, no randomness, no ordering that depends on the
|
|
59
|
+
# filesystem, and LC_ALL is pinned to C below so that POSIX character classes
|
|
60
|
+
# and ranges in a registry regex cannot vary with the caller's locale.
|
|
61
|
+
#
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# USAGE
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# scripts/validate-typed-object.sh [options] <type> <value>
|
|
66
|
+
# scripts/validate-typed-object.sh [options] <type> - # value on stdin
|
|
67
|
+
# scripts/validate-typed-object.sh [options] --value-file <path> <type>
|
|
68
|
+
#
|
|
69
|
+
# Options:
|
|
70
|
+
# --registry <path> Type registry to read. Default: <script dir>/../
|
|
71
|
+
# templates/playbook-types.json, resolved relative to
|
|
72
|
+
# this script so the repo root and the .claude/ and
|
|
73
|
+
# .agents/ mirrors each use their own copy.
|
|
74
|
+
# --value-file <path> Read the value from a file instead of an argument.
|
|
75
|
+
# -h, --help Print this usage block and exit 0.
|
|
76
|
+
#
|
|
77
|
+
# Environment:
|
|
78
|
+
# JENGA_PLAYBOOK_TYPES_FILE Same as --registry (the flag wins if both are
|
|
79
|
+
# given). This is the test-injection hook, in the
|
|
80
|
+
# style of JENGA_PLAYBOOKS_TEST_ROOT.
|
|
81
|
+
#
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# OUTPUT — one JSON object on stdout, on every path
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# {
|
|
86
|
+
# "outcome": "conforming" | "normalized" | "non-conforming" | "error",
|
|
87
|
+
# "type": "<the type name as given>",
|
|
88
|
+
# "raw": "<the value exactly as supplied, before any transform>",
|
|
89
|
+
# "value": "<the value the caller should use>" | null,
|
|
90
|
+
# "normalize_applied": ["<transform>", ...],
|
|
91
|
+
# "reason": "<why>" | null,
|
|
92
|
+
# "error": "<error kind>" // present only when outcome is "error"
|
|
93
|
+
# }
|
|
94
|
+
#
|
|
95
|
+
# `value` is the raw value on the conforming path, the normalized value on the
|
|
96
|
+
# normalized path, and null on the non-conforming and error paths. `reason` is
|
|
97
|
+
# null unless something went wrong.
|
|
98
|
+
#
|
|
99
|
+
# E62_S02 needs to distinguish "already fine" from "I had to fix it" so it can
|
|
100
|
+
# warn: that is `outcome` (conforming vs normalized), and equivalently the exit
|
|
101
|
+
# code (0 vs 2), so a shell consumer can branch without parsing JSON at all.
|
|
102
|
+
#
|
|
103
|
+
# The three OUTCOME paths write to stdout only — stderr stays empty, so a
|
|
104
|
+
# non-conforming value is not noise in a caller's log. The ERROR paths write a
|
|
105
|
+
# human-readable `validate-typed-object.sh: error: ...` line to stderr as well,
|
|
106
|
+
# matching the neighbouring shared helpers.
|
|
107
|
+
#
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# EXIT CODES
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# 0 conforming — satisfies `verify` as given (or `verify` is null)
|
|
112
|
+
# 1 usage error — bad arguments
|
|
113
|
+
# 2 normalized — did not conform as given, but conforms after
|
|
114
|
+
# `normalize`; the normalized value is in `value`
|
|
115
|
+
# 3 non-conforming — fails `verify` even after `normalize`
|
|
116
|
+
# 4 unknown type — the type name is not a key of the registry's `types`
|
|
117
|
+
# 5 environment — jq missing, registry missing / unreadable /
|
|
118
|
+
# unparseable / not a type registry
|
|
119
|
+
# 6 registry contract violation — a descriptor names a `verify` rule kind or
|
|
120
|
+
# a `normalize` transform that the closed vocabulary
|
|
121
|
+
# does not admit, or that this validator does not
|
|
122
|
+
# implement, or is otherwise malformed
|
|
123
|
+
#
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# SEMANTICS
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# A value is treated as a list of lines, per the registry header. The list is
|
|
128
|
+
# derived with ordinary POSIX line semantics: the empty value yields zero
|
|
129
|
+
# elements, and a single terminating newline is a terminator rather than an
|
|
130
|
+
# extra empty element ("a\n" is one element, "a\n\n" is two).
|
|
131
|
+
#
|
|
132
|
+
# Evaluation order is fixed:
|
|
133
|
+
# 1. validate the descriptor's contract (rule kinds, transform names) — this
|
|
134
|
+
# happens BEFORE the value is looked at, so a malformed descriptor cannot
|
|
135
|
+
# pass merely because the value happened to be conforming already;
|
|
136
|
+
# 2. `verify: null` short-circuits to conforming — always, unconditionally;
|
|
137
|
+
# 3. verify the value as given -> conforming;
|
|
138
|
+
# 4. otherwise apply `normalize` left to right and verify again ->
|
|
139
|
+
# normalized, or non-conforming.
|
|
140
|
+
#
|
|
141
|
+
# `per_line` is the rule kind defined today: a POSIX ERE that every element of
|
|
142
|
+
# the list must match. Matching uses `grep -E`, NOT jq's `test()`, because the
|
|
143
|
+
# registry's patterns are POSIX ERE by deliberate choice ([0-9] and
|
|
144
|
+
# [^[:space:]], never \d or \S) precisely because the consumer is a shell
|
|
145
|
+
# script. A `verify` object carrying several rule kinds requires all of them to
|
|
146
|
+
# hold.
|
|
147
|
+
#
|
|
148
|
+
# KNOWN SEMANTIC EDGE, deliberately left literal: "every element matches" is
|
|
149
|
+
# vacuously TRUE of an empty list, so a value that normalizes down to nothing
|
|
150
|
+
# (e.g. "" or " , " for id_list) is reported conforming rather than
|
|
151
|
+
# non-conforming. That is the honest reading of the rule as the registry states
|
|
152
|
+
# it. The alternative — a non-empty requirement — is NOT invented here, because
|
|
153
|
+
# inventing it would mean hardcoding a rule the registry never expressed; the
|
|
154
|
+
# right way to express it is a new rule kind in the registry (a data edit plus
|
|
155
|
+
# its implementation), which is exactly the extension path the format provides.
|
|
156
|
+
# Flagged for E62_S02 rather than silently decided here.
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
set -euo pipefail
|
|
160
|
+
|
|
161
|
+
# Pathname expansion off. Registry-sourced names are carried in arrays and
|
|
162
|
+
# expanded quoted, so nothing should reach a glob context — this is the belt to
|
|
163
|
+
# that braces. A registry entry of "*" reaching an unquoted expansion would glob
|
|
164
|
+
# against the caller's cwd, making the verdict depend on where the script was
|
|
165
|
+
# run from, which is the one thing it must never do.
|
|
166
|
+
set -f
|
|
167
|
+
|
|
168
|
+
# Pinned for determinism: character classes ([[:space:]]) and ranges in a
|
|
169
|
+
# registry regex must not depend on the caller's locale.
|
|
170
|
+
LC_ALL=C
|
|
171
|
+
export LC_ALL
|
|
172
|
+
|
|
173
|
+
SELF="$(basename "$0")"
|
|
174
|
+
|
|
175
|
+
# What this script can EXECUTE. See the header: this is NOT the vocabulary —
|
|
176
|
+
# the registry owns that — it is the implementation inventory, and the two are
|
|
177
|
+
# cross-checked against each other in both directions before any value is
|
|
178
|
+
# evaluated. Keep each list in step with its dispatcher below.
|
|
179
|
+
IMPLEMENTED_RULE_KINDS=("per_line")
|
|
180
|
+
IMPLEMENTED_TRANSFORMS=("split_on_comma" "trim" "drop_empty")
|
|
181
|
+
|
|
182
|
+
# The bare-identifier shape the registry header mandates for a transform name.
|
|
183
|
+
IDENTIFIER_ERE='^[a-z][a-z0-9_]*$'
|
|
184
|
+
|
|
185
|
+
TYPE_NAME=""
|
|
186
|
+
RAW_VALUE=""
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Output
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
usage() {
|
|
193
|
+
cat <<'USAGE'
|
|
194
|
+
Usage:
|
|
195
|
+
validate-typed-object.sh [options] <type> <value>
|
|
196
|
+
validate-typed-object.sh [options] <type> - # value on stdin
|
|
197
|
+
validate-typed-object.sh [options] --value-file <path> <type>
|
|
198
|
+
|
|
199
|
+
Options:
|
|
200
|
+
--registry <path> Type registry (default: ../templates/playbook-types.json
|
|
201
|
+
relative to this script; JENGA_PLAYBOOK_TYPES_FILE also
|
|
202
|
+
sets it).
|
|
203
|
+
--value-file <path> Read the value from a file instead of an argument.
|
|
204
|
+
-h, --help Show this help.
|
|
205
|
+
|
|
206
|
+
Exit codes:
|
|
207
|
+
0 conforming 1 usage 2 normalized 3 non-conforming
|
|
208
|
+
4 unknown type 5 environment 6 registry contract violation
|
|
209
|
+
USAGE
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# emit_outcome <outcome> <exit-code> <value-json> <applied-json> <reason-json>
|
|
213
|
+
emit_outcome() {
|
|
214
|
+
jq -n \
|
|
215
|
+
--arg outcome "$1" \
|
|
216
|
+
--arg type "$TYPE_NAME" \
|
|
217
|
+
--arg raw "$RAW_VALUE" \
|
|
218
|
+
--argjson value "$3" \
|
|
219
|
+
--argjson applied "$4" \
|
|
220
|
+
--argjson reason "$5" \
|
|
221
|
+
'{outcome: $outcome, type: $type, raw: $raw, value: $value, normalize_applied: $applied, reason: $reason}'
|
|
222
|
+
exit "$2"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# fail <exit-code> <error-kind> <message>
|
|
226
|
+
fail() {
|
|
227
|
+
local code="$1" kind="$2" msg="$3"
|
|
228
|
+
printf '%s: error: %s\n' "$SELF" "$msg" >&2
|
|
229
|
+
# jq is how the JSON half of the contract gets written, so the one error that
|
|
230
|
+
# cannot honour it is a missing jq. Every other error path does.
|
|
231
|
+
if command -v jq > /dev/null 2>&1; then
|
|
232
|
+
jq -n \
|
|
233
|
+
--arg outcome "error" \
|
|
234
|
+
--arg error "$kind" \
|
|
235
|
+
--arg type "$TYPE_NAME" \
|
|
236
|
+
--arg raw "$RAW_VALUE" \
|
|
237
|
+
--arg reason "$msg" \
|
|
238
|
+
'{outcome: $outcome, type: $type, raw: $raw, value: null, normalize_applied: [], reason: $reason, error: $error}'
|
|
239
|
+
fi
|
|
240
|
+
exit "$code"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Small helpers
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
# list_contains <needle> [<candidate> ...]
|
|
248
|
+
#
|
|
249
|
+
# Membership is compared on WHOLE strings, never on whitespace-split tokens. A
|
|
250
|
+
# hand-edited registry entry such as "extract the ids the skill meant" has to be
|
|
251
|
+
# reported as that entry, in full — splitting it would report a verdict about
|
|
252
|
+
# the token "extract" and describe a registry the user does not have.
|
|
253
|
+
list_contains() {
|
|
254
|
+
local needle="$1" candidate
|
|
255
|
+
shift
|
|
256
|
+
for candidate in "$@"; do
|
|
257
|
+
if [ "$candidate" = "$needle" ]; then
|
|
258
|
+
return 0
|
|
259
|
+
fi
|
|
260
|
+
done
|
|
261
|
+
return 1
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# read_lines_into <array-name> < <newline-delimited stream>
|
|
265
|
+
#
|
|
266
|
+
# One entry per line, read WHOLE — never split on whitespace. A registry is a
|
|
267
|
+
# JSON file a human can hand-edit, so an entry may legally contain spaces, and
|
|
268
|
+
# "extract the ids the skill meant" has to reach the identifier check intact to
|
|
269
|
+
# be rejected by name rather than by one of its tokens.
|
|
270
|
+
#
|
|
271
|
+
# A newline INSIDE an entry would defeat the line framing, so that case is ruled
|
|
272
|
+
# out up front instead: assert_no_newlines below rejects any such entry before
|
|
273
|
+
# it is ever read here.
|
|
274
|
+
read_lines_into() {
|
|
275
|
+
local name="$1" item
|
|
276
|
+
eval "$name=()"
|
|
277
|
+
while IFS= read -r item; do
|
|
278
|
+
eval "$name+=(\"\$item\")"
|
|
279
|
+
done
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# assert_no_newlines <jq-filter-yielding-strings> <what>
|
|
283
|
+
#
|
|
284
|
+
# Line framing above is only safe if no entry contains a newline. Rather than
|
|
285
|
+
# silently mis-framing such an entry into two, reject it as the malformed
|
|
286
|
+
# registry content it is.
|
|
287
|
+
assert_no_newlines() {
|
|
288
|
+
jq -e --arg t "$TYPE_NAME" "$1 | all(type == \"string\" and (contains(\"\n\") | not))" \
|
|
289
|
+
"$REGISTRY_FILE" > /dev/null 2>&1 \
|
|
290
|
+
|| fail 6 "malformed_registry_entry" "$2 in $REGISTRY_FILE contains an entry that is not a newline-free string"
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# join_with_comma <item> [<item> ...]
|
|
294
|
+
join_with_comma() {
|
|
295
|
+
local out="" item
|
|
296
|
+
for item in "$@"; do
|
|
297
|
+
if [ -z "$out" ]; then
|
|
298
|
+
out="$item"
|
|
299
|
+
else
|
|
300
|
+
out="$out, $item"
|
|
301
|
+
fi
|
|
302
|
+
done
|
|
303
|
+
printf '%s' "$out"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
# Reads a whole file (or stdin, when $1 is "-") into RAW_VALUE without losing
|
|
307
|
+
# trailing newlines — command substitution strips them, hence the sentinel.
|
|
308
|
+
read_value_from() {
|
|
309
|
+
local src="$1" buf
|
|
310
|
+
if [ "$src" = "-" ]; then
|
|
311
|
+
buf="$(cat; printf 'x')"
|
|
312
|
+
else
|
|
313
|
+
[ -r "$src" ] || fail 5 "value_file_unreadable" "value file not readable: $src"
|
|
314
|
+
buf="$(cat -- "$src"; printf 'x')"
|
|
315
|
+
fi
|
|
316
|
+
RAW_VALUE="${buf%x}"
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
# Argument parsing
|
|
321
|
+
# ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
REGISTRY_FILE="${JENGA_PLAYBOOK_TYPES_FILE:-}"
|
|
324
|
+
VALUE_FILE=""
|
|
325
|
+
VALUE_GIVEN=0
|
|
326
|
+
POSITIONAL_COUNT=0
|
|
327
|
+
POS_TYPE=""
|
|
328
|
+
POS_VALUE=""
|
|
329
|
+
|
|
330
|
+
add_positional() {
|
|
331
|
+
case "$POSITIONAL_COUNT" in
|
|
332
|
+
0) POS_TYPE="$1" ;;
|
|
333
|
+
1) POS_VALUE="$1" ;;
|
|
334
|
+
*) fail 1 "usage" "unexpected extra argument: $1" ;;
|
|
335
|
+
esac
|
|
336
|
+
POSITIONAL_COUNT=$((POSITIONAL_COUNT + 1))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
END_OF_OPTIONS=0
|
|
340
|
+
while [ "$#" -gt 0 ]; do
|
|
341
|
+
if [ "$END_OF_OPTIONS" -eq 1 ]; then
|
|
342
|
+
add_positional "$1"
|
|
343
|
+
shift
|
|
344
|
+
continue
|
|
345
|
+
fi
|
|
346
|
+
case "$1" in
|
|
347
|
+
--registry)
|
|
348
|
+
[ "$#" -ge 2 ] || fail 1 "usage" "--registry requires a path"
|
|
349
|
+
REGISTRY_FILE="$2"
|
|
350
|
+
shift 2
|
|
351
|
+
;;
|
|
352
|
+
--registry=*)
|
|
353
|
+
REGISTRY_FILE="${1#*=}"
|
|
354
|
+
shift
|
|
355
|
+
;;
|
|
356
|
+
--value-file)
|
|
357
|
+
[ "$#" -ge 2 ] || fail 1 "usage" "--value-file requires a path"
|
|
358
|
+
VALUE_FILE="$2"
|
|
359
|
+
shift 2
|
|
360
|
+
;;
|
|
361
|
+
--value-file=*)
|
|
362
|
+
VALUE_FILE="${1#*=}"
|
|
363
|
+
shift
|
|
364
|
+
;;
|
|
365
|
+
-h|--help)
|
|
366
|
+
usage
|
|
367
|
+
exit 0
|
|
368
|
+
;;
|
|
369
|
+
--)
|
|
370
|
+
END_OF_OPTIONS=1
|
|
371
|
+
shift
|
|
372
|
+
;;
|
|
373
|
+
-)
|
|
374
|
+
add_positional "$1"
|
|
375
|
+
shift
|
|
376
|
+
;;
|
|
377
|
+
-*)
|
|
378
|
+
fail 1 "usage" "unknown option: $1"
|
|
379
|
+
;;
|
|
380
|
+
*)
|
|
381
|
+
add_positional "$1"
|
|
382
|
+
shift
|
|
383
|
+
;;
|
|
384
|
+
esac
|
|
385
|
+
done
|
|
386
|
+
|
|
387
|
+
[ "$POSITIONAL_COUNT" -ge 1 ] || { usage >&2; fail 1 "usage" "a type name is required"; }
|
|
388
|
+
TYPE_NAME="$POS_TYPE"
|
|
389
|
+
|
|
390
|
+
if [ -n "$VALUE_FILE" ]; then
|
|
391
|
+
[ "$POSITIONAL_COUNT" -le 1 ] || fail 1 "usage" "--value-file and a positional value are mutually exclusive"
|
|
392
|
+
read_value_from "$VALUE_FILE"
|
|
393
|
+
VALUE_GIVEN=1
|
|
394
|
+
elif [ "$POSITIONAL_COUNT" -eq 2 ]; then
|
|
395
|
+
if [ "$POS_VALUE" = "-" ]; then
|
|
396
|
+
read_value_from "-"
|
|
397
|
+
else
|
|
398
|
+
RAW_VALUE="$POS_VALUE"
|
|
399
|
+
fi
|
|
400
|
+
VALUE_GIVEN=1
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
# An absent value and an empty value are different questions, and only the
|
|
404
|
+
# first is a usage error. `validate-typed-object.sh id_list ""` is a legitimate
|
|
405
|
+
# thing to ask.
|
|
406
|
+
[ "$VALUE_GIVEN" -eq 1 ] || { usage >&2; fail 1 "usage" "a value is required (pass it as an argument, as '-' for stdin, or via --value-file)"; }
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Registry resolution and structural sanity
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
command -v jq > /dev/null 2>&1 || fail 5 "jq_missing" "jq is required but not found on PATH"
|
|
413
|
+
|
|
414
|
+
if [ -z "$REGISTRY_FILE" ]; then
|
|
415
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
416
|
+
REGISTRY_FILE="$SCRIPT_DIR/../templates/playbook-types.json"
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
[ -r "$REGISTRY_FILE" ] || fail 5 "registry_unreadable" "type registry not readable: $REGISTRY_FILE"
|
|
420
|
+
jq -e 'type == "object"' "$REGISTRY_FILE" > /dev/null 2>&1 \
|
|
421
|
+
|| fail 5 "registry_unparseable" "type registry is not a JSON object: $REGISTRY_FILE"
|
|
422
|
+
|
|
423
|
+
jq -e '(.types | type) == "object"' "$REGISTRY_FILE" > /dev/null 2>&1 \
|
|
424
|
+
|| fail 5 "registry_malformed" "type registry has no 'types' object: $REGISTRY_FILE"
|
|
425
|
+
jq -e '(.verify_rules | type) == "array"' "$REGISTRY_FILE" > /dev/null 2>&1 \
|
|
426
|
+
|| fail 5 "registry_malformed" "type registry has no 'verify_rules' array: $REGISTRY_FILE"
|
|
427
|
+
jq -e '(.normalize_vocabulary | type) == "array"' "$REGISTRY_FILE" > /dev/null 2>&1 \
|
|
428
|
+
|| fail 5 "registry_malformed" "type registry has no 'normalize_vocabulary' array: $REGISTRY_FILE"
|
|
429
|
+
|
|
430
|
+
# The two enumerations, read from the registry. Never assumed, never defaulted.
|
|
431
|
+
assert_no_newlines '.verify_rules' "'verify_rules'"
|
|
432
|
+
assert_no_newlines '.normalize_vocabulary' "'normalize_vocabulary'"
|
|
433
|
+
REGISTRY_RULE_KINDS=()
|
|
434
|
+
REGISTRY_TRANSFORMS=()
|
|
435
|
+
read_lines_into REGISTRY_RULE_KINDS < <(jq -r '.verify_rules[]' "$REGISTRY_FILE")
|
|
436
|
+
read_lines_into REGISTRY_TRANSFORMS < <(jq -r '.normalize_vocabulary[]' "$REGISTRY_FILE")
|
|
437
|
+
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
# Type lookup
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
if ! jq -e --arg t "$TYPE_NAME" '.types | has($t)' "$REGISTRY_FILE" > /dev/null 2>&1; then
|
|
443
|
+
KNOWN_TYPES="$(jq -r '.types | keys_unsorted | join(", ")' "$REGISTRY_FILE")"
|
|
444
|
+
fail 4 "unknown_type" "unknown type '$TYPE_NAME' — not a key of 'types' in $REGISTRY_FILE (known types: $KNOWN_TYPES)"
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
jq -e --arg t "$TYPE_NAME" '(.types[$t] | type) == "object"' "$REGISTRY_FILE" > /dev/null 2>&1 \
|
|
448
|
+
|| fail 6 "malformed_descriptor" "descriptor for type '$TYPE_NAME' is not an object"
|
|
449
|
+
|
|
450
|
+
# "a descriptor never carries any other key" — registry header.
|
|
451
|
+
EXTRA_KEYS="$(jq -r --arg t "$TYPE_NAME" '.types[$t] | keys_unsorted - ["verify", "normalize"] | join(", ")' "$REGISTRY_FILE")"
|
|
452
|
+
[ -z "$EXTRA_KEYS" ] \
|
|
453
|
+
|| fail 6 "malformed_descriptor" "descriptor for type '$TYPE_NAME' carries key(s) outside {verify, normalize}: $EXTRA_KEYS"
|
|
454
|
+
|
|
455
|
+
# ---------------------------------------------------------------------------
|
|
456
|
+
# Descriptor contract validation — BEFORE the value is looked at, so a
|
|
457
|
+
# malformed descriptor cannot slip through on a value that already conformed.
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
VERIFY_TYPE="$(jq -r --arg t "$TYPE_NAME" '.types[$t].verify | type' "$REGISTRY_FILE")"
|
|
461
|
+
case "$VERIFY_TYPE" in
|
|
462
|
+
"null"|"object") : ;;
|
|
463
|
+
*) fail 6 "malformed_descriptor" "type '$TYPE_NAME': 'verify' must be an object or null, got $VERIFY_TYPE" ;;
|
|
464
|
+
esac
|
|
465
|
+
|
|
466
|
+
RULE_KINDS=()
|
|
467
|
+
if [ "$VERIFY_TYPE" = "object" ]; then
|
|
468
|
+
assert_no_newlines '.types[$t].verify | keys_unsorted' "the 'verify' rule kinds of type '$TYPE_NAME'"
|
|
469
|
+
read_lines_into RULE_KINDS < <(jq -r --arg t "$TYPE_NAME" '.types[$t].verify | keys_unsorted[]' "$REGISTRY_FILE")
|
|
470
|
+
for kind in ${RULE_KINDS[@]+"${RULE_KINDS[@]}"}; do
|
|
471
|
+
list_contains "$kind" ${REGISTRY_RULE_KINDS[@]+"${REGISTRY_RULE_KINDS[@]}"} \
|
|
472
|
+
|| fail 6 "unknown_rule_kind" "type '$TYPE_NAME': verify rule kind '$kind' is not a member of 'verify_rules' ($(join_with_comma ${REGISTRY_RULE_KINDS[@]+"${REGISTRY_RULE_KINDS[@]}"})) in $REGISTRY_FILE"
|
|
473
|
+
list_contains "$kind" "${IMPLEMENTED_RULE_KINDS[@]}" \
|
|
474
|
+
|| fail 6 "unimplemented_rule_kind" "type '$TYPE_NAME': verify rule kind '$kind' is enumerated in 'verify_rules' but $SELF implements none of it — adding a rule kind requires code, not only a registry edit (implemented: $(join_with_comma "${IMPLEMENTED_RULE_KINDS[@]}"))"
|
|
475
|
+
done
|
|
476
|
+
fi
|
|
477
|
+
|
|
478
|
+
NORMALIZE_TYPE="$(jq -r --arg t "$TYPE_NAME" '.types[$t].normalize | type' "$REGISTRY_FILE")"
|
|
479
|
+
case "$NORMALIZE_TYPE" in
|
|
480
|
+
"null"|"array") : ;;
|
|
481
|
+
*) fail 6 "malformed_descriptor" "type '$TYPE_NAME': 'normalize' must be an array, got $NORMALIZE_TYPE" ;;
|
|
482
|
+
esac
|
|
483
|
+
|
|
484
|
+
TRANSFORMS=()
|
|
485
|
+
if [ "$NORMALIZE_TYPE" = "array" ]; then
|
|
486
|
+
jq -e --arg t "$TYPE_NAME" '.types[$t].normalize | all(type == "string")' "$REGISTRY_FILE" > /dev/null 2>&1 \
|
|
487
|
+
|| fail 6 "malformed_descriptor" "type '$TYPE_NAME': every 'normalize' entry must be a string"
|
|
488
|
+
assert_no_newlines '.types[$t].normalize' "the 'normalize' list of type '$TYPE_NAME'"
|
|
489
|
+
read_lines_into TRANSFORMS < <(jq -r --arg t "$TYPE_NAME" '.types[$t].normalize[]' "$REGISTRY_FILE")
|
|
490
|
+
for transform in ${TRANSFORMS[@]+"${TRANSFORMS[@]}"}; do
|
|
491
|
+
# The identifier check runs FIRST and on the WHOLE entry. A hand-edited
|
|
492
|
+
# "extract the ids the skill meant" must be rejected as that entry, by name,
|
|
493
|
+
# rather than as some token inside it — which is what the registry header
|
|
494
|
+
# means by the format being structurally incapable of carrying a semantic
|
|
495
|
+
# instruction.
|
|
496
|
+
printf '%s\n' "$transform" | grep -Eq -e "$IDENTIFIER_ERE" \
|
|
497
|
+
|| fail 6 "malformed_transform_name" "type '$TYPE_NAME': normalize entry '$transform' is not a bare identifier matching $IDENTIFIER_ERE — a free-text or semantic-extraction instruction is not representable in this format"
|
|
498
|
+
list_contains "$transform" ${REGISTRY_TRANSFORMS[@]+"${REGISTRY_TRANSFORMS[@]}"} \
|
|
499
|
+
|| fail 6 "unknown_transform" "type '$TYPE_NAME': normalize transform '$transform' is not a member of 'normalize_vocabulary' ($(join_with_comma ${REGISTRY_TRANSFORMS[@]+"${REGISTRY_TRANSFORMS[@]}"})) in $REGISTRY_FILE"
|
|
500
|
+
list_contains "$transform" "${IMPLEMENTED_TRANSFORMS[@]}" \
|
|
501
|
+
|| fail 6 "unimplemented_transform" "type '$TYPE_NAME': normalize transform '$transform' is enumerated in 'normalize_vocabulary' but $SELF has no implementation for it — adding a transform requires code, not only a registry edit (implemented: $(join_with_comma "${IMPLEMENTED_TRANSFORMS[@]}"))"
|
|
502
|
+
done
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
APPLIED_JSON="$(jq -c --arg t "$TYPE_NAME" '.types[$t].normalize // []' "$REGISTRY_FILE")"
|
|
506
|
+
EMPTY_JSON='[]'
|
|
507
|
+
|
|
508
|
+
# ---------------------------------------------------------------------------
|
|
509
|
+
# `verify: null` — always conforming, never failing. Short-circuits here, after
|
|
510
|
+
# the descriptor contract check above and before any line handling, because
|
|
511
|
+
# prose has no checkable shape and no transform could change that verdict.
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
if [ "$VERIFY_TYPE" = "null" ]; then
|
|
515
|
+
emit_outcome "conforming" 0 "$(jq -n --arg v "$RAW_VALUE" '$v')" "$EMPTY_JSON" "null"
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
# The value as a list of lines
|
|
520
|
+
# ---------------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
ELEMS=()
|
|
523
|
+
|
|
524
|
+
split_into_lines() {
|
|
525
|
+
local s="$1" line
|
|
526
|
+
ELEMS=()
|
|
527
|
+
# The empty value is zero elements, per POSIX line semantics.
|
|
528
|
+
[ -n "$s" ] || return 0
|
|
529
|
+
# A single terminating newline terminates the last line; it does not add an
|
|
530
|
+
# empty one. "a\n" -> [a]; "a\n\n" -> [a, ""].
|
|
531
|
+
s="${s%$'\n'}"
|
|
532
|
+
while :; do
|
|
533
|
+
case "$s" in
|
|
534
|
+
*$'\n'*)
|
|
535
|
+
line="${s%%$'\n'*}"
|
|
536
|
+
ELEMS+=("$line")
|
|
537
|
+
s="${s#*$'\n'}"
|
|
538
|
+
;;
|
|
539
|
+
*)
|
|
540
|
+
ELEMS+=("$s")
|
|
541
|
+
break
|
|
542
|
+
;;
|
|
543
|
+
esac
|
|
544
|
+
done
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
join_elems() {
|
|
548
|
+
local out="" i=0
|
|
549
|
+
if [ "${#ELEMS[@]}" -eq 0 ]; then
|
|
550
|
+
printf '%s' ""
|
|
551
|
+
return 0
|
|
552
|
+
fi
|
|
553
|
+
while [ "$i" -lt "${#ELEMS[@]}" ]; do
|
|
554
|
+
if [ "$i" -eq 0 ]; then
|
|
555
|
+
out="${ELEMS[$i]}"
|
|
556
|
+
else
|
|
557
|
+
out="$out
|
|
558
|
+
${ELEMS[$i]}"
|
|
559
|
+
fi
|
|
560
|
+
i=$((i + 1))
|
|
561
|
+
done
|
|
562
|
+
printf '%s' "$out"
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
# ---------------------------------------------------------------------------
|
|
566
|
+
# Transforms — the closed vocabulary, implemented exactly as the registry
|
|
567
|
+
# header describes it. The `*)` arm is unreachable: an unimplemented name was
|
|
568
|
+
# already rejected above. It stays as a guard against the two lists drifting.
|
|
569
|
+
# ---------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
transform_split_on_comma() {
|
|
572
|
+
local i=0 rest part
|
|
573
|
+
local -a out
|
|
574
|
+
out=()
|
|
575
|
+
while [ "$i" -lt "${#ELEMS[@]}" ]; do
|
|
576
|
+
rest="${ELEMS[$i]}"
|
|
577
|
+
while :; do
|
|
578
|
+
case "$rest" in
|
|
579
|
+
*,*)
|
|
580
|
+
part="${rest%%,*}"
|
|
581
|
+
out+=("$part")
|
|
582
|
+
rest="${rest#*,}"
|
|
583
|
+
;;
|
|
584
|
+
*)
|
|
585
|
+
out+=("$rest")
|
|
586
|
+
break
|
|
587
|
+
;;
|
|
588
|
+
esac
|
|
589
|
+
done
|
|
590
|
+
i=$((i + 1))
|
|
591
|
+
done
|
|
592
|
+
ELEMS=()
|
|
593
|
+
if [ "${#out[@]}" -gt 0 ]; then
|
|
594
|
+
ELEMS=("${out[@]}")
|
|
595
|
+
fi
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
# Strips leading and trailing whitespace from one element.
|
|
599
|
+
trim_one() {
|
|
600
|
+
local s="$1"
|
|
601
|
+
s="${s#"${s%%[![:space:]]*}"}"
|
|
602
|
+
s="${s%"${s##*[![:space:]]}"}"
|
|
603
|
+
printf '%s' "$s"
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
transform_trim() {
|
|
607
|
+
local i=0
|
|
608
|
+
local -a out
|
|
609
|
+
out=()
|
|
610
|
+
while [ "$i" -lt "${#ELEMS[@]}" ]; do
|
|
611
|
+
out+=("$(trim_one "${ELEMS[$i]}")")
|
|
612
|
+
i=$((i + 1))
|
|
613
|
+
done
|
|
614
|
+
ELEMS=()
|
|
615
|
+
if [ "${#out[@]}" -gt 0 ]; then
|
|
616
|
+
ELEMS=("${out[@]}")
|
|
617
|
+
fi
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
# "removes elements that are empty after trimming" — registry header. It drops
|
|
621
|
+
# whitespace-only elements without itself rewriting the survivors, so its
|
|
622
|
+
# behaviour does not depend on whether `trim` happened to run before it.
|
|
623
|
+
transform_drop_empty() {
|
|
624
|
+
local i=0
|
|
625
|
+
local -a out
|
|
626
|
+
out=()
|
|
627
|
+
while [ "$i" -lt "${#ELEMS[@]}" ]; do
|
|
628
|
+
if [ -n "$(trim_one "${ELEMS[$i]}")" ]; then
|
|
629
|
+
out+=("${ELEMS[$i]}")
|
|
630
|
+
fi
|
|
631
|
+
i=$((i + 1))
|
|
632
|
+
done
|
|
633
|
+
ELEMS=()
|
|
634
|
+
if [ "${#out[@]}" -gt 0 ]; then
|
|
635
|
+
ELEMS=("${out[@]}")
|
|
636
|
+
fi
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
apply_transform() {
|
|
640
|
+
case "$1" in
|
|
641
|
+
split_on_comma) transform_split_on_comma ;;
|
|
642
|
+
trim) transform_trim ;;
|
|
643
|
+
drop_empty) transform_drop_empty ;;
|
|
644
|
+
*) fail 6 "unimplemented_transform" "internal: no implementation dispatched for transform '$1'" ;;
|
|
645
|
+
esac
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
# ---------------------------------------------------------------------------
|
|
649
|
+
# Verification
|
|
650
|
+
# ---------------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
FAIL_KIND=""
|
|
653
|
+
FAIL_RULE=""
|
|
654
|
+
FAIL_INDEX=""
|
|
655
|
+
FAIL_ELEMENT=""
|
|
656
|
+
|
|
657
|
+
# Compiled once per rule kind, so an uncompilable pattern is reported as the
|
|
658
|
+
# registry contract violation it is rather than as a failed match.
|
|
659
|
+
assert_ere_compiles() {
|
|
660
|
+
local kind="$1" pattern="$2" rc=0
|
|
661
|
+
printf '' | grep -Eq -e "$pattern" || rc=$?
|
|
662
|
+
if [ "$rc" -gt 1 ]; then
|
|
663
|
+
fail 6 "uncompilable_pattern" "type '$TYPE_NAME': verify rule '$kind' pattern does not compile as a POSIX ERE (grep -E exited $rc): $pattern"
|
|
664
|
+
fi
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
rule_per_line() {
|
|
668
|
+
local pattern="$1" i=0 rc
|
|
669
|
+
while [ "$i" -lt "${#ELEMS[@]}" ]; do
|
|
670
|
+
rc=0
|
|
671
|
+
printf '%s\n' "${ELEMS[$i]}" | grep -Eq -e "$pattern" || rc=$?
|
|
672
|
+
if [ "$rc" -gt 1 ]; then
|
|
673
|
+
fail 6 "uncompilable_pattern" "type '$TYPE_NAME': verify rule 'per_line' pattern does not compile as a POSIX ERE (grep -E exited $rc): $pattern"
|
|
674
|
+
fi
|
|
675
|
+
if [ "$rc" -ne 0 ]; then
|
|
676
|
+
FAIL_KIND="per_line"
|
|
677
|
+
FAIL_RULE="$pattern"
|
|
678
|
+
FAIL_INDEX="$i"
|
|
679
|
+
FAIL_ELEMENT="${ELEMS[$i]}"
|
|
680
|
+
return 1
|
|
681
|
+
fi
|
|
682
|
+
i=$((i + 1))
|
|
683
|
+
done
|
|
684
|
+
return 0
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
# Returns 0 when every rule in the descriptor's `verify` object holds.
|
|
688
|
+
verify_current() {
|
|
689
|
+
local kind pattern value_type
|
|
690
|
+
FAIL_KIND=""
|
|
691
|
+
FAIL_RULE=""
|
|
692
|
+
FAIL_INDEX=""
|
|
693
|
+
FAIL_ELEMENT=""
|
|
694
|
+
for kind in ${RULE_KINDS[@]+"${RULE_KINDS[@]}"}; do
|
|
695
|
+
value_type="$(jq -r --arg t "$TYPE_NAME" --arg k "$kind" '.types[$t].verify[$k] | type' "$REGISTRY_FILE")"
|
|
696
|
+
case "$kind" in
|
|
697
|
+
per_line)
|
|
698
|
+
[ "$value_type" = "string" ] \
|
|
699
|
+
|| fail 6 "malformed_rule" "type '$TYPE_NAME': verify rule 'per_line' must be a string pattern, got $value_type"
|
|
700
|
+
pattern="$(jq -r --arg t "$TYPE_NAME" --arg k "$kind" '.types[$t].verify[$k]' "$REGISTRY_FILE")"
|
|
701
|
+
assert_ere_compiles "$kind" "$pattern"
|
|
702
|
+
if ! rule_per_line "$pattern"; then
|
|
703
|
+
return 1
|
|
704
|
+
fi
|
|
705
|
+
;;
|
|
706
|
+
*)
|
|
707
|
+
fail 6 "unimplemented_rule_kind" "internal: no implementation dispatched for verify rule kind '$kind'"
|
|
708
|
+
;;
|
|
709
|
+
esac
|
|
710
|
+
done
|
|
711
|
+
return 0
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
# ---------------------------------------------------------------------------
|
|
715
|
+
# The three outcomes
|
|
716
|
+
# ---------------------------------------------------------------------------
|
|
717
|
+
|
|
718
|
+
split_into_lines "$RAW_VALUE"
|
|
719
|
+
|
|
720
|
+
if verify_current; then
|
|
721
|
+
emit_outcome "conforming" 0 "$(jq -n --arg v "$RAW_VALUE" '$v')" "$EMPTY_JSON" "null"
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
# Remember the as-given failure: it, not the post-normalize one, is the more
|
|
725
|
+
# useful thing to report if normalization does not rescue the value.
|
|
726
|
+
PRE_FAIL_KIND="$FAIL_KIND"
|
|
727
|
+
PRE_FAIL_RULE="$FAIL_RULE"
|
|
728
|
+
PRE_FAIL_INDEX="$FAIL_INDEX"
|
|
729
|
+
PRE_FAIL_ELEMENT="$FAIL_ELEMENT"
|
|
730
|
+
|
|
731
|
+
if [ "${#TRANSFORMS[@]}" -gt 0 ]; then
|
|
732
|
+
for transform in "${TRANSFORMS[@]}"; do
|
|
733
|
+
apply_transform "$transform"
|
|
734
|
+
done
|
|
735
|
+
|
|
736
|
+
if verify_current; then
|
|
737
|
+
emit_outcome "normalized" 2 "$(jq -n --arg v "$(join_elems)" '$v')" "$APPLIED_JSON" "null"
|
|
738
|
+
fi
|
|
739
|
+
fi
|
|
740
|
+
|
|
741
|
+
# Non-conforming. The reason names the type, the failing rule, the element that
|
|
742
|
+
# failed it, and the raw pre-normalization value — everything a caller needs to
|
|
743
|
+
# report the failure without re-deriving any of it.
|
|
744
|
+
if [ "${#TRANSFORMS[@]}" -gt 0 ]; then
|
|
745
|
+
REASON="type '$TYPE_NAME': value does not satisfy verify rule '$FAIL_KIND' (/$FAIL_RULE/) even after normalize [$(join_with_comma "${TRANSFORMS[@]}")]; element $FAIL_INDEX of the normalized value is '$FAIL_ELEMENT' (as given, element $PRE_FAIL_INDEX was '$PRE_FAIL_ELEMENT'); raw value: '$RAW_VALUE'"
|
|
746
|
+
else
|
|
747
|
+
REASON="type '$TYPE_NAME': value does not satisfy verify rule '$PRE_FAIL_KIND' (/$PRE_FAIL_RULE/) and the type declares no normalize transforms; element $PRE_FAIL_INDEX is '$PRE_FAIL_ELEMENT'; raw value: '$RAW_VALUE'"
|
|
748
|
+
fi
|
|
749
|
+
|
|
750
|
+
emit_outcome "non-conforming" 3 "null" "$APPLIED_JSON" "$(jq -n --arg r "$REASON" '$r')"
|