@gmb/bitmark-parser-generator 5.23.0 → 5.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -709,10 +709,12 @@ var TextFormat = {
709
709
  };
710
710
  var CardSetConfigKey = {
711
711
  flashcard: "flashcard",
712
+ flashcard1: "flashcard1",
712
713
  definitionList: "definitionList",
713
714
  elements: "elements",
714
715
  statements: "statements",
715
716
  quiz: "quiz",
717
+ quizResponses: "quizResponses",
716
718
  feedback: "feedback",
717
719
  questions: "questions",
718
720
  matchPairs: "matchPairs",
@@ -724,6 +726,7 @@ var CardSetConfigKey = {
724
726
  pronunciationTable: "pronunciationTable",
725
727
  botActionResponses: "botActionResponses",
726
728
  exampleBitList: "exampleBitList",
729
+ pageFooterSections: "pageFooterSections",
727
730
  clozeList: "clozeList",
728
731
  ingredients: "ingredients",
729
732
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1128,12 +1131,17 @@ var groupKeys = {
1128
1131
  group_standardAllBits: "group_standardAllBits",
1129
1132
  group_standardItemLead: "group_standardItemLead",
1130
1133
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1134
+ group_standardInstructionHint: "group_standardInstructionHint",
1131
1135
  group_standardTags: "group_standardTags",
1136
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1132
1137
  group_imageSource: "group_imageSource",
1133
1138
  group_technicalTerm: "group_technicalTerm",
1134
1139
  group_person: "group_person",
1135
1140
  group_gap: "group_gap",
1136
1141
  group_trueFalse: "group_trueFalse",
1142
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1143
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1144
+ group_trueFalseResponses: "group_trueFalseResponses",
1137
1145
  group_markConfig: "group_markConfig",
1138
1146
  group_mark: "group_mark",
1139
1147
  group_bookCommon: "group_bookCommon",
@@ -1141,14 +1149,18 @@ var groupKeys = {
1141
1149
  group_advertisingCommon: "group_advertisingCommon",
1142
1150
  group_platformStylesCommon: "group_platformStylesCommon",
1143
1151
  group_quizCommon: "group_quizCommon",
1152
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1153
+ group_imageNoZoom: "group_imageNoZoom",
1144
1154
  group_resourceBitTags: "group_resourceBitTags",
1145
1155
  group_resourceCommon: "group_resourceCommon",
1146
1156
  group_resourceImageCommon: "group_resourceImageCommon",
1157
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1147
1158
  group_resourceAudioCommon: "group_resourceAudioCommon",
1148
1159
  group_resourceVideoCommon: "group_resourceVideoCommon",
1149
1160
  group_resourceIcon: "group_resourceIcon",
1150
1161
  // Alias for image
1151
1162
  group_resourceImage: "group_resourceImage",
1163
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1152
1164
  group_resourceImageEmbed: "group_resourceImageEmbed",
1153
1165
  group_resourceImageLink: "group_resourceImageLink",
1154
1166
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1422,6 +1434,7 @@ var propertyKeys = {
1422
1434
  property_src4x: "@src4x",
1423
1435
  property_srcAlt: "@srcAlt",
1424
1436
  property_stripePricingTableId: "@stripePricingTableId",
1437
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1425
1438
  property_stripePublishableKey: "@stripePublishableKey",
1426
1439
  property_subject: "@subject",
1427
1440
  property_subtype: "@subtype",
@@ -2164,7 +2177,7 @@ var CARDSETS = {
2164
2177
  variants: [
2165
2178
  {
2166
2179
  jsonKey: "question.text",
2167
- exportJsonKey: { question: { text: "$" } },
2180
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2168
2181
  tags: [
2169
2182
  {
2170
2183
  key: ConfigKey.group_standardTags,
@@ -2180,6 +2193,136 @@ var CARDSETS = {
2180
2193
  description: "Icon resource for the flashcard.",
2181
2194
  jsonKey: "question.icon|resource(type=image, key=image)",
2182
2195
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2196
+ },
2197
+ {
2198
+ key: ConfigKey.property_example,
2199
+ exportJsonKey: [
2200
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2201
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2202
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2203
+ ],
2204
+ description: "Example marker for the flashcard question.",
2205
+ format: TagFormat.bitmarkText,
2206
+ nullable: true
2207
+ }
2208
+ ]
2209
+ }
2210
+ ]
2211
+ },
2212
+ {
2213
+ name: "answer",
2214
+ variants: [
2215
+ {
2216
+ jsonKey: "answer.text",
2217
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2218
+ tags: [
2219
+ {
2220
+ key: ConfigKey.group_standardTags,
2221
+ description: "Standard tags for the flashcard."
2222
+ },
2223
+ {
2224
+ exportJsonKey: { title: "$" },
2225
+ key: ConfigKey.tag_title,
2226
+ description: "Title of the flashcard."
2227
+ },
2228
+ {
2229
+ key: ConfigKey.group_resourceIcon,
2230
+ description: "Icon resource for the flashcard.",
2231
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2232
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2233
+ },
2234
+ {
2235
+ key: ConfigKey.property_example,
2236
+ exportJsonKey: [
2237
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2238
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2239
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2240
+ ],
2241
+ description: "Example marker for the flashcard answer.",
2242
+ format: TagFormat.bitmarkText,
2243
+ nullable: true
2244
+ }
2245
+ ]
2246
+ },
2247
+ {
2248
+ jsonKey: "alternativeAnswers[].text",
2249
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2250
+ tags: [
2251
+ {
2252
+ key: ConfigKey.group_standardTags,
2253
+ description: "Standard tags for the flashcard."
2254
+ },
2255
+ {
2256
+ exportJsonKey: { title: "$" },
2257
+ key: ConfigKey.tag_title,
2258
+ description: "Title of the flashcard."
2259
+ },
2260
+ {
2261
+ key: ConfigKey.group_resourceIcon,
2262
+ description: "Icon resource for the flashcard.",
2263
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2264
+ exportJsonKey: {
2265
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2266
+ }
2267
+ }
2268
+ ],
2269
+ repeatCount: Count.infinity
2270
+ }
2271
+ ]
2272
+ }
2273
+ ]
2274
+ },
2275
+ //
2276
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2277
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2278
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2279
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2280
+ // cardset.
2281
+ //
2282
+ [CardSetConfigKey.flashcard1]: {
2283
+ jsonKey: "cards",
2284
+ exportJsonKey: { cards: "$" },
2285
+ sections: {
2286
+ default: {
2287
+ jsonKey: "cards",
2288
+ exportJsonKey: { cards: "$" },
2289
+ isDefault: true,
2290
+ maxCount: 1
2291
+ }
2292
+ },
2293
+ sides: [
2294
+ {
2295
+ name: "question",
2296
+ variants: [
2297
+ {
2298
+ jsonKey: "question.text",
2299
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2300
+ tags: [
2301
+ {
2302
+ key: ConfigKey.group_standardTags,
2303
+ description: "Standard tags for the flashcard."
2304
+ },
2305
+ {
2306
+ exportJsonKey: { title: "$" },
2307
+ key: ConfigKey.tag_title,
2308
+ description: "Title of the flashcard."
2309
+ },
2310
+ {
2311
+ key: ConfigKey.group_resourceIcon,
2312
+ description: "Icon resource for the flashcard.",
2313
+ jsonKey: "question.icon|resource(type=image, key=image)",
2314
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2315
+ },
2316
+ {
2317
+ key: ConfigKey.property_example,
2318
+ exportJsonKey: [
2319
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2320
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2321
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2322
+ ],
2323
+ description: "Example marker for the flashcard question.",
2324
+ format: TagFormat.bitmarkText,
2325
+ nullable: true
2183
2326
  }
2184
2327
  ]
2185
2328
  }
@@ -2190,7 +2333,7 @@ var CARDSETS = {
2190
2333
  variants: [
2191
2334
  {
2192
2335
  jsonKey: "answer.text",
2193
- exportJsonKey: { answer: { text: "$" } },
2336
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2194
2337
  tags: [
2195
2338
  {
2196
2339
  key: ConfigKey.group_standardTags,
@@ -2206,6 +2349,17 @@ var CARDSETS = {
2206
2349
  description: "Icon resource for the flashcard.",
2207
2350
  jsonKey: "answer.icon|resource(type=image, key=image)",
2208
2351
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2352
+ },
2353
+ {
2354
+ key: ConfigKey.property_example,
2355
+ exportJsonKey: [
2356
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2357
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2358
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2359
+ ],
2360
+ description: "Example marker for the flashcard answer.",
2361
+ format: TagFormat.bitmarkText,
2362
+ nullable: true
2209
2363
  }
2210
2364
  ]
2211
2365
  },
@@ -2249,7 +2403,7 @@ var CARDSETS = {
2249
2403
  variants: [
2250
2404
  {
2251
2405
  jsonKey: "term.text",
2252
- exportJsonKey: { term: { text: "$" } },
2406
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2253
2407
  tags: [
2254
2408
  {
2255
2409
  key: ConfigKey.group_standardTags,
@@ -2268,6 +2422,17 @@ var CARDSETS = {
2268
2422
  format: TagFormat.plainText,
2269
2423
  jsonKey: "term.icon|resource(type=image, key=image)",
2270
2424
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2425
+ },
2426
+ {
2427
+ key: ConfigKey.property_example,
2428
+ exportJsonKey: [
2429
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2430
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2431
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2432
+ ],
2433
+ description: "Example marker for the definition term.",
2434
+ format: TagFormat.bitmarkText,
2435
+ nullable: true
2271
2436
  }
2272
2437
  ]
2273
2438
  }
@@ -2278,7 +2443,7 @@ var CARDSETS = {
2278
2443
  variants: [
2279
2444
  {
2280
2445
  jsonKey: "definition.text",
2281
- exportJsonKey: { definition: { text: "$" } },
2446
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2282
2447
  tags: [
2283
2448
  {
2284
2449
  key: ConfigKey.group_standardTags,
@@ -2296,6 +2461,17 @@ var CARDSETS = {
2296
2461
  description: "Icon resource for the definition.",
2297
2462
  jsonKey: "definition.icon|resource(type=image, key=image)",
2298
2463
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2464
+ },
2465
+ {
2466
+ key: ConfigKey.property_example,
2467
+ exportJsonKey: [
2468
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2469
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2470
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2471
+ ],
2472
+ description: "Example marker for the definition.",
2473
+ format: TagFormat.bitmarkText,
2474
+ nullable: true
2299
2475
  }
2300
2476
  ]
2301
2477
  },
@@ -2340,11 +2516,51 @@ var CARDSETS = {
2340
2516
  {
2341
2517
  jsonKey: "key",
2342
2518
  exportJsonKey: { key: "$" },
2519
+ format: TextFormat.plainText,
2343
2520
  tags: [
2344
2521
  {
2345
2522
  key: ConfigKey.group_standardTags,
2346
2523
  description: "Standard tags for the match pair."
2347
2524
  },
2525
+ {
2526
+ // Pair-level example: emit `isExample` alongside `example`
2527
+ // (matches OLD parser's per-pair allow-list behaviour).
2528
+ //
2529
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2530
+ // in the cardset) fills each pair's `example` with `V`
2531
+ // once per pair from the keys-side cascade fire (single
2532
+ // parsed_variant per pair — no per-value duplication).
2533
+ //
2534
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2535
+ // at the pair-object level. BPG `parseMatchPairs`
2536
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2537
+ // per PAIR (the `exampleCard` accumulator), placed at the
2538
+ // pair object. Unlike matchMatrix (per-cell example),
2539
+ // matchPairs has no nested per-cell layer, so the pair-key
2540
+ // `[@example:V]` must double-emit as the visible field.
2541
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2542
+ // expects `example: "packen"` at pair-object level from
2543
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2544
+ // draft applied to both keys-side tags; the matchPairs
2545
+ // half was reverted after fixture parity testing.
2546
+ key: ConfigKey.property_example,
2547
+ exportJsonKey: [
2548
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2549
+ {
2550
+ predicates: ["@absent", { $cascade: "*" }],
2551
+ rule: {
2552
+ isExample: true,
2553
+ example: "$cascade",
2554
+ "@bit": { isExample: true }
2555
+ }
2556
+ },
2557
+ { "@absent": { isExample: true } },
2558
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2559
+ ],
2560
+ description: "Example marker / value on the match pair.",
2561
+ format: TagFormat.bitmarkText,
2562
+ nullable: true
2563
+ },
2348
2564
  {
2349
2565
  key: ConfigKey.tag_title,
2350
2566
  description: "Title of the match pair.",
@@ -2353,9 +2569,19 @@ var CARDSETS = {
2353
2569
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2354
2570
  },
2355
2571
  {
2572
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2573
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2574
+ // only fires when NO variant in the active card explicitly
2575
+ // carries `[@isCaseSensitive]`. When a value-side variant
2576
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2577
+ // synth is skipped here; the value-side explicit fire owns
2578
+ // the pair-level emission.
2356
2579
  key: ConfigKey.property_isCaseSensitive,
2357
2580
  exportJsonKey: [
2358
- { "@absent": { isCaseSensitive: "$ancestor" } },
2581
+ {
2582
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2583
+ rule: { isCaseSensitive: "$ancestor" }
2584
+ },
2359
2585
  { isCaseSensitive: "$" }
2360
2586
  ],
2361
2587
  description: "Property to indicate if the match is case sensitive.",
@@ -2384,10 +2610,85 @@ var CARDSETS = {
2384
2610
  {
2385
2611
  jsonKey: "values[]",
2386
2612
  exportJsonKey: { values: ["$"] },
2613
+ format: TextFormat.plainText,
2387
2614
  tags: [
2388
2615
  {
2389
- key: ConfigKey.group_standardTags,
2390
- description: "Standard tags for the match pair."
2616
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2617
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2618
+ // for sideIdx > 0; the schema-side equivalent is to use a
2619
+ // group_standardTags variant that omits group_standardItemLead.
2620
+ key: ConfigKey.group_standardTagsNoItemLead,
2621
+ description: "Standard tags for the match pair (values side, no item/lead)."
2622
+ },
2623
+ {
2624
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2625
+ // `@example` from the bit header fills each pair's
2626
+ // `example` with the pair's first value (mirrors BPG's
2627
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2628
+ //
2629
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2630
+ // TAG 415 (keys side) which fires once per pair — keeping
2631
+ // it here would emit `example` twice per pair (one per
2632
+ // values-side parsed_variant) and merge them into a
2633
+ // multi-paragraph array.
2634
+ //
2635
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2636
+ // source `$` value (the current `++`-iteration's value
2637
+ // text). For single-value pairs this matches the old
2638
+ // `$parent.values[0]`; for multi-value pairs with
2639
+ // `[@example]` attached to a non-first value, it correctly
2640
+ // resolves to that value.
2641
+ //
2642
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2643
+ // routes pair-key `[@example:V]` (a per-pair cascade
2644
+ // source, bound via per-card CascadeCounters scope) and
2645
+ // bit-header `[@example:V]` (cardset cascade) into the
2646
+ // pair-level `example` via `$cascade`. The aggregator in
2647
+ // `populate_variants` fires this rule ONCE per pair (BPG
2648
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2649
+ // per-iter pollution.
2650
+ key: ConfigKey.property_example,
2651
+ exportJsonKey: [
2652
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2653
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2654
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2655
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2656
+ // scope opened by `build_card_item`.
2657
+ {
2658
+ predicates: ["@keyonly"],
2659
+ maxEmits: 1,
2660
+ rule: {
2661
+ isExample: true,
2662
+ example: "$parent",
2663
+ "@bit": { isExample: true }
2664
+ }
2665
+ },
2666
+ {
2667
+ predicates: ["@absent", { $cascade: "*" }],
2668
+ rule: {
2669
+ isExample: true,
2670
+ example: "$cascade",
2671
+ "@bit": { isExample: true }
2672
+ }
2673
+ },
2674
+ {
2675
+ "@absent": {
2676
+ isExample: true,
2677
+ example: "$parent",
2678
+ "@bit": { isExample: true }
2679
+ }
2680
+ },
2681
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2682
+ // form so per-`++`-iter values-side fires also first-win.
2683
+ {
2684
+ predicates: [],
2685
+ maxEmits: 1,
2686
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2687
+ }
2688
+ ],
2689
+ description: "Example marker / value on the match pair (values side).",
2690
+ format: TagFormat.bitmarkText,
2691
+ nullable: true
2391
2692
  },
2392
2693
  {
2393
2694
  key: ConfigKey.tag_title,
@@ -2397,9 +2698,13 @@ var CARDSETS = {
2397
2698
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2398
2699
  },
2399
2700
  {
2701
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2400
2702
  key: ConfigKey.property_isCaseSensitive,
2401
2703
  exportJsonKey: [
2402
- { "@absent": { isCaseSensitive: "$ancestor" } },
2704
+ {
2705
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2706
+ rule: { isCaseSensitive: "$ancestor" }
2707
+ },
2403
2708
  { isCaseSensitive: "$" }
2404
2709
  ],
2405
2710
  description: "Property to indicate if the match is case sensitive.",
@@ -2425,6 +2730,7 @@ var CARDSETS = {
2425
2730
  {
2426
2731
  jsonKey: "key",
2427
2732
  exportJsonKey: { key: "$" },
2733
+ format: TextFormat.plainText,
2428
2734
  tags: [
2429
2735
  {
2430
2736
  key: ConfigKey.group_standardTags,
@@ -2454,10 +2760,12 @@ var CARDSETS = {
2454
2760
  {
2455
2761
  jsonKey: "values[]",
2456
2762
  exportJsonKey: { values: ["$"] },
2763
+ format: TextFormat.plainText,
2457
2764
  tags: [
2458
2765
  {
2459
- key: ConfigKey.group_standardTags,
2460
- description: "Standard tags for the audio match pair."
2766
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2767
+ key: ConfigKey.group_standardTagsNoItemLead,
2768
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2461
2769
  },
2462
2770
  {
2463
2771
  key: ConfigKey.tag_title,
@@ -2490,6 +2798,7 @@ var CARDSETS = {
2490
2798
  {
2491
2799
  jsonKey: "key",
2492
2800
  exportJsonKey: { key: "$" },
2801
+ format: TextFormat.plainText,
2493
2802
  tags: [
2494
2803
  {
2495
2804
  key: ConfigKey.group_standardTags,
@@ -2519,10 +2828,12 @@ var CARDSETS = {
2519
2828
  {
2520
2829
  jsonKey: "values[]",
2521
2830
  exportJsonKey: { values: ["$"] },
2831
+ format: TextFormat.plainText,
2522
2832
  tags: [
2523
2833
  {
2524
- key: ConfigKey.group_standardTags,
2525
- description: "Standard tags for the image match pair."
2834
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2835
+ key: ConfigKey.group_standardTagsNoItemLead,
2836
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2526
2837
  },
2527
2838
  {
2528
2839
  key: ConfigKey.tag_title,
@@ -2555,20 +2866,49 @@ var CARDSETS = {
2555
2866
  {
2556
2867
  jsonKey: "key",
2557
2868
  exportJsonKey: { key: "$" },
2869
+ format: TextFormat.plainText,
2558
2870
  tags: [
2559
2871
  {
2560
2872
  key: ConfigKey.group_standardItemLeadInstructionHint,
2561
2873
  description: "Standard tags for lead, instruction, and hint."
2562
2874
  },
2563
2875
  {
2876
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
2877
+ // `[@example:V]` is consumed as the per-pair cascade source.
2878
+ // PLAN-077 R14 follow-up: investigation showed the fixture
2879
+ // reference is INCONSISTENT — bits without a cardset-level
2880
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
2881
+ // pair-level `example: V` at pair scope, but bits WITH a
2882
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
2883
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
2884
+ // example. Emitting `example: $` here satisfies bit 3 but
2885
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
2886
+ // 3. Conditional emission would require runtime gating on
2887
+ // the cardset cascade scope state. Left dropped pending a
2888
+ // dedicated plan — accepted divergence on bit 3's pair-
2889
+ // level emission (cells still get the per-cell `example`
2890
+ // correctly via PLAN-077 §4.2 + §4.3).
2564
2891
  key: ConfigKey.property_example,
2565
2892
  exportJsonKey: [
2566
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2567
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2568
- { isExample: true, example: "$", "@bit": { isExample: true } }
2893
+ {
2894
+ "@keyonly": {
2895
+ isExample: true,
2896
+ "@bit": { isExample: true },
2897
+ "@card": { isExample: true }
2898
+ }
2899
+ },
2900
+ {
2901
+ "@absent": {
2902
+ isExample: true,
2903
+ "@bit": { isExample: true },
2904
+ "@card": { isExample: true }
2905
+ }
2906
+ },
2907
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2569
2908
  ],
2570
2909
  description: "Example text for the match matrix.",
2571
- format: TagFormat.plainText
2910
+ format: TagFormat.plainText,
2911
+ nullable: true
2572
2912
  },
2573
2913
  {
2574
2914
  key: ConfigKey.tag_title,
@@ -2596,35 +2936,96 @@ var CARDSETS = {
2596
2936
  variants: [
2597
2937
  {
2598
2938
  jsonKey: "cells[{s}].values[]",
2599
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
2939
+ exportJsonKey: [
2940
+ // PLAN-085 / R14: the positional-blank emission for an
2941
+ // effectively-absent cell side fires ONLY when the active
2942
+ // card has at least one `[#]` variant fire. This matches
2943
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
2944
+ // `[#]` exists anywhere in the title card, BPG returns
2945
+ // undefined and no `heading` is emitted. The phantom
2946
+ // `forValues:['']` placeholder is appropriate only when
2947
+ // the card is being treated as a title-row.
2948
+ {
2949
+ predicates: ["@absent", { "@variantHasTag": "#" }],
2950
+ rule: { "@bit": { heading: { forValues: [""] } } }
2951
+ },
2952
+ { cells: { $s: { values: ["$"] } } }
2953
+ ],
2954
+ format: TextFormat.plainText,
2600
2955
  tags: [
2601
2956
  {
2602
2957
  key: ConfigKey.group_standardItemLeadInstructionHint,
2603
2958
  description: "Standard tags for lead, instruction, and hint."
2604
2959
  },
2605
2960
  {
2961
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2962
+ // source `$` value (the current `++`-iteration's cell text).
2963
+ // Replaces `$parent.values[0]` which pinned to position 0
2964
+ // regardless of which `++`-value `[@example]` was on.
2965
+ //
2966
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2967
+ // routes pair-key `[@example:V]` (a per-pair cascade
2968
+ // source, bound via per-card CascadeCounters scope) and
2969
+ // bit-header `[@example:V]` (cardset cascade) into the
2970
+ // per-cell `example` via `$cascade`. The aggregator in
2971
+ // `populate_variants` fires this rule ONCE per cell (BPG
2972
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
2973
+ // per-iter pollution.
2974
+ // @card scope-shift writes matrix-row container isExample.
2606
2975
  key: ConfigKey.property_example,
2607
2976
  exportJsonKey: [
2608
2977
  {
2609
2978
  "@keyonly": {
2610
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2611
- "@bit": { isExample: true }
2979
+ cells: { $s: { isExample: true, example: "$parent" } },
2980
+ "@bit": { isExample: true },
2981
+ "@card": { isExample: true }
2612
2982
  }
2613
2983
  },
2614
2984
  {
2615
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
2985
+ predicates: ["@absent", { $cascade: "*" }],
2986
+ rule: {
2987
+ cells: { $s: { isExample: true, example: "$cascade" } },
2988
+ "@bit": { isExample: true },
2989
+ "@card": { isExample: true }
2990
+ }
2616
2991
  },
2617
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
2992
+ {
2993
+ "@absent": {
2994
+ cells: { $s: { isExample: true, example: "$parent" } },
2995
+ "@bit": { isExample: true },
2996
+ "@card": { isExample: true }
2997
+ }
2998
+ },
2999
+ {
3000
+ cells: { $s: { isExample: true, example: "$" } },
3001
+ "@bit": { isExample: true },
3002
+ "@card": { isExample: true }
3003
+ }
2618
3004
  ],
2619
3005
  description: "Example text for the match matrix.",
2620
- format: TagFormat.plainText
3006
+ format: TagFormat.bitmarkText,
3007
+ nullable: true
2621
3008
  },
2622
3009
  {
3010
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3011
+ // empty-string positional blank into `forValues` so the
3012
+ // array stays aligned with the cell positions (matches BPG
3013
+ // `extractHeadingCard`'s "all value-side headings present"
3014
+ // shape). The `@keyonly` rule is required for two reasons:
3015
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3016
+ // in the unconditional rule has no source for bare); and
3017
+ // 2) the presence of any `@keyonly` rule disables the
3018
+ // optimised-mode `should_suppress_outcome` strip (see
3019
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3020
+ // survives the all-natural-defaults check.
2623
3021
  key: ConfigKey.tag_title,
2624
3022
  description: "Title of the match matrix.",
2625
3023
  format: TagFormat.plainText,
2626
3024
  jsonKey: "^heading.forValues",
2627
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3025
+ exportJsonKey: [
3026
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3027
+ { "@bit": { heading: { forValues: ["$"] } } }
3028
+ ]
2628
3029
  },
2629
3030
  {
2630
3031
  key: ConfigKey.property_isCaseSensitive,
@@ -2673,6 +3074,35 @@ var CARDSETS = {
2673
3074
  {
2674
3075
  key: ConfigKey.group_standardTags,
2675
3076
  description: "Standard tags for the statement."
3077
+ },
3078
+ {
3079
+ // PLAN-072: cascade-fired `@example` from the bit header
3080
+ // fills each statement entry's `example` with the entry's
3081
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3082
+ // for the statements cardset. Per-array-item strip removes
3083
+ // `example: false` from incorrect entries so only correct
3084
+ // entries retain `example: true`.
3085
+ key: ConfigKey.property_example,
3086
+ exportJsonKey: [
3087
+ {
3088
+ "@keyonly": {
3089
+ isExample: true,
3090
+ example: true,
3091
+ "@bit": { isExample: true }
3092
+ }
3093
+ },
3094
+ {
3095
+ "@absent": {
3096
+ isExample: true,
3097
+ example: "$parent.isCorrect",
3098
+ "@bit": { isExample: true }
3099
+ }
3100
+ },
3101
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3102
+ ],
3103
+ description: "Example marker on the statement. Bare `[@example]` emits the boolean marker; valued `[@example:true|false]` emits the literal value. Cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3104
+ format: TagFormat.boolean,
3105
+ nullable: true
2676
3106
  }
2677
3107
  ],
2678
3108
  bodyAllowed: false
@@ -2701,6 +3131,71 @@ var CARDSETS = {
2701
3131
  {
2702
3132
  key: ConfigKey.group_standardTags,
2703
3133
  description: "Standard tags for the quiz."
3134
+ },
3135
+ {
3136
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3137
+ // quiz choices is delegated to group_trueFalse's
3138
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3139
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3140
+ // would over-fire on subsequent correct choices and on
3141
+ // incorrect ones — drop it.
3142
+ key: ConfigKey.property_example,
3143
+ exportJsonKey: [
3144
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3145
+ { isExample: true, "@bit": { isExample: true } }
3146
+ ],
3147
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3148
+ format: TagFormat.boolean,
3149
+ nullable: true
3150
+ }
3151
+ ],
3152
+ bodyAllowed: false
3153
+ }
3154
+ ]
3155
+ }
3156
+ ]
3157
+ },
3158
+ //
3159
+ // quiz (responses)
3160
+ //
3161
+ [CardSetConfigKey.quizResponses]: {
3162
+ jsonKey: "quizzes",
3163
+ exportJsonKey: { quizzes: "$" },
3164
+ sides: [
3165
+ {
3166
+ name: "responses",
3167
+ variants: [
3168
+ {
3169
+ jsonKey: null,
3170
+ tags: [
3171
+ {
3172
+ key: ConfigKey.group_trueFalseResponses,
3173
+ description: "Group for true/false questions."
3174
+ },
3175
+ {
3176
+ key: ConfigKey.group_standardTags,
3177
+ description: "Standard tags for the quiz."
3178
+ },
3179
+ {
3180
+ // PLAN-072: cascade-fired `@example` from the bit header
3181
+ // fills each response entry's `example` with the entry's
3182
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3183
+ // for the quiz-responses cardset (statements-style).
3184
+ key: ConfigKey.property_example,
3185
+ exportJsonKey: [
3186
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3187
+ {
3188
+ "@absent": {
3189
+ isExample: true,
3190
+ example: "$parent.isCorrect",
3191
+ "@bit": { isExample: true }
3192
+ }
3193
+ },
3194
+ { isExample: true, "@bit": { isExample: true } }
3195
+ ],
3196
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3197
+ format: TagFormat.boolean,
3198
+ nullable: true
2704
3199
  }
2705
3200
  ],
2706
3201
  bodyAllowed: false
@@ -2726,6 +3221,73 @@ var CARDSETS = {
2726
3221
  key: ConfigKey.group_standardItemLeadInstructionHint,
2727
3222
  description: "Standard tags for lead, instruction, and hint."
2728
3223
  },
3224
+ {
3225
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3226
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3227
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3228
+ // feedback-specific shape.
3229
+ //
3230
+ // Chain @example: `[+ X][@example]` writes
3231
+ // `isExample/example` onto the X choice only. BPG's
3232
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3233
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3234
+ // there is no bubble to bit-level or feedbacks-level
3235
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3236
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3237
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3238
+ // no feedback dispatch, so bit-header `[@example]` does
3239
+ // not flow into feedback choices.
3240
+ key: ConfigKey.tag_true,
3241
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3242
+ description: "Reason-required choice for feedback (`[+]`).",
3243
+ maxCount: Count.infinity,
3244
+ format: TagFormat.plainText,
3245
+ chain: [
3246
+ {
3247
+ key: ConfigKey.property_example,
3248
+ exportJsonKey: [
3249
+ { "@keyonly": { isExample: true, example: true } },
3250
+ { isExample: true, example: "$" }
3251
+ ],
3252
+ description: "Per-choice example marker for feedback `[+]`.",
3253
+ format: TagFormat.boolean,
3254
+ maxCount: 1,
3255
+ nullable: true
3256
+ },
3257
+ {
3258
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3259
+ // choice; same for `[?]` (hint), `[%]` (item),
3260
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3261
+ // Mirrors group_trueFalse's `+` chain shape.
3262
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3263
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3264
+ }
3265
+ ]
3266
+ },
3267
+ {
3268
+ key: ConfigKey.tag_false,
3269
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3270
+ description: "Reason-not-required choice for feedback (`[-]`).",
3271
+ maxCount: Count.infinity,
3272
+ format: TagFormat.plainText,
3273
+ chain: [
3274
+ {
3275
+ key: ConfigKey.property_example,
3276
+ exportJsonKey: [
3277
+ { "@keyonly": { isExample: true, example: true } },
3278
+ { isExample: true, example: "$" }
3279
+ ],
3280
+ description: "Per-choice example marker for feedback `[-]`.",
3281
+ format: TagFormat.boolean,
3282
+ maxCount: 1,
3283
+ nullable: true
3284
+ },
3285
+ {
3286
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3287
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3288
+ }
3289
+ ]
3290
+ },
2729
3291
  {
2730
3292
  key: ConfigKey.group_trueFalse,
2731
3293
  description: "Group for true/false feedback."
@@ -2746,31 +3308,71 @@ var CARDSETS = {
2746
3308
  name: "reason",
2747
3309
  variants: [
2748
3310
  {
3311
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3312
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3313
+ // under `reason.*`; each tag here spells out the full path.
3314
+ // Body is a plain string (BPG `parseFeedback` uses
3315
+ // `cardBodyStr ?? ''` for `reason.text`).
2749
3316
  jsonKey: "reason.text",
2750
3317
  exportJsonKey: { reason: { text: "$" } },
3318
+ format: TextFormat.plainText,
2751
3319
  tags: [
2752
3320
  {
2753
- key: ConfigKey.group_standardItemLeadInstructionHint,
2754
- description: "Standard tags for lead, instruction, and hint."
3321
+ // Instruction lands under reason. Hint/item/lead/etc.
3322
+ // omitted: not exercised by any fixture and BPG's
3323
+ // `parseFeedback` spreads only the tags actually present
3324
+ // — adding the rest can be done lazily as fixtures
3325
+ // require them, with the same `{reason: {…}}` wrapper.
3326
+ key: ConfigKey.tag_instruction,
3327
+ exportJsonKey: { reason: { instruction: "$" } },
3328
+ description: "Instruction for the feedback reason.",
3329
+ format: TagFormat.bitmarkText
2755
3330
  },
2756
3331
  {
2757
3332
  key: ConfigKey.property_reasonableNumOfChars,
2758
3333
  exportJsonKey: [
2759
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2760
- { reasonableNumOfChars: "$" }
3334
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3335
+ { reason: { reasonableNumOfChars: "$" } }
2761
3336
  ],
2762
3337
  description: "Property for reasonable number of characters.",
2763
3338
  format: TagFormat.number
2764
3339
  },
2765
3340
  {
3341
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3342
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3343
+ // cascade. Local emission only.
3344
+ //
3345
+ // Both `reason.example` and `feedbacks[].example` are
3346
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3347
+ // here is `bitmarkText` so `$` coerces to a paragraph
3348
+ // array at both write sites.
2766
3349
  key: ConfigKey.property_example,
2767
3350
  exportJsonKey: [
2768
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2769
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2770
- { isExample: true, example: "$", "@bit": { isExample: true } }
3351
+ {
3352
+ // Bare `[@example]`: reason.example is TextAst BPG
3353
+ // coerces the boolean default to a paragraph
3354
+ // containing the string "true". feedbacks[].example
3355
+ // is not TextAst, stays a boolean. Two literal writes.
3356
+ "@keyonly": {
3357
+ reason: {
3358
+ isExample: true,
3359
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3360
+ },
3361
+ isExample: true,
3362
+ example: true,
3363
+ "@bit": { isExample: true }
3364
+ }
3365
+ },
3366
+ {
3367
+ reason: { isExample: true, example: "$" },
3368
+ isExample: true,
3369
+ example: "$",
3370
+ "@bit": { isExample: true }
3371
+ }
2771
3372
  ],
2772
- description: "Example text for the feedback.",
2773
- format: TagFormat.plainText
3373
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3374
+ format: TagFormat.bitmarkText,
3375
+ nullable: true
2774
3376
  },
2775
3377
  {
2776
3378
  key: ConfigKey.tag_title,
@@ -2834,6 +3436,22 @@ var CARDSETS = {
2834
3436
  {
2835
3437
  key: ConfigKey.group_standardTags,
2836
3438
  description: "Standard tags for the question."
3439
+ },
3440
+ {
3441
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3442
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3443
+ // does not propagate `example`/`isExample` onto question
3444
+ // entries. Only entry-local `[@example]` fires (handled by
3445
+ // `@keyonly` and the unconditional fallback). No `@absent`
3446
+ // rule.
3447
+ key: ConfigKey.property_example,
3448
+ exportJsonKey: [
3449
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3450
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3451
+ ],
3452
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3453
+ format: TagFormat.bitmarkText,
3454
+ nullable: true
2837
3455
  }
2838
3456
  ]
2839
3457
  }
@@ -2860,14 +3478,16 @@ var CARDSETS = {
2860
3478
  description: "Standard tags for lead, instruction, and hint."
2861
3479
  },
2862
3480
  {
3481
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3482
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2863
3483
  key: ConfigKey.property_example,
2864
3484
  exportJsonKey: [
2865
3485
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2866
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2867
3486
  { isExample: true, example: "$", "@bit": { isExample: true } }
2868
3487
  ],
2869
- description: "Example text for the element.",
2870
- format: TagFormat.plainText
3488
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3489
+ format: TagFormat.plainText,
3490
+ nullable: true
2871
3491
  }
2872
3492
  ],
2873
3493
  repeatCount: Count.infinity
@@ -2879,9 +3499,57 @@ var CARDSETS = {
2879
3499
  //
2880
3500
  // table
2881
3501
  //
3502
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3503
+ // output (`{ columns?, data, columnWidths? }`):
3504
+ //
3505
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3506
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3507
+ // no qualifier, so they route to the `table-body` (default) section.
3508
+ //
3509
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3510
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3511
+ // the first header card's cells to `columns` and demote remaining
3512
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3513
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3514
+ // here (no per-cell metadata in basic shape) — declared on the cell
3515
+ // variant below.
3516
+ //
2882
3517
  [CardSetConfigKey.table]: {
2883
- jsonKey: "table.data",
2884
- exportJsonKey: { table: { data: "$" } },
3518
+ jsonKey: null,
3519
+ exportJsonKey: null,
3520
+ sections: {
3521
+ "table-header": {
3522
+ // First header card's row of cells → `columns`. Subsequent header
3523
+ // cards' rows → `data` (per-card append; `[$]` shape because
3524
+ // per-card emission fires once per card, multiple fires deep_merge-
3525
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3526
+ // both ROUTE cards to this section (when no explicit `==== table-
3527
+ // header ====` qualifier) and SELECT the rule body once routed.
3528
+ // First rule = first card with any `[#]` → `columns`; second rule
3529
+ // = subsequent cards in this section → `data` (per-row append).
3530
+ jsonKey: "table.columns",
3531
+ exportJsonKey: [
3532
+ {
3533
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3534
+ rule: { "@bit": { table: { columns: "$" } } }
3535
+ },
3536
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3537
+ { "@bit": { table: { data: ["$"] } } }
3538
+ ]
3539
+ },
3540
+ "table-body": {
3541
+ // Batched emission: `$` is the entire 2D array of body card rows.
3542
+ // This is the default section (no qualifier → cards land here).
3543
+ jsonKey: "table.data",
3544
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3545
+ isDefault: true
3546
+ },
3547
+ "table-footer": {
3548
+ // Batched emission: `$` is the entire 2D array of footer rows.
3549
+ jsonKey: "table.data",
3550
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3551
+ }
3552
+ },
2885
3553
  sides: [
2886
3554
  {
2887
3555
  name: "cell",
@@ -2922,6 +3590,8 @@ var CARDSETS = {
2922
3590
  },
2923
3591
  {
2924
3592
  key: ConfigKey.property_tableColWidth,
3593
+ jsonKey: "^table.columnWidths[{s}]",
3594
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
2925
3595
  description: "Width for the column.",
2926
3596
  format: TagFormat.number
2927
3597
  }
@@ -2940,7 +3610,22 @@ var CARDSETS = {
2940
3610
  sections: {
2941
3611
  "table-header": {
2942
3612
  jsonKey: "table.header.rows",
2943
- exportJsonKey: { table: { header: { rows: "$" } } },
3613
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3614
+ // Cards with no explicit `==== table-header ====` qualifier
3615
+ // route here if FIRST CARD contains any `[#]` tag in any
3616
+ // variant. Explicit-qualifier cards fall through to the
3617
+ // unconditional fallback rule. Both rules use the per-card
3618
+ // append shape `rows: ['$']` — the runtime triggers per-card
3619
+ // emission whenever the pattern carries predicates, and each
3620
+ // fire appends a single-element array (multi-fire deep-merge
3621
+ // concatenates).
3622
+ exportJsonKey: [
3623
+ {
3624
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3625
+ rule: { table: { header: { rows: ["$"] } } }
3626
+ },
3627
+ { table: { header: { rows: ["$"] } } }
3628
+ ],
2944
3629
  sideJsonKey: "cells[{s}]|set(title=true)",
2945
3630
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
2946
3631
  },
@@ -2973,13 +3658,13 @@ var CARDSETS = {
2973
3658
  },
2974
3659
  {
2975
3660
  key: ConfigKey.tag_title,
2976
- jsonKey: ".",
2977
- exportJsonKey: {
2978
- "@bit": {
2979
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
2980
- }
2981
- },
2982
- description: "Title of the table cell."
3661
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3662
+ // `content` field. When the card is in the `table-header`
3663
+ // section, the section's sideExportJsonKey wraps each
3664
+ // cell with `title: true` automatically.
3665
+ jsonKey: "content",
3666
+ exportJsonKey: { content: "$" },
3667
+ description: "Title text of the table cell (header rows)."
2983
3668
  },
2984
3669
  {
2985
3670
  key: ConfigKey.property_tableCellType,
@@ -3011,6 +3696,9 @@ var CARDSETS = {
3011
3696
  },
3012
3697
  {
3013
3698
  key: ConfigKey.property_tableColWidth,
3699
+ // PLAN-080: write to the cell's `colwidth` field.
3700
+ jsonKey: "colwidth",
3701
+ exportJsonKey: { colwidth: "$" },
3014
3702
  description: "Width for the column.",
3015
3703
  format: TagFormat.number
3016
3704
  }
@@ -3048,7 +3736,8 @@ var CARDSETS = {
3048
3736
  {
3049
3737
  key: ConfigKey.resource_audio,
3050
3738
  description: "Audio resource for the pronunciation table.",
3051
- jsonKey: "audio"
3739
+ jsonKey: "audio",
3740
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3052
3741
  }
3053
3742
  ],
3054
3743
  repeatCount: Count.infinity
@@ -3119,14 +3808,17 @@ var CARDSETS = {
3119
3808
  format: TagFormat.plainText
3120
3809
  },
3121
3810
  {
3811
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
3812
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
3813
+ // header cascade.
3122
3814
  key: ConfigKey.property_example,
3123
3815
  exportJsonKey: [
3124
3816
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3125
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3126
3817
  { isExample: true, example: "$", "@bit": { isExample: true } }
3127
3818
  ],
3128
- description: "Example text for the bot action response.",
3129
- format: TagFormat.plainText
3819
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
3820
+ format: TagFormat.plainText,
3821
+ nullable: true
3130
3822
  }
3131
3823
  ]
3132
3824
  }
@@ -3192,6 +3884,38 @@ var CARDSETS = {
3192
3884
  ]
3193
3885
  },
3194
3886
  //
3887
+ // page-footer-sections — mirrors example-bit-list but emits cards under
3888
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
3889
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
3890
+ // routing as pure config so the Rust serializer can pick it up.
3891
+ //
3892
+ [CardSetConfigKey.pageFooterSections]: {
3893
+ jsonKey: "sections",
3894
+ exportJsonKey: { sections: "$" },
3895
+ sides: [
3896
+ {
3897
+ name: "item",
3898
+ variants: [
3899
+ {
3900
+ jsonKey: "body",
3901
+ exportJsonKey: { body: "$" },
3902
+ tags: [
3903
+ {
3904
+ key: ConfigKey.group_standardTags,
3905
+ description: "Standard tags for page-footer section bits."
3906
+ },
3907
+ {
3908
+ exportJsonKey: { title: "$" },
3909
+ key: ConfigKey.tag_title,
3910
+ description: "Title of the page-footer section."
3911
+ }
3912
+ ]
3913
+ }
3914
+ ]
3915
+ }
3916
+ ]
3917
+ },
3918
+ //
3195
3919
  // ingredients
3196
3920
  //
3197
3921
  [CardSetConfigKey.ingredients]: {
@@ -3522,7 +4246,11 @@ var GROUPS = {
3522
4246
  {
3523
4247
  key: ConfigKey.property_groupTag,
3524
4248
  jsonKey: "groupTag.name",
3525
- exportJsonKey: { groupTag: { name: "$" } },
4249
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4250
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4251
+ // same value fold into one entry; chained `@tag` children's
4252
+ // arrays merge as sets.
4253
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3526
4254
  description: "The group tag(s) for the bit",
3527
4255
  format: TagFormat.plainText,
3528
4256
  maxCount: Count.infinity,
@@ -3695,6 +4423,24 @@ var GROUPS = {
3695
4423
  }
3696
4424
  ]
3697
4425
  },
4426
+ [ConfigKey.group_standardInstructionHint]: {
4427
+ type: GroupConfigType.standard,
4428
+ description: "Standard group for instruction and hint tags only (no item/lead/pageNumber/marginNumber chain). Used by sides where item/lead are owned by another side (e.g. matchPairs value-side, which BPG strips item/lead from at sideIdx > 0).",
4429
+ tags: [
4430
+ {
4431
+ exportJsonKey: { instruction: "$" },
4432
+ key: ConfigKey.tag_instruction,
4433
+ name: "Instruction",
4434
+ description: "The instruction for the bit"
4435
+ },
4436
+ {
4437
+ exportJsonKey: { hint: "$" },
4438
+ key: ConfigKey.tag_hint,
4439
+ name: "Hint",
4440
+ description: "The hint for the bit"
4441
+ }
4442
+ ]
4443
+ },
3698
4444
  [ConfigKey.group_standardTags]: {
3699
4445
  type: GroupConfigType.standard,
3700
4446
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3708,17 +4454,45 @@ var GROUPS = {
3708
4454
  description: "Item, lead, page number, margin number, instruction and hint tags"
3709
4455
  },
3710
4456
  {
3711
- // Bit-level example tag: bare/synthesized records the marker only;
3712
- // valued [@example:text] emits `isExample: true` and `example` as
3713
- // PM-rendered rich text (bitmark+ format).
4457
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4458
+ // - bare `[@example]`: consume only (records cascade marker, emits
4459
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4460
+ // for bits NOT in its `isTopLevelExample` allow-list).
4461
+ // - synthesized (descendant cascade): still emits `isExample: true`
4462
+ // at the descendant scope.
4463
+ // - valued `[@example:text]`: emit the example value only; bit-level
4464
+ // `isExample` is left to bubble from descendants.
4465
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4466
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4467
+ // matchMatrix, match, essay, etc.) override this entry with the
4468
+ // legacy emit-isExample shape.
3714
4469
  key: ConfigKey.property_example,
3715
- exportJsonKey: [
3716
- { "@keyonly": { isExample: true } },
3717
- { "@absent": { isExample: true } },
3718
- { isExample: true, example: "$" }
3719
- ],
4470
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3720
4471
  description: "The example(s) for the bit",
3721
- format: TagFormat.bitmarkText
4472
+ format: TagFormat.bitmarkText,
4473
+ nullable: true
4474
+ }
4475
+ ]
4476
+ },
4477
+ [ConfigKey.group_standardTagsNoItemLead]: {
4478
+ type: GroupConfigType.standard,
4479
+ description: "Standard tags mirror of group_standardTags but excluding the item/lead/pageNumber/marginNumber chain. Used by sides where item/lead are owned by another side (e.g. matchPairs value-side \u2014 BPG `parseMatchPairs` strips tags.item / tags.lead for sideIdx > 0).",
4480
+ tags: [
4481
+ {
4482
+ key: ConfigKey.group_standardAllBits,
4483
+ description: "All standard tags which apply to all bits"
4484
+ },
4485
+ {
4486
+ key: ConfigKey.group_standardInstructionHint,
4487
+ description: "Instruction and hint tags only (no item/lead chain)."
4488
+ },
4489
+ {
4490
+ // Mirror of group_standardTags property_example entry.
4491
+ key: ConfigKey.property_example,
4492
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4493
+ description: "The example(s) for the bit",
4494
+ format: TagFormat.bitmarkText,
4495
+ nullable: true
3722
4496
  }
3723
4497
  ]
3724
4498
  },
@@ -3783,8 +4557,8 @@ var GROUPS = {
3783
4557
  tags: [
3784
4558
  {
3785
4559
  key: ConfigKey.property_person,
3786
- jsonKey: "partner.name",
3787
- exportJsonKey: { partner: { name: "$" } },
4560
+ jsonKey: "person.name",
4561
+ exportJsonKey: { person: { name: "$" } },
3788
4562
  description: "A person",
3789
4563
  format: TagFormat.plainText,
3790
4564
  chain: [
@@ -3804,8 +4578,8 @@ var GROUPS = {
3804
4578
  {
3805
4579
  // Deprecated (parter renamed to person)
3806
4580
  key: ConfigKey.property_partner,
3807
- jsonKey: "partner.name",
3808
- exportJsonKey: { partner: { name: "$" } },
4581
+ jsonKey: "person.name",
4582
+ exportJsonKey: { person: { name: "$" } },
3809
4583
  description: "A partner",
3810
4584
  format: TagFormat.plainText,
3811
4585
  chain: [
@@ -3829,7 +4603,9 @@ var GROUPS = {
3829
4603
  description: "Gap chain",
3830
4604
  tags: [
3831
4605
  {
3832
- exportJsonKey: { solutions: [["$"]] },
4606
+ exportJsonKey: {
4607
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4608
+ },
3833
4609
  key: ConfigKey.tag_gap,
3834
4610
  description: "The value of a gap in the content",
3835
4611
  maxCount: Count.infinity,
@@ -3848,27 +4624,37 @@ var GROUPS = {
3848
4624
  description: "Item, lead, page number, margin number, instruction and hint tags"
3849
4625
  },
3850
4626
  {
4627
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4628
+ // `__solutionsAst[length - 1]` — the solution closest to the
4629
+ // `[@example]` tag at parse time. Our chain accumulator is built
4630
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4631
+ // the moment `[@example]` fires, `solutions` only contains the
4632
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4633
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4634
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4635
+ // solutions=["solution"], `[-1]`="solution".
3851
4636
  key: ConfigKey.property_example,
3852
4637
  jsonKey: "example",
3853
4638
  exportJsonKey: [
3854
4639
  {
3855
4640
  "@keyonly": {
3856
4641
  isExample: true,
3857
- example: "$parent.solutions[0]",
4642
+ example: "$parent.solutions[-1]",
3858
4643
  "@bit": { isExample: true }
3859
4644
  }
3860
4645
  },
3861
4646
  {
3862
4647
  "@absent": {
3863
4648
  isExample: true,
3864
- example: "$parent.solutions[0]",
4649
+ example: "$parent.solutions[-1]",
3865
4650
  "@bit": { isExample: true }
3866
4651
  }
3867
4652
  },
3868
4653
  { isExample: true, example: "$", "@bit": { isExample: true } }
3869
4654
  ],
3870
4655
  description: "An example for the gap",
3871
- format: TagFormat.bitmarkText
4656
+ format: TagFormat.bitmarkText,
4657
+ nullable: true
3872
4658
  },
3873
4659
  {
3874
4660
  key: ConfigKey.property_isCaseSensitive,
@@ -3917,6 +4703,24 @@ var GROUPS = {
3917
4703
  description: "Item, lead, page number, margin number, instruction and hint tags"
3918
4704
  },
3919
4705
  {
4706
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4707
+ // `@example` from the bit header marks EVERY choice with
4708
+ // `isExample: true` (matches BPG output), but only the
4709
+ // FIRST correct choice gets `example: true` (BPG
4710
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4711
+ //
4712
+ // Two `@absent` rules, picked first-match-wins:
4713
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4714
+ // `maxEmits:1` — fires once for the first correct.
4715
+ // 2. Plain `@absent` — fires on every remaining entry
4716
+ // (subsequent corrects and all incorrects), emitting
4717
+ // `isExample` only.
4718
+ // The `serialize_cards` cascade-counter scope holds the cap
4719
+ // across the whole cardset.
4720
+ // @card scope-shift writes choice-container isExample (e.g.
4721
+ // `quizzes[i].isExample`) — was previously provided by the
4722
+ // universal `bubble_is_example` post-walk, which was removed
4723
+ // in favour of explicit per-tag config. SYNTAX.md §8.
3920
4724
  key: ConfigKey.property_example,
3921
4725
  jsonKey: "example",
3922
4726
  exportJsonKey: [
@@ -3924,20 +4728,37 @@ var GROUPS = {
3924
4728
  "@keyonly": {
3925
4729
  isExample: true,
3926
4730
  example: "$parent.isCorrect",
3927
- "@bit": { isExample: true }
4731
+ "@bit": { isExample: true },
4732
+ "@card": { isExample: true }
4733
+ }
4734
+ },
4735
+ {
4736
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4737
+ maxEmits: 1,
4738
+ rule: {
4739
+ isExample: true,
4740
+ example: true,
4741
+ "@bit": { isExample: true },
4742
+ "@card": { isExample: true }
3928
4743
  }
3929
4744
  },
3930
4745
  {
3931
4746
  "@absent": {
3932
4747
  isExample: true,
3933
- example: "$parent.isCorrect",
3934
- "@bit": { isExample: true }
4748
+ "@bit": { isExample: true },
4749
+ "@card": { isExample: true }
3935
4750
  }
3936
4751
  },
3937
- { isExample: true, example: "$", "@bit": { isExample: true } }
4752
+ {
4753
+ isExample: true,
4754
+ example: "$",
4755
+ "@bit": { isExample: true },
4756
+ "@card": { isExample: true }
4757
+ }
3938
4758
  ],
3939
4759
  description: "An example for the true/false statement/question",
3940
- format: TagFormat.bitmarkText
4760
+ format: TagFormat.boolean,
4761
+ nullable: true
3941
4762
  }
3942
4763
  ]
3943
4764
  },
@@ -3969,6 +4790,12 @@ var GROUPS = {
3969
4790
  description: "Item, lead, page number, margin number, instruction and hint tags"
3970
4791
  },
3971
4792
  {
4793
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4794
+ // `@example` on a `-` (incorrect choice) emits
4795
+ // `isExample: true` only — no `example` (BPG fixtures show
4796
+ // `isExample` on every choice, but `example` only on the
4797
+ // first correct via firstCorrectOnly cascade).
4798
+ // @card scope-shift writes choice-container isExample.
3972
4799
  key: ConfigKey.property_example,
3973
4800
  jsonKey: "example",
3974
4801
  exportJsonKey: [
@@ -3976,20 +4803,460 @@ var GROUPS = {
3976
4803
  "@keyonly": {
3977
4804
  isExample: true,
3978
4805
  example: "$parent.isCorrect",
3979
- "@bit": { isExample: true }
4806
+ "@bit": { isExample: true },
4807
+ "@card": { isExample: true }
3980
4808
  }
3981
4809
  },
3982
4810
  {
3983
4811
  "@absent": {
4812
+ isExample: true,
4813
+ "@bit": { isExample: true },
4814
+ "@card": { isExample: true }
4815
+ }
4816
+ },
4817
+ {
4818
+ isExample: true,
4819
+ example: "$",
4820
+ "@bit": { isExample: true },
4821
+ "@card": { isExample: true }
4822
+ }
4823
+ ],
4824
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
4825
+ format: TagFormat.boolean,
4826
+ nullable: true
4827
+ }
4828
+ ]
4829
+ }
4830
+ ]
4831
+ },
4832
+ [ConfigKey.group_trueFalseResponses]: {
4833
+ type: GroupConfigType.standard,
4834
+ description: "True/False Responses chain",
4835
+ tags: [
4836
+ {
4837
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4838
+ key: ConfigKey.tag_true,
4839
+ description: "True value for a true/false statement/question",
4840
+ maxCount: Count.infinity,
4841
+ format: TagFormat.plainText,
4842
+ chain: [
4843
+ {
4844
+ key: ConfigKey.tag_true,
4845
+ jsonKey: "responses[]|set(isCorrect=true)",
4846
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4847
+ description: "True values for a true/false statement/question",
4848
+ maxCount: Count.infinity,
4849
+ format: TagFormat.plainText
4850
+ },
4851
+ {
4852
+ key: ConfigKey.tag_false,
4853
+ jsonKey: "responses[]|set(isCorrect=false)",
4854
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4855
+ description: "False values for a true/false statement/question",
4856
+ maxCount: Count.infinity,
4857
+ format: TagFormat.plainText
4858
+ },
4859
+ {
4860
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4861
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4862
+ },
4863
+ {
4864
+ // @card scope-shift writes response-container isExample.
4865
+ key: ConfigKey.property_example,
4866
+ jsonKey: "example",
4867
+ exportJsonKey: [
4868
+ {
4869
+ "@keyonly": {
3984
4870
  isExample: true,
3985
4871
  example: "$parent.isCorrect",
3986
- "@bit": { isExample: true }
4872
+ "@bit": { isExample: true },
4873
+ "@card": { isExample: true }
3987
4874
  }
3988
4875
  },
3989
- { isExample: true, example: "$", "@bit": { isExample: true } }
4876
+ {
4877
+ "@absent": {
4878
+ isExample: "$parent.isCorrect",
4879
+ example: "$parent.isCorrect",
4880
+ "@bit": { isExample: "$parent.isCorrect" },
4881
+ "@card": { isExample: "$parent.isCorrect" }
4882
+ }
4883
+ },
4884
+ {
4885
+ isExample: true,
4886
+ example: "$",
4887
+ "@bit": { isExample: true },
4888
+ "@card": { isExample: true }
4889
+ }
3990
4890
  ],
3991
4891
  description: "An example for the true/false statement/question",
3992
- format: TagFormat.bitmarkText
4892
+ format: TagFormat.boolean,
4893
+ nullable: true
4894
+ }
4895
+ ]
4896
+ },
4897
+ {
4898
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4899
+ key: ConfigKey.tag_false,
4900
+ description: "False value for a true/false statement/question",
4901
+ maxCount: Count.infinity,
4902
+ format: TagFormat.plainText,
4903
+ chain: [
4904
+ {
4905
+ key: ConfigKey.tag_true,
4906
+ jsonKey: "responses[]|set(isCorrect=true)",
4907
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4908
+ description: "True values for a true/false statement/question",
4909
+ maxCount: Count.infinity,
4910
+ format: TagFormat.plainText
4911
+ },
4912
+ {
4913
+ key: ConfigKey.tag_false,
4914
+ jsonKey: "responses[]|set(isCorrect=false)",
4915
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4916
+ description: "False values for a true/false statement/question",
4917
+ maxCount: Count.infinity,
4918
+ format: TagFormat.plainText
4919
+ },
4920
+ {
4921
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4922
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4923
+ },
4924
+ {
4925
+ // @card scope-shift writes response-container isExample.
4926
+ key: ConfigKey.property_example,
4927
+ jsonKey: "example",
4928
+ exportJsonKey: [
4929
+ {
4930
+ "@keyonly": {
4931
+ isExample: true,
4932
+ example: "$parent.isCorrect",
4933
+ "@bit": { isExample: true },
4934
+ "@card": { isExample: true }
4935
+ }
4936
+ },
4937
+ {
4938
+ "@absent": {
4939
+ isExample: true,
4940
+ example: "$parent.isCorrect",
4941
+ "@bit": { isExample: true },
4942
+ "@card": { isExample: true }
4943
+ }
4944
+ },
4945
+ {
4946
+ isExample: true,
4947
+ example: "$",
4948
+ "@bit": { isExample: true },
4949
+ "@card": { isExample: true }
4950
+ }
4951
+ ],
4952
+ description: "An example for the true/false statement/question",
4953
+ format: TagFormat.boolean,
4954
+ nullable: true
4955
+ }
4956
+ ]
4957
+ }
4958
+ ]
4959
+ },
4960
+ [ConfigKey.group_trueFalseInlineSelect]: {
4961
+ type: GroupConfigType.standard,
4962
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
4963
+ tags: [
4964
+ {
4965
+ exportJsonKey: {
4966
+ "@body-inline": {
4967
+ type: "select",
4968
+ attrs: { options: [{ text: "$", isCorrect: true }] }
4969
+ }
4970
+ },
4971
+ key: ConfigKey.tag_true,
4972
+ description: "True value for an inline select body bit",
4973
+ maxCount: Count.infinity,
4974
+ format: TagFormat.plainText,
4975
+ chain: [
4976
+ {
4977
+ key: ConfigKey.tag_true,
4978
+ exportJsonKey: {},
4979
+ description: "True values for an inline select body bit",
4980
+ maxCount: Count.infinity,
4981
+ format: TagFormat.plainText
4982
+ },
4983
+ {
4984
+ key: ConfigKey.tag_false,
4985
+ exportJsonKey: {},
4986
+ description: "False values for an inline select body bit",
4987
+ maxCount: Count.infinity,
4988
+ format: TagFormat.plainText
4989
+ },
4990
+ {
4991
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4992
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4993
+ },
4994
+ {
4995
+ // PLAN-083: cascade-fired `@example` from bit header fires
4996
+ // `isExample:true, example:true` on the FIRST CORRECT option
4997
+ // only (BPG `fillBooleanExample(select.options,
4998
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
4999
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5000
+ // OR — any cascade ⇒ bit marked) and
5001
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5002
+ // on the select node).
5003
+ key: ConfigKey.property_example,
5004
+ jsonKey: "example",
5005
+ exportJsonKey: [
5006
+ {
5007
+ "@keyonly": {
5008
+ isExample: true,
5009
+ example: "$parent.isCorrect",
5010
+ "@bit": { isExample: true },
5011
+ "@body-inline": { attrs: { isExample: true } }
5012
+ }
5013
+ },
5014
+ {
5015
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5016
+ maxEmits: 1,
5017
+ rule: {
5018
+ isExample: true,
5019
+ example: true,
5020
+ "@bit": { isExample: true },
5021
+ "@body-inline": { attrs: { isExample: true } }
5022
+ }
5023
+ },
5024
+ {
5025
+ isExample: true,
5026
+ example: "$",
5027
+ "@bit": { isExample: true },
5028
+ "@body-inline": { attrs: { isExample: true } }
5029
+ }
5030
+ ],
5031
+ description: "An example for the inline select option (first correct only)",
5032
+ format: TagFormat.boolean,
5033
+ nullable: true
5034
+ }
5035
+ ]
5036
+ },
5037
+ {
5038
+ exportJsonKey: {
5039
+ "@body-inline": {
5040
+ type: "select",
5041
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5042
+ }
5043
+ },
5044
+ key: ConfigKey.tag_false,
5045
+ description: "False value for an inline select body bit",
5046
+ maxCount: Count.infinity,
5047
+ format: TagFormat.plainText,
5048
+ chain: [
5049
+ {
5050
+ key: ConfigKey.tag_true,
5051
+ exportJsonKey: {},
5052
+ description: "True values for an inline select body bit",
5053
+ maxCount: Count.infinity,
5054
+ format: TagFormat.plainText
5055
+ },
5056
+ {
5057
+ key: ConfigKey.tag_false,
5058
+ exportJsonKey: {},
5059
+ description: "False values for an inline select body bit",
5060
+ maxCount: Count.infinity,
5061
+ format: TagFormat.plainText
5062
+ },
5063
+ {
5064
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5065
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5066
+ },
5067
+ {
5068
+ // PLAN-083: identical to the `+` lead's chain @example —
5069
+ // cascade reaches the first correct option regardless of
5070
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5071
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5072
+ key: ConfigKey.property_example,
5073
+ jsonKey: "example",
5074
+ exportJsonKey: [
5075
+ {
5076
+ "@keyonly": {
5077
+ isExample: true,
5078
+ example: "$parent.isCorrect",
5079
+ "@bit": { isExample: true },
5080
+ "@body-inline": { attrs: { isExample: true } }
5081
+ }
5082
+ },
5083
+ {
5084
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5085
+ maxEmits: 1,
5086
+ rule: {
5087
+ isExample: true,
5088
+ example: true,
5089
+ "@bit": { isExample: true },
5090
+ "@body-inline": { attrs: { isExample: true } }
5091
+ }
5092
+ },
5093
+ {
5094
+ isExample: true,
5095
+ example: "$",
5096
+ "@bit": { isExample: true },
5097
+ "@body-inline": { attrs: { isExample: true } }
5098
+ }
5099
+ ],
5100
+ description: "An example for the inline select option (first correct only)",
5101
+ format: TagFormat.boolean,
5102
+ nullable: true
5103
+ }
5104
+ ]
5105
+ }
5106
+ ]
5107
+ },
5108
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5109
+ type: GroupConfigType.standard,
5110
+ description: 'True/False chain composed inline (highlight body-bit). +/- tags emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}` inline body nodes. Used by highlight-text bit.',
5111
+ tags: [
5112
+ {
5113
+ exportJsonKey: {
5114
+ "@body-inline": {
5115
+ type: "highlight",
5116
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5117
+ }
5118
+ },
5119
+ key: ConfigKey.tag_true,
5120
+ description: "Highlighted span in the body (correct)",
5121
+ maxCount: Count.infinity,
5122
+ format: TagFormat.plainText,
5123
+ chain: [
5124
+ {
5125
+ key: ConfigKey.tag_true,
5126
+ exportJsonKey: {},
5127
+ description: "Highlighted span values (correct)",
5128
+ maxCount: Count.infinity,
5129
+ format: TagFormat.plainText
5130
+ },
5131
+ {
5132
+ key: ConfigKey.tag_false,
5133
+ exportJsonKey: {},
5134
+ description: "Highlighted span values (incorrect)",
5135
+ maxCount: Count.infinity,
5136
+ format: TagFormat.plainText
5137
+ },
5138
+ {
5139
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5140
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5141
+ },
5142
+ {
5143
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5144
+ // `@example` from the bit header fires `example: true` +
5145
+ // `isExample: true` on the FIRST correct highlight only
5146
+ // (BPG `fillBooleanExample(highlight.texts,
5147
+ // firstCorrectOnly=true)`). Incorrect highlights get
5148
+ // nothing (unlike choices, the highlight-text fixture
5149
+ // shows no `isExample` on incorrect entries).
5150
+ key: ConfigKey.property_example,
5151
+ jsonKey: "example",
5152
+ exportJsonKey: [
5153
+ {
5154
+ "@keyonly": {
5155
+ isExample: true,
5156
+ example: "$parent.isCorrect",
5157
+ "@bit": { isExample: true },
5158
+ "@body-inline": { attrs: { isExample: true } }
5159
+ }
5160
+ },
5161
+ {
5162
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5163
+ maxEmits: 1,
5164
+ rule: {
5165
+ isExample: true,
5166
+ example: true,
5167
+ "@bit": { isExample: true },
5168
+ "@body-inline": { attrs: { isExample: true } }
5169
+ }
5170
+ },
5171
+ {
5172
+ isExample: true,
5173
+ example: "$",
5174
+ "@bit": { isExample: true },
5175
+ "@body-inline": { attrs: { isExample: true } }
5176
+ }
5177
+ ],
5178
+ description: "An example for the highlighted span (first correct only)",
5179
+ format: TagFormat.boolean,
5180
+ nullable: true
5181
+ }
5182
+ ]
5183
+ },
5184
+ {
5185
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5186
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5187
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5188
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5189
+ // through and nothing fires. The natural-default strip removes
5190
+ // `isCorrect: false` from the final texts entries (matches fixture).
5191
+ exportJsonKey: {
5192
+ "@body-inline": {
5193
+ type: "highlight",
5194
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5195
+ }
5196
+ },
5197
+ key: ConfigKey.tag_false,
5198
+ description: "Highlighted span in the body (incorrect)",
5199
+ maxCount: Count.infinity,
5200
+ format: TagFormat.plainText,
5201
+ chain: [
5202
+ {
5203
+ key: ConfigKey.tag_true,
5204
+ exportJsonKey: {},
5205
+ description: "Highlighted span values (correct)",
5206
+ maxCount: Count.infinity,
5207
+ format: TagFormat.plainText
5208
+ },
5209
+ {
5210
+ key: ConfigKey.tag_false,
5211
+ exportJsonKey: {},
5212
+ description: "Highlighted span values (incorrect)",
5213
+ maxCount: Count.infinity,
5214
+ format: TagFormat.plainText
5215
+ },
5216
+ {
5217
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5218
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5219
+ },
5220
+ {
5221
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5222
+ // `@example` on a `-` (incorrect highlight) emits nothing
5223
+ // (highlight-text fixture has no `isExample` on incorrect
5224
+ // entries; firstCorrectOnly cascade skips them entirely).
5225
+ // No `@absent` rule.
5226
+ // PLAN-083: identical to the `+` lead's chain @example —
5227
+ // cascade reaches the first correct option regardless of
5228
+ // which lead (+/-) opens the chain.
5229
+ key: ConfigKey.property_example,
5230
+ jsonKey: "example",
5231
+ exportJsonKey: [
5232
+ {
5233
+ "@keyonly": {
5234
+ isExample: true,
5235
+ example: "$parent.isCorrect",
5236
+ "@bit": { isExample: true },
5237
+ "@body-inline": { attrs: { isExample: true } }
5238
+ }
5239
+ },
5240
+ {
5241
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5242
+ maxEmits: 1,
5243
+ rule: {
5244
+ isExample: true,
5245
+ example: true,
5246
+ "@bit": { isExample: true },
5247
+ "@body-inline": { attrs: { isExample: true } }
5248
+ }
5249
+ },
5250
+ {
5251
+ isExample: true,
5252
+ example: "$",
5253
+ "@bit": { isExample: true },
5254
+ "@body-inline": { attrs: { isExample: true } }
5255
+ }
5256
+ ],
5257
+ description: "An example for the highlighted span (first correct only)",
5258
+ format: TagFormat.boolean,
5259
+ nullable: true
3993
5260
  }
3994
5261
  ]
3995
5262
  }
@@ -4002,7 +5269,7 @@ var GROUPS = {
4002
5269
  {
4003
5270
  key: ConfigKey.property_mark,
4004
5271
  jsonKey: "marks.mark",
4005
- exportJsonKey: { marks: { mark: ["$"] } },
5272
+ exportJsonKey: { marks: [{ mark: "$" }] },
4006
5273
  description: "The mark configuration",
4007
5274
  format: TagFormat.plainText,
4008
5275
  maxCount: Count.infinity,
@@ -4026,7 +5293,9 @@ var GROUPS = {
4026
5293
  description: "Mark chain",
4027
5294
  tags: [
4028
5295
  {
4029
- exportJsonKey: { solution: ["$"] },
5296
+ exportJsonKey: {
5297
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5298
+ },
4030
5299
  key: ConfigKey.tag_mark,
4031
5300
  description: "The marked content",
4032
5301
  maxCount: Count.infinity,
@@ -4035,6 +5304,7 @@ var GROUPS = {
4035
5304
  {
4036
5305
  key: ConfigKey.property_mark,
4037
5306
  jsonKey: "mark",
5307
+ exportJsonKey: { mark: "$" },
4038
5308
  description: "The mark configuration",
4039
5309
  format: TagFormat.plainText
4040
5310
  },
@@ -4047,7 +5317,8 @@ var GROUPS = {
4047
5317
  { isExample: true, example: "$", "@bit": { isExample: true } }
4048
5318
  ],
4049
5319
  description: "An example for the marked content",
4050
- format: TagFormat.bitmarkText
5320
+ format: TagFormat.bitmarkText,
5321
+ nullable: true
4051
5322
  }
4052
5323
  ]
4053
5324
  }
@@ -4476,6 +5747,42 @@ var GROUPS = {
4476
5747
  }
4477
5748
  ]
4478
5749
  },
5750
+ [ConfigKey.group_backgroundWallpaper]: {
5751
+ type: GroupConfigType.standard,
5752
+ description: "Background wallpaper tags",
5753
+ tags: [
5754
+ {
5755
+ key: ConfigKey.resource_backgroundWallpaper,
5756
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
5757
+ description: "Background wallpaper for the image, used to set a background for the image",
5758
+ chain: [
5759
+ {
5760
+ key: ConfigKey.group_resourceImageCommon,
5761
+ description: "Common resource image tags for images"
5762
+ }
5763
+ ]
5764
+ }
5765
+ ]
5766
+ },
5767
+ [ConfigKey.group_imageNoZoom]: {
5768
+ type: GroupConfigType.standard,
5769
+ description: "Image bit tags",
5770
+ tags: [
5771
+ {
5772
+ key: ConfigKey.group_backgroundWallpaper,
5773
+ description: "Background wallpaper tags for images, used to define background properties for images"
5774
+ },
5775
+ {
5776
+ key: ConfigKey.group_resourceBitTags,
5777
+ description: "Resource bit tags for images, used to define additional properties for images"
5778
+ },
5779
+ {
5780
+ key: ConfigKey.group_resourceImageNoZoom,
5781
+ description: "Resource image tags for images, used to attach images to the bit",
5782
+ minCount: 1
5783
+ }
5784
+ ]
5785
+ },
4479
5786
  //
4480
5787
  // Resource groups
4481
5788
  //
@@ -4595,6 +5902,58 @@ var GROUPS = {
4595
5902
  }
4596
5903
  ]
4597
5904
  },
5905
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
5906
+ type: GroupConfigType.standard,
5907
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
5908
+ tags: [
5909
+ {
5910
+ key: ConfigKey.group_resourceCommon,
5911
+ description: "Common resource properties"
5912
+ },
5913
+ {
5914
+ key: ConfigKey.property_src1x,
5915
+ description: "The source URL for the image at 1x resolution",
5916
+ format: TagFormat.plainText
5917
+ },
5918
+ {
5919
+ key: ConfigKey.property_src2x,
5920
+ description: "The source URL for the image at 2x resolution",
5921
+ format: TagFormat.plainText
5922
+ },
5923
+ {
5924
+ key: ConfigKey.property_src3x,
5925
+ description: "The source URL for the image at 3x resolution",
5926
+ format: TagFormat.plainText
5927
+ },
5928
+ {
5929
+ key: ConfigKey.property_src4x,
5930
+ description: "The source URL for the image at 4x resolution",
5931
+ format: TagFormat.plainText
5932
+ },
5933
+ {
5934
+ key: ConfigKey.property_width,
5935
+ description: "The width of the image",
5936
+ format: TagFormat.number
5937
+ },
5938
+ {
5939
+ key: ConfigKey.property_height,
5940
+ description: "The height of the image",
5941
+ format: TagFormat.number
5942
+ },
5943
+ {
5944
+ key: ConfigKey.property_alt,
5945
+ description: "The alternative text for the image",
5946
+ format: TagFormat.plainText
5947
+ },
5948
+ // @zoomDisabled with a true default
5949
+ {
5950
+ key: ConfigKey.property_zoomDisabled,
5951
+ description: "If true, zooming is disabled for the image (defaults to true)",
5952
+ format: TagFormat.boolean,
5953
+ defaultValue: "true"
5954
+ }
5955
+ ]
5956
+ },
4598
5957
  [ConfigKey.group_resourceAudioCommon]: {
4599
5958
  type: GroupConfigType.standard,
4600
5959
  description: "Common properties for audio resources",
@@ -4684,22 +6043,26 @@ var GROUPS = {
4684
6043
  {
4685
6044
  key: ConfigKey.property_src1x,
4686
6045
  description: "The source URL for the video at 1x resolution",
4687
- format: TagFormat.plainText
6046
+ format: TagFormat.plainText,
6047
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4688
6048
  },
4689
6049
  {
4690
6050
  key: ConfigKey.property_src2x,
4691
6051
  description: "The source URL for the video at 2x resolution",
4692
- format: TagFormat.plainText
6052
+ format: TagFormat.plainText,
6053
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4693
6054
  },
4694
6055
  {
4695
6056
  key: ConfigKey.property_src3x,
4696
6057
  description: "The source URL for the video at 3x resolution",
4697
- format: TagFormat.plainText
6058
+ format: TagFormat.plainText,
6059
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4698
6060
  },
4699
6061
  {
4700
6062
  key: ConfigKey.property_src4x,
4701
6063
  description: "The source URL for the video at 4x resolution",
4702
- format: TagFormat.plainText
6064
+ format: TagFormat.plainText,
6065
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4703
6066
  }
4704
6067
  ]
4705
6068
  },
@@ -4712,6 +6075,7 @@ var GROUPS = {
4712
6075
  tags: [
4713
6076
  {
4714
6077
  key: ConfigKey.resource_icon,
6078
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4715
6079
  description: "The icon resource",
4716
6080
  chain: [
4717
6081
  {
@@ -4739,12 +6103,30 @@ var GROUPS = {
4739
6103
  }
4740
6104
  ]
4741
6105
  },
6106
+ [ConfigKey.group_resourceImageNoZoom]: {
6107
+ type: GroupConfigType.resource,
6108
+ description: "Image resource where @zoomDisabled defaults to true",
6109
+ tags: [
6110
+ {
6111
+ key: ConfigKey.resource_image,
6112
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6113
+ description: "The image resource (no-zoom default)",
6114
+ chain: [
6115
+ {
6116
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6117
+ description: "Common image properties with @zoomDisabled defaulting to true"
6118
+ }
6119
+ ]
6120
+ }
6121
+ ]
6122
+ },
4742
6123
  [ConfigKey.group_resourceImagePortrait]: {
4743
6124
  type: GroupConfigType.resource,
4744
6125
  description: "Portrait image resource",
4745
6126
  tags: [
4746
6127
  {
4747
6128
  key: ConfigKey.resource_imagePortrait,
6129
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4748
6130
  description: "The portrait image resource",
4749
6131
  chain: [
4750
6132
  {
@@ -4761,6 +6143,7 @@ var GROUPS = {
4761
6143
  tags: [
4762
6144
  {
4763
6145
  key: ConfigKey.resource_imageLandscape,
6146
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4764
6147
  description: "The landscape image resource",
4765
6148
  chain: [
4766
6149
  {
@@ -4777,6 +6160,7 @@ var GROUPS = {
4777
6160
  tags: [
4778
6161
  {
4779
6162
  key: ConfigKey.resource_imageEmbed,
6163
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4780
6164
  description: "The embedded image resource",
4781
6165
  chain: [
4782
6166
  {
@@ -4950,6 +6334,7 @@ var GROUPS = {
4950
6334
  tags: [
4951
6335
  {
4952
6336
  key: ConfigKey.resource_article,
6337
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
4953
6338
  description: "The article resource",
4954
6339
  chain: [
4955
6340
  {
@@ -4966,6 +6351,7 @@ var GROUPS = {
4966
6351
  tags: [
4967
6352
  {
4968
6353
  key: ConfigKey.resource_articleEmbed,
6354
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
4969
6355
  description: "The embedded article resource",
4970
6356
  chain: [
4971
6357
  {
@@ -4982,6 +6368,7 @@ var GROUPS = {
4982
6368
  tags: [
4983
6369
  {
4984
6370
  key: ConfigKey.resource_articleLink,
6371
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
4985
6372
  description: "The link to the article resource",
4986
6373
  chain: [
4987
6374
  {
@@ -5252,7 +6639,12 @@ var ConfigHydrator = class {
5252
6639
  isDefault: v.isDefault,
5253
6640
  sideJsonKey: v.sideJsonKey,
5254
6641
  sideExportJsonKey: v.sideExportJsonKey,
5255
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6642
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6643
+ // PLAN-085: cardinality fields pass through; defaults applied
6644
+ // downstream by the config consumer (Rust treats `0` / undefined
6645
+ // as unbounded).
6646
+ minCount: v.minCount,
6647
+ maxCount: v.maxCount
5256
6648
  };
5257
6649
  }
5258
6650
  return out;
@@ -5467,7 +6859,8 @@ var BITS = {
5467
6859
  { isExample: true, example: "$" }
5468
6860
  ],
5469
6861
  description: "The example text for the bit",
5470
- format: TagFormat.plainText
6862
+ format: TagFormat.plainText,
6863
+ nullable: true
5471
6864
  }
5472
6865
  ],
5473
6866
  rootExampleType: ExampleType.string
@@ -5806,7 +7199,8 @@ var BITS = {
5806
7199
  { isExample: true, example: "$" }
5807
7200
  ],
5808
7201
  description: "The example text for the bit",
5809
- format: TagFormat.plainText
7202
+ format: TagFormat.plainText,
7203
+ nullable: true
5810
7204
  }
5811
7205
  ],
5812
7206
  rootExampleType: ExampleType.string
@@ -5969,7 +7363,8 @@ var BITS = {
5969
7363
  { isExample: true, example: "$" }
5970
7364
  ],
5971
7365
  description: "The example text for the bit",
5972
- format: TagFormat.plainText
7366
+ format: TagFormat.plainText,
7367
+ nullable: true
5973
7368
  }
5974
7369
  ],
5975
7370
  rootExampleType: ExampleType.string
@@ -6120,7 +7515,8 @@ var BITS = {
6120
7515
  { isExample: true, example: "$" }
6121
7516
  ],
6122
7517
  description: "The example text for the bit",
6123
- format: TagFormat.plainText
7518
+ format: TagFormat.plainText,
7519
+ nullable: true
6124
7520
  }
6125
7521
  ],
6126
7522
  rootExampleType: ExampleType.string
@@ -6715,8 +8111,31 @@ var BITS = {
6715
8111
  description: "Tags for gaps in cloze and multiple choice text bits"
6716
8112
  },
6717
8113
  {
6718
- key: ConfigKey.group_trueFalse,
8114
+ key: ConfigKey.group_trueFalseInlineSelect,
6719
8115
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8116
+ },
8117
+ {
8118
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8119
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8120
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8121
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8122
+ key: ConfigKey.property_isCaseSensitive,
8123
+ exportJsonKey: {},
8124
+ description: "If the cloze answers are case sensitive",
8125
+ format: TagFormat.boolean,
8126
+ defaultValue: "true"
8127
+ },
8128
+ {
8129
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8130
+ key: ConfigKey.property_example,
8131
+ exportJsonKey: [
8132
+ { "@keyonly": { isExample: true } },
8133
+ { "@absent": { isExample: true } },
8134
+ { isExample: true, example: "$" }
8135
+ ],
8136
+ description: "The example(s) for the bit",
8137
+ format: TagFormat.bitmarkText,
8138
+ nullable: true
6720
8139
  }
6721
8140
  ]
6722
8141
  },
@@ -6755,18 +8174,56 @@ var BITS = {
6755
8174
  {
6756
8175
  key: ConfigKey.group_gap,
6757
8176
  description: "Tags for gaps in cloze bits"
8177
+ },
8178
+ {
8179
+ // Override inherited @example: cloze is in OLD parser's
8180
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8181
+ // emits `isExample: true`.
8182
+ key: ConfigKey.property_example,
8183
+ exportJsonKey: [
8184
+ { "@keyonly": { isExample: true } },
8185
+ { "@absent": { isExample: true } },
8186
+ { isExample: true, example: "$" }
8187
+ ],
8188
+ description: "The example(s) for the bit",
8189
+ format: TagFormat.bitmarkText,
8190
+ nullable: true
6758
8191
  }
6759
8192
  ]
6760
8193
  },
6761
8194
  [BitType.clozeInstructionGrouped]: {
6762
8195
  since: "1.3.0",
6763
8196
  baseBitType: BitType.cloze,
6764
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8197
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8198
+ tags: [
8199
+ {
8200
+ key: ConfigKey.property_quizCountItems,
8201
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8202
+ format: TagFormat.boolean,
8203
+ defaultValue: "true"
8204
+ }
8205
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8206
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8207
+ ]
6765
8208
  },
6766
8209
  [BitType.clozeSolutionGrouped]: {
6767
8210
  since: "1.3.0",
6768
8211
  baseBitType: BitType.cloze,
6769
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8212
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8213
+ tags: [
8214
+ {
8215
+ key: ConfigKey.property_quizCountItems,
8216
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8217
+ format: TagFormat.boolean,
8218
+ defaultValue: "true"
8219
+ },
8220
+ {
8221
+ key: ConfigKey.property_quizStrikethroughSolutions,
8222
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8223
+ format: TagFormat.boolean,
8224
+ defaultValue: "true"
8225
+ }
8226
+ ]
6770
8227
  },
6771
8228
  [BitType.clozeSeveral]: {
6772
8229
  since: "3.5.0",
@@ -6802,6 +8259,17 @@ var BITS = {
6802
8259
  {
6803
8260
  key: ConfigKey.group_quizCommon,
6804
8261
  description: "Common tags for quiz bits"
8262
+ },
8263
+ {
8264
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8265
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8266
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8267
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8268
+ key: ConfigKey.property_isCaseSensitive,
8269
+ exportJsonKey: {},
8270
+ description: "If the cloze answers are case sensitive",
8271
+ format: TagFormat.boolean,
8272
+ defaultValue: "true"
6805
8273
  }
6806
8274
  ],
6807
8275
  cardSet: CardSetConfigKey.clozeList
@@ -6968,6 +8436,7 @@ var BITS = {
6968
8436
  tags: [
6969
8437
  {
6970
8438
  key: ConfigKey.group_person,
8439
+ exportJsonKey: { partner: { name: "$" } },
6971
8440
  description: "Tags for the person in the conversation"
6972
8441
  }
6973
8442
  ]
@@ -7406,7 +8875,8 @@ var BITS = {
7406
8875
  { isExample: true, example: "$" }
7407
8876
  ],
7408
8877
  description: "The example text for the essay bit",
7409
- format: TagFormat.plainText
8878
+ format: TagFormat.plainText,
8879
+ nullable: true
7410
8880
  }
7411
8881
  ],
7412
8882
  rootExampleType: ExampleType.string
@@ -7440,7 +8910,8 @@ var BITS = {
7440
8910
  { isExample: true, example: "$" }
7441
8911
  ],
7442
8912
  description: "The example text for the example bit",
7443
- format: TagFormat.plainText
8913
+ format: TagFormat.plainText,
8914
+ nullable: true
7444
8915
  }
7445
8916
  ],
7446
8917
  rootExampleType: ExampleType.string
@@ -7526,7 +8997,19 @@ var BITS = {
7526
8997
  [BitType.articleAi]: {
7527
8998
  since: "1.3.0",
7528
8999
  baseBitType: BitType.article,
7529
- description: "Article AI bit, used to create AI-generated articles"
9000
+ description: "Article AI bit, used to create AI-generated articles",
9001
+ tags: [
9002
+ {
9003
+ // Shadows the inherited @aiGenerated from the standard group: this
9004
+ // bit type forces aiGenerated:true regardless of source value or
9005
+ // absence. Literal `true` in the export pattern ignores `$`.
9006
+ key: ConfigKey.property_aiGenerated,
9007
+ description: "AI-generated flag, forced true for article-ai bits",
9008
+ format: TagFormat.boolean,
9009
+ defaultValue: "true",
9010
+ exportJsonKey: { aiGenerated: true }
9011
+ }
9012
+ ]
7530
9013
  },
7531
9014
  [BitType.articleAttachment]: {
7532
9015
  since: "1.3.0",
@@ -7851,7 +9334,19 @@ var BITS = {
7851
9334
  [BitType.noteAi]: {
7852
9335
  since: "1.3.0",
7853
9336
  baseBitType: BitType.note,
7854
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9337
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9338
+ tags: [
9339
+ {
9340
+ // Shadows the inherited @aiGenerated from the standard group: this
9341
+ // bit type forces aiGenerated:true regardless of source value or
9342
+ // absence. Literal `true` in the export pattern ignores `$`.
9343
+ key: ConfigKey.property_aiGenerated,
9344
+ description: "AI-generated flag, forced true for article-ai bits",
9345
+ format: TagFormat.boolean,
9346
+ defaultValue: "true",
9347
+ exportJsonKey: { aiGenerated: true }
9348
+ }
9349
+ ]
7855
9350
  },
7856
9351
  [BitType.notebookArticle]: {
7857
9352
  since: "1.3.0",
@@ -7976,7 +9471,19 @@ var BITS = {
7976
9471
  [BitType.summaryAi]: {
7977
9472
  since: "1.3.0",
7978
9473
  baseBitType: BitType.summary,
7979
- description: "AI-generated summary bit"
9474
+ description: "AI-generated summary bit",
9475
+ tags: [
9476
+ {
9477
+ // Shadows the inherited @aiGenerated from the standard group: this
9478
+ // bit type forces aiGenerated:true regardless of source value or
9479
+ // absence. Literal `true` in the export pattern ignores `$`.
9480
+ key: ConfigKey.property_aiGenerated,
9481
+ description: "AI-generated flag, forced true for article-ai bits",
9482
+ format: TagFormat.boolean,
9483
+ defaultValue: "true",
9484
+ exportJsonKey: { aiGenerated: true }
9485
+ }
9486
+ ]
7980
9487
  },
7981
9488
  [BitType.videoTranscript]: {
7982
9489
  since: "1.3.0",
@@ -8250,7 +9757,7 @@ var BITS = {
8250
9757
  description: "Product ID for the module product, used to link to a specific product",
8251
9758
  format: TagFormat.plainText,
8252
9759
  minCount: 1,
8253
- maxCount: Count.infinity
9760
+ maxCount: 1
8254
9761
  }
8255
9762
  ]
8256
9763
  },
@@ -8303,10 +9810,13 @@ var BITS = {
8303
9810
  description: "Resource bit tags for logo grave images, used to define additional properties"
8304
9811
  },
8305
9812
  {
8306
- // Image resource
8307
- key: ConfigKey.group_resourceImage,
9813
+ // Image resource — fully-expanded shape so each `&image` emission
9814
+ // produces a complete `{type, image: {src}}` object inside the
9815
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
9816
+ // `images[].image` via the `[]` last-element-merge semantic.
9817
+ key: ConfigKey.group_resourceImageNoZoom,
8308
9818
  description: "Resource image tags for logo grave images, used to attach images",
8309
- exportJsonKey: { images: "$" },
9819
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8310
9820
  minCount: 1,
8311
9821
  maxCount: Count.infinity
8312
9822
  }
@@ -8596,6 +10106,10 @@ var BITS = {
8596
10106
  tags: [
8597
10107
  {
8598
10108
  key: ConfigKey.property_book,
10109
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10110
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10111
+ // inside the most-recently-appended entry as `reference: $`.
10112
+ exportJsonKey: { book: [{ book: "$" }] },
8599
10113
  description: "Book reference for the page, used to link to a specific book",
8600
10114
  maxCount: Count.infinity,
8601
10115
  chain: [
@@ -8663,7 +10177,9 @@ var BITS = {
8663
10177
  format: TagFormat.plainText
8664
10178
  }
8665
10179
  ],
8666
- cardSet: CardSetConfigKey.exampleBitList
10180
+ // page-footer emits cards under `sections` (vs `listItems` for the
10181
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10182
+ cardSet: CardSetConfigKey.pageFooterSections
8667
10183
  },
8668
10184
  [BitType.legend]: {
8669
10185
  since: "3.12.0",
@@ -8705,6 +10221,20 @@ var BITS = {
8705
10221
  since: "1.34.0",
8706
10222
  baseBitType: BitType._standard,
8707
10223
  description: "Definition list bit, used to create lists of definitions in articles or books",
10224
+ tags: [
10225
+ {
10226
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10227
+ key: ConfigKey.property_example,
10228
+ exportJsonKey: [
10229
+ { "@keyonly": { isExample: true } },
10230
+ { "@absent": { isExample: true } },
10231
+ { isExample: true, example: "$" }
10232
+ ],
10233
+ description: "The example(s) for the bit",
10234
+ format: TagFormat.bitmarkText,
10235
+ nullable: true
10236
+ }
10237
+ ],
8708
10238
  cardSet: CardSetConfigKey.definitionList
8709
10239
  },
8710
10240
  [BitType.metaSearchDefaultTerms]: {
@@ -8727,6 +10257,18 @@ var BITS = {
8727
10257
  {
8728
10258
  key: ConfigKey.group_quizCommon,
8729
10259
  description: "Common quiz tags for flashcards"
10260
+ },
10261
+ {
10262
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10263
+ key: ConfigKey.property_example,
10264
+ exportJsonKey: [
10265
+ { "@keyonly": { isExample: true } },
10266
+ { "@absent": { isExample: true } },
10267
+ { isExample: true, example: "$" }
10268
+ ],
10269
+ description: "The example(s) for the bit",
10270
+ format: TagFormat.bitmarkText,
10271
+ nullable: true
8730
10272
  }
8731
10273
  ],
8732
10274
  cardSet: CardSetConfigKey.flashcard
@@ -8734,7 +10276,11 @@ var BITS = {
8734
10276
  [BitType.flashcard1]: {
8735
10277
  since: "1.3.0",
8736
10278
  baseBitType: BitType.flashcard,
8737
- description: "Flashcard 1 bit"
10279
+ description: "Flashcard 1 bit",
10280
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10281
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10282
+ // bit type structurally allows only a single card.
10283
+ cardSet: CardSetConfigKey.flashcard1
8738
10284
  },
8739
10285
  [BitType.qAndACard]: {
8740
10286
  since: "3.25.0",
@@ -8769,8 +10315,24 @@ var BITS = {
8769
10315
  description: "Common quiz tags for highlighted text"
8770
10316
  },
8771
10317
  {
8772
- key: ConfigKey.group_trueFalse,
8773
- description: "True/False quiz tags for highlighted text"
10318
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10319
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10320
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10321
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10322
+ key: ConfigKey.group_trueFalseInlineHighlight,
10323
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10324
+ },
10325
+ {
10326
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10327
+ key: ConfigKey.property_example,
10328
+ exportJsonKey: [
10329
+ { "@keyonly": { isExample: true } },
10330
+ { "@absent": { isExample: true } },
10331
+ { isExample: true, example: "$" }
10332
+ ],
10333
+ description: "The example(s) for the bit",
10334
+ format: TagFormat.bitmarkText,
10335
+ nullable: true
8774
10336
  }
8775
10337
  ]
8776
10338
  },
@@ -8780,15 +10342,8 @@ var BITS = {
8780
10342
  description: "Image bit, used to create images in articles or books",
8781
10343
  tags: [
8782
10344
  {
8783
- key: ConfigKey.resource_backgroundWallpaper,
8784
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8785
- description: "Background wallpaper for the image, used to set a background for the image",
8786
- chain: [
8787
- {
8788
- key: ConfigKey.group_resourceImageCommon,
8789
- description: "Common resource image tags for images"
8790
- }
8791
- ]
10345
+ key: ConfigKey.group_backgroundWallpaper,
10346
+ description: "Background wallpaper tags for images, used to define background properties for images"
8792
10347
  },
8793
10348
  {
8794
10349
  key: ConfigKey.group_resourceBitTags,
@@ -8891,8 +10446,15 @@ var BITS = {
8891
10446
  },
8892
10447
  [BitType.imageSeparator]: {
8893
10448
  since: "1.4.15",
8894
- baseBitType: BitType.image,
8895
- description: "Image separator bit, used to create separators in articles or books"
10449
+ baseBitType: BitType._standard,
10450
+ description: "Image separator bit, used to create separators in articles or books",
10451
+ tags: [
10452
+ {
10453
+ key: ConfigKey.group_imageNoZoom,
10454
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10455
+ }
10456
+ ],
10457
+ resourceAttachmentAllowed: false
8896
10458
  },
8897
10459
  [BitType.imageSeparatorAlt]: {
8898
10460
  since: "1.16.0",
@@ -8931,15 +10493,20 @@ var BITS = {
8931
10493
  },
8932
10494
  [BitType.pageBanner]: {
8933
10495
  since: "1.4.3",
8934
- baseBitType: BitType.image,
10496
+ baseBitType: BitType._standard,
8935
10497
  description: "Page banner bit, used to create banners for pages in articles or books",
8936
10498
  tags: [
8937
10499
  {
8938
10500
  key: ConfigKey.property_slug,
8939
10501
  description: "Slug for the page banner, used to identify the banner in the system",
8940
10502
  format: TagFormat.plainText
10503
+ },
10504
+ {
10505
+ key: ConfigKey.group_imageNoZoom,
10506
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
8941
10507
  }
8942
- ]
10508
+ ],
10509
+ resourceAttachmentAllowed: false
8943
10510
  },
8944
10511
  [BitType.pageHero]: {
8945
10512
  since: "1.11.0",
@@ -8954,7 +10521,7 @@ var BITS = {
8954
10521
  },
8955
10522
  [BitType.tableImage]: {
8956
10523
  since: "1.5.15",
8957
- baseBitType: BitType.table,
10524
+ baseBitType: BitType.tableExtended,
8958
10525
  description: "Table image bit, used to create images in tables in articles or books",
8959
10526
  tags: [
8960
10527
  {
@@ -8963,15 +10530,8 @@ var BITS = {
8963
10530
  format: TagFormat.bitmarkText
8964
10531
  },
8965
10532
  {
8966
- key: ConfigKey.resource_backgroundWallpaper,
8967
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8968
- description: "Background wallpaper for the image, used to set a background for the image",
8969
- chain: [
8970
- {
8971
- key: ConfigKey.group_resourceImageCommon,
8972
- description: "Common resource image tags for images"
8973
- }
8974
- ]
10533
+ key: ConfigKey.group_backgroundWallpaper,
10534
+ description: "Background wallpaper tags for images, used to define background properties for images"
8975
10535
  },
8976
10536
  {
8977
10537
  key: ConfigKey.group_resourceBitTags,
@@ -9114,10 +10674,10 @@ var BITS = {
9114
10674
  description: "Resource bit tags for logo grave images, used to define additional properties"
9115
10675
  },
9116
10676
  {
9117
- // Image resource
9118
- key: ConfigKey.group_resourceImage,
10677
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10678
+ key: ConfigKey.group_resourceImageNoZoom,
9119
10679
  description: "Resource image tags for logo grave images, used to attach images",
9120
- exportJsonKey: { logos: "$" },
10680
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9121
10681
  minCount: 1,
9122
10682
  maxCount: Count.infinity
9123
10683
  }
@@ -9127,7 +10687,17 @@ var BITS = {
9127
10687
  [BitType.prototypeImages]: {
9128
10688
  since: "1.6.1",
9129
10689
  baseBitType: BitType.imagesLogoGrave,
9130
- description: "Prototype images bit, used to create prototype images in articles or books"
10690
+ description: "Prototype images bit, used to create prototype images in articles or books",
10691
+ tags: [
10692
+ {
10693
+ // Override inherited `logos` wrapper with `images` for this bit type.
10694
+ key: ConfigKey.group_resourceImageNoZoom,
10695
+ description: "Resource image tags for prototype images",
10696
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10697
+ minCount: 1,
10698
+ maxCount: Count.infinity
10699
+ }
10700
+ ]
9131
10701
  },
9132
10702
  [BitType.internalLink]: {
9133
10703
  since: "1.3.0",
@@ -9157,6 +10727,18 @@ var BITS = {
9157
10727
  exportJsonKey: {},
9158
10728
  description: "Reasonable number of characters for the interview, used to limit input size",
9159
10729
  format: TagFormat.number
10730
+ },
10731
+ {
10732
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10733
+ key: ConfigKey.property_example,
10734
+ exportJsonKey: [
10735
+ { "@keyonly": { isExample: true } },
10736
+ { "@absent": { isExample: true } },
10737
+ { isExample: true, example: "$" }
10738
+ ],
10739
+ description: "The example(s) for the bit",
10740
+ format: TagFormat.bitmarkText,
10741
+ nullable: true
9160
10742
  }
9161
10743
  ],
9162
10744
  cardSet: CardSetConfigKey.questions
@@ -9515,6 +11097,18 @@ var BITS = {
9515
11097
  {
9516
11098
  key: ConfigKey.group_mark,
9517
11099
  description: "Mark tags, used to define the content of marks"
11100
+ },
11101
+ {
11102
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11103
+ key: ConfigKey.property_example,
11104
+ exportJsonKey: [
11105
+ { "@keyonly": { isExample: true } },
11106
+ { "@absent": { isExample: true } },
11107
+ { isExample: true, example: "$" }
11108
+ ],
11109
+ description: "The example(s) for the bit",
11110
+ format: TagFormat.bitmarkText,
11111
+ nullable: true
9518
11112
  }
9519
11113
  ]
9520
11114
  },
@@ -9534,6 +11128,23 @@ var BITS = {
9534
11128
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9535
11129
  format: TagFormat.boolean,
9536
11130
  defaultValue: "true"
11131
+ },
11132
+ {
11133
+ // Override inherited @example: in `isTopLevelExample` allow-list
11134
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11135
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11136
+ // Bit-level fires emit only `isExample`; the @example value (when
11137
+ // provided) is recorded in the ancestor stack and consumed by
11138
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11139
+ key: ConfigKey.property_example,
11140
+ exportJsonKey: [
11141
+ { "@keyonly": { isExample: true } },
11142
+ { "@absent": { isExample: true } },
11143
+ { isExample: true }
11144
+ ],
11145
+ description: "The example(s) for the bit",
11146
+ format: TagFormat.bitmarkText,
11147
+ nullable: true
9537
11148
  }
9538
11149
  ],
9539
11150
  cardSet: CardSetConfigKey.matchPairs
@@ -9618,6 +11229,18 @@ var BITS = {
9618
11229
  {
9619
11230
  key: ConfigKey.group_trueFalse,
9620
11231
  description: "True/False tags for multiple choice 1 questions"
11232
+ },
11233
+ {
11234
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11235
+ key: ConfigKey.property_example,
11236
+ exportJsonKey: [
11237
+ { "@keyonly": { isExample: true } },
11238
+ { "@absent": { isExample: true } },
11239
+ { isExample: true, example: "$" }
11240
+ ],
11241
+ description: "The example(s) for the bit",
11242
+ format: TagFormat.bitmarkText,
11243
+ nullable: true
9621
11244
  }
9622
11245
  ]
9623
11246
  },
@@ -9639,8 +11262,20 @@ var BITS = {
9639
11262
  {
9640
11263
  key: ConfigKey.group_trueFalse,
9641
11264
  description: "True/False tags for multiple choice questions"
9642
- }
11265
+ },
9643
11266
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11267
+ {
11268
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11269
+ key: ConfigKey.property_example,
11270
+ exportJsonKey: [
11271
+ { "@keyonly": { isExample: true } },
11272
+ { "@absent": { isExample: true } },
11273
+ { isExample: true, example: "$" }
11274
+ ],
11275
+ description: "The example(s) for the bit",
11276
+ format: TagFormat.bitmarkText,
11277
+ nullable: true
11278
+ }
9644
11279
  ],
9645
11280
  cardSet: CardSetConfigKey.quiz
9646
11281
  },
@@ -9660,8 +11295,20 @@ var BITS = {
9660
11295
  description: "Common quiz tags for multiple choice text questions"
9661
11296
  },
9662
11297
  {
9663
- key: ConfigKey.group_trueFalse,
11298
+ key: ConfigKey.group_trueFalseInlineSelect,
9664
11299
  description: "True/False tags for multiple choice text questions"
11300
+ },
11301
+ {
11302
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11303
+ key: ConfigKey.property_example,
11304
+ exportJsonKey: [
11305
+ { "@keyonly": { isExample: true } },
11306
+ { "@absent": { isExample: true } },
11307
+ { isExample: true, example: "$" }
11308
+ ],
11309
+ description: "The example(s) for the bit",
11310
+ format: TagFormat.bitmarkText,
11311
+ nullable: true
9665
11312
  }
9666
11313
  ]
9667
11314
  },
@@ -9686,8 +11333,20 @@ var BITS = {
9686
11333
  description: "Common quiz tags for multiple response 1 questions"
9687
11334
  },
9688
11335
  {
9689
- key: ConfigKey.group_trueFalse,
11336
+ key: ConfigKey.group_trueFalseResponses,
9690
11337
  description: "True/False tags for multiple response 1 questions"
11338
+ },
11339
+ {
11340
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11341
+ key: ConfigKey.property_example,
11342
+ exportJsonKey: [
11343
+ { "@keyonly": { isExample: true } },
11344
+ { "@absent": { isExample: true } },
11345
+ { isExample: true, example: "$" }
11346
+ ],
11347
+ description: "The example(s) for the bit",
11348
+ format: TagFormat.bitmarkText,
11349
+ nullable: true
9691
11350
  }
9692
11351
  ]
9693
11352
  },
@@ -9707,12 +11366,24 @@ var BITS = {
9707
11366
  description: "Common quiz tags for multiple response questions"
9708
11367
  },
9709
11368
  {
9710
- key: ConfigKey.group_trueFalse,
11369
+ key: ConfigKey.group_trueFalseResponses,
9711
11370
  description: "True/False tags for multiple response questions"
9712
- }
11371
+ },
9713
11372
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11373
+ {
11374
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11375
+ key: ConfigKey.property_example,
11376
+ exportJsonKey: [
11377
+ { "@keyonly": { isExample: true } },
11378
+ { "@absent": { isExample: true } },
11379
+ { isExample: true, example: "$" }
11380
+ ],
11381
+ description: "The example(s) for the bit",
11382
+ format: TagFormat.bitmarkText,
11383
+ nullable: true
11384
+ }
9714
11385
  ],
9715
- cardSet: CardSetConfigKey.quiz
11386
+ cardSet: CardSetConfigKey.quizResponses
9716
11387
  },
9717
11388
  [BitType.coachSelfReflectionMultipleResponse]: {
9718
11389
  since: "1.3.0",
@@ -9962,6 +11633,8 @@ var BITS = {
9962
11633
  },
9963
11634
  {
9964
11635
  key: ConfigKey.resource_platformIcon,
11636
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11637
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
9965
11638
  description: "The platform icon",
9966
11639
  chain: [
9967
11640
  {
@@ -9997,6 +11670,8 @@ var BITS = {
9997
11670
  tags: [
9998
11671
  {
9999
11672
  key: ConfigKey.resource_platformLogo,
11673
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11674
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10000
11675
  description: "The platform logo",
10001
11676
  chain: [
10002
11677
  {
@@ -10208,7 +11883,8 @@ var BITS = {
10208
11883
  { isExample: true, example: "$" }
10209
11884
  ],
10210
11885
  description: "The example text for the review note bit",
10211
- format: TagFormat.plainText
11886
+ format: TagFormat.plainText,
11887
+ nullable: true
10212
11888
  }
10213
11889
  ],
10214
11890
  rootExampleType: ExampleType.string
@@ -10295,7 +11971,8 @@ var BITS = {
10295
11971
  { isExample: true, example: "$" }
10296
11972
  ],
10297
11973
  description: "The example flag for the bit (boolean)",
10298
- format: TagFormat.plainText
11974
+ format: TagFormat.boolean,
11975
+ nullable: true
10299
11976
  }
10300
11977
  ],
10301
11978
  cardSet: CardSetConfigKey.elements,
@@ -10569,15 +12246,8 @@ var BITS = {
10569
12246
  format: TagFormat.bitmarkText
10570
12247
  },
10571
12248
  {
10572
- key: ConfigKey.resource_backgroundWallpaper,
10573
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10574
- description: "Background wallpaper for the image, used to set a background for the image",
10575
- chain: [
10576
- {
10577
- key: ConfigKey.group_resourceImageCommon,
10578
- description: "Common resource image tags for images"
10579
- }
10580
- ]
12249
+ key: ConfigKey.group_backgroundWallpaper,
12250
+ description: "Background wallpaper tags for images, used to define background properties for images"
10581
12251
  },
10582
12252
  {
10583
12253
  key: ConfigKey.group_resourceBitTags,
@@ -10954,16 +12624,20 @@ var BITS = {
10954
12624
  format: TagFormat.plainText
10955
12625
  },
10956
12626
  {
10957
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12627
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12628
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12629
+ // not a `choices[]` array. Format is bitmarkText so the statement
12630
+ // body renders as a ProseMirror tree, matching reference fixtures.
12631
+ exportJsonKey: { statement: "$", isCorrect: true },
10958
12632
  key: ConfigKey.tag_true,
10959
12633
  description: "Tag for the true option in the true/false question",
10960
- format: TagFormat.plainText
12634
+ format: TagFormat.bitmarkText
10961
12635
  },
10962
12636
  {
10963
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12637
+ exportJsonKey: { statement: "$", isCorrect: false },
10964
12638
  key: ConfigKey.tag_false,
10965
12639
  description: "Tag for the false option in the true/false question",
10966
- format: TagFormat.plainText
12640
+ format: TagFormat.bitmarkText
10967
12641
  },
10968
12642
  {
10969
12643
  // Override inherited @example from group_standardTags: boolean root
@@ -10974,14 +12648,24 @@ var BITS = {
10974
12648
  // sufficient for `[+...]` (correct) statements which dominate the
10975
12649
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
10976
12650
  // need either a runtime extension or an explicit `[@example:false]`.
12651
+ //
12652
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12653
+ // (a natural default that gets stripped in optimised mode, matching
12654
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12655
+ // STRING `"false"` which doesn't strip.
10977
12656
  key: ConfigKey.property_example,
10978
12657
  exportJsonKey: [
10979
12658
  { "@keyonly": { isExample: true, example: true } },
10980
12659
  { "@absent": { isExample: true, example: true } },
12660
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12661
+ // is the natural default for boolean, stripped in optimised mode.
12662
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12663
+ { "@value=false": { isExample: true } },
10981
12664
  { isExample: true, example: "$" }
10982
12665
  ],
10983
12666
  description: "The example flag for the bit (boolean)",
10984
- format: TagFormat.plainText
12667
+ format: TagFormat.boolean,
12668
+ nullable: true
10985
12669
  }
10986
12670
  ],
10987
12671
  rootExampleType: ExampleType.boolean
@@ -11005,6 +12689,18 @@ var BITS = {
11005
12689
  key: ConfigKey.property_labelFalse,
11006
12690
  description: "Label for the false option in the true/false question",
11007
12691
  format: TagFormat.plainText
12692
+ },
12693
+ {
12694
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12695
+ key: ConfigKey.property_example,
12696
+ exportJsonKey: [
12697
+ { "@keyonly": { isExample: true } },
12698
+ { "@absent": { isExample: true } },
12699
+ { isExample: true, example: "$" }
12700
+ ],
12701
+ description: "The example(s) for the bit",
12702
+ format: TagFormat.bitmarkText,
12703
+ nullable: true
11008
12704
  }
11009
12705
  ],
11010
12706
  cardSet: CardSetConfigKey.statements
@@ -11194,6 +12890,11 @@ var BITS = {
11194
12890
  baseBitType: BitType.vendorStripePricingTable,
11195
12891
  description: "Stripe pricing table bit with print-on-request customer/product IDs, used to embed Stripe pricing tables tied to a specific print-on-request customer and product",
11196
12892
  tags: [
12893
+ {
12894
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
12895
+ description: "Descriptive name for the Stripe pricing table",
12896
+ format: TagFormat.plainText
12897
+ },
11197
12898
  {
11198
12899
  key: ConfigKey.property_printOnRequestCustomerId,
11199
12900
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11670,7 +13371,7 @@ var instance2 = new Config();
11670
13371
  // src/generated/package_info.ts
11671
13372
  var PACKAGE_INFO = {
11672
13373
  "name": "@gmb/bitmark-parser-generator",
11673
- "version": "5.23.0",
13374
+ "version": "5.24.0",
11674
13375
  "license": "ISC"};
11675
13376
  var Environment = {
11676
13377
  unknown: "",
@@ -12603,6 +14304,8 @@ var NodeType = {
12603
14304
  stillImageFilmLink: "stillImageFilmLink",
12604
14305
  stripePricingTableId: "stripePricingTableId",
12605
14306
  stripePricingTableIdValue: "stripePricingTableIdValue",
14307
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14308
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12606
14309
  stripePublishableKey: "stripePublishableKey",
12607
14310
  stripePublishableKeyValue: "stripePublishableKeyValue",
12608
14311
  subject: "subject",
@@ -25844,6 +27547,12 @@ var Builder = class extends BaseBuilder {
25844
27547
  data.stripePricingTableId,
25845
27548
  options
25846
27549
  ),
27550
+ stripePricingTableDescriptiveName: this.toAstProperty(
27551
+ bitType,
27552
+ ConfigKey.property_stripePricingTableDescriptiveName,
27553
+ data.stripePricingTableDescriptiveName,
27554
+ options
27555
+ ),
25847
27556
  stripePublishableKey: this.toAstProperty(
25848
27557
  bitType,
25849
27558
  ConfigKey.property_stripePublishableKey,
@@ -33996,6 +35705,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
33996
35705
  const cardSetType = bitConfig.cardSet?.configKey;
33997
35706
  switch (cardSetType) {
33998
35707
  case CardSetConfigKey.flashcard:
35708
+ case CardSetConfigKey.flashcard1:
33999
35709
  case CardSetConfigKey.definitionList:
34000
35710
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34001
35711
  break;
@@ -34009,6 +35719,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34009
35719
  result = parseFeedback(context, bitType, processedCardSet);
34010
35720
  break;
34011
35721
  case CardSetConfigKey.quiz:
35722
+ case CardSetConfigKey.quizResponses:
34012
35723
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34013
35724
  break;
34014
35725
  case CardSetConfigKey.questions:
@@ -34035,6 +35746,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34035
35746
  break;
34036
35747
  case CardSetConfigKey.clozeList:
34037
35748
  case CardSetConfigKey.exampleBitList:
35749
+ case CardSetConfigKey.pageFooterSections:
34038
35750
  case CardSetConfigKey.bookReferenceList:
34039
35751
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34040
35752
  break;
@@ -40729,6 +42441,13 @@ var ConfigBuilder = class {
40729
42441
  const cardSetConfig = CARDSETS[cardSetKey];
40730
42442
  if (!cardSetConfig) return void 0;
40731
42443
  const normalizedKey = normalizeCardKey(cardSetKey);
42444
+ const variantTagEntriesTypeOrder = [
42445
+ BitTagConfigKeyType.group,
42446
+ BitTagConfigKeyType.tag,
42447
+ BitTagConfigKeyType.property,
42448
+ BitTagConfigKeyType.resource,
42449
+ BitTagConfigKeyType.unknown
42450
+ ];
40732
42451
  const sides = cardSetConfig.sides.map((side, sideIdx) => {
40733
42452
  const sidePath = `cardSets.${normalizedKey} / sides.${side.name ?? `[${sideIdx}]`}`;
40734
42453
  const variants = side.variants.map((variant, variantIdx) => {
@@ -40739,7 +42458,7 @@ var ConfigBuilder = class {
40739
42458
  const tagB = b[1];
40740
42459
  const typeA = typeFromConfigKey(tagA.key);
40741
42460
  const typeB = typeFromConfigKey(tagB.key);
40742
- const typeOrder = tagEntriesTypeOrder.indexOf(typeA) - tagEntriesTypeOrder.indexOf(typeB);
42461
+ const typeOrder = variantTagEntriesTypeOrder.indexOf(typeA) - variantTagEntriesTypeOrder.indexOf(typeB);
40743
42462
  return typeOrder;
40744
42463
  });
40745
42464
  for (const [, variantTag] of variantTagEntries) {
@@ -40779,6 +42498,10 @@ var ConfigBuilder = class {
40779
42498
  name: sectionName,
40780
42499
  ...section.isDefault ? { isDefault: true } : {},
40781
42500
  ...cardJsonKeyField(section, "jsonKey", "exportJsonKey", sectionPath),
42501
+ // PLAN-085: per-section cardinality. Emit only when non-default
42502
+ // (treat `0` / undefined as "unbounded — omit").
42503
+ ...section.minCount != null && section.minCount !== 0 ? { min: section.minCount } : {},
42504
+ ...section.maxCount != null && section.maxCount !== 0 ? { max: section.maxCount } : {},
40782
42505
  sides: cardSides
40783
42506
  };
40784
42507
  });
@@ -40848,10 +42571,20 @@ var ConfigBuilder = class {
40848
42571
  return typeOrder;
40849
42572
  });
40850
42573
  const isInheritedFrom = bitGroupConfigKeys.has(b.bitType);
42574
+ const indexByKey = /* @__PURE__ */ new Map();
42575
+ const pushOrReplace = (entry) => {
42576
+ const existing = indexByKey.get(entry.key);
42577
+ if (existing !== void 0) {
42578
+ tags2[existing] = entry;
42579
+ } else {
42580
+ indexByKey.set(entry.key, tags2.length);
42581
+ tags2.push(entry);
42582
+ }
42583
+ };
40851
42584
  if (isInheritedFrom) {
40852
42585
  const resolvedGroups = resolveGroupReferences(`group-${b.bitType}`);
40853
42586
  for (const ref of resolvedGroups) {
40854
- tags2.push({
42587
+ pushOrReplace({
40855
42588
  type: "group",
40856
42589
  key: ref.key,
40857
42590
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -40864,7 +42597,7 @@ var ConfigBuilder = class {
40864
42597
  if (b.baseBitType) {
40865
42598
  const resolvedGroups = resolveGroupReferences(`group-${b.baseBitType}`);
40866
42599
  for (const ref of resolvedGroups) {
40867
- tags2.push({
42600
+ pushOrReplace({
40868
42601
  type: "group",
40869
42602
  key: ref.key,
40870
42603
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -40875,7 +42608,9 @@ var ConfigBuilder = class {
40875
42608
  }
40876
42609
  }
40877
42610
  for (const [_tagKey, tag] of tagEntries) {
40878
- tags2.push(...processTagEntries(tag, []));
42611
+ for (const entry of processTagEntries(tag, [])) {
42612
+ pushOrReplace(entry);
42613
+ }
40879
42614
  }
40880
42615
  }
40881
42616
  let cardRef;