Skip to content

fix(yaml): quoted scalars spanning lines were truncated to their first line on the stpa-yaml read path (REQ-364) - #976

Open
avrabe wants to merge 1 commit into
mainfrom
fix/req-364-multiline-quoted-scalars
Open

avrabe wants to merge 1 commit into
mainfrom
fix/req-364-multiline-quoted-scalars

Conversation

@avrabe

@avrabe avrabe commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #969. Stacked on #970 (REQ-363), now merged; rebased onto main.

What

A single- or double-quoted scalar that spans lines was truncated by the rowan read path at its first line, opening quote kept and the rest undecoded, with no diagnostic. rowan is the production reader for stpa-yaml sources, and PyYAML's default safe_dump wraps any long quoted string at 80 columns — so STPA files written by Python tooling lose descriptions silently.

Reproduced end to end on 0.37.0 with a PyYAML-written hazards file: rivet validatePASS, exit 0, while

rivet get H-1 → description: "'Controller issues: a ''quoted'' brake command with a colon: and a"

On this branch the same file returns the whole sentence. One-line quoted scalars were always fine.

Latent on this repository: PyYAML's composer finds 0 multi-line quoted scalars across all 118 tracked YAML files, and serde_yaml (what rivet writes with) keeps long quoted strings on one line. The exposure is files rivet didn't write. It is also a precondition of REQ-346, like REQ-361 and REQ-363.

Fix, in two layers

  • yaml_cst: scan_quoted_scalar continues a quoted token past a line break only when the next non-empty line is indented strictly deeper than the line the quote opened on. Otherwise the scalar is unclosed and falls back to a plain scalar, as before. The lexer also tokenizes block scalar bodies, so this rule is what stops an apostrophe in prose (Rivet's) from swallowing later keys. It also bounds an older runaway: a backslash at a line end, or "C:\", carried a double-quoted token past its line with no limit.
  • yaml_hir: fold_flow_break implements YAML 1.2 §7.3.1/§7.3.2 folding for both decode sites (titles/descriptions and field values): white space before an unescaped break dropped, one break → a space, each empty line → a newline, indentation dropped, an escaped break joins lines but keeps empty lines, and white space produced by an escape is protected from the trim.

Evidence (expected values from PyYAML)

  • tests/differential_scalars.rs, a new rowan-vs-serde_yaml gate over five families: one-line quoted scalars (also checked against the intended value), literal blocks, 1024 generated multi-line quoted scalars (escapes, doubled quotes, escaped and trailing white space, empty lines, escaped breaks, CRLF), quotes and unclosed quotes inside block bodies followed by a quoted key a runaway scan would close on, and a named survey. Before the fix, 5 of its 8 multi-line styles diverged.
  • 11 unit tests in yaml_cst.rs / yaml_hir.rs, where the --lib mutation gate can see them.

Negative controls (each restored; all redden the lib tests alone)

Mutation Caught by
continuation rule removed sibling-key test and parse_actual_hazards_file — an unbounded scan corrupts this repo's own STPA hazards
rule loosened to deeper-or-equal only an_unclosed_quote_does_not_swallow_a_sibling_key
lexer back to one line 8 tests
no trim / escaped break as space / empty lines as space / no escape protection / CRLF not joined / single quotes not folded the test named for each

Local gates on this rebased head

fmt 0 · clippy --all-targets -D warnings (1.97.0) 0 · cargo test --workspace 0 (2461 passed, 0 failed, corpus differential at 0 divergences) · cli_commands 218/218 · rivet validate 0 · rivet docs check 0 · yamllint 0.

REQ-364 is verified, release v0.38.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_015HMQUV3u86jN2hmCtXNTc9

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

📐 Rivet artifact delta

Change Count
Added 1
Removed 0
Modified 0
Downstream impacted (depth ≤ 5) 0

Graph

graph LR
  REQ_364["REQ-364"]:::added
  classDef added fill:#d4edda,stroke:#28a745,color:#155724
  classDef removed fill:#f8d7da,stroke:#dc3545,color:#721c24
  classDef modified fill:#fff3cd,stroke:#ffc107,color:#856404
  classDef overflow fill:#e2e3e5,stroke:#6c757d,color:#495057,stroke-dasharray: 3 3
Loading
Added
  • REQ-364

📎 Full HTML dashboard attached as workflow artifact rivet-delta-pr-976download from the workflow run.

Posted by rivet-delta workflow. The graph shows only changed artifacts; open the HTML dashboard (above) for full context.

…t line on the stpa-yaml read path (REQ-364)

A single- or double-quoted scalar that continued onto a later line came
back from the rowan path as its first line, opening quote included and
the rest undecoded, with no diagnostic. rowan is the production reader
for stpa-yaml sources. PyYAML's default safe_dump wraps long quoted
strings at 80 columns, so STPA files written by Python tooling lost
their descriptions silently. Reproduced on rivet 0.37.0 with a
PyYAML-written hazards file. rivet validate passed, and rivet get H-1
returned the text up to the first break prefixed with the quote. The
same file on this branch returns the whole sentence.

Latent on this repository. PyYAML's composer finds no multi-line quoted
scalar in any of the 118 tracked YAML files, and serde_yaml, which rivet
writes with, keeps long quoted strings on one line. It is a
precondition of REQ-346 for the same reason as REQ-361 and REQ-363.

Two layers, as with REQ-363:

- yaml_cst. scan_quoted_scalar lets a quoted token continue past a
  line break when the next non-empty line is indented strictly deeper
  than the line the quote opened on. Otherwise the scalar is unclosed
  and falls back to a plain scalar as before. The lexer also tokenizes
  block scalar bodies, and the rule is what keeps an apostrophe in
  prose from swallowing later keys. It also bounds an older runaway,
  where a backslash at a line end or an escaped quote carried a
  double-quoted token past its line with no limit.
- yaml_hir. fold_flow_break implements YAML 1.2 7.3.1 and 7.3.2 line
  folding for both decode sites (titles and descriptions, and field
  values). White space before an unescaped break is dropped, one break
  becomes a space, each empty line a newline, and indentation is
  dropped. An escaped break joins lines and keeps empty lines, and
  white space produced by an escape is protected from the trim.

Evidence, with PyYAML as the source of expected values:

- tests/differential_scalars.rs, a new rowan-vs-serde_yaml gate over
  five families. One-line quoted scalars are also checked against the
  intended value, then literal blocks, then 1024 generated multi-line
  quoted scalars (escapes, doubled quotes, escaped and trailing white
  space, empty lines, escaped breaks, CRLF). Next, quotes and unclosed
  quotes inside block bodies, followed by a quoted key that a runaway
  scan would close on. Last, a named survey. Before the fix 5 of its 8
  multi-line styles diverged.
- 11 unit tests in yaml_cst.rs and yaml_hir.rs, where the lib-only
  mutation gate can see them.

Negative controls, each restored, all reddening the lib tests alone.
Removing the continuation rule fails the sibling-key test and
parse_actual_hazards_file, the repository's own STPA hazards. Relaxing
it to deeper-or-equal is caught only by
an_unclosed_quote_does_not_swallow_a_sibling_key. A one-line lexer
fails 8 tests. Six HIR mutations each fail the test named for their
behaviour: no trim, escaped break as a space, empty lines as a space,
no escape protection, CRLF not joined, and single quotes not folded.

Confirmed locally on this rebased head with fmt --check, clippy
--all-targets -D warnings on 1.97.0, cargo test --workspace (2461
passing, 0 failed, including the corpus differential at 0 divergences,
so this change adds none), cli_commands 218 of 218, rivet validate,
rivet docs check and yamllint, all exit 0.

Fixes: REQ-364
Refs: REQ-346

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HMQUV3u86jN2hmCtXNTc9
@avrabe
avrabe force-pushed the fix/req-364-multiline-quoted-scalars branch from 6fd463e to 96f6f17 Compare September 17, 2026 18:25

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Rivet Criterion Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.

Benchmark suite Current: 96f6f17 Previous: f16245a Ratio
store_insert/10000 17098039 ns/iter (± 1525897) 12844170 ns/iter (± 331034) 1.33
store_lookup/100 1947 ns/iter (± 9) 1606 ns/iter (± 10) 1.21
store_lookup/1000 24240 ns/iter (± 101) 19373 ns/iter (± 101) 1.25
store_lookup/10000 358394 ns/iter (± 12120) 289329 ns/iter (± 1813) 1.24
store_by_type/100 95 ns/iter (± 0) 76 ns/iter (± 0) 1.25
store_by_type/1000 95 ns/iter (± 0) 76 ns/iter (± 0) 1.25
store_by_type/10000 95 ns/iter (± 2) 76 ns/iter (± 0) 1.25
diff/1000 719128 ns/iter (± 8050) 581112 ns/iter (± 7534) 1.24
diff/10000 11082434 ns/iter (± 476317) 7291375 ns/iter (± 149125) 1.52
query/100 1176 ns/iter (± 5) 910 ns/iter (± 14) 1.29
query/1000 14799 ns/iter (± 37) 11672 ns/iter (± 131) 1.27

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.29730% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rivet-core/src/yaml_cst.rs 94.69% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stpa-yaml silently truncates multi-line quoted scalars — PyYAML safe_dump output loses descriptions past the first line

1 participant