Annotation grammar → OptionRule
The italic text left-aligned under an option's description in STEP #3 / STEP #4 carries
its compatibility annotations. Each fixture directory's annotation_corpus.json is every distinct block that guide
contains, wrapped lines already joined, and its expected_rules.json maps each to the
OptionRule list it must produce. The corpus is regenerated from the parser's own input
and asserted to be exhaustive, so widening the set of tables the parser reads means
regenerating it. The count, example_page and example_option fields are
informational and are not asserted.
OptionRule.type is one of excludes, requires_one_of, only_one_of. codes are upper-cased
with non-alphanumerics stripped (-X9 → X9), and must be 2–4 alphanumeric characters.
Clauses
A block is split on ;. Empty clauses (the corpus contains ... ; ; ...) are dropped.
Each clause is classified by its leading keyword:
| clause | emits |
|---|---|
M/H <A> OR <B> OR <C> | one requires_one_of with [A, B, C] |
N/A W/<A> | one excludes with [A] |
W/O <A> | one excludes with [A] |
ONLY 1: <A> OR <B> | one only_one_of with [A, B] |
INCLUDED W/<A> OR <B> | nothing — names parent packages, not a constraint |
W/<A> with no N/A / O | nothing — labels an alternate price/content variant |
Removes <A>[ with <B>] | nothing — describes package content, not orderability |
$<n> MSRP W/ <A> / $<n> w/ <A> | nothing — an alternate price |
includes package <A>, <B> AND <C> content | nothing — a GroupInclusion, handled separately |
Not available with <X> CPOS | nothing — 2U_ / 24_ are package prefixes, not option codes |
free text (*PLEASE NOTE…, By selecting a Pro One…, URLs) | nothing |
One block yields one rule per clause, in the order the clauses are printed. Duplicate
rules (same type + same codes set) collapse to one.
Trailing scope qualifiers
An M/H or N/A W/ clause may end with a further W/<code>:
M/H ETM W/2TR—2TRis CPP-formatted (3 chars, leading digit). This scopes the whole clause to the trim whose identifier is the CPP's last character (R). Emit the rule only on rows whosetrim_identifierisR; on every other row omit the clause entirely.M/H AVF W/XBC,M/H A7B OR A7D W/AVF AND XBC—XBC/AVFare letter-initial, so not CPP-formatted. They are price/content-variant labels: ignore the qualifier and keep the base clause (requires_one_of [AVF],requires_one_of [A7B, A7D]).M/H AH5 OR A7B OR A7D OR DSA W/O ACE—W/Ois an exclusion, not a scope: emit bothrequires_one_of [AH5, A7B, A7D, DSA]andexcludes [ACE].
Because the corpus is classified independently of any row, expected_rules.json records
the unscoped result: for a CPP-scoped clause it lists the rule plus the trim it is
restricted to, under scoped_to_trim. The row-level filter is applied by the parser and
tested separately.
Resolved ambiguities
Reviewers classified each guide's corpus independently from this document before the parser was pointed at it, and agreed with each other and with the parser on every block. The first pass — the RAM guide — raised the same four gaps in the wording above from two reviewers working separately; these are the rulings:
-
A clause can be glued to free text. Joining wrapped lines drops the separator, so a real clause sometimes shares a
;-segment with free text —"…special paint. M/H A7K OR A7M". Parse from the first keyword onward, not from the start of the segment. Also coversW/A7A M/H 2TR(the bareW/A7Ais skipped,requires_one_of [2TR]is emitted) and…4FT Not available with 2U_ CPOS. -
A code list ends at the first non-code token.
ONLY 1: X9P OR … OR XQW By selecting a Pro One…yields the seven codes and stops atBy. -
Digit-leading codes are real codes when they are a clause's operand.
M/H 2UP OR 24W,N/A W/24A,M/H 2TRall keep their codes. CPP-format only means "trim scope" for a trailingW/<code>qualifier. -
INCLUDED W/swallows its whole clause.INCLUDED W/APP W/O PX8; M/H PFP OR PR6emits onlyrequires_one_of [PFP, PR6]. TheW/O PX8reads as a condition on the inclusion — "bundled into APP unless PX8 is ordered" — not as a constraint on the option, so theINCLUDED W/classification wins over ruling 1's "parse from the first keyword onward". Two reviewers reached this reading independently; it is recorded here because the two rules genuinely collide on this one string. -
A partially scoped block records the scope at block level. Corpus entry 78 scopes two of its six clauses to trim
R;scoped_to_trimhas nowhere to say "these two", so it records"R"for the block. The parser applies it the same way. This is a known coarsening, not a reading of the page. -
A block scoped to more than one trim keeps the first in print order. RAM 279402 corpus entry 96 carries
M/H ASQ W/2TW,N/A W/*H9 W/2TPandN/A W/WRD W/2TR— three different trims in one block — andscoped_to_trimhas one slot. It records"W", reading forward as ruling 1 does, so appending a clause to a block cannot retroactively change its scope. This is the coarsening of ruling 5 taken to its limit; a per-clause scope would need a schema change, and until then the block's other two scopes are simply not expressed. -
An annotation that wraps onto the next page is one block. RAM 279360 p59's last row,
PRO ONE PLUS DATA PACK 4YR (X9Y), printsONLY 1: X9P OR … OR XQWat the foot of p59 and continuesBy selecting a Pro One data package, …at the head of p60, above that page's first row. It is one annotation and one occurrence. Recording the tail as a block of its own would invent a 139th block and, worse, would leave the p60 half of the row without theonly_one_ofrule its head carries. The parser joins across the break. Measured, RAM 278388's three variants at pp. 57→58, 62→63 and 128→129 are page-break wraps of exactly the pattern this ruling's headline covers, not print truncations. Treating them as separate blocks cost real rules: the same mechanism at pp. 35→36 and 88→89 putsXML's whole annotation on the following page. Not joining losesrequires_one_of [AHU]andexcludes [XBC]— which that guide's own spot-check assertsXMLmust carry. Three independent readers found this on three guides. Always join across a page break. -
ANDseparates a code list exactly asORdoes. RAM 3500 printsINCLUDED W/A7S AND A7UonTOWING TECHNOLOGY GROUP (AAN), and the sibling RAM 2500 printsM/H A7S OR A7U— the same pair of equipment groups in the other spelling, which is what settles it. Ruling 2 ("a code list ends at the first non-code token") was written for text running on after a list,… OR XQW By selecting a Pro One…, andANDis not that. It reads the list, as theincludes package <A>, <B> AND <C> contentform above always did. -
Rules come from the back matter too, and the corpus must contain those blocks. The corpus was scoped to annotations printed under a STEP #3/#4 option row. A sweep of every line of every page against the grammar's own keyword set found clauses in four further places, 215 occurrences over 101 distinct blocks, none of them in any corpus. Every one of the ten hand-reads flagged the first two:
- GROUP DETAIL item suffixes. RAM 1500 p6 prints
• ACTIVE DRIVING ASSIST SYSTEM (SJJ) - W/O 2TY,• HEAD UP DISPLAY (LBK) - W/O 2T1 OR 2TY,• EXTERIOR 115V AC OUTLET (XBE) - W/O XBHand five more of the same pattern, all inHelvetica-BoldOblique 7.0on the item's own line.W/Ois a documented exclusion keyword, so these are clauses; they produce 15 of that guide's 19 rule-bearing records while its corpus holds two blocks, neither of them these. - PACKAGE CONTENT flush-right qualifiers.
… (JKV)withW/O ABDset flush right in the same cell, andW/O 2TA OR 2TB OR …onCXL— Grand Cherokee L p3/p8. - The STEP #3 "Included in Equipment Groups" listing, whose
+ itemlines carry the same suffix pattern as GROUP DETAIL's:+ IP BADGE "BIG HORN" (MSE) - W/O 2TYunder groupA7E, RAM 2500 p21. 119 occurrences across the three RAM heavy-duty guides, in plain uprightHelvetica— this source is not typographically marked at all, which is why the filter has to be lexical rather than by face. - POWERTRAIN AVAILABILITY cells. A trim's cell on a powertrain row holds the CPP code
the row is offered under and a clause beneath it —
29EoverM/H WRE, Grand Wagoneer p2. The clause qualifies that one (trim, CPP) cell and not the row: on RAM 1500 p2 theRUMBLE BEEcolumn reads261/M/H WPVwhile392reads262with nothing. 8 occurrences on three guides.
Counted across the set: 54 such records on the Charger, 12 on the Grand Wagoneer, 12 on the Grand Cherokee L, 15 on the RAM 1500.
with_rulestherefore exceeded what the corpus could explain, andtest_annotation_corpus_is_exhaustivepassed only because it compared the corpus against a parser sharing the same blind spot. The corpus now covers any distinct annotation block wherever it is printed, selected byannotations.contains_clause— the grammar's own keyword set. This admits near-misses such asCAPLESS FUEL FILL W/O DISCRIMINATOR (XJM), recording them as yielding no rule rather than leaving them out of the fixture entirely.Two entries had also been lost from the corpora during the hand-read pass — the page-break-truncated variants of the
Pro One data packagedisclaimer on RAM 2500 and 279402, where the annotation runs off the foot of the page and the row on the last band holds only part of it. Widening the extractor is what revealed them. - GROUP DETAIL item suffixes. RAM 1500 p6 prints
Known awkward cases
W/A7A M/H 2TR— a leading bareW/A7Aconditions the clause ("with A7A, must have 2TR").OptionRulecannot express a conditional, and the documented rule is that a bareW/is never extracted, so onlyrequires_one_of [2TR]is emitted. This over-applies on rows without A7A; recorded here rather than silently dropped.N/A W/ACE W/O ACE— the qualifier repeats the excluded code. Both clauses produceexcludes [ACE]; they collapse to one rule.W/O <A> OR <B>— three readers noted this pattern has no row in the clause table above. It is oneexcludescarrying every operand:- W/O 2T1 OR 2TY→excludes [2T1, 2TY], by the same list-reading asM/H <A> OR <B>.M/H A7H; M/H AAN OR A7H; M/H AAU OR A7H— three separaterequires_one_ofrules, not one merged list. EachM/Hclause is its own constraint.