Skip to content

ci: check the sentence corpus JSON in the spec job - #185

Merged
jbpros merged 17 commits into
mainfrom
ci/sentence-corpus-check
Oct 13, 2026
Merged

jbpros merged 17 commits into
mainfrom
ci/sentence-corpus-check

Conversation

@jbpros

@jbpros jbpros commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Runs python3 spec/sentences_corpus.py --check in the spec job and lists it in AGENTS.md. It fails when spec/sentences-corpus.json no longer holds every line of spec/sentences-corpus.md.

This goes after #181, which adds the script and the JSON. It's separate so that #181's squashed commit stays core:. Its base is core/sentence-parser, so once #181 merges it needs rebasing onto main.

🤖 Generated with Claude Code

jbpros and others added 15 commits October 12, 2026 23:33
spec/sentences_corpus.py pulls every sentence, for line and from line
out of sentences-corpus.md into sentences-corpus.json, which OPA loads
as data.spec.sentence_corpus, so a test can parse each one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the parser for spec/sentences.abnf to ergo.rego: _sentence reads
a check, _for_line a for line and _from_line a from line, each into a
JSON shape that mirrors the sentence, or into a list of problems when
the line is written wrong. _s_check_text and its two siblings print a
parsed line back in the spelling the report will use.

The grammar is flat, so the parser is layered regular expressions: one
splits the assertions, one reads an assertion's parts, one reads the
conditions of a where, and the phrase is picked by its leading words.
The patterns are built once per call in _s_patterns and passed down,
because Wasm rebuilds a constant rule at every reference, and the
phrase union is factored by argument kind to keep them small. Every
corpus line parses and prints back unchanged, in 11 seconds for all
306 under Wasm.

Nothing calls it from ergo.report yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…take within

Single quotes are only safe for a string with a backslash when it has
no single quote, no two backslashes in a row, no backslash at the end
and no control character, or parsing the printed sentence gives a
different value. The written-wrong table loses the is empy row, since
that sentence parses as a comparison with a field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An index with a leading zero and a string with a bad escape stopped
the evaluation instead of giving a problem. A value or key starting
with true, false, null or empty was rejected, because the split took
the first match; each piece now has to end at the separator or the
end. The parser accepted a where on a plain path, a lone number on
the left, a phrase other than a comparison after count of, sum of or
plus, and a for name given twice; each is a problem now. A quoted
grammar word is a key, and the dot in front of a key is spelling, not
part of the parse, so the same check always reads the same. A position
written in brackets at the end of a path reads and prints as item n of,
and a string prints in single quotes only when that reads back to the
same value. The messages for an unclosed quote, a missing phrase, a
missing number after within and an unknown  now say what the spec
promises.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
So that count of item 1 of x, which is how the report prints
count of x[0], is a sentence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ree messages

count of x[0] printed as count of item 1 of x, which did not parse,
because a count or sum took no position. The unknown-name message
skips quoted strings and names each name once, the invalid-string
message gives a column so that a tab or newline in the string does not
break the row, and the missing-phrase message quotes is and exists as
the spec does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…time lacks

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ike it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The parser and sentences.md already read it as a right-side path, where
a quoted word is a value and not a key, so $input named by each of "T"
doesn't parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A number has to be 0 or have a magnitude between 2.2250738585072014e-308
and 1.7976931348623157e308, as in a policy. The parser now checks the
size from the text first. Comparing a number whose exponent is beyond
one million crashed OPA with "panic: illegal value", as in
x is between -6 and -80e-08923553, and a number above 1.8e308 made
x is between 1 and 1e400 fail with "doesn't parse".

Problems now keep the order of their columns, so column 6 comes before
column 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The squashed commit is core:, so the workflow change goes in a ci: pull
request after this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttern

x is between 1e400 and 1 said "min above max" too, and x matches 1e400
printed the parser's own marker as an invalid pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbpros jbpros added the enhancement New feature or request label Oct 13, 2026
jbpros and others added 2 commits October 13, 2026 07:49
sentences.md says the number after within is 0 or more, as REFERENCE.md
says for today's within, but x is 1 within -1 parsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jbpros added a commit that referenced this pull request Oct 13, 2026
First implementation PR after #180: the parser for
`spec/sentences.abnf`, in `ergo.rego`, not yet called by `ergo.report`.

**What it does.** `_sentence(text)` reads a check, `_for_line(text)` a
`for` line and `_from_line(text)` a `from` line, each into a JSON shape
that mirrors the sentence (quantifier, path, `where` conditions, phrase
and its arguments, or `terms` for `count of`, `sum of` and `plus`), or
into `{"problems": [...]}` when the line is written wrong.
`_s_check_text` and its two siblings print a parsed line back in one
spelling: `is` before a value and `equals` before a field, `is one of`
before values and `is in` before a path, `an` before `object`, `item 3
of` for a position that ends a path, single quotes for a string with a
backslash when that reads back the same, one space between words. Two
spellings of one check give the same parse: the dot in front of a key,
brackets or words for a position, and runs of spaces are spelling only.

**How.** The grammar is flat, so the parser is layered regular
expressions: one splits the assertions on `and`, one reads an
assertion's parts, one reads the conditions of a `where`, one picks the
phrase. Each split piece has to end at its separator or the end of the
line, so the regex engine backtracks past a short match like `false` in
`false-positive`. Rego can't recurse and this needs none.

**Wasm.** The first version took over two minutes for the corpus under
Wasm, because Wasm rebuilds a constant rule at every reference and the
patterns were a tree of constants. They're now built once per call in
`_s_patterns` and passed down, and the phrase union is factored by
argument kind. The corpus test takes 20 s under Wasm and 0.3 s with `opa
test`.

**Tests.** Every one of the 306 corpus lines parses and prints back byte
for byte, from `spec/sentences-corpus.json`, which
`spec/sentences_corpus.py` writes from the markdown. The CI check that
it's current is in #185, after this PR, so the squashed commit stays
`core:`. Unit tests cover each phrase, each path form, the printed
spellings, and the messages for sentences written wrong.

**Review.** A fresh Opus review found twelve things, four of them real
bugs: a leading-zero index and a bad string escape stopped the
evaluation, keys starting with `true`, `false`, `null` or `empty` were
rejected, and the parser accepted a `where` on a plain path, a lone
number on the left, a non-comparison after `count of`, and a `for` name
given twice. All twelve are fixed in the second `core:` commit, with the
spec changed in two places it was wrong: when a string prints in single
quotes, and the `is empy` row of the written-wrong table, since that
sentence parses as a comparison with a field.

**Second review.** A second review compared the parser with the ABNF,
converted to a Lark grammar, on 6,000 changed corpus lines and 11,616
random sentences built from the grammar, and printed back every line the
parser accepted. It found three things, fixed in the last commits:

- `x is between -6 and -80e-08923553` crashed OPA with `panic: illegal
value`. OPA (1.19.0, 1.20.2 and 1.21.2) panics when it compares a number
whose exponent is beyond ±1,000,000, reported as
open-policy-agent/opa#9365. The parser now works out a number's size
from its text before `to_number`, and applies the rule a policy number
already follows: `0` or a magnitude between `2.2250738585072014e-308`
and `1.7976931348623157e308`. Otherwise the sentence gets `number out of
range: <number>`. `sentences.md` says so.
- That also fixes `x is between 1 and 1e400`, which failed with "doesn't
parse" because OPA can't compare numbers above 1.8e308.
- The ABNF gave `named by each of` a left-side path, where `"T"` is a
key. The parser and `sentences.md` read a right-side path, so the ABNF
now says `right-path`.

Problems now keep their column order, so column 6 comes before column
20. A third review found that `x is 1 within -1` parsed, though the spec
says the number after `within` is 0 or more. It now gets `"within" needs
a number of 0 or more`.

**Not in this PR.** Wiring into `ergo.report` and the semantics of each
phrase. `spec/sentences.md` now says ergo reads sentences but runs none.

Checks run here: `opa check`, `opa fmt`, `opa test` with coverage (every
line reached), `--strict-builtin-errors`, Regal, no comments, OPA 1.20.2
`fmt`, Wasm tests in Docker, and the Wasm JS runtime script (1009 pass,
10.5 MB). On OPA 1.19, two of the 11,616 random sentences didn't print
back the same inside one big evaluation, because 1.19 thinks `0.5e-7`
equals `0.5e-70`. That's the known 1.19 number bug, and OPA 1.20.2
prints all of them back correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Base automatically changed from core/sentence-parser to main October 13, 2026 05:55
@jbpros
jbpros marked this pull request as ready for review October 13, 2026 05:55
@jbpros
jbpros merged commit f8fad21 into main Oct 13, 2026
7 checks passed
@jbpros
jbpros deleted the ci/sentence-corpus-check branch October 13, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant