Commit Graph

110 Commits

Author SHA1 Message Date
b7bcfe1a01 fix(parsing): enforce the full YAML 1.2 double-quoted escape table
The double-quoted escape decoder recognized only a subset of escapes
(" \ \/ \b \f \n \r \t \u) and silently passed any other character
through. That accepted invalid escapes such as \. and \', and mangled
valid ones the subset omitted: \x and \U hex escapes, \v \e \0, the named
\N \_ \L \P, and a backslash before a space, were emitted as their literal
letters (\x0d became the text x0d).

The decoder now maps every escape in the YAML 1.2 table to its character,
reads \x (8-bit) and \U (32-bit) hex escapes alongside \u, treats a
backslash before a line break as an escaped line break that joins the
lines with no separator, and rejects an unknown or truncated escape with
a diagnostic. Single-character escapes move to a lookup table so the
decoder stays within the complexity limit.

Measured against the official YAML test-suite, parse-level conformance
rises from 247/333 (74.2%) to 249/333 (74.8%) as two invalid documents
are now rejected, and value-level from 166 to 170 of comparable cases
(90.4%) as the previously-mangled escapes decode correctly, with no
regression.
2026-06-27 16:19:25 +02:00
1d07fce635 fix(parsing): reject a comment glued to earlier content on its line
A '#' that begins a comment must be preceded by whitespace or open the
line; glued to earlier same-line content (e.g. "value"# c, [ a, b ]#c,
>#c, a flow entry then ,#c) it is neither a comment nor, in those
positions, plain-scalar content, so the document is malformed. Such input
was previously accepted as a comment.

A '#' that opens a line, follows a space, or sits inside a plain scalar
(a#b, where it stays literal content and never reaches this check) is
unaffected. The scanner tracks whether a real token has been scanned on
the current physical line, reset on each line break, rather than comparing
token line numbers: the streaming tokenizer drains its token buffer
between scans, and a block scalar's blank line emits an empty scalar
attributed to the next line, either of which would misread as content.

Measured against the official YAML test-suite, parse-level conformance
rises from 243/333 (73.0%) to 247/333 (74.2%): four wrongly-accepted
invalid documents are now rejected, with no valid document regressing.
The scalar-scanning routines move to Scanner.Scalars.swift to keep the
file within the length limit.
2026-06-27 15:53:16 +02:00
b80e9c1c63 fix(parsing): comments and over-indented blanks ending block scalars
A block scalar terminated by a less-indented comment line failed with a
spurious 'unexpected indentation': once the smaller indentation ended the
scalar, the comment's own column was processed as a dedent, and because no
indentation level held that width the dedent raised an error. A comment's
indentation does not participate in block structure, so a comment that ends
a block scalar is now left for the main loop to scan, with the block
scalar's own first content line (which may itself begin with '#') still
treated as content.

Separately, a whitespace-only line indented past a block scalar's content
level lost its surplus spaces because the width was consumed before the
line was recognized as blank; the surplus is now preserved as the line's
content, so an over-indented blank contributes the correct value.

Measured against the official YAML test-suite, parse-level conformance
rises from 240/333 (72.1%) to 243/333 (73.0%) with matching value-level
gains, and no regression.
2026-06-27 15:34:13 +02:00
ceec6cca84 fix(parsing): allow a space before the colon of a quoted mapping key
A quoted mapping key followed by a space and then ':' ('"key" : value')
failed to parse: the scanner synthesizes the implicit mapping-key token by
checking the character immediately after the key, but a quoted scalar
stops at its closing quote, leaving the separating space, whereas a plain
scalar consumes that space into itself and ends at the colon. The quoted
path now looks past spaces for the colon, which the spec allows between an
implicit key and its value indicator.

Quoted keys with no space, quoted values, and plain keys (which already
worked with a space) are unaffected. Measured against the official YAML
test-suite, parse-level conformance rises from 238/333 (71.5%) to
240/333 (72.1%), with no regression.
2026-06-27 01:19:28 +02:00
0ae88855da fix(parsing): allow an anchor or tag on a non-first mapping key
A block mapping pair after the first could not carry an anchor or tag on
its key (k: 1 then &b key2: 2): the first pair's key properties are
consumed when the mapping is entered, but subsequent pairs were not, so an
anchored key past the first failed with 'expected mapping key, found
anchor'. The pair parser now consumes leading key properties for every
pair, attaching them to the key node, and a block mapping continues across
an anchor or tag token that begins a key.

Normal mappings, anchored values, sequences, and explicit keys are
unaffected. Measured against the official YAML test-suite, parse-level
conformance rises from 235/333 (70.6%) to 238/333 (71.5%), with no
regression.
2026-06-27 00:21:38 +02:00
58f30b4343 fix(parsing): an explicit key with no value maps to null
The block mapping parser required a ':' after every explicit key ('? key'),
so a key with no value, including several '? key' entries in a row (a set),
failed to parse with 'expected mapping value'. An explicit key with no
following ':' now takes an implicit null value, shared with the existing
'key:'-with-nothing-after path via one emitNullMappingValue helper.

Explicit keys with values and complex keys in general are unaffected. A
test that asserted '? [a, b]' followed by a block entry is a missing-value
error was corrected: the key is a valid null entry, and the trailing
sequence is now rejected as extra content (a second root node), which is
the more accurate diagnosis.

Measured against the official YAML test-suite, parse-level conformance
rises from 229/333 (68.8%) to 235/333 (70.6%), with several of the newly
parsed documents also value-correct and no regression.
2026-06-26 22:28:13 +02:00
82d5b57fd6 fix(parsing): ignore an unrecognized directive instead of rejecting it
A directive the processor does not recognize (e.g. %FOO) before a
document is now ignored, matching the YAML spec (emit a warning, otherwise
no effect), rather than failing the whole document with an
unsupportedDirective error. %YAML and %TAG handling is unchanged, and a
directive after content, where a directive cannot legally appear, is still
rejected via the existing guard.

Two tests and a fixture that pinned the old reject-everything behavior are
updated: the unknown-directive case moves from the unsupported-gaps list,
and the scanner-error test now exercises a directive-after-content case
(still an error) rather than an unknown directive before content.

Measured against the official YAML test-suite, parse-level conformance
rises from 227/333 (68.2%) to 229/333 (68.8%), with the two newly-parsed
documents also value-correct and no regression.
2026-06-26 22:20:18 +02:00
688f0d5c12 fix(parsing): fold a document-root plain scalar across column-1 lines
A plain scalar at the document root failed to parse when it spanned
multiple lines at column 1 (`a\nb\nc`): the continuation check required a
continuation to be indented more than the scalar, but a root scalar has
nothing enclosing it, so its enclosing-block indent is -1 and a
continuation may start at column 1. The base indentation now returns -1 at
the root, and the continuation stops at a document marker (--- / ...) so a
scalar never folds across a document boundary.

A stream test that asserted two bare plain lines (first / second) were
rejected as extra content was corrected: that input is a valid multi-line
scalar; genuine extra content (a flow node followed by a scalar) is still
rejected, which the test now uses.

Measured against the official YAML test-suite, parse-level conformance
rises from 221/333 (66.4%) to 227/333 (68.2%), with four more documents
also now value-correct and no regression across the existing suite.
2026-06-26 21:30:49 +02:00
27e8defac6 fix(parsing): fold line breaks in multi-line single-quoted scalars
Single-quoted scalars share the same flow line folding as double-quoted,
which the previous commit added: a single break folds to a space, a blank
line to one newline, whitespace adjacent to a break is stripped, and the
first-line leading and last-line trailing whitespace are kept. The
single-quoted decoder kept its raw breaks; it now reuses the shared
foldFlowScalarLineBreaks helper, while doubled quotes ('') still escape to
one quote.

Measured against the official YAML test-suite, value-level conformance
rises from 143/169 (84.6%) to 147/169 (87.0%) with no regression.
Parse-level is unchanged: these documents already parsed, just to the
wrong tree.
2026-06-26 21:18:55 +02:00
08e1979085 fix(parsing): fold line breaks in multi-line double-quoted scalars
The double-quoted decoder appended real line breaks and continuation-line
indentation verbatim, so a multi-line "..." scalar kept its raw newlines
and leading whitespace instead of folding. It now applies YAML flow
folding at each unescaped break: a single break folds to a space, a blank
line to one newline, and whitespace adjacent to a break (the line's
trailing, the next line's leading) is stripped. First-line leading and
last-line trailing whitespace stay (they sit next to a quote, not a
break), and \n escapes are still decoded to literal newlines.

Measured against the official YAML test-suite, value-level conformance
rises from 135/169 (79.9%) to 143/169 (84.6%) with no regression.
Parse-level is unchanged: these documents already parsed, just to the
wrong tree, which only the value-level metric exposes.
2026-06-26 21:13:13 +02:00
455cd07f4a fix(parsing): keep a root node that begins on the document-marker line
A document-start marker scan skipped the rest of the '---' line
unconditionally, so a root node written on that same line was discarded:
a block-scalar header ('--- >' / '--- |') was dropped and its body
mis-read as a plain scalar (folded to spaces, no trailing newline), and a
same-line plain or flow node was lost entirely. The document start now
skips only trailing spaces and leaves any real same-line content for the
parser; an empty rest-of-line or a comment is still consumed, and a node
on the line below the marker is unaffected. Document end ('...') still
takes no same-line node.

Measured against the official YAML test-suite, value-level conformance
rises from 130/170 (76.5%) to 135/169 (79.9%) with no regression.
Parse-level is unchanged: these documents already parsed, just to the
wrong tree, which only the value-level metric exposes.
2026-06-26 20:50:24 +02:00
c888a113f2 test: add value-level YAML conformance against in.json
Parse-level conformance only asks whether a document parses or is
rejected; a parser can accept a document and build the wrong tree and
still score. The harness now also compares the parsed tree to each case's
in.json, the canonical expected value, the honest metric.

Value-level baseline: 130/170 (76.5%) of comparable single-document
cases. It immediately exposes 40 documents that parse without error but to
the wrong tree, invisible to parse-level: double-quoted multi-line folding
(the newline+indent should fold to one space), scalar resolution (a value
read as null instead of a string), and block-literal indentation. 49 more
do not parse, and 12 multi-doc/non-string-key cases are not comparable
through a single JSON value and are counted as skipped, not failed.

Every future parsing fix is now judged against ground truth, not just
whether it threw.
2026-06-26 20:18:32 +02:00
d90a091ab0 fix(parsing): fold a plain scalar value that begins on its own line
A multi-line plain scalar whose value sits one line below its mapping key
stopped after the first line: the continuation check compared the next
line's indent against the value's own indent, which had been pushed onto
the indentation stack, so a continuation at the same indent failed the
strict '>'. It now compares against the parent block's indent (the level
below the value's, when the scalar begins exactly at the indentation top),
matching the spec: a continuation must exceed the mapping's indentation,
not the value's own. Inline values are unaffected, since their top-of-
stack already is the parent block's indent.

Measured against the official YAML test-suite, parse-level conformance
rises from 211/333 (63.4%) to 221/333 (66.4%) with no regression across
the existing 306 tests. The +10 covers the multi-line plain scalar family
the harness flagged, not just the case investigated.
2026-06-26 18:52:09 +02:00
eaf6a01a0e test: break down YAML test-suite parse failures by cause
The harness now histograms should-parse failures by ParseError
diagnosticCode, so the conformance gap is actionable rather than a single
number. The breakdown corrects an earlier heuristic guess: multi-document
streams are not the lever (1 of 78 failures). The real clusters are
unexpectedToken=49 (explicit/complex keys, multi-line plain scalars,
tagged keys, document-marker + node combinations), expectedNode=11, and
unsupportedDirective=10.
2026-06-26 18:26:21 +02:00
9298536e52 test: measure conformance against the official YAML test-suite
Adds an opt-in harness that runs PureYAML over the canonical YAML 1.2
corpus (github.com/yaml/yaml-test-suite) and reports a parse-level pass
rate: a should-parse case must parse, a should-error case must be
rejected. The data is fetched into a gitignored location by
scripts/fetch-yaml-test-suite.sh, never vendored, and the run is gated
behind PUREYAML_RUN_YAML_TEST_SUITE=1 so the default suite is unaffected.

This turns 'best YAML library' from an assertion into a tracked number.
Baseline: 211/333 (63.4%) parse-level: 177/255 valid documents parse,
34/78 invalid documents are rejected. So 78 valid docs are not yet parsed
and 44 invalid docs are wrongly accepted. The stricter value-level check
against each case's in.json is the next step.
2026-06-26 17:43:19 +02:00
985bd7baac test: round-trip the full scalar type matrix through Codable containers
Enumerate every scalar Codable type (Int8 through Int64, Int, UInt
through UInt64, Float, Double, Bool, String) through both a keyed
container, via an all-scalars struct, and an unkeyed container, via
per-type arrays. This drives the keyed encode and decode container
methods and the encoder's scalar ladder to near-full coverage and pins
exact round-trip equality for each type.
2026-06-26 09:04:54 +02:00
2f6de52654 test: cover the remaining low-level paths
Pin the last untested-but-correct paths surfaced by the coverage audit:
flow-style emission of double scalars, BatchReport.description across
multiple sources, the unkeyed superDecoder reading successive indexes,
the PreflightScanner scheme-colon exception that must not flag a missing
mapping space, and the tagged stream-validation throwing overload that
wraps failures in a document-indexed issue collection. Each assertion was
measured against actual behavior before being pinned.
2026-06-26 08:59:24 +02:00
ceb12295a4 fix(parsing): preserve leading blank lines in block scalars
Blank lines between a block scalar header and its first content line come
before the indent token, so the earlier blank-line capture skipped them.
Buffer leading blanks while the block indentation is still pending and
flush them as empty content once the indent is established, in the shared
indentation path used by both scan loops. Also fix folded bodies to emit
a break for a leading blank rather than folding it into a leading space.
This closes the last block-scalar blank-line edge from #57.
2026-06-26 08:42:16 +02:00
234e0382e6 refactor(parsing): remove three unreachable ParseError cases
emptyDocument, expectedMappingKey, and tabIndentation were never thrown
anywhere in the parser. Empty and comment-only documents parse to null,
the mapping-key path throws unexpectedToken with an "expected mapping
key" message, and tabs in indentation are reported by the validation
layer's PreflightScanner rather than the parser. Drop the dead cases and
their switch arms and description test assertions. No reachable behavior
changes; the PropertyMutation.tabIndentation test mutation is unrelated
and untouched.
2026-06-26 08:37:53 +02:00
4358cf02cc fix(parsing): keep more-indented folded lines literal
A folded block scalar folded every non-blank line into the surrounding
text, including more-indented lines, which YAML keeps literal with their
line breaks. Detect a more-indented line by its retained leading
whitespace and keep the breaks adjacent to it instead of folding them.
Plain and blank-line folding are unchanged.
2026-06-26 08:13:43 +02:00
d3ff3823a5 fix(parsing): preserve blank lines in block scalars
The scanner dropped every blank line within and after a block scalar, so
a literal block lost internal blank lines, a folded block turned a blank
line into a space rather than a newline, and the keep indicator could not
preserve trailing blank lines. Capture blank lines as empty content in
both the batch scan loop and the streaming TokenCursor loop, which had
drifted as duplicate copies. Rework blockScalarValue to apply literal
versus folded line semantics and clip/strip/keep chomping over the
captured lines; content without blank lines is unchanged. Re-pin the
multiline-scalar test whose expectation encoded the old dropped-blank
behavior, and add a chomping-and-folding test matrix.
2026-06-26 08:09:45 +02:00
934c9eafa3 test: cover numeric ladder, NaN hashing, unkeyed edges, error paths
Fill the highest-value coverage gaps found in the Codable bridge and
parser: the full integer-width decode ladder and per-width out-of-range
errors, Float round-trip, NaN equality and hash consistency (the bespoke
nan == nan invariant), unkeyed decodeNil false-branch and nested-index
mismatch, and the previously untested expectedNode, unterminated flow
mapping, explicit block-scalar under-indent, and buffered invalid-merge
error paths.
2026-06-26 07:50:08 +02:00
fadde56a05 fix(parsing): classify the keep block-scalar chomping indicator
The scanner consumed the `+` indicator of a `|+` / `>+` block scalar but
mapped it to nothing, so keep was silently treated as clip, and a scanner
test baked that in. Add a `keep` case to BlockScalarChomping, recognize
`+` in the header scanner, and correct the test. Full keep value semantics
(preserving trailing blank lines) depend on scanner blank-line capture and
are tracked in #57; this commit fixes the token classification only.
2026-06-26 07:50:07 +02:00
44abc12e80 docs: record rules conformance, import contract, and round-trip law
Add docs/rules-conformance.md documenting how the repo conforms to the
vendored canon, the documentation.md vs dependency-free conflict and its
scoped resolution, the domain-mismatched rules that do not apply, and the
conscious deviations. Add docs/package-import-contract-status.md. State
the round-trip law (Lambda1/Lambda2) in DESIGN.md.
2026-06-26 06:14:24 +02:00
7270359d14 ci(githooks): enforce format, lint, and conventional commits
Run SwiftFormat and SwiftLint on staged Swift files in the pre-commit
hook (formatting-and-linting.md), gracefully skipping when a tool is
absent since CI re-runs both. Enforce the Conventional Commits subject
format and the 72-character subject limit in the commit-msg hook
(commits.md). Correct the pre-push comment that named the removed ci.yml.
2026-06-26 06:14:24 +02:00
c4f7f9dccc refactor(code-style): one top-level type per source file
Split the six files that declared multiple sibling types under one
namespace extension so each file declares exactly one top-level type:
the Tagged node component types (Node, NodeKind, Scalar, Sequence,
Mapping, Pair), the validation report types (Report, Source,
SourceReport, BatchReport), DiagnosticKind, TokenKind, ScannerFlowContext
kind, and the event-stream composer helpers. Public type names are
unchanged; the shared report-rendering helpers move from file-private to
internal so the split report types still reach them.

Make check-namespacing.sh namespace-aware: the previous column-anchored
scan was blind to types declared inside indented extension blocks, so it
passed while the rule was violated. It now counts direct members of a
PureYAML namespace extension and ignores nested types and private helpers.
2026-06-26 06:14:21 +02:00
9ae6c3baff refactor(parsing): share merge-key resolution between composers
The buffered and streaming event composers each carried an identical
copy of the merge-key (<<) override logic and its invalid-value error.
Extract both into one internal Parsing.MergeResolution helper so the
precedence rules (local keys win, earlier sources win, inherited then
local order) have a single source of truth. No behavior change; all
tests pass.
2026-06-26 05:50:28 +02:00
87e3e5cbab docs(security): fix contact address and correct the scope section
The reporting address was missing its @ and was unusable. The scope
section described an unrelated static-site generator; replace it with
the YAML parsing, validation, and emitting surface that is actually in
scope (malformed-input DoS, misrepresented values, validation as a
trust boundary).
2026-06-26 05:50:08 +02:00
ea31bb5864 chore(ci): split CI into per-workflow files with truthful badges
Rule 8.1 forbids one multi-job workflow fronted by several cosmetically
labeled badges that all resolve to the same whole-workflow status. Split
ci.yml into one workflow per concern (gates, macOS, Linux, Windows,
WASM, corpus), each with a unique name, and point each README badge at
its own workflow through the native per-workflow badge. The WASM
workflow notes in a header that it is a build gate, not a test gate
(Rule 8.3). Add a static MIT license badge linking to LICENSE
(Rule 8.2). Step contents are unchanged from the previous jobs.
2026-06-26 05:48:11 +02:00
9acfd57acc chore(issues): add required complexity field and a form labeler
Add the Complexity dropdown that Rule 1.6 requires on both issue forms
(status date, priority, complexity), alongside the existing per-kind
fields. Add a labeler workflow that maps the Priority dropdown to the
priority: high label on issue open and edit, closing the Rule 1.6
gotcha where form dropdowns produce body text but never apply a label.
Complexity stays a body field only, never a label, per Rule 2.1.
2026-06-26 05:48:01 +02:00
5f3892f2c1 chore(rules): vendor the rules-swift canon into docs/rules-swift
Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into
docs/rules-swift/ as a read-only reference alongside the repo's tailored
docs/rules/. PROVENANCE.md records the source, commit, and what was
included or excluded; the upstream repo's own VCS, CI, and lint tooling
is omitted as machinery rather than rule content.

Exclude the vendored tree from the style gate (scripts/check-style.sh)
and the pre-commit hook: the canon's enforcement docs quote the
forbidden attribution phrases to define them, the same reason the
repo's own enforcement files are already excluded.
2026-06-26 05:36:12 +02:00
16a47df118 docs(parsing): document why the nesting bound is necessary and sufficient 2026-06-23 11:08:30 +02:00
64a171bff4 fix(parsing): bound nesting depth so pathological input errors instead of crashing 2026-06-23 09:29:45 +02:00
5d76d184cb fix(parsing): treat leading '#' in block scalars as literal content 2026-06-23 04:00:07 +02:00
c13b7c97fd test: demonstrate yaml/json parsing and generation round-trips
Add a suite covering the four supported conversions around a shared
Codable value: parsing YAML, parsing JSON (via the same API, since YAML
1.2 is a JSON superset), generating YAML, and generating strict JSON
through Foundation's JSONEncoder. Each path asserts exact output and
round-trips back to the original value.
2026-06-08 00:13:56 +02:00
dc8d977f78 fix(license): keep LICENSE to standard MIT text so it is detected as MIT
Move the attribution notice out of LICENSE into ATTRIBUTION.md. The appended
notice dropped the file below GitHub's license-match threshold, so it showed as
Other/NOASSERTION instead of MIT, which blocks dependency adoption.
2026-06-07 20:45:59 +02:00
28cfd3839d chore(hooks): block Cursor attribution locally and in CI
Reject Cursor co-author trailers, cursor.com agent emails, and machine
attribution tells in commit messages and staged content, plus a Cursor
author or committer identity. Add a workflow enforcing the same rule
server-side on push and pull request.
2026-06-07 09:43:29 +02:00
7006d1eb52 chore(style): skip vendored yaml fixture corpora
Keep repo prose checks focused on authored files while preserving upstream fixture contents verbatim.
2026-06-06 08:25:03 +02:00
ca96772207 test(corpus): add public real yaml seeds
Vendor the pinned public YAML seeds used by the real-corpus and Yams differential gates.
2026-06-06 08:24:12 +02:00
b9215d4015 chore(hooks): skip vendored yaml fixture corpora
Exclude checked-in fixture corpus data from prose-style checks so upstream YAML content can remain faithful.
2026-06-06 08:24:08 +02:00
aa74cf4332 test(corpus): add remaining acme catalog fixtures
Add the remaining sanitized ACME catalog modules for multi-file corpus coverage.
2026-06-06 08:16:37 +02:00
7c069dce66 test(corpus): add acme catalog service fixtures
Add sanitized ACME service modules that exercise cross-file OpenAPI-style YAML structures.
2026-06-06 08:03:57 +02:00
8184470cd6 test(corpus): add acme catalog core fixtures
Add the first sanitized ACME catalog modules used for multi-file real-world corpus coverage.
2026-06-06 08:03:27 +02:00
6f76f31c88 test(corpus): add real yaml corpus manifest
Record the checked corpus provenance and fixture metadata used by production hardening gates.
2026-06-06 08:03:21 +02:00
208b892019 feat(parser): harden diagnostics and streaming
Add structured validation diagnostics, JSON and Yams comparison gates, and lazy token/event parsing so production behavior is measured without retaining full intermediate parser arrays.
2026-06-06 07:57:48 +02:00
9f68671c16 docs(corpus): document real seed gates 2026-06-06 01:07:45 +02:00
5f420cdf02 docs(release): update 0.1.1 notes 2026-06-06 00:38:50 +02:00
1bcec9593b ci: use checkout action with node 24 2026-06-06 00:32:53 +02:00
0a649c4d55 docs(release): prepare 0.1.1 2026-06-06 00:28:38 +02:00
36364cfcac docs(readme): add production hardening epic 2026-06-06 00:02:29 +02:00