Skip to main content

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 (-X9X9), 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:

clauseemits
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 / Onothing — 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> contentnothing — a GroupInclusion, handled separately
Not available with <X> CPOSnothing2U_ / 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/2TR2TR is 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 whose trim_identifier is R; on every other row omit the clause entirely.
  • M/H AVF W/XBC, M/H A7B OR A7D W/AVF AND XBCXBC / AVF are 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 ACEW/O is an exclusion, not a scope: emit both requires_one_of [AH5, A7B, A7D, DSA] and excludes [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:

  1. 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 covers W/A7A M/H 2TR (the bare W/A7A is skipped, requires_one_of [2TR] is emitted) and …4FT Not available with 2U_ CPOS.

  2. 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 at By.

  3. Digit-leading codes are real codes when they are a clause's operand. M/H 2UP OR 24W, N/A W/24A, M/H 2TR all keep their codes. CPP-format only means "trim scope" for a trailing W/<code> qualifier.

  4. INCLUDED W/ swallows its whole clause. INCLUDED W/APP W/O PX8; M/H PFP OR PR6 emits only requires_one_of [PFP, PR6]. The W/O PX8 reads as a condition on the inclusion — "bundled into APP unless PX8 is ordered" — not as a constraint on the option, so the INCLUDED 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.

  5. A partially scoped block records the scope at block level. Corpus entry 78 scopes two of its six clauses to trim R; scoped_to_trim has 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.

  6. 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/2TP and N/A W/WRD W/2TR — three different trims in one block — and scoped_to_trim has 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.

  7. An annotation that wraps onto the next page is one block. RAM 279360 p59's last row, PRO ONE PLUS DATA PACK 4YR (X9Y), prints ONLY 1: X9P OR … OR XQW at the foot of p59 and continues By 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 the only_one_of rule 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 puts XML's whole annotation on the following page. Not joining loses requires_one_of [AHU] and excludes [XBC] — which that guide's own spot-check asserts XML must carry. Three independent readers found this on three guides. Always join across a page break.

  8. AND separates a code list exactly as OR does. RAM 3500 prints INCLUDED W/A7S AND A7U on TOWING TECHNOLOGY GROUP (AAN), and the sibling RAM 2500 prints M/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…, and AND is not that. It reads the list, as the includes package <A>, <B> AND <C> content form above always did.

  9. 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 XBH and five more of the same pattern, all in Helvetica-BoldOblique 7.0 on the item's own line. W/O is 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) with W/O ABD set flush right in the same cell, and W/O 2TA OR 2TB OR … on CXL — Grand Cherokee L p3/p8.
    • The STEP #3 "Included in Equipment Groups" listing, whose + item lines carry the same suffix pattern as GROUP DETAIL's: + IP BADGE "BIG HORN" (MSE) - W/O 2TY under group A7E, RAM 2500 p21. 119 occurrences across the three RAM heavy-duty guides, in plain upright Helvetica — 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 — 29E over M/H WRE, Grand Wagoneer p2. The clause qualifies that one (trim, CPP) cell and not the row: on RAM 1500 p2 the RUMBLE BEE column reads 261 / M/H WPV while 392 reads 262 with 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_rules therefore exceeded what the corpus could explain, and test_annotation_corpus_is_exhaustive passed 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 by annotations.contains_clause — the grammar's own keyword set. This admits near-misses such as CAPLESS 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 package disclaimer 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.

Known awkward cases

  • W/A7A M/H 2TR — a leading bare W/A7A conditions the clause ("with A7A, must have 2TR"). OptionRule cannot express a conditional, and the documented rule is that a bare W/ is never extracted, so only requires_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 produce excludes [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 one excludes carrying every operand: - W/O 2T1 OR 2TYexcludes [2T1, 2TY], by the same list-reading as M/H <A> OR <B>.
  • M/H A7H; M/H AAN OR A7H; M/H AAU OR A7H — three separate requires_one_of rules, not one merged list. Each M/H clause is its own constraint.