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

@@ -9,8 +9,10 @@ set -euo pipefail
# 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
# (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
# copy (padding with blank lines so every other line number is unchanged), then
# This script flattens an affected description to a one-line scalar in a scratch
# 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
# `vale` directly: same args, same exit code, bar the two documented divergences
# below.
@@ -61,6 +63,20 @@ vale_args=()
path_args=()
pending_flag=""
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
if [[ -n "$pending_flag" ]]; then
# Value of a separated two-argv flag. It is never a lint target, however
@@ -76,11 +92,12 @@ for arg in "$@"; do
fi
;;
--output|--path)
# `--output` is either a built-in style name (`line`, `JSON`) or a
# template file; only the file form needs resolving. `--path` is
# likewise a path. Anything that names nothing is passed through and
# See `is_builtin_output` above for why the built-in `--output` names
# are excluded first. Anything that names nothing is passed through and
# 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")
else
vale_args+=("$arg")
@@ -114,7 +131,9 @@ for arg in "$@"; do
# other path-valued flags.
--output=*|--path=*)
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")
else
vale_args+=("$arg")
@@ -286,13 +305,14 @@ def continuation_lines(rest):
def emit(value):
"""Render `value` as a one-line YAML scalar whose source text spells the
value out verbatim. Vale locates the description by matching the parsed
value back against the source, so a scalar carrying any escape — `''` in a
"""Render `value` as a YAML scalar whose source text spells the value out
verbatim. Vale locates the description by matching the parsed value back
against the source, so a scalar carrying any escape — `''` in a
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
whole `text.frontmatter.description` scope vanish, the same failure this
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
and value[0] not in PLAIN_UNSAFE_FIRST
and ': ' not in value
@@ -304,11 +324,14 @@ def emit(value):
if '"' not in value and '\\' not in value:
return '"' + value + '"' # double-quoted: only `"`/`\` would
# 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
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only —
# never written back to the real file.
return "'" + value.replace("'", '’') + "'"
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
# literal block can — a block scalar's body has no escape syntax at all, so
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
# the description scope against it (the header above says the same of the
# `|` 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)
@@ -392,11 +415,21 @@ if header_m:
newline = fm.find('\n', value_end)
span_end = len(fm) if newline == -1 else newline + 1
trailer = fm[value_end:span_end].rstrip('\n')
# One line replaces the span, so the blank-line pad is one short of the
# newline count it displaced — every later line number is unchanged.
pad = '\n' * (fm[head_start:span_end].count('\n') - 1)
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer
+ '\n' + pad + fm[span_end:])
scalar = emit(flat)
# A trailing comment carried across from the original line stays on the
# `description:` line itself: after a block scalar's `|-` header it is
# still a comment, but inside the block body it would become part of the
# 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.end():])

View File

@@ -9,8 +9,10 @@ set -euo pipefail
# 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
# (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
# copy (padding with blank lines so every other line number is unchanged), then
# This script flattens an affected description to a one-line scalar in a scratch
# 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
# `vale` directly: same args, same exit code, bar the two documented divergences
# below.
@@ -61,6 +63,20 @@ vale_args=()
path_args=()
pending_flag=""
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
if [[ -n "$pending_flag" ]]; then
# Value of a separated two-argv flag. It is never a lint target, however
@@ -76,11 +92,12 @@ for arg in "$@"; do
fi
;;
--output|--path)
# `--output` is either a built-in style name (`line`, `JSON`) or a
# template file; only the file form needs resolving. `--path` is
# likewise a path. Anything that names nothing is passed through and
# See `is_builtin_output` above for why the built-in `--output` names
# are excluded first. Anything that names nothing is passed through and
# 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")
else
vale_args+=("$arg")
@@ -114,7 +131,9 @@ for arg in "$@"; do
# other path-valued flags.
--output=*|--path=*)
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")
else
vale_args+=("$arg")
@@ -286,13 +305,14 @@ def continuation_lines(rest):
def emit(value):
"""Render `value` as a one-line YAML scalar whose source text spells the
value out verbatim. Vale locates the description by matching the parsed
value back against the source, so a scalar carrying any escape — `''` in a
"""Render `value` as a YAML scalar whose source text spells the value out
verbatim. Vale locates the description by matching the parsed value back
against the source, so a scalar carrying any escape — `''` in a
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
whole `text.frontmatter.description` scope vanish, the same failure this
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
and value[0] not in PLAIN_UNSAFE_FIRST
and ': ' not in value
@@ -304,11 +324,14 @@ def emit(value):
if '"' not in value and '\\' not in value:
return '"' + value + '"' # double-quoted: only `"`/`\` would
# 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
# U+2019 for the apostrophe keeps the scope alive, at the cost of any style
# rule whose token contains a literal ASCII apostrophe. Scratch-copy only —
# never written back to the real file.
return "'" + value.replace("'", '’') + "'"
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
# literal block can — a block scalar's body has no escape syntax at all, so
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
# the description scope against it (the header above says the same of the
# `|` 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)
@@ -392,11 +415,21 @@ if header_m:
newline = fm.find('\n', value_end)
span_end = len(fm) if newline == -1 else newline + 1
trailer = fm[value_end:span_end].rstrip('\n')
# One line replaces the span, so the blank-line pad is one short of the
# newline count it displaced — every later line number is unchanged.
pad = '\n' * (fm[head_start:span_end].count('\n') - 1)
new_fm = (fm[:head_start] + 'description: ' + emit(flat) + trailer
+ '\n' + pad + fm[span_end:])
scalar = emit(flat)
# A trailing comment carried across from the original line stays on the
# `description:` line itself: after a block scalar's `|-` header it is
# still a comment, but inside the block body it would become part of the
# 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.end():])