fix(kyberforge): carry apostrophes verbatim through a |- literal block

The flattener's last-resort branch rewrote ASCII ' to U+2019, justified as the
one combination no YAML scalar can carry verbatim. That claim was false: a |-
literal block with a single indented content line carries ', ", \ and ": "
verbatim and keeps text.frontmatter.description matching — as the wrapper's own
docstring already said of literal blocks. The rewrite fired on 12 of 54
in-scope files, silently disabling every rule whose token contains an
apostrophe. Case 20 pinned only that the scope stayed alive, so it passed
either way.

The emission site now splits the emitted scalar on its first newline so a
carried-over trailing comment stays on the "description: |-" header line rather
than becoming part of the value, and pads by span_lines - 1 - newlines. The pad
stays non-negative because the branch is only reachable when the original span
is at least two lines. Verified across all 73 in-scope files: no line-count
changes, and exactly the 12 expected files take the new branch.

One reported position moves: an alert on a description that is itself flagged
shifts from the key line to the block's content line, both inside the original
span. YAML cannot put a literal block's content on the key's own line, so this
is unavoidable; no line at or after the end of any description span moves.

Also: --output no longer absolutises the built-in style names line, JSON and
CLI, which a same-named file or directory in cwd turned into a template path
(exit 2, E100 Runtime error). And case 19's empty-baseline guard no longer
lets five dependent comparisons print vacuous passes — while fixing it the
guard turned out to be unreachable, since under pipefail an alert-free report
aborted the script at the assignment.

Refs: #85
ADR: 0014
This commit is contained in:
2026-08-09 17:23:24 +00:00
parent d25355077f
commit ad1e5aaa9b
4 changed files with 242 additions and 82 deletions

View File

@@ -168,3 +168,21 @@ that makes the two disagree already touches `$HOOKS_MANIFEST`, itself a release-
unreadable tagged tree (shallow clone, truncated fetch) fails closed rather than silently degrading unreadable tagged tree (shallow clone, truncated fetch) fails closed rather than silently degrading
to worktree-only derivation; a manifest simply absent at the tag — legitimate, it was added since — to worktree-only derivation; a manifest simply absent at the tag — legitimate, it was added since —
does not. does not.
**Update — the flattener rewrites no characters.** This ADR never recorded it as a decision, but
`vale-wrap.sh`'s flattener carried a lossy last-resort branch: when a description needed quoting
*and* held an ASCII apostrophe *and* held a double quote or backslash, it substituted U+2019 (`’`)
for every `'` before writing the scratch copy, on the stated rationale that no verbatim YAML scalar
could carry that combination. The rationale was wrong. A `|-` literal block with a single indented
content line carries `'`, `"`, `\` and `: ` byte for byte — a block scalar's body has no escape
syntax at all — and vale's `text.frontmatter.description` scope still matches and fires rules on it
(verified against vale 3.15.2; it is the same property that makes the `|` blocks in the wrapper's
header safe to leave unflattened). The branch fired on 12 of the 54 in-scope files in this repo,
silently disabling every rule whose token contains an apostrophe on each of them. The flattener now
emits that literal block instead, so its output is verbatim in all four forms and no Vale rule can
be silently disabled by the prefilter. The `|-` form is two physical lines where the three inline
forms are one, so the blank-line pad that preserves later line numbers drops by one — reachable
only when the original span is already two or more lines, so the pad count stays non-negative.
`tests/test-vale-wrap.sh` case 20 asserts an apostrophe-bearing token actually fires on a flattened
description in all three apostrophe-carrying branches, and case 20b pins the pad arithmetic against
a body line's true line number.

View File

@@ -9,8 +9,10 @@ set -euo pipefail
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed # the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
# value keeps exactly the line breaks the source has, and vale matches it fine # value keeps exactly the line breaks the source has, and vale matches it fine
# (verified against vale 3.15.2), so literal blocks are deliberately left alone. # (verified against vale 3.15.2), so literal blocks are deliberately left alone.
# This script flattens an affected description to one physical line in a scratch # This script flattens an affected description to a one-line scalar in a scratch
# copy (padding with blank lines so every other line number is unchanged), then # copy — or, for the rare value no inline scalar can spell out verbatim, to a
# `|-` literal block with a single content line, which vale matches just as well
# (padding with blank lines so every other line number is unchanged), then
# runs the real `vale` binary against the copies. Drop-in replacement for calling # runs the real `vale` binary against the copies. Drop-in replacement for calling
# `vale` directly: same args, same exit code, bar the two documented divergences # `vale` directly: same args, same exit code, bar the two documented divergences
# below. # below.
@@ -61,6 +63,20 @@ vale_args=()
path_args=() path_args=()
pending_flag="" pending_flag=""
config_given=false config_given=false
# `--output` takes either one of vale's built-in style names or a template file
# path. Only the file form needs absolutizing, and the built-in names have to be
# excluded by name *before* the existence test below: a file or directory
# literally called `line` in the caller's cwd would otherwise rewrite the
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
# names — it is always a path — so the check is keyed on the flag too.
is_builtin_output() {
case "$2" in
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
*) false ;;
esac
}
for arg in "$@"; do for arg in "$@"; do
if [[ -n "$pending_flag" ]]; then if [[ -n "$pending_flag" ]]; then
# Value of a separated two-argv flag. It is never a lint target, however # Value of a separated two-argv flag. It is never a lint target, however
@@ -76,11 +92,12 @@ for arg in "$@"; do
fi fi
;; ;;
--output|--path) --output|--path)
# `--output` is either a built-in style name (`line`, `JSON`) or a # See `is_builtin_output` above for why the built-in `--output` names
# template file; only the file form needs resolving. `--path` is # are excluded first. Anything that names nothing is passed through and
# likewise a path. Anything that names nothing is passed through and
# left for vale to interpret. # left for vale to interpret.
if [[ "$arg" != /* && -e "$arg" ]]; then if is_builtin_output "$pending_flag" "$arg"; then
vale_args+=("$arg")
elif [[ "$arg" != /* && -e "$arg" ]]; then
vale_args+=("$cwd/$arg") vale_args+=("$cwd/$arg")
else else
vale_args+=("$arg") vale_args+=("$arg")
@@ -114,7 +131,9 @@ for arg in "$@"; do
# other path-valued flags. # other path-valued flags.
--output=*|--path=*) --output=*|--path=*)
flag_val="${arg#*=}" flag_val="${arg#*=}"
if [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then if is_builtin_output "${arg%%=*}" "$flag_val"; then
vale_args+=("$arg")
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
vale_args+=("${arg%%=*}=$cwd/$flag_val") vale_args+=("${arg%%=*}=$cwd/$flag_val")
else else
vale_args+=("$arg") vale_args+=("$arg")
@@ -286,13 +305,14 @@ def continuation_lines(rest):
def emit(value): def emit(value):
"""Render `value` as a one-line YAML scalar whose source text spells the """Render `value` as a YAML scalar whose source text spells the value out
value out verbatim. Vale locates the description by matching the parsed verbatim. Vale locates the description by matching the parsed value back
value back against the source, so a scalar carrying any escape — `''` in a against the source, so a scalar carrying any escape — `''` in a
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
whole `text.frontmatter.description` scope vanish, the same failure this whole `text.frontmatter.description` scope vanish, the same failure this
script exists to work around. Verbatim forms only, therefore, tried in script exists to work around. Verbatim forms only, therefore, tried in
descending order of fidelity.""" descending order of fidelity. The first three occupy one physical line; the
`|-` fallback occupies two, which the caller accounts for when padding."""
if (value if (value
and value[0] not in PLAIN_UNSAFE_FIRST and value[0] not in PLAIN_UNSAFE_FIRST
and ': ' not in value and ': ' not in value
@@ -304,11 +324,14 @@ def emit(value):
if '"' not in value and '\\' not in value: if '"' not in value and '\\' not in value:
return '"' + value + '"' # double-quoted: only `"`/`\` would return '"' + value + '"' # double-quoted: only `"`/`\` would
# Last resort: the value needs quoting AND holds an apostrophe AND a double # Last resort: the value needs quoting AND holds an apostrophe AND a double
# quote or backslash, so no verbatim YAML scalar can carry it. Substituting # quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style # literal block can — a block scalar's body has no escape syntax at all, so
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only — # `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
# never written back to the real file. # the description scope against it (the header above says the same of the
return "'" + value.replace("'", '’') + "'" # `|` blocks this script deliberately leaves alone; verified against vale
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
# value is exactly `value` with no trailing newline.
return '|-\n ' + value
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL) fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
@@ -392,11 +415,21 @@ if header_m:
newline = fm.find('\n', value_end) newline = fm.find('\n', value_end)
span_end = len(fm) if newline == -1 else newline + 1 span_end = len(fm) if newline == -1 else newline + 1
trailer = fm[value_end:span_end].rstrip('\n') trailer = fm[value_end:span_end].rstrip('\n')
# One line replaces the span, so the blank-line pad is one short of the scalar = emit(flat)
# newline count it displaced — every later line number is unchanged. # A trailing comment carried across from the original line stays on the
pad = '\n' * (fm[head_start:span_end].count('\n') - 1) # `description:` line itself: after a block scalar's `|-` header it is
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer # still a comment, but inside the block body it would become part of the
+ '\n' + pad + fm[span_end:]) # value.
head, newline_sep, block_body = scalar.partition('\n')
# The replacement displaces the whole span, so the blank-line pad makes
# up the difference between the lines it displaced and the lines it
# occupies — every later line number is unchanged. That is one line for
# the three inline forms and two for the `|-` block; the span itself is
# at least two lines here (`value_lines >= 2` is a precondition), so the
# pad count never goes negative.
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
new_fm = (fm[:head_start] + 'description: ' + head + trailer
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
content = (fm_match.group(1) + new_fm + fm_match.group(3) content = (fm_match.group(1) + new_fm + fm_match.group(3)
+ content[fm_match.end():]) + content[fm_match.end():])

View File

@@ -9,8 +9,10 @@ set -euo pipefail
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed # the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
# value keeps exactly the line breaks the source has, and vale matches it fine # value keeps exactly the line breaks the source has, and vale matches it fine
# (verified against vale 3.15.2), so literal blocks are deliberately left alone. # (verified against vale 3.15.2), so literal blocks are deliberately left alone.
# This script flattens an affected description to one physical line in a scratch # This script flattens an affected description to a one-line scalar in a scratch
# copy (padding with blank lines so every other line number is unchanged), then # copy — or, for the rare value no inline scalar can spell out verbatim, to a
# `|-` literal block with a single content line, which vale matches just as well
# (padding with blank lines so every other line number is unchanged), then
# runs the real `vale` binary against the copies. Drop-in replacement for calling # runs the real `vale` binary against the copies. Drop-in replacement for calling
# `vale` directly: same args, same exit code, bar the two documented divergences # `vale` directly: same args, same exit code, bar the two documented divergences
# below. # below.
@@ -61,6 +63,20 @@ vale_args=()
path_args=() path_args=()
pending_flag="" pending_flag=""
config_given=false config_given=false
# `--output` takes either one of vale's built-in style names or a template file
# path. Only the file form needs absolutizing, and the built-in names have to be
# excluded by name *before* the existence test below: a file or directory
# literally called `line` in the caller's cwd would otherwise rewrite the
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
# names — it is always a path — so the check is keyed on the flag too.
is_builtin_output() {
case "$2" in
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
*) false ;;
esac
}
for arg in "$@"; do for arg in "$@"; do
if [[ -n "$pending_flag" ]]; then if [[ -n "$pending_flag" ]]; then
# Value of a separated two-argv flag. It is never a lint target, however # Value of a separated two-argv flag. It is never a lint target, however
@@ -76,11 +92,12 @@ for arg in "$@"; do
fi fi
;; ;;
--output|--path) --output|--path)
# `--output` is either a built-in style name (`line`, `JSON`) or a # See `is_builtin_output` above for why the built-in `--output` names
# template file; only the file form needs resolving. `--path` is # are excluded first. Anything that names nothing is passed through and
# likewise a path. Anything that names nothing is passed through and
# left for vale to interpret. # left for vale to interpret.
if [[ "$arg" != /* && -e "$arg" ]]; then if is_builtin_output "$pending_flag" "$arg"; then
vale_args+=("$arg")
elif [[ "$arg" != /* && -e "$arg" ]]; then
vale_args+=("$cwd/$arg") vale_args+=("$cwd/$arg")
else else
vale_args+=("$arg") vale_args+=("$arg")
@@ -114,7 +131,9 @@ for arg in "$@"; do
# other path-valued flags. # other path-valued flags.
--output=*|--path=*) --output=*|--path=*)
flag_val="${arg#*=}" flag_val="${arg#*=}"
if [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then if is_builtin_output "${arg%%=*}" "$flag_val"; then
vale_args+=("$arg")
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
vale_args+=("${arg%%=*}=$cwd/$flag_val") vale_args+=("${arg%%=*}=$cwd/$flag_val")
else else
vale_args+=("$arg") vale_args+=("$arg")
@@ -286,13 +305,14 @@ def continuation_lines(rest):
def emit(value): def emit(value):
"""Render `value` as a one-line YAML scalar whose source text spells the """Render `value` as a YAML scalar whose source text spells the value out
value out verbatim. Vale locates the description by matching the parsed verbatim. Vale locates the description by matching the parsed value back
value back against the source, so a scalar carrying any escape — `''` in a against the source, so a scalar carrying any escape — `''` in a
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
whole `text.frontmatter.description` scope vanish, the same failure this whole `text.frontmatter.description` scope vanish, the same failure this
script exists to work around. Verbatim forms only, therefore, tried in script exists to work around. Verbatim forms only, therefore, tried in
descending order of fidelity.""" descending order of fidelity. The first three occupy one physical line; the
`|-` fallback occupies two, which the caller accounts for when padding."""
if (value if (value
and value[0] not in PLAIN_UNSAFE_FIRST and value[0] not in PLAIN_UNSAFE_FIRST
and ': ' not in value and ': ' not in value
@@ -304,11 +324,14 @@ def emit(value):
if '"' not in value and '\\' not in value: if '"' not in value and '\\' not in value:
return '"' + value + '"' # double-quoted: only `"`/`\` would return '"' + value + '"' # double-quoted: only `"`/`\` would
# Last resort: the value needs quoting AND holds an apostrophe AND a double # Last resort: the value needs quoting AND holds an apostrophe AND a double
# quote or backslash, so no verbatim YAML scalar can carry it. Substituting # quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style # literal block can — a block scalar's body has no escape syntax at all, so
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only — # `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
# never written back to the real file. # the description scope against it (the header above says the same of the
return "'" + value.replace("'", '’') + "'" # `|` blocks this script deliberately leaves alone; verified against vale
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
# value is exactly `value` with no trailing newline.
return '|-\n ' + value
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL) fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
@@ -392,11 +415,21 @@ if header_m:
newline = fm.find('\n', value_end) newline = fm.find('\n', value_end)
span_end = len(fm) if newline == -1 else newline + 1 span_end = len(fm) if newline == -1 else newline + 1
trailer = fm[value_end:span_end].rstrip('\n') trailer = fm[value_end:span_end].rstrip('\n')
# One line replaces the span, so the blank-line pad is one short of the scalar = emit(flat)
# newline count it displaced — every later line number is unchanged. # A trailing comment carried across from the original line stays on the
pad = '\n' * (fm[head_start:span_end].count('\n') - 1) # `description:` line itself: after a block scalar's `|-` header it is
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer # still a comment, but inside the block body it would become part of the
+ '\n' + pad + fm[span_end:]) # value.
head, newline_sep, block_body = scalar.partition('\n')
# The replacement displaces the whole span, so the blank-line pad makes
# up the difference between the lines it displaced and the lines it
# occupies — every later line number is unchanged. That is one line for
# the three inline forms and two for the `|-` block; the span itself is
# at least two lines here (`value_lines >= 2` is a precondition), so the
# pad count never goes negative.
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
new_fm = (fm[:head_start] + 'description: ' + head + trailer
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
content = (fm_match.group(1) + new_fm + fm_match.group(3) content = (fm_match.group(1) + new_fm + fm_match.group(3)
+ content[fm_match.end():]) + content[fm_match.end():])

View File

@@ -443,8 +443,11 @@ fi
# is declared and never reset to empty: it cannot be empty at any expansion # is declared and never reset to empty: it cannot be empty at any expansion
# site, so the construct is not a hazard there and demanding the guarded form # site, so the construct is not a hazard there and demanding the guarded form
# would be a wrong test. The file list covers every script this repo ships or # would be a wrong test. The file list covers every script this repo ships or
# runs that a macOS user reaches: the wrapper itself plus the two pre-commit # runs that a macOS user reaches: the wrapper itself, the two pre-commit hook
# hook scripts. # scripts, and the test runner AGENTS.md tells contributors to run by hand.
# `mapfile` is checked alongside, because it is bash 4.0+ and the expansion scan
# cannot see it — run-tests.sh carried one until it was replaced with a
# `while read` loop, and nothing would have caught its return.
echo "" echo ""
echo "--- no unguarded array expansion remains in the macOS-facing scripts ---" echo "--- no unguarded array expansion remains in the macOS-facing scripts ---"
unguarded_expansions() { unguarded_expansions() {
@@ -470,11 +473,19 @@ HAZARDS16=""
for BASH32_SCRIPT in \ for BASH32_SCRIPT in \
"$SCRIPT" \ "$SCRIPT" \
"$REPO_ROOT/scripts/skill-size-check.sh" \ "$REPO_ROOT/scripts/skill-size-check.sh" \
"$REPO_ROOT/scripts/check-release-needed.sh"; do "$REPO_ROOT/scripts/check-release-needed.sh" \
"$REPO_ROOT/tests/run-tests.sh"; do
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")" FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
if [[ -n "$FOUND16" ]]; then if [[ -n "$FOUND16" ]]; then
HAZARDS16+="$FOUND16 " HAZARDS16+="$FOUND16 "
fi fi
# `mapfile`/`readarray` are bash 4.0+ builtins with no 3.2 fallback. Whole-line
# comments are blanked first so prose naming the builtin is not a hit.
FOUND16B="$(awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$BASH32_SCRIPT" \
| grep -nE '(^|[^[:alnum:]_])(mapfile|readarray)[[:space:]]' || true)"
if [[ -n "$FOUND16B" ]]; then
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16B "
fi
done done
if [[ -n "$HAZARDS16" ]]; then if [[ -n "$HAZARDS16" ]]; then
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')" fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"
@@ -588,10 +599,15 @@ make_form_fixture() {
} }
# Alert text with the `line:col` prefix and ANSI colouring stripped, sorted. # Alert text with the `line:col` prefix and ANSI colouring stripped, sorted.
# The `|| true` matters under this file's `set -o pipefail`: a report with no
# alerts at all makes grep exit 1, which would abort the whole run inside the
# command substitutions below — silently, before the empty-baseline guard could
# print anything. Returning empty output instead is what makes that guard
# reachable.
alert_text() { alert_text() {
echo "$1" \ echo "$1" \
| sed -E 's/\x1b\[[0-9;]*m//g' \ | sed -E 's/\x1b\[[0-9;]*m//g' \
| grep -oE '(error|warning|suggestion)[[:space:]]+.*' \ | { grep -oE '(error|warning|suggestion)[[:space:]]+.*' || true; } \
| sed -E 's/[[:space:]]+/ /g' \ | sed -E 's/[[:space:]]+/ /g' \
| sort | sort
} }
@@ -601,29 +617,35 @@ echo "--- every multi-line description form reports what its single-line form re
FIXTURE19_SINGLE="$(make_form_fixture single)" FIXTURE19_SINGLE="$(make_form_fixture single)"
BASELINE19="$(alert_text "$(run_wrap "$FIXTURE19_SINGLE" --config "$VALE_CONFIG" "$REL_SKILL19")")" BASELINE19="$(alert_text "$(run_wrap "$FIXTURE19_SINGLE" --config "$VALE_CONFIG" "$REL_SKILL19")")"
if [[ -z "$BASELINE19" ]]; then if [[ -z "$BASELINE19" ]]; then
fail "the single-line baseline reported nothing — the comparison below would be vacuous" # The loop below has to be skipped, not merely reported on: an empty baseline
# compares equal to five empty results, so it would print five vacuous PASSes
# alongside this one FAIL. The FAIL alone still fails the run at the end.
fail "the single-line baseline reported nothing — the comparisons below would be vacuous, so they are skipped"
else
for FORM19 in folded plain dquote squote keyonly; do
DIR19="$(make_form_fixture "$FORM19")"
BARE19="$(cd "$DIR19" && vale --config "$VALE_CONFIG" "$REL_SKILL19" 2>&1 || true)"
GOT19="$(alert_text "$(run_wrap "$DIR19" --config "$VALE_CONFIG" "$REL_SKILL19")")"
if echo "$BARE19" | grep -q "VagueWording"; then
fail "bare vale already flags the $FORM19 form, so this case can't detect a silently-skipped flattening"
elif [[ "$GOT19" == "$BASELINE19" ]]; then
pass "a $FORM19 multi-line description reports the same alerts as its single-line form"
else
fail "a $FORM19 multi-line description diverged from its single-line form: got [$GOT19]"
fi
done
fi fi
for FORM19 in folded plain dquote squote keyonly; do
DIR19="$(make_form_fixture "$FORM19")"
BARE19="$(cd "$DIR19" && vale --config "$VALE_CONFIG" "$REL_SKILL19" 2>&1 || true)"
GOT19="$(alert_text "$(run_wrap "$DIR19" --config "$VALE_CONFIG" "$REL_SKILL19")")"
if echo "$BARE19" | grep -q "VagueWording"; then
fail "bare vale already flags the $FORM19 form, so this case can't detect a silently-skipped flattening"
elif [[ "$GOT19" == "$BASELINE19" ]]; then
pass "a $FORM19 multi-line description reports the same alerts as its single-line form"
else
fail "a $FORM19 multi-line description diverged from its single-line form: got [$GOT19]"
fi
done
# --- 20. A style token containing an ASCII apostrophe matches inside a # --- 20. A style token containing an ASCII apostrophe matches inside a
# flattened description. The flattener used to substitute U+2019 for every `'` # flattened description. The flattener used to substitute U+2019 for every `'`
# before writing the scratch copy, so no rule whose token carried an apostrophe # before writing the scratch copy, so no rule whose token carried an apostrophe
# could ever fire on a flattened description — a silent, rule-shaped blind spot. # could ever fire on a flattened description — a silent, rule-shaped blind spot.
# Both branches that can hold an apostrophe verbatim are exercised: a value that # All three branches that can hold an apostrophe are exercised: a value that is
# is safe unquoted, and one that must be quoted (it contains `: `) and so has to # safe unquoted; one that must be quoted (it contains `: `) and so lands in a
# land in a double-quoted scalar, since a single-quoted one would need the `''` # double-quoted scalar, since a single-quoted one would need the `''` escape
# escape that kills the scope outright. # that kills the scope outright; and one that also holds a double quote, which
# no inline scalar can spell verbatim and which therefore lands in a `|-`
# literal block.
echo "" echo ""
echo "--- a style token containing an apostrophe matches in a flattened description ---" echo "--- a style token containing an apostrophe matches in a flattened description ---"
APOS_STYLE="$(mktemp -d)" APOS_STYLE="$(mktemp -d)"
@@ -638,6 +660,17 @@ ignorecase: true
tokens: tokens:
- "user's task" - "user's task"
EOF EOF
# A body-scoped companion rule, used by case 20b to read back the line number of
# a line *after* the frontmatter — the only way to catch the blank-line pad
# being off in either direction.
cat > "$APOS_STYLE/styles/Apostrophe/Body.yml" <<'EOF'
extends: existence
message: "body token: '%s'"
level: error
scope: text
tokens:
- flattening marker phrase
EOF
cat > "$APOS_STYLE/.vale.ini" <<'EOF' cat > "$APOS_STYLE/.vale.ini" <<'EOF'
StylesPath = styles StylesPath = styles
@@ -668,7 +701,23 @@ Body.
EOF EOF
)" )"
new_fixture "$FIXTURE20_QUOTED" new_fixture "$FIXTURE20_QUOTED"
for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED"; do # Needs quoting (`: `), holds an apostrophe AND a double quote — the one
# combination no inline scalar can carry, so this is the `|-` literal-block
# branch. The VagueWording tokens are there for case 20b, which reuses it.
FIXTURE20_BLOCK="$(make_raw_fixture <<'EOF'
---
name: zzzskill
description: >
Triggers on: the user's task and "audit this" phrasing, which helps
with and utilize things across a second physical line.
---
Body carrying a flattening marker phrase for the line-number check.
EOF
)"
new_fixture "$FIXTURE20_BLOCK"
for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED" \
"literal-block:$FIXTURE20_BLOCK"; do
if run_wrap "${CASE20#*:}" --config "$APOS_STYLE/.vale.ini" "$REL_SKILL19" \ if run_wrap "${CASE20#*:}" --config "$APOS_STYLE/.vale.ini" "$REL_SKILL19" \
| grep -q "Apostrophe.Token"; then | grep -q "Apostrophe.Token"; then
pass "an apostrophe-bearing token matches in a flattened ${CASE20%%:*} description" pass "an apostrophe-bearing token matches in a flattened ${CASE20%%:*} description"
@@ -677,29 +726,31 @@ for CASE20 in "unquoted:$FIXTURE20_PLAIN" "double-quoted:$FIXTURE20_QUOTED"; do
fi fi
done done
# --- 20b. The one combination no verbatim YAML scalar can carry — needs # --- 20b. The `|-` literal-block branch that case 20 just proved lossless must
# quoting, holds an apostrophe, and holds a double quote — falls back to the # also keep the rest of the scope working and keep the line accounting right.
# lossy U+2019 substitution. Apostrophe-bearing tokens are lost there by # The block is 2 physical lines where every inline form is 1, so the blank-line
# design, but the scope must stay alive so every other rule still fires. # pad that preserves later line numbers has to drop by one. The second
# assertion pins that arithmetic against the body line's true number: case 3's
# `<= original line count` bound would not, since a pad that is one line short
# shifts every later line *up*, staying inside the bound while still lying.
echo "" echo ""
echo "--- the unrepresentable combination keeps the description scope alive ---" echo "--- the |- literal-block fallback lints normally and preserves line numbers ---"
FIXTURE20C="$(make_raw_fixture <<'EOF' OUT20B=$(run_wrap "$FIXTURE20_BLOCK" --config "$VALE_CONFIG" "$REL_SKILL19")
--- if echo "$OUT20B" | grep -q "VagueWording"; then
name: zzzskill
description: >
Triggers on: the user's "audit this" phrasing, which helps with
and utilize things across a second physical line.
---
Body.
EOF
)"
new_fixture "$FIXTURE20C"
if run_wrap "$FIXTURE20C" --config "$VALE_CONFIG" "$REL_SKILL19" | grep -q "VagueWording"; then
pass "a description needing quotes with both an apostrophe and a double quote is still linted" pass "a description needing quotes with both an apostrophe and a double quote is still linted"
else else
fail "a description needing quotes with both an apostrophe and a double quote produced no alerts" fail "a description needing quotes with both an apostrophe and a double quote produced no alerts"
fi fi
WANT20B_LINE="$(grep -n 'flattening marker phrase' "$FIXTURE20_BLOCK/$REL_SKILL19" | cut -d: -f1)"
# `--output line` prints `file:line:col:Rule:message`, so the line number reads
# back without any wrapping or colour to strip.
GOT20B_LINE="$(run_wrap "$FIXTURE20_BLOCK" --config "$APOS_STYLE/.vale.ini" --output line "$REL_SKILL19" \
| grep 'Apostrophe.Body' | head -1 | cut -d: -f2)"
if [[ "$GOT20B_LINE" == "$WANT20B_LINE" ]]; then
pass "a body line after a |- flattened description keeps its original line number ($WANT20B_LINE)"
else
fail "the |- block's blank-line pad shifted the body: vale reported line $GOT20B_LINE, the file has it at $WANT20B_LINE"
fi
# --- 21. A symlinked file inside a directory argument is mirrored and linted. # --- 21. A symlinked file inside a directory argument is mirrored and linted.
# Vale follows symlinks (both a symlinked file and a file under a symlinked # Vale follows symlinks (both a symlinked file and a file under a symlinked
@@ -766,6 +817,31 @@ else
fail "a typo'd path exited $RC23 but the message does not name it: $OUT23" fail "a typo'd path exited $RC23 but the message does not name it: $OUT23"
fi fi
# --- 24. `--output`'s built-in style names must not be path-absolutized. The
# wrapper rewrites path-valued flag values to absolute form so they still
# resolve after the `cd` into the scratch mirror, deciding with an `-e`
# existence test — but `line`, `JSON` and `CLI` are style names, not paths. With
# a file or directory of that name sitting in the caller's cwd the test hit, the
# built-in became `$cwd/line`, and vale flipped into template mode and died with
# `E100 [template] Runtime error` where bare vale prints a normal report.
echo ""
echo "--- a built-in --output style name survives a same-named entry in the cwd ---"
FIXTURE24="$(make_fixture 2)"
new_fixture "$FIXTURE24"
mkdir -p "$FIXTURE24/line"
: > "$FIXTURE24/JSON"
for FORM24 in "--output line" "--output=line" "--output JSON" "--output=JSON"; do
# shellcheck disable=SC2086 # deliberate word splitting of the argv fixture
OUT24="$(run_wrap "$FIXTURE24" --config "$VALE_CONFIG" $FORM24 "$REL_SKILL19")"
if echo "$OUT24" | grep -q "E100"; then
fail "'$FORM24' was rewritten to a cwd path and vale flipped into template mode — the bug this test guards against"
elif echo "$OUT24" | grep -q "VagueWording"; then
pass "'$FORM24' is passed through as a built-in style name"
else
fail "'$FORM24' produced no alert: $OUT24"
fi
done
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] [[ $FAIL -eq 0 ]]