From ad1e5aaa9bf0e339982ea23f69bae935a28c47b9 Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sun, 9 Aug 2026 17:23:24 +0000 Subject: [PATCH] fix(kyberforge): carry apostrophes verbatim through a |- literal block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...14-vale-prefilter-ships-from-the-plugin.md | 18 ++ .../skills/agent-audit/scripts/vale-wrap.sh | 75 ++++++--- .../skills/skill-audit/scripts/vale-wrap.sh | 75 ++++++--- tests/test-vale-wrap.sh | 156 +++++++++++++----- 4 files changed, 242 insertions(+), 82 deletions(-) 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 ]]