diff --git a/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md b/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md index 7226dea..91a6e91 100644 --- a/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md +++ b/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md @@ -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 to worktree-only derivation; a manifest simply absent at the tag — legitimate, it was added since — 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. diff --git a/plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh b/plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh index 66f3057..dae60b4 100755 --- a/plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh +++ b/plugins/kyberforge/skills/agent-audit/scripts/vale-wrap.sh @@ -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():]) diff --git a/plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh b/plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh index 66f3057..dae60b4 100755 --- a/plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh +++ b/plugins/kyberforge/skills/skill-audit/scripts/vale-wrap.sh @@ -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():]) diff --git a/tests/test-vale-wrap.sh b/tests/test-vale-wrap.sh index fa1cc48..c823a38 100755 --- a/tests/test-vale-wrap.sh +++ b/tests/test-vale-wrap.sh @@ -443,8 +443,11 @@ fi # 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 # 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 -# hook scripts. +# runs that a macOS user reaches: the wrapper itself, the two pre-commit hook +# 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 "--- no unguarded array expansion remains in the macOS-facing scripts ---" unguarded_expansions() { @@ -470,11 +473,19 @@ HAZARDS16="" for BASH32_SCRIPT in \ "$SCRIPT" \ "$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")" if [[ -n "$FOUND16" ]]; then HAZARDS16+="$FOUND16 " 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 if [[ -n "$HAZARDS16" ]]; then 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. +# 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() { echo "$1" \ | 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' \ | sort } @@ -601,29 +617,35 @@ echo "--- every multi-line description form reports what its single-line form re FIXTURE19_SINGLE="$(make_form_fixture single)" BASELINE19="$(alert_text "$(run_wrap "$FIXTURE19_SINGLE" --config "$VALE_CONFIG" "$REL_SKILL19")")" 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 -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 # flattened description. The flattener used to substitute U+2019 for every `'` # 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. -# Both branches that can hold an apostrophe verbatim are exercised: a value that -# is safe unquoted, and one that must be quoted (it contains `: `) and so has to -# land in a double-quoted scalar, since a single-quoted one would need the `''` -# escape that kills the scope outright. +# All three branches that can hold an apostrophe are exercised: a value that is +# safe unquoted; one that must be quoted (it contains `: `) and so lands in a +# double-quoted scalar, since a single-quoted one would need the `''` escape +# 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 "--- a style token containing an apostrophe matches in a flattened description ---" APOS_STYLE="$(mktemp -d)" @@ -638,6 +660,17 @@ ignorecase: true tokens: - "user's task" 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' StylesPath = styles @@ -668,7 +701,23 @@ Body. EOF )" 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" \ | grep -q "Apostrophe.Token"; then 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 done -# --- 20b. The one combination no verbatim YAML scalar can carry — needs -# quoting, holds an apostrophe, and holds a double quote — falls back to the -# lossy U+2019 substitution. Apostrophe-bearing tokens are lost there by -# design, but the scope must stay alive so every other rule still fires. +# --- 20b. The `|-` literal-block branch that case 20 just proved lossless must +# also keep the rest of the scope working and keep the line accounting right. +# The block is 2 physical lines where every inline form is 1, so the blank-line +# 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 "--- the unrepresentable combination keeps the description scope alive ---" -FIXTURE20C="$(make_raw_fixture <<'EOF' ---- -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 +echo "--- the |- literal-block fallback lints normally and preserves line numbers ---" +OUT20B=$(run_wrap "$FIXTURE20_BLOCK" --config "$VALE_CONFIG" "$REL_SKILL19") +if echo "$OUT20B" | grep -q "VagueWording"; then pass "a description needing quotes with both an apostrophe and a double quote is still linted" else fail "a description needing quotes with both an apostrophe and a double quote produced no alerts" 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. # 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" 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 "Results: $PASS passed, $FAIL failed" [[ $FAIL -eq 0 ]]