@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/index.cjs CHANGED
@@ -792,10 +792,12 @@ var TextFormat = {
792
792
  var import_superenum7 = require("@ncoderz/superenum");
793
793
  var CardSetConfigKey = {
794
794
  flashcard: "flashcard",
795
+ flashcard1: "flashcard1",
795
796
  definitionList: "definitionList",
796
797
  elements: "elements",
797
798
  statements: "statements",
798
799
  quiz: "quiz",
800
+ quizResponses: "quizResponses",
799
801
  feedback: "feedback",
800
802
  questions: "questions",
801
803
  matchPairs: "matchPairs",
@@ -807,6 +809,7 @@ var CardSetConfigKey = {
807
809
  pronunciationTable: "pronunciationTable",
808
810
  botActionResponses: "botActionResponses",
809
811
  exampleBitList: "exampleBitList",
812
+ pageFooterSections: "pageFooterSections",
810
813
  clozeList: "clozeList",
811
814
  ingredients: "ingredients",
812
815
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1223,12 +1226,17 @@ var groupKeys = {
1223
1226
  group_standardAllBits: "group_standardAllBits",
1224
1227
  group_standardItemLead: "group_standardItemLead",
1225
1228
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1229
+ group_standardInstructionHint: "group_standardInstructionHint",
1226
1230
  group_standardTags: "group_standardTags",
1231
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1227
1232
  group_imageSource: "group_imageSource",
1228
1233
  group_technicalTerm: "group_technicalTerm",
1229
1234
  group_person: "group_person",
1230
1235
  group_gap: "group_gap",
1231
1236
  group_trueFalse: "group_trueFalse",
1237
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1238
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1239
+ group_trueFalseResponses: "group_trueFalseResponses",
1232
1240
  group_markConfig: "group_markConfig",
1233
1241
  group_mark: "group_mark",
1234
1242
  group_bookCommon: "group_bookCommon",
@@ -1236,14 +1244,18 @@ var groupKeys = {
1236
1244
  group_advertisingCommon: "group_advertisingCommon",
1237
1245
  group_platformStylesCommon: "group_platformStylesCommon",
1238
1246
  group_quizCommon: "group_quizCommon",
1247
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1248
+ group_imageNoZoom: "group_imageNoZoom",
1239
1249
  group_resourceBitTags: "group_resourceBitTags",
1240
1250
  group_resourceCommon: "group_resourceCommon",
1241
1251
  group_resourceImageCommon: "group_resourceImageCommon",
1252
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1242
1253
  group_resourceAudioCommon: "group_resourceAudioCommon",
1243
1254
  group_resourceVideoCommon: "group_resourceVideoCommon",
1244
1255
  group_resourceIcon: "group_resourceIcon",
1245
1256
  // Alias for image
1246
1257
  group_resourceImage: "group_resourceImage",
1258
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1247
1259
  group_resourceImageEmbed: "group_resourceImageEmbed",
1248
1260
  group_resourceImageLink: "group_resourceImageLink",
1249
1261
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1520,6 +1532,7 @@ var propertyKeys = {
1520
1532
  property_src4x: "@src4x",
1521
1533
  property_srcAlt: "@srcAlt",
1522
1534
  property_stripePricingTableId: "@stripePricingTableId",
1535
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1523
1536
  property_stripePublishableKey: "@stripePublishableKey",
1524
1537
  property_subject: "@subject",
1525
1538
  property_subtype: "@subtype",
@@ -2271,7 +2284,7 @@ var CARDSETS = {
2271
2284
  variants: [
2272
2285
  {
2273
2286
  jsonKey: "question.text",
2274
- exportJsonKey: { question: { text: "$" } },
2287
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2275
2288
  tags: [
2276
2289
  {
2277
2290
  key: ConfigKey.group_standardTags,
@@ -2287,6 +2300,136 @@ var CARDSETS = {
2287
2300
  description: "Icon resource for the flashcard.",
2288
2301
  jsonKey: "question.icon|resource(type=image, key=image)",
2289
2302
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2303
+ },
2304
+ {
2305
+ key: ConfigKey.property_example,
2306
+ exportJsonKey: [
2307
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2308
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2309
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2310
+ ],
2311
+ description: "Example marker for the flashcard question.",
2312
+ format: TagFormat.bitmarkText,
2313
+ nullable: true
2314
+ }
2315
+ ]
2316
+ }
2317
+ ]
2318
+ },
2319
+ {
2320
+ name: "answer",
2321
+ variants: [
2322
+ {
2323
+ jsonKey: "answer.text",
2324
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2325
+ tags: [
2326
+ {
2327
+ key: ConfigKey.group_standardTags,
2328
+ description: "Standard tags for the flashcard."
2329
+ },
2330
+ {
2331
+ exportJsonKey: { title: "$" },
2332
+ key: ConfigKey.tag_title,
2333
+ description: "Title of the flashcard."
2334
+ },
2335
+ {
2336
+ key: ConfigKey.group_resourceIcon,
2337
+ description: "Icon resource for the flashcard.",
2338
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2339
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2340
+ },
2341
+ {
2342
+ key: ConfigKey.property_example,
2343
+ exportJsonKey: [
2344
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2345
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2346
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2347
+ ],
2348
+ description: "Example marker for the flashcard answer.",
2349
+ format: TagFormat.bitmarkText,
2350
+ nullable: true
2351
+ }
2352
+ ]
2353
+ },
2354
+ {
2355
+ jsonKey: "alternativeAnswers[].text",
2356
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2357
+ tags: [
2358
+ {
2359
+ key: ConfigKey.group_standardTags,
2360
+ description: "Standard tags for the flashcard."
2361
+ },
2362
+ {
2363
+ exportJsonKey: { title: "$" },
2364
+ key: ConfigKey.tag_title,
2365
+ description: "Title of the flashcard."
2366
+ },
2367
+ {
2368
+ key: ConfigKey.group_resourceIcon,
2369
+ description: "Icon resource for the flashcard.",
2370
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2371
+ exportJsonKey: {
2372
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2373
+ }
2374
+ }
2375
+ ],
2376
+ repeatCount: Count.infinity
2377
+ }
2378
+ ]
2379
+ }
2380
+ ]
2381
+ },
2382
+ //
2383
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2384
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2385
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2386
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2387
+ // cardset.
2388
+ //
2389
+ [CardSetConfigKey.flashcard1]: {
2390
+ jsonKey: "cards",
2391
+ exportJsonKey: { cards: "$" },
2392
+ sections: {
2393
+ default: {
2394
+ jsonKey: "cards",
2395
+ exportJsonKey: { cards: "$" },
2396
+ isDefault: true,
2397
+ maxCount: 1
2398
+ }
2399
+ },
2400
+ sides: [
2401
+ {
2402
+ name: "question",
2403
+ variants: [
2404
+ {
2405
+ jsonKey: "question.text",
2406
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2407
+ tags: [
2408
+ {
2409
+ key: ConfigKey.group_standardTags,
2410
+ description: "Standard tags for the flashcard."
2411
+ },
2412
+ {
2413
+ exportJsonKey: { title: "$" },
2414
+ key: ConfigKey.tag_title,
2415
+ description: "Title of the flashcard."
2416
+ },
2417
+ {
2418
+ key: ConfigKey.group_resourceIcon,
2419
+ description: "Icon resource for the flashcard.",
2420
+ jsonKey: "question.icon|resource(type=image, key=image)",
2421
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2422
+ },
2423
+ {
2424
+ key: ConfigKey.property_example,
2425
+ exportJsonKey: [
2426
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2427
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2428
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2429
+ ],
2430
+ description: "Example marker for the flashcard question.",
2431
+ format: TagFormat.bitmarkText,
2432
+ nullable: true
2290
2433
  }
2291
2434
  ]
2292
2435
  }
@@ -2297,7 +2440,7 @@ var CARDSETS = {
2297
2440
  variants: [
2298
2441
  {
2299
2442
  jsonKey: "answer.text",
2300
- exportJsonKey: { answer: { text: "$" } },
2443
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2301
2444
  tags: [
2302
2445
  {
2303
2446
  key: ConfigKey.group_standardTags,
@@ -2313,6 +2456,17 @@ var CARDSETS = {
2313
2456
  description: "Icon resource for the flashcard.",
2314
2457
  jsonKey: "answer.icon|resource(type=image, key=image)",
2315
2458
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2459
+ },
2460
+ {
2461
+ key: ConfigKey.property_example,
2462
+ exportJsonKey: [
2463
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2464
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2465
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2466
+ ],
2467
+ description: "Example marker for the flashcard answer.",
2468
+ format: TagFormat.bitmarkText,
2469
+ nullable: true
2316
2470
  }
2317
2471
  ]
2318
2472
  },
@@ -2356,7 +2510,7 @@ var CARDSETS = {
2356
2510
  variants: [
2357
2511
  {
2358
2512
  jsonKey: "term.text",
2359
- exportJsonKey: { term: { text: "$" } },
2513
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2360
2514
  tags: [
2361
2515
  {
2362
2516
  key: ConfigKey.group_standardTags,
@@ -2375,6 +2529,17 @@ var CARDSETS = {
2375
2529
  format: TagFormat.plainText,
2376
2530
  jsonKey: "term.icon|resource(type=image, key=image)",
2377
2531
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2532
+ },
2533
+ {
2534
+ key: ConfigKey.property_example,
2535
+ exportJsonKey: [
2536
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2537
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2538
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2539
+ ],
2540
+ description: "Example marker for the definition term.",
2541
+ format: TagFormat.bitmarkText,
2542
+ nullable: true
2378
2543
  }
2379
2544
  ]
2380
2545
  }
@@ -2385,7 +2550,7 @@ var CARDSETS = {
2385
2550
  variants: [
2386
2551
  {
2387
2552
  jsonKey: "definition.text",
2388
- exportJsonKey: { definition: { text: "$" } },
2553
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2389
2554
  tags: [
2390
2555
  {
2391
2556
  key: ConfigKey.group_standardTags,
@@ -2403,6 +2568,17 @@ var CARDSETS = {
2403
2568
  description: "Icon resource for the definition.",
2404
2569
  jsonKey: "definition.icon|resource(type=image, key=image)",
2405
2570
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2571
+ },
2572
+ {
2573
+ key: ConfigKey.property_example,
2574
+ exportJsonKey: [
2575
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2576
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2577
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2578
+ ],
2579
+ description: "Example marker for the definition.",
2580
+ format: TagFormat.bitmarkText,
2581
+ nullable: true
2406
2582
  }
2407
2583
  ]
2408
2584
  },
@@ -2447,11 +2623,51 @@ var CARDSETS = {
2447
2623
  {
2448
2624
  jsonKey: "key",
2449
2625
  exportJsonKey: { key: "$" },
2626
+ format: TextFormat.plainText,
2450
2627
  tags: [
2451
2628
  {
2452
2629
  key: ConfigKey.group_standardTags,
2453
2630
  description: "Standard tags for the match pair."
2454
2631
  },
2632
+ {
2633
+ // Pair-level example: emit `isExample` alongside `example`
2634
+ // (matches OLD parser's per-pair allow-list behaviour).
2635
+ //
2636
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2637
+ // in the cardset) fills each pair's `example` with `V`
2638
+ // once per pair from the keys-side cascade fire (single
2639
+ // parsed_variant per pair — no per-value duplication).
2640
+ //
2641
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2642
+ // at the pair-object level. BPG `parseMatchPairs`
2643
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2644
+ // per PAIR (the `exampleCard` accumulator), placed at the
2645
+ // pair object. Unlike matchMatrix (per-cell example),
2646
+ // matchPairs has no nested per-cell layer, so the pair-key
2647
+ // `[@example:V]` must double-emit as the visible field.
2648
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2649
+ // expects `example: "packen"` at pair-object level from
2650
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2651
+ // draft applied to both keys-side tags; the matchPairs
2652
+ // half was reverted after fixture parity testing.
2653
+ key: ConfigKey.property_example,
2654
+ exportJsonKey: [
2655
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2656
+ {
2657
+ predicates: ["@absent", { $cascade: "*" }],
2658
+ rule: {
2659
+ isExample: true,
2660
+ example: "$cascade",
2661
+ "@bit": { isExample: true }
2662
+ }
2663
+ },
2664
+ { "@absent": { isExample: true } },
2665
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2666
+ ],
2667
+ description: "Example marker / value on the match pair.",
2668
+ format: TagFormat.bitmarkText,
2669
+ nullable: true
2670
+ },
2455
2671
  {
2456
2672
  key: ConfigKey.tag_title,
2457
2673
  description: "Title of the match pair.",
@@ -2460,9 +2676,19 @@ var CARDSETS = {
2460
2676
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2461
2677
  },
2462
2678
  {
2679
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2680
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2681
+ // only fires when NO variant in the active card explicitly
2682
+ // carries `[@isCaseSensitive]`. When a value-side variant
2683
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2684
+ // synth is skipped here; the value-side explicit fire owns
2685
+ // the pair-level emission.
2463
2686
  key: ConfigKey.property_isCaseSensitive,
2464
2687
  exportJsonKey: [
2465
- { "@absent": { isCaseSensitive: "$ancestor" } },
2688
+ {
2689
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2690
+ rule: { isCaseSensitive: "$ancestor" }
2691
+ },
2466
2692
  { isCaseSensitive: "$" }
2467
2693
  ],
2468
2694
  description: "Property to indicate if the match is case sensitive.",
@@ -2491,10 +2717,85 @@ var CARDSETS = {
2491
2717
  {
2492
2718
  jsonKey: "values[]",
2493
2719
  exportJsonKey: { values: ["$"] },
2720
+ format: TextFormat.plainText,
2494
2721
  tags: [
2495
2722
  {
2496
- key: ConfigKey.group_standardTags,
2497
- description: "Standard tags for the match pair."
2723
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2724
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2725
+ // for sideIdx > 0; the schema-side equivalent is to use a
2726
+ // group_standardTags variant that omits group_standardItemLead.
2727
+ key: ConfigKey.group_standardTagsNoItemLead,
2728
+ description: "Standard tags for the match pair (values side, no item/lead)."
2729
+ },
2730
+ {
2731
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2732
+ // `@example` from the bit header fills each pair's
2733
+ // `example` with the pair's first value (mirrors BPG's
2734
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2735
+ //
2736
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2737
+ // TAG 415 (keys side) which fires once per pair — keeping
2738
+ // it here would emit `example` twice per pair (one per
2739
+ // values-side parsed_variant) and merge them into a
2740
+ // multi-paragraph array.
2741
+ //
2742
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2743
+ // source `$` value (the current `++`-iteration's value
2744
+ // text). For single-value pairs this matches the old
2745
+ // `$parent.values[0]`; for multi-value pairs with
2746
+ // `[@example]` attached to a non-first value, it correctly
2747
+ // resolves to that value.
2748
+ //
2749
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2750
+ // routes pair-key `[@example:V]` (a per-pair cascade
2751
+ // source, bound via per-card CascadeCounters scope) and
2752
+ // bit-header `[@example:V]` (cardset cascade) into the
2753
+ // pair-level `example` via `$cascade`. The aggregator in
2754
+ // `populate_variants` fires this rule ONCE per pair (BPG
2755
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2756
+ // per-iter pollution.
2757
+ key: ConfigKey.property_example,
2758
+ exportJsonKey: [
2759
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2760
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2761
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2762
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2763
+ // scope opened by `build_card_item`.
2764
+ {
2765
+ predicates: ["@keyonly"],
2766
+ maxEmits: 1,
2767
+ rule: {
2768
+ isExample: true,
2769
+ example: "$parent",
2770
+ "@bit": { isExample: true }
2771
+ }
2772
+ },
2773
+ {
2774
+ predicates: ["@absent", { $cascade: "*" }],
2775
+ rule: {
2776
+ isExample: true,
2777
+ example: "$cascade",
2778
+ "@bit": { isExample: true }
2779
+ }
2780
+ },
2781
+ {
2782
+ "@absent": {
2783
+ isExample: true,
2784
+ example: "$parent",
2785
+ "@bit": { isExample: true }
2786
+ }
2787
+ },
2788
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2789
+ // form so per-`++`-iter values-side fires also first-win.
2790
+ {
2791
+ predicates: [],
2792
+ maxEmits: 1,
2793
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2794
+ }
2795
+ ],
2796
+ description: "Example marker / value on the match pair (values side).",
2797
+ format: TagFormat.bitmarkText,
2798
+ nullable: true
2498
2799
  },
2499
2800
  {
2500
2801
  key: ConfigKey.tag_title,
@@ -2504,9 +2805,13 @@ var CARDSETS = {
2504
2805
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2505
2806
  },
2506
2807
  {
2808
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2507
2809
  key: ConfigKey.property_isCaseSensitive,
2508
2810
  exportJsonKey: [
2509
- { "@absent": { isCaseSensitive: "$ancestor" } },
2811
+ {
2812
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2813
+ rule: { isCaseSensitive: "$ancestor" }
2814
+ },
2510
2815
  { isCaseSensitive: "$" }
2511
2816
  ],
2512
2817
  description: "Property to indicate if the match is case sensitive.",
@@ -2532,6 +2837,7 @@ var CARDSETS = {
2532
2837
  {
2533
2838
  jsonKey: "key",
2534
2839
  exportJsonKey: { key: "$" },
2840
+ format: TextFormat.plainText,
2535
2841
  tags: [
2536
2842
  {
2537
2843
  key: ConfigKey.group_standardTags,
@@ -2561,10 +2867,12 @@ var CARDSETS = {
2561
2867
  {
2562
2868
  jsonKey: "values[]",
2563
2869
  exportJsonKey: { values: ["$"] },
2870
+ format: TextFormat.plainText,
2564
2871
  tags: [
2565
2872
  {
2566
- key: ConfigKey.group_standardTags,
2567
- description: "Standard tags for the audio match pair."
2873
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2874
+ key: ConfigKey.group_standardTagsNoItemLead,
2875
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2568
2876
  },
2569
2877
  {
2570
2878
  key: ConfigKey.tag_title,
@@ -2597,6 +2905,7 @@ var CARDSETS = {
2597
2905
  {
2598
2906
  jsonKey: "key",
2599
2907
  exportJsonKey: { key: "$" },
2908
+ format: TextFormat.plainText,
2600
2909
  tags: [
2601
2910
  {
2602
2911
  key: ConfigKey.group_standardTags,
@@ -2626,10 +2935,12 @@ var CARDSETS = {
2626
2935
  {
2627
2936
  jsonKey: "values[]",
2628
2937
  exportJsonKey: { values: ["$"] },
2938
+ format: TextFormat.plainText,
2629
2939
  tags: [
2630
2940
  {
2631
- key: ConfigKey.group_standardTags,
2632
- description: "Standard tags for the image match pair."
2941
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2942
+ key: ConfigKey.group_standardTagsNoItemLead,
2943
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2633
2944
  },
2634
2945
  {
2635
2946
  key: ConfigKey.tag_title,
@@ -2662,20 +2973,49 @@ var CARDSETS = {
2662
2973
  {
2663
2974
  jsonKey: "key",
2664
2975
  exportJsonKey: { key: "$" },
2976
+ format: TextFormat.plainText,
2665
2977
  tags: [
2666
2978
  {
2667
2979
  key: ConfigKey.group_standardItemLeadInstructionHint,
2668
2980
  description: "Standard tags for lead, instruction, and hint."
2669
2981
  },
2670
2982
  {
2983
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
2984
+ // `[@example:V]` is consumed as the per-pair cascade source.
2985
+ // PLAN-077 R14 follow-up: investigation showed the fixture
2986
+ // reference is INCONSISTENT — bits without a cardset-level
2987
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
2988
+ // pair-level `example: V` at pair scope, but bits WITH a
2989
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
2990
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
2991
+ // example. Emitting `example: $` here satisfies bit 3 but
2992
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
2993
+ // 3. Conditional emission would require runtime gating on
2994
+ // the cardset cascade scope state. Left dropped pending a
2995
+ // dedicated plan — accepted divergence on bit 3's pair-
2996
+ // level emission (cells still get the per-cell `example`
2997
+ // correctly via PLAN-077 §4.2 + §4.3).
2671
2998
  key: ConfigKey.property_example,
2672
2999
  exportJsonKey: [
2673
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2674
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2675
- { isExample: true, example: "$", "@bit": { isExample: true } }
3000
+ {
3001
+ "@keyonly": {
3002
+ isExample: true,
3003
+ "@bit": { isExample: true },
3004
+ "@card": { isExample: true }
3005
+ }
3006
+ },
3007
+ {
3008
+ "@absent": {
3009
+ isExample: true,
3010
+ "@bit": { isExample: true },
3011
+ "@card": { isExample: true }
3012
+ }
3013
+ },
3014
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2676
3015
  ],
2677
3016
  description: "Example text for the match matrix.",
2678
- format: TagFormat.plainText
3017
+ format: TagFormat.plainText,
3018
+ nullable: true
2679
3019
  },
2680
3020
  {
2681
3021
  key: ConfigKey.tag_title,
@@ -2703,35 +3043,96 @@ var CARDSETS = {
2703
3043
  variants: [
2704
3044
  {
2705
3045
  jsonKey: "cells[{s}].values[]",
2706
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
3046
+ exportJsonKey: [
3047
+ // PLAN-085 / R14: the positional-blank emission for an
3048
+ // effectively-absent cell side fires ONLY when the active
3049
+ // card has at least one `[#]` variant fire. This matches
3050
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
3051
+ // `[#]` exists anywhere in the title card, BPG returns
3052
+ // undefined and no `heading` is emitted. The phantom
3053
+ // `forValues:['']` placeholder is appropriate only when
3054
+ // the card is being treated as a title-row.
3055
+ {
3056
+ predicates: ["@absent", { "@variantHasTag": "#" }],
3057
+ rule: { "@bit": { heading: { forValues: [""] } } }
3058
+ },
3059
+ { cells: { $s: { values: ["$"] } } }
3060
+ ],
3061
+ format: TextFormat.plainText,
2707
3062
  tags: [
2708
3063
  {
2709
3064
  key: ConfigKey.group_standardItemLeadInstructionHint,
2710
3065
  description: "Standard tags for lead, instruction, and hint."
2711
3066
  },
2712
3067
  {
3068
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3069
+ // source `$` value (the current `++`-iteration's cell text).
3070
+ // Replaces `$parent.values[0]` which pinned to position 0
3071
+ // regardless of which `++`-value `[@example]` was on.
3072
+ //
3073
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3074
+ // routes pair-key `[@example:V]` (a per-pair cascade
3075
+ // source, bound via per-card CascadeCounters scope) and
3076
+ // bit-header `[@example:V]` (cardset cascade) into the
3077
+ // per-cell `example` via `$cascade`. The aggregator in
3078
+ // `populate_variants` fires this rule ONCE per cell (BPG
3079
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3080
+ // per-iter pollution.
3081
+ // @card scope-shift writes matrix-row container isExample.
2713
3082
  key: ConfigKey.property_example,
2714
3083
  exportJsonKey: [
2715
3084
  {
2716
3085
  "@keyonly": {
2717
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2718
- "@bit": { isExample: true }
3086
+ cells: { $s: { isExample: true, example: "$parent" } },
3087
+ "@bit": { isExample: true },
3088
+ "@card": { isExample: true }
2719
3089
  }
2720
3090
  },
2721
3091
  {
2722
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
3092
+ predicates: ["@absent", { $cascade: "*" }],
3093
+ rule: {
3094
+ cells: { $s: { isExample: true, example: "$cascade" } },
3095
+ "@bit": { isExample: true },
3096
+ "@card": { isExample: true }
3097
+ }
2723
3098
  },
2724
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
3099
+ {
3100
+ "@absent": {
3101
+ cells: { $s: { isExample: true, example: "$parent" } },
3102
+ "@bit": { isExample: true },
3103
+ "@card": { isExample: true }
3104
+ }
3105
+ },
3106
+ {
3107
+ cells: { $s: { isExample: true, example: "$" } },
3108
+ "@bit": { isExample: true },
3109
+ "@card": { isExample: true }
3110
+ }
2725
3111
  ],
2726
3112
  description: "Example text for the match matrix.",
2727
- format: TagFormat.plainText
3113
+ format: TagFormat.bitmarkText,
3114
+ nullable: true
2728
3115
  },
2729
3116
  {
3117
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3118
+ // empty-string positional blank into `forValues` so the
3119
+ // array stays aligned with the cell positions (matches BPG
3120
+ // `extractHeadingCard`'s "all value-side headings present"
3121
+ // shape). The `@keyonly` rule is required for two reasons:
3122
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3123
+ // in the unconditional rule has no source for bare); and
3124
+ // 2) the presence of any `@keyonly` rule disables the
3125
+ // optimised-mode `should_suppress_outcome` strip (see
3126
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3127
+ // survives the all-natural-defaults check.
2730
3128
  key: ConfigKey.tag_title,
2731
3129
  description: "Title of the match matrix.",
2732
3130
  format: TagFormat.plainText,
2733
3131
  jsonKey: "^heading.forValues",
2734
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3132
+ exportJsonKey: [
3133
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3134
+ { "@bit": { heading: { forValues: ["$"] } } }
3135
+ ]
2735
3136
  },
2736
3137
  {
2737
3138
  key: ConfigKey.property_isCaseSensitive,
@@ -2780,6 +3181,35 @@ var CARDSETS = {
2780
3181
  {
2781
3182
  key: ConfigKey.group_standardTags,
2782
3183
  description: "Standard tags for the statement."
3184
+ },
3185
+ {
3186
+ // PLAN-072: cascade-fired `@example` from the bit header
3187
+ // fills each statement entry's `example` with the entry's
3188
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3189
+ // for the statements cardset. Per-array-item strip removes
3190
+ // `example: false` from incorrect entries so only correct
3191
+ // entries retain `example: true`.
3192
+ key: ConfigKey.property_example,
3193
+ exportJsonKey: [
3194
+ {
3195
+ "@keyonly": {
3196
+ isExample: true,
3197
+ example: true,
3198
+ "@bit": { isExample: true }
3199
+ }
3200
+ },
3201
+ {
3202
+ "@absent": {
3203
+ isExample: true,
3204
+ example: "$parent.isCorrect",
3205
+ "@bit": { isExample: true }
3206
+ }
3207
+ },
3208
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3209
+ ],
3210
+ 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`.",
3211
+ format: TagFormat.boolean,
3212
+ nullable: true
2783
3213
  }
2784
3214
  ],
2785
3215
  bodyAllowed: false
@@ -2808,6 +3238,71 @@ var CARDSETS = {
2808
3238
  {
2809
3239
  key: ConfigKey.group_standardTags,
2810
3240
  description: "Standard tags for the quiz."
3241
+ },
3242
+ {
3243
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3244
+ // quiz choices is delegated to group_trueFalse's
3245
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3246
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3247
+ // would over-fire on subsequent correct choices and on
3248
+ // incorrect ones — drop it.
3249
+ key: ConfigKey.property_example,
3250
+ exportJsonKey: [
3251
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3252
+ { isExample: true, "@bit": { isExample: true } }
3253
+ ],
3254
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3255
+ format: TagFormat.boolean,
3256
+ nullable: true
3257
+ }
3258
+ ],
3259
+ bodyAllowed: false
3260
+ }
3261
+ ]
3262
+ }
3263
+ ]
3264
+ },
3265
+ //
3266
+ // quiz (responses)
3267
+ //
3268
+ [CardSetConfigKey.quizResponses]: {
3269
+ jsonKey: "quizzes",
3270
+ exportJsonKey: { quizzes: "$" },
3271
+ sides: [
3272
+ {
3273
+ name: "responses",
3274
+ variants: [
3275
+ {
3276
+ jsonKey: null,
3277
+ tags: [
3278
+ {
3279
+ key: ConfigKey.group_trueFalseResponses,
3280
+ description: "Group for true/false questions."
3281
+ },
3282
+ {
3283
+ key: ConfigKey.group_standardTags,
3284
+ description: "Standard tags for the quiz."
3285
+ },
3286
+ {
3287
+ // PLAN-072: cascade-fired `@example` from the bit header
3288
+ // fills each response entry's `example` with the entry's
3289
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3290
+ // for the quiz-responses cardset (statements-style).
3291
+ key: ConfigKey.property_example,
3292
+ exportJsonKey: [
3293
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3294
+ {
3295
+ "@absent": {
3296
+ isExample: true,
3297
+ example: "$parent.isCorrect",
3298
+ "@bit": { isExample: true }
3299
+ }
3300
+ },
3301
+ { isExample: true, "@bit": { isExample: true } }
3302
+ ],
3303
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3304
+ format: TagFormat.boolean,
3305
+ nullable: true
2811
3306
  }
2812
3307
  ],
2813
3308
  bodyAllowed: false
@@ -2833,6 +3328,73 @@ var CARDSETS = {
2833
3328
  key: ConfigKey.group_standardItemLeadInstructionHint,
2834
3329
  description: "Standard tags for lead, instruction, and hint."
2835
3330
  },
3331
+ {
3332
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3333
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3334
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3335
+ // feedback-specific shape.
3336
+ //
3337
+ // Chain @example: `[+ X][@example]` writes
3338
+ // `isExample/example` onto the X choice only. BPG's
3339
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3340
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3341
+ // there is no bubble to bit-level or feedbacks-level
3342
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3343
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3344
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3345
+ // no feedback dispatch, so bit-header `[@example]` does
3346
+ // not flow into feedback choices.
3347
+ key: ConfigKey.tag_true,
3348
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3349
+ description: "Reason-required choice for feedback (`[+]`).",
3350
+ maxCount: Count.infinity,
3351
+ format: TagFormat.plainText,
3352
+ chain: [
3353
+ {
3354
+ key: ConfigKey.property_example,
3355
+ exportJsonKey: [
3356
+ { "@keyonly": { isExample: true, example: true } },
3357
+ { isExample: true, example: "$" }
3358
+ ],
3359
+ description: "Per-choice example marker for feedback `[+]`.",
3360
+ format: TagFormat.boolean,
3361
+ maxCount: 1,
3362
+ nullable: true
3363
+ },
3364
+ {
3365
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3366
+ // choice; same for `[?]` (hint), `[%]` (item),
3367
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3368
+ // Mirrors group_trueFalse's `+` chain shape.
3369
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3370
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3371
+ }
3372
+ ]
3373
+ },
3374
+ {
3375
+ key: ConfigKey.tag_false,
3376
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3377
+ description: "Reason-not-required choice for feedback (`[-]`).",
3378
+ maxCount: Count.infinity,
3379
+ format: TagFormat.plainText,
3380
+ chain: [
3381
+ {
3382
+ key: ConfigKey.property_example,
3383
+ exportJsonKey: [
3384
+ { "@keyonly": { isExample: true, example: true } },
3385
+ { isExample: true, example: "$" }
3386
+ ],
3387
+ description: "Per-choice example marker for feedback `[-]`.",
3388
+ format: TagFormat.boolean,
3389
+ maxCount: 1,
3390
+ nullable: true
3391
+ },
3392
+ {
3393
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3394
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3395
+ }
3396
+ ]
3397
+ },
2836
3398
  {
2837
3399
  key: ConfigKey.group_trueFalse,
2838
3400
  description: "Group for true/false feedback."
@@ -2853,31 +3415,71 @@ var CARDSETS = {
2853
3415
  name: "reason",
2854
3416
  variants: [
2855
3417
  {
3418
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3419
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3420
+ // under `reason.*`; each tag here spells out the full path.
3421
+ // Body is a plain string (BPG `parseFeedback` uses
3422
+ // `cardBodyStr ?? ''` for `reason.text`).
2856
3423
  jsonKey: "reason.text",
2857
3424
  exportJsonKey: { reason: { text: "$" } },
3425
+ format: TextFormat.plainText,
2858
3426
  tags: [
2859
3427
  {
2860
- key: ConfigKey.group_standardItemLeadInstructionHint,
2861
- description: "Standard tags for lead, instruction, and hint."
3428
+ // Instruction lands under reason. Hint/item/lead/etc.
3429
+ // omitted: not exercised by any fixture and BPG's
3430
+ // `parseFeedback` spreads only the tags actually present
3431
+ // — adding the rest can be done lazily as fixtures
3432
+ // require them, with the same `{reason: {…}}` wrapper.
3433
+ key: ConfigKey.tag_instruction,
3434
+ exportJsonKey: { reason: { instruction: "$" } },
3435
+ description: "Instruction for the feedback reason.",
3436
+ format: TagFormat.bitmarkText
2862
3437
  },
2863
3438
  {
2864
3439
  key: ConfigKey.property_reasonableNumOfChars,
2865
3440
  exportJsonKey: [
2866
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2867
- { reasonableNumOfChars: "$" }
3441
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3442
+ { reason: { reasonableNumOfChars: "$" } }
2868
3443
  ],
2869
3444
  description: "Property for reasonable number of characters.",
2870
3445
  format: TagFormat.number
2871
3446
  },
2872
3447
  {
3448
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3449
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3450
+ // cascade. Local emission only.
3451
+ //
3452
+ // Both `reason.example` and `feedbacks[].example` are
3453
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3454
+ // here is `bitmarkText` so `$` coerces to a paragraph
3455
+ // array at both write sites.
2873
3456
  key: ConfigKey.property_example,
2874
3457
  exportJsonKey: [
2875
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2876
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2877
- { isExample: true, example: "$", "@bit": { isExample: true } }
3458
+ {
3459
+ // Bare `[@example]`: reason.example is TextAst BPG
3460
+ // coerces the boolean default to a paragraph
3461
+ // containing the string "true". feedbacks[].example
3462
+ // is not TextAst, stays a boolean. Two literal writes.
3463
+ "@keyonly": {
3464
+ reason: {
3465
+ isExample: true,
3466
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3467
+ },
3468
+ isExample: true,
3469
+ example: true,
3470
+ "@bit": { isExample: true }
3471
+ }
3472
+ },
3473
+ {
3474
+ reason: { isExample: true, example: "$" },
3475
+ isExample: true,
3476
+ example: "$",
3477
+ "@bit": { isExample: true }
3478
+ }
2878
3479
  ],
2879
- description: "Example text for the feedback.",
2880
- format: TagFormat.plainText
3480
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3481
+ format: TagFormat.bitmarkText,
3482
+ nullable: true
2881
3483
  },
2882
3484
  {
2883
3485
  key: ConfigKey.tag_title,
@@ -2941,6 +3543,22 @@ var CARDSETS = {
2941
3543
  {
2942
3544
  key: ConfigKey.group_standardTags,
2943
3545
  description: "Standard tags for the question."
3546
+ },
3547
+ {
3548
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3549
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3550
+ // does not propagate `example`/`isExample` onto question
3551
+ // entries. Only entry-local `[@example]` fires (handled by
3552
+ // `@keyonly` and the unconditional fallback). No `@absent`
3553
+ // rule.
3554
+ key: ConfigKey.property_example,
3555
+ exportJsonKey: [
3556
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3557
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3558
+ ],
3559
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3560
+ format: TagFormat.bitmarkText,
3561
+ nullable: true
2944
3562
  }
2945
3563
  ]
2946
3564
  }
@@ -2967,14 +3585,16 @@ var CARDSETS = {
2967
3585
  description: "Standard tags for lead, instruction, and hint."
2968
3586
  },
2969
3587
  {
3588
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3589
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2970
3590
  key: ConfigKey.property_example,
2971
3591
  exportJsonKey: [
2972
3592
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2973
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2974
3593
  { isExample: true, example: "$", "@bit": { isExample: true } }
2975
3594
  ],
2976
- description: "Example text for the element.",
2977
- format: TagFormat.plainText
3595
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3596
+ format: TagFormat.plainText,
3597
+ nullable: true
2978
3598
  }
2979
3599
  ],
2980
3600
  repeatCount: Count.infinity
@@ -2986,9 +3606,57 @@ var CARDSETS = {
2986
3606
  //
2987
3607
  // table
2988
3608
  //
3609
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3610
+ // output (`{ columns?, data, columnWidths? }`):
3611
+ //
3612
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3613
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3614
+ // no qualifier, so they route to the `table-body` (default) section.
3615
+ //
3616
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3617
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3618
+ // the first header card's cells to `columns` and demote remaining
3619
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3620
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3621
+ // here (no per-cell metadata in basic shape) — declared on the cell
3622
+ // variant below.
3623
+ //
2989
3624
  [CardSetConfigKey.table]: {
2990
- jsonKey: "table.data",
2991
- exportJsonKey: { table: { data: "$" } },
3625
+ jsonKey: null,
3626
+ exportJsonKey: null,
3627
+ sections: {
3628
+ "table-header": {
3629
+ // First header card's row of cells → `columns`. Subsequent header
3630
+ // cards' rows → `data` (per-card append; `[$]` shape because
3631
+ // per-card emission fires once per card, multiple fires deep_merge-
3632
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3633
+ // both ROUTE cards to this section (when no explicit `==== table-
3634
+ // header ====` qualifier) and SELECT the rule body once routed.
3635
+ // First rule = first card with any `[#]` → `columns`; second rule
3636
+ // = subsequent cards in this section → `data` (per-row append).
3637
+ jsonKey: "table.columns",
3638
+ exportJsonKey: [
3639
+ {
3640
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3641
+ rule: { "@bit": { table: { columns: "$" } } }
3642
+ },
3643
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3644
+ { "@bit": { table: { data: ["$"] } } }
3645
+ ]
3646
+ },
3647
+ "table-body": {
3648
+ // Batched emission: `$` is the entire 2D array of body card rows.
3649
+ // This is the default section (no qualifier → cards land here).
3650
+ jsonKey: "table.data",
3651
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3652
+ isDefault: true
3653
+ },
3654
+ "table-footer": {
3655
+ // Batched emission: `$` is the entire 2D array of footer rows.
3656
+ jsonKey: "table.data",
3657
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3658
+ }
3659
+ },
2992
3660
  sides: [
2993
3661
  {
2994
3662
  name: "cell",
@@ -3029,6 +3697,8 @@ var CARDSETS = {
3029
3697
  },
3030
3698
  {
3031
3699
  key: ConfigKey.property_tableColWidth,
3700
+ jsonKey: "^table.columnWidths[{s}]",
3701
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
3032
3702
  description: "Width for the column.",
3033
3703
  format: TagFormat.number
3034
3704
  }
@@ -3047,7 +3717,22 @@ var CARDSETS = {
3047
3717
  sections: {
3048
3718
  "table-header": {
3049
3719
  jsonKey: "table.header.rows",
3050
- exportJsonKey: { table: { header: { rows: "$" } } },
3720
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3721
+ // Cards with no explicit `==== table-header ====` qualifier
3722
+ // route here if FIRST CARD contains any `[#]` tag in any
3723
+ // variant. Explicit-qualifier cards fall through to the
3724
+ // unconditional fallback rule. Both rules use the per-card
3725
+ // append shape `rows: ['$']` — the runtime triggers per-card
3726
+ // emission whenever the pattern carries predicates, and each
3727
+ // fire appends a single-element array (multi-fire deep-merge
3728
+ // concatenates).
3729
+ exportJsonKey: [
3730
+ {
3731
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3732
+ rule: { table: { header: { rows: ["$"] } } }
3733
+ },
3734
+ { table: { header: { rows: ["$"] } } }
3735
+ ],
3051
3736
  sideJsonKey: "cells[{s}]|set(title=true)",
3052
3737
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
3053
3738
  },
@@ -3080,13 +3765,13 @@ var CARDSETS = {
3080
3765
  },
3081
3766
  {
3082
3767
  key: ConfigKey.tag_title,
3083
- jsonKey: ".",
3084
- exportJsonKey: {
3085
- "@bit": {
3086
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
3087
- }
3088
- },
3089
- description: "Title of the table cell."
3768
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3769
+ // `content` field. When the card is in the `table-header`
3770
+ // section, the section's sideExportJsonKey wraps each
3771
+ // cell with `title: true` automatically.
3772
+ jsonKey: "content",
3773
+ exportJsonKey: { content: "$" },
3774
+ description: "Title text of the table cell (header rows)."
3090
3775
  },
3091
3776
  {
3092
3777
  key: ConfigKey.property_tableCellType,
@@ -3118,6 +3803,9 @@ var CARDSETS = {
3118
3803
  },
3119
3804
  {
3120
3805
  key: ConfigKey.property_tableColWidth,
3806
+ // PLAN-080: write to the cell's `colwidth` field.
3807
+ jsonKey: "colwidth",
3808
+ exportJsonKey: { colwidth: "$" },
3121
3809
  description: "Width for the column.",
3122
3810
  format: TagFormat.number
3123
3811
  }
@@ -3155,7 +3843,8 @@ var CARDSETS = {
3155
3843
  {
3156
3844
  key: ConfigKey.resource_audio,
3157
3845
  description: "Audio resource for the pronunciation table.",
3158
- jsonKey: "audio"
3846
+ jsonKey: "audio",
3847
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3159
3848
  }
3160
3849
  ],
3161
3850
  repeatCount: Count.infinity
@@ -3226,14 +3915,17 @@ var CARDSETS = {
3226
3915
  format: TagFormat.plainText
3227
3916
  },
3228
3917
  {
3918
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
3919
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
3920
+ // header cascade.
3229
3921
  key: ConfigKey.property_example,
3230
3922
  exportJsonKey: [
3231
3923
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3232
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3233
3924
  { isExample: true, example: "$", "@bit": { isExample: true } }
3234
3925
  ],
3235
- description: "Example text for the bot action response.",
3236
- format: TagFormat.plainText
3926
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
3927
+ format: TagFormat.plainText,
3928
+ nullable: true
3237
3929
  }
3238
3930
  ]
3239
3931
  }
@@ -3299,6 +3991,38 @@ var CARDSETS = {
3299
3991
  ]
3300
3992
  },
3301
3993
  //
3994
+ // page-footer-sections — mirrors example-bit-list but emits cards under
3995
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
3996
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
3997
+ // routing as pure config so the Rust serializer can pick it up.
3998
+ //
3999
+ [CardSetConfigKey.pageFooterSections]: {
4000
+ jsonKey: "sections",
4001
+ exportJsonKey: { sections: "$" },
4002
+ sides: [
4003
+ {
4004
+ name: "item",
4005
+ variants: [
4006
+ {
4007
+ jsonKey: "body",
4008
+ exportJsonKey: { body: "$" },
4009
+ tags: [
4010
+ {
4011
+ key: ConfigKey.group_standardTags,
4012
+ description: "Standard tags for page-footer section bits."
4013
+ },
4014
+ {
4015
+ exportJsonKey: { title: "$" },
4016
+ key: ConfigKey.tag_title,
4017
+ description: "Title of the page-footer section."
4018
+ }
4019
+ ]
4020
+ }
4021
+ ]
4022
+ }
4023
+ ]
4024
+ },
4025
+ //
3302
4026
  // ingredients
3303
4027
  //
3304
4028
  [CardSetConfigKey.ingredients]: {
@@ -3629,7 +4353,11 @@ var GROUPS = {
3629
4353
  {
3630
4354
  key: ConfigKey.property_groupTag,
3631
4355
  jsonKey: "groupTag.name",
3632
- exportJsonKey: { groupTag: { name: "$" } },
4356
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4357
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4358
+ // same value fold into one entry; chained `@tag` children's
4359
+ // arrays merge as sets.
4360
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3633
4361
  description: "The group tag(s) for the bit",
3634
4362
  format: TagFormat.plainText,
3635
4363
  maxCount: Count.infinity,
@@ -3802,6 +4530,24 @@ var GROUPS = {
3802
4530
  }
3803
4531
  ]
3804
4532
  },
4533
+ [ConfigKey.group_standardInstructionHint]: {
4534
+ type: GroupConfigType.standard,
4535
+ 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).",
4536
+ tags: [
4537
+ {
4538
+ exportJsonKey: { instruction: "$" },
4539
+ key: ConfigKey.tag_instruction,
4540
+ name: "Instruction",
4541
+ description: "The instruction for the bit"
4542
+ },
4543
+ {
4544
+ exportJsonKey: { hint: "$" },
4545
+ key: ConfigKey.tag_hint,
4546
+ name: "Hint",
4547
+ description: "The hint for the bit"
4548
+ }
4549
+ ]
4550
+ },
3805
4551
  [ConfigKey.group_standardTags]: {
3806
4552
  type: GroupConfigType.standard,
3807
4553
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3815,17 +4561,45 @@ var GROUPS = {
3815
4561
  description: "Item, lead, page number, margin number, instruction and hint tags"
3816
4562
  },
3817
4563
  {
3818
- // Bit-level example tag: bare/synthesized records the marker only;
3819
- // valued [@example:text] emits `isExample: true` and `example` as
3820
- // PM-rendered rich text (bitmark+ format).
4564
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4565
+ // - bare `[@example]`: consume only (records cascade marker, emits
4566
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4567
+ // for bits NOT in its `isTopLevelExample` allow-list).
4568
+ // - synthesized (descendant cascade): still emits `isExample: true`
4569
+ // at the descendant scope.
4570
+ // - valued `[@example:text]`: emit the example value only; bit-level
4571
+ // `isExample` is left to bubble from descendants.
4572
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4573
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4574
+ // matchMatrix, match, essay, etc.) override this entry with the
4575
+ // legacy emit-isExample shape.
3821
4576
  key: ConfigKey.property_example,
3822
- exportJsonKey: [
3823
- { "@keyonly": { isExample: true } },
3824
- { "@absent": { isExample: true } },
3825
- { isExample: true, example: "$" }
3826
- ],
4577
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3827
4578
  description: "The example(s) for the bit",
3828
- format: TagFormat.bitmarkText
4579
+ format: TagFormat.bitmarkText,
4580
+ nullable: true
4581
+ }
4582
+ ]
4583
+ },
4584
+ [ConfigKey.group_standardTagsNoItemLead]: {
4585
+ type: GroupConfigType.standard,
4586
+ 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).",
4587
+ tags: [
4588
+ {
4589
+ key: ConfigKey.group_standardAllBits,
4590
+ description: "All standard tags which apply to all bits"
4591
+ },
4592
+ {
4593
+ key: ConfigKey.group_standardInstructionHint,
4594
+ description: "Instruction and hint tags only (no item/lead chain)."
4595
+ },
4596
+ {
4597
+ // Mirror of group_standardTags property_example entry.
4598
+ key: ConfigKey.property_example,
4599
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4600
+ description: "The example(s) for the bit",
4601
+ format: TagFormat.bitmarkText,
4602
+ nullable: true
3829
4603
  }
3830
4604
  ]
3831
4605
  },
@@ -3890,8 +4664,8 @@ var GROUPS = {
3890
4664
  tags: [
3891
4665
  {
3892
4666
  key: ConfigKey.property_person,
3893
- jsonKey: "partner.name",
3894
- exportJsonKey: { partner: { name: "$" } },
4667
+ jsonKey: "person.name",
4668
+ exportJsonKey: { person: { name: "$" } },
3895
4669
  description: "A person",
3896
4670
  format: TagFormat.plainText,
3897
4671
  chain: [
@@ -3911,8 +4685,8 @@ var GROUPS = {
3911
4685
  {
3912
4686
  // Deprecated (parter renamed to person)
3913
4687
  key: ConfigKey.property_partner,
3914
- jsonKey: "partner.name",
3915
- exportJsonKey: { partner: { name: "$" } },
4688
+ jsonKey: "person.name",
4689
+ exportJsonKey: { person: { name: "$" } },
3916
4690
  description: "A partner",
3917
4691
  format: TagFormat.plainText,
3918
4692
  chain: [
@@ -3936,7 +4710,9 @@ var GROUPS = {
3936
4710
  description: "Gap chain",
3937
4711
  tags: [
3938
4712
  {
3939
- exportJsonKey: { solutions: [["$"]] },
4713
+ exportJsonKey: {
4714
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4715
+ },
3940
4716
  key: ConfigKey.tag_gap,
3941
4717
  description: "The value of a gap in the content",
3942
4718
  maxCount: Count.infinity,
@@ -3955,27 +4731,37 @@ var GROUPS = {
3955
4731
  description: "Item, lead, page number, margin number, instruction and hint tags"
3956
4732
  },
3957
4733
  {
4734
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4735
+ // `__solutionsAst[length - 1]` — the solution closest to the
4736
+ // `[@example]` tag at parse time. Our chain accumulator is built
4737
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4738
+ // the moment `[@example]` fires, `solutions` only contains the
4739
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4740
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4741
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4742
+ // solutions=["solution"], `[-1]`="solution".
3958
4743
  key: ConfigKey.property_example,
3959
4744
  jsonKey: "example",
3960
4745
  exportJsonKey: [
3961
4746
  {
3962
4747
  "@keyonly": {
3963
4748
  isExample: true,
3964
- example: "$parent.solutions[0]",
4749
+ example: "$parent.solutions[-1]",
3965
4750
  "@bit": { isExample: true }
3966
4751
  }
3967
4752
  },
3968
4753
  {
3969
4754
  "@absent": {
3970
4755
  isExample: true,
3971
- example: "$parent.solutions[0]",
4756
+ example: "$parent.solutions[-1]",
3972
4757
  "@bit": { isExample: true }
3973
4758
  }
3974
4759
  },
3975
4760
  { isExample: true, example: "$", "@bit": { isExample: true } }
3976
4761
  ],
3977
4762
  description: "An example for the gap",
3978
- format: TagFormat.bitmarkText
4763
+ format: TagFormat.bitmarkText,
4764
+ nullable: true
3979
4765
  },
3980
4766
  {
3981
4767
  key: ConfigKey.property_isCaseSensitive,
@@ -4024,6 +4810,24 @@ var GROUPS = {
4024
4810
  description: "Item, lead, page number, margin number, instruction and hint tags"
4025
4811
  },
4026
4812
  {
4813
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4814
+ // `@example` from the bit header marks EVERY choice with
4815
+ // `isExample: true` (matches BPG output), but only the
4816
+ // FIRST correct choice gets `example: true` (BPG
4817
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4818
+ //
4819
+ // Two `@absent` rules, picked first-match-wins:
4820
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4821
+ // `maxEmits:1` — fires once for the first correct.
4822
+ // 2. Plain `@absent` — fires on every remaining entry
4823
+ // (subsequent corrects and all incorrects), emitting
4824
+ // `isExample` only.
4825
+ // The `serialize_cards` cascade-counter scope holds the cap
4826
+ // across the whole cardset.
4827
+ // @card scope-shift writes choice-container isExample (e.g.
4828
+ // `quizzes[i].isExample`) — was previously provided by the
4829
+ // universal `bubble_is_example` post-walk, which was removed
4830
+ // in favour of explicit per-tag config. SYNTAX.md §8.
4027
4831
  key: ConfigKey.property_example,
4028
4832
  jsonKey: "example",
4029
4833
  exportJsonKey: [
@@ -4031,20 +4835,37 @@ var GROUPS = {
4031
4835
  "@keyonly": {
4032
4836
  isExample: true,
4033
4837
  example: "$parent.isCorrect",
4034
- "@bit": { isExample: true }
4838
+ "@bit": { isExample: true },
4839
+ "@card": { isExample: true }
4840
+ }
4841
+ },
4842
+ {
4843
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4844
+ maxEmits: 1,
4845
+ rule: {
4846
+ isExample: true,
4847
+ example: true,
4848
+ "@bit": { isExample: true },
4849
+ "@card": { isExample: true }
4035
4850
  }
4036
4851
  },
4037
4852
  {
4038
4853
  "@absent": {
4039
4854
  isExample: true,
4040
- example: "$parent.isCorrect",
4041
- "@bit": { isExample: true }
4855
+ "@bit": { isExample: true },
4856
+ "@card": { isExample: true }
4042
4857
  }
4043
4858
  },
4044
- { isExample: true, example: "$", "@bit": { isExample: true } }
4859
+ {
4860
+ isExample: true,
4861
+ example: "$",
4862
+ "@bit": { isExample: true },
4863
+ "@card": { isExample: true }
4864
+ }
4045
4865
  ],
4046
4866
  description: "An example for the true/false statement/question",
4047
- format: TagFormat.bitmarkText
4867
+ format: TagFormat.boolean,
4868
+ nullable: true
4048
4869
  }
4049
4870
  ]
4050
4871
  },
@@ -4076,6 +4897,12 @@ var GROUPS = {
4076
4897
  description: "Item, lead, page number, margin number, instruction and hint tags"
4077
4898
  },
4078
4899
  {
4900
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4901
+ // `@example` on a `-` (incorrect choice) emits
4902
+ // `isExample: true` only — no `example` (BPG fixtures show
4903
+ // `isExample` on every choice, but `example` only on the
4904
+ // first correct via firstCorrectOnly cascade).
4905
+ // @card scope-shift writes choice-container isExample.
4079
4906
  key: ConfigKey.property_example,
4080
4907
  jsonKey: "example",
4081
4908
  exportJsonKey: [
@@ -4083,20 +4910,460 @@ var GROUPS = {
4083
4910
  "@keyonly": {
4084
4911
  isExample: true,
4085
4912
  example: "$parent.isCorrect",
4086
- "@bit": { isExample: true }
4913
+ "@bit": { isExample: true },
4914
+ "@card": { isExample: true }
4087
4915
  }
4088
4916
  },
4089
4917
  {
4090
4918
  "@absent": {
4919
+ isExample: true,
4920
+ "@bit": { isExample: true },
4921
+ "@card": { isExample: true }
4922
+ }
4923
+ },
4924
+ {
4925
+ isExample: true,
4926
+ example: "$",
4927
+ "@bit": { isExample: true },
4928
+ "@card": { isExample: true }
4929
+ }
4930
+ ],
4931
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
4932
+ format: TagFormat.boolean,
4933
+ nullable: true
4934
+ }
4935
+ ]
4936
+ }
4937
+ ]
4938
+ },
4939
+ [ConfigKey.group_trueFalseResponses]: {
4940
+ type: GroupConfigType.standard,
4941
+ description: "True/False Responses chain",
4942
+ tags: [
4943
+ {
4944
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4945
+ key: ConfigKey.tag_true,
4946
+ description: "True value for a true/false statement/question",
4947
+ maxCount: Count.infinity,
4948
+ format: TagFormat.plainText,
4949
+ chain: [
4950
+ {
4951
+ key: ConfigKey.tag_true,
4952
+ jsonKey: "responses[]|set(isCorrect=true)",
4953
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4954
+ description: "True values for a true/false statement/question",
4955
+ maxCount: Count.infinity,
4956
+ format: TagFormat.plainText
4957
+ },
4958
+ {
4959
+ key: ConfigKey.tag_false,
4960
+ jsonKey: "responses[]|set(isCorrect=false)",
4961
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4962
+ description: "False values for a true/false statement/question",
4963
+ maxCount: Count.infinity,
4964
+ format: TagFormat.plainText
4965
+ },
4966
+ {
4967
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4968
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4969
+ },
4970
+ {
4971
+ // @card scope-shift writes response-container isExample.
4972
+ key: ConfigKey.property_example,
4973
+ jsonKey: "example",
4974
+ exportJsonKey: [
4975
+ {
4976
+ "@keyonly": {
4091
4977
  isExample: true,
4092
4978
  example: "$parent.isCorrect",
4093
- "@bit": { isExample: true }
4979
+ "@bit": { isExample: true },
4980
+ "@card": { isExample: true }
4094
4981
  }
4095
4982
  },
4096
- { isExample: true, example: "$", "@bit": { isExample: true } }
4983
+ {
4984
+ "@absent": {
4985
+ isExample: "$parent.isCorrect",
4986
+ example: "$parent.isCorrect",
4987
+ "@bit": { isExample: "$parent.isCorrect" },
4988
+ "@card": { isExample: "$parent.isCorrect" }
4989
+ }
4990
+ },
4991
+ {
4992
+ isExample: true,
4993
+ example: "$",
4994
+ "@bit": { isExample: true },
4995
+ "@card": { isExample: true }
4996
+ }
4097
4997
  ],
4098
4998
  description: "An example for the true/false statement/question",
4099
- format: TagFormat.bitmarkText
4999
+ format: TagFormat.boolean,
5000
+ nullable: true
5001
+ }
5002
+ ]
5003
+ },
5004
+ {
5005
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
5006
+ key: ConfigKey.tag_false,
5007
+ description: "False value for a true/false statement/question",
5008
+ maxCount: Count.infinity,
5009
+ format: TagFormat.plainText,
5010
+ chain: [
5011
+ {
5012
+ key: ConfigKey.tag_true,
5013
+ jsonKey: "responses[]|set(isCorrect=true)",
5014
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5015
+ description: "True values for a true/false statement/question",
5016
+ maxCount: Count.infinity,
5017
+ format: TagFormat.plainText
5018
+ },
5019
+ {
5020
+ key: ConfigKey.tag_false,
5021
+ jsonKey: "responses[]|set(isCorrect=false)",
5022
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
5023
+ description: "False values for a true/false statement/question",
5024
+ maxCount: Count.infinity,
5025
+ format: TagFormat.plainText
5026
+ },
5027
+ {
5028
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5029
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5030
+ },
5031
+ {
5032
+ // @card scope-shift writes response-container isExample.
5033
+ key: ConfigKey.property_example,
5034
+ jsonKey: "example",
5035
+ exportJsonKey: [
5036
+ {
5037
+ "@keyonly": {
5038
+ isExample: true,
5039
+ example: "$parent.isCorrect",
5040
+ "@bit": { isExample: true },
5041
+ "@card": { isExample: true }
5042
+ }
5043
+ },
5044
+ {
5045
+ "@absent": {
5046
+ isExample: true,
5047
+ example: "$parent.isCorrect",
5048
+ "@bit": { isExample: true },
5049
+ "@card": { isExample: true }
5050
+ }
5051
+ },
5052
+ {
5053
+ isExample: true,
5054
+ example: "$",
5055
+ "@bit": { isExample: true },
5056
+ "@card": { isExample: true }
5057
+ }
5058
+ ],
5059
+ description: "An example for the true/false statement/question",
5060
+ format: TagFormat.boolean,
5061
+ nullable: true
5062
+ }
5063
+ ]
5064
+ }
5065
+ ]
5066
+ },
5067
+ [ConfigKey.group_trueFalseInlineSelect]: {
5068
+ type: GroupConfigType.standard,
5069
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5070
+ tags: [
5071
+ {
5072
+ exportJsonKey: {
5073
+ "@body-inline": {
5074
+ type: "select",
5075
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5076
+ }
5077
+ },
5078
+ key: ConfigKey.tag_true,
5079
+ description: "True value for an inline select body bit",
5080
+ maxCount: Count.infinity,
5081
+ format: TagFormat.plainText,
5082
+ chain: [
5083
+ {
5084
+ key: ConfigKey.tag_true,
5085
+ exportJsonKey: {},
5086
+ description: "True values for an inline select body bit",
5087
+ maxCount: Count.infinity,
5088
+ format: TagFormat.plainText
5089
+ },
5090
+ {
5091
+ key: ConfigKey.tag_false,
5092
+ exportJsonKey: {},
5093
+ description: "False values for an inline select body bit",
5094
+ maxCount: Count.infinity,
5095
+ format: TagFormat.plainText
5096
+ },
5097
+ {
5098
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5099
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5100
+ },
5101
+ {
5102
+ // PLAN-083: cascade-fired `@example` from bit header fires
5103
+ // `isExample:true, example:true` on the FIRST CORRECT option
5104
+ // only (BPG `fillBooleanExample(select.options,
5105
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5106
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5107
+ // OR — any cascade ⇒ bit marked) and
5108
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5109
+ // on the select node).
5110
+ key: ConfigKey.property_example,
5111
+ jsonKey: "example",
5112
+ exportJsonKey: [
5113
+ {
5114
+ "@keyonly": {
5115
+ isExample: true,
5116
+ example: "$parent.isCorrect",
5117
+ "@bit": { isExample: true },
5118
+ "@body-inline": { attrs: { isExample: true } }
5119
+ }
5120
+ },
5121
+ {
5122
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5123
+ maxEmits: 1,
5124
+ rule: {
5125
+ isExample: true,
5126
+ example: true,
5127
+ "@bit": { isExample: true },
5128
+ "@body-inline": { attrs: { isExample: true } }
5129
+ }
5130
+ },
5131
+ {
5132
+ isExample: true,
5133
+ example: "$",
5134
+ "@bit": { isExample: true },
5135
+ "@body-inline": { attrs: { isExample: true } }
5136
+ }
5137
+ ],
5138
+ description: "An example for the inline select option (first correct only)",
5139
+ format: TagFormat.boolean,
5140
+ nullable: true
5141
+ }
5142
+ ]
5143
+ },
5144
+ {
5145
+ exportJsonKey: {
5146
+ "@body-inline": {
5147
+ type: "select",
5148
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5149
+ }
5150
+ },
5151
+ key: ConfigKey.tag_false,
5152
+ description: "False value for an inline select body bit",
5153
+ maxCount: Count.infinity,
5154
+ format: TagFormat.plainText,
5155
+ chain: [
5156
+ {
5157
+ key: ConfigKey.tag_true,
5158
+ exportJsonKey: {},
5159
+ description: "True values for an inline select body bit",
5160
+ maxCount: Count.infinity,
5161
+ format: TagFormat.plainText
5162
+ },
5163
+ {
5164
+ key: ConfigKey.tag_false,
5165
+ exportJsonKey: {},
5166
+ description: "False values for an inline select body bit",
5167
+ maxCount: Count.infinity,
5168
+ format: TagFormat.plainText
5169
+ },
5170
+ {
5171
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5172
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5173
+ },
5174
+ {
5175
+ // PLAN-083: identical to the `+` lead's chain @example —
5176
+ // cascade reaches the first correct option regardless of
5177
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5178
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5179
+ key: ConfigKey.property_example,
5180
+ jsonKey: "example",
5181
+ exportJsonKey: [
5182
+ {
5183
+ "@keyonly": {
5184
+ isExample: true,
5185
+ example: "$parent.isCorrect",
5186
+ "@bit": { isExample: true },
5187
+ "@body-inline": { attrs: { isExample: true } }
5188
+ }
5189
+ },
5190
+ {
5191
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5192
+ maxEmits: 1,
5193
+ rule: {
5194
+ isExample: true,
5195
+ example: true,
5196
+ "@bit": { isExample: true },
5197
+ "@body-inline": { attrs: { isExample: true } }
5198
+ }
5199
+ },
5200
+ {
5201
+ isExample: true,
5202
+ example: "$",
5203
+ "@bit": { isExample: true },
5204
+ "@body-inline": { attrs: { isExample: true } }
5205
+ }
5206
+ ],
5207
+ description: "An example for the inline select option (first correct only)",
5208
+ format: TagFormat.boolean,
5209
+ nullable: true
5210
+ }
5211
+ ]
5212
+ }
5213
+ ]
5214
+ },
5215
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5216
+ type: GroupConfigType.standard,
5217
+ 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.',
5218
+ tags: [
5219
+ {
5220
+ exportJsonKey: {
5221
+ "@body-inline": {
5222
+ type: "highlight",
5223
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5224
+ }
5225
+ },
5226
+ key: ConfigKey.tag_true,
5227
+ description: "Highlighted span in the body (correct)",
5228
+ maxCount: Count.infinity,
5229
+ format: TagFormat.plainText,
5230
+ chain: [
5231
+ {
5232
+ key: ConfigKey.tag_true,
5233
+ exportJsonKey: {},
5234
+ description: "Highlighted span values (correct)",
5235
+ maxCount: Count.infinity,
5236
+ format: TagFormat.plainText
5237
+ },
5238
+ {
5239
+ key: ConfigKey.tag_false,
5240
+ exportJsonKey: {},
5241
+ description: "Highlighted span values (incorrect)",
5242
+ maxCount: Count.infinity,
5243
+ format: TagFormat.plainText
5244
+ },
5245
+ {
5246
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5247
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5248
+ },
5249
+ {
5250
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5251
+ // `@example` from the bit header fires `example: true` +
5252
+ // `isExample: true` on the FIRST correct highlight only
5253
+ // (BPG `fillBooleanExample(highlight.texts,
5254
+ // firstCorrectOnly=true)`). Incorrect highlights get
5255
+ // nothing (unlike choices, the highlight-text fixture
5256
+ // shows no `isExample` on incorrect entries).
5257
+ key: ConfigKey.property_example,
5258
+ jsonKey: "example",
5259
+ exportJsonKey: [
5260
+ {
5261
+ "@keyonly": {
5262
+ isExample: true,
5263
+ example: "$parent.isCorrect",
5264
+ "@bit": { isExample: true },
5265
+ "@body-inline": { attrs: { isExample: true } }
5266
+ }
5267
+ },
5268
+ {
5269
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5270
+ maxEmits: 1,
5271
+ rule: {
5272
+ isExample: true,
5273
+ example: true,
5274
+ "@bit": { isExample: true },
5275
+ "@body-inline": { attrs: { isExample: true } }
5276
+ }
5277
+ },
5278
+ {
5279
+ isExample: true,
5280
+ example: "$",
5281
+ "@bit": { isExample: true },
5282
+ "@body-inline": { attrs: { isExample: true } }
5283
+ }
5284
+ ],
5285
+ description: "An example for the highlighted span (first correct only)",
5286
+ format: TagFormat.boolean,
5287
+ nullable: true
5288
+ }
5289
+ ]
5290
+ },
5291
+ {
5292
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5293
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5294
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5295
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5296
+ // through and nothing fires. The natural-default strip removes
5297
+ // `isCorrect: false` from the final texts entries (matches fixture).
5298
+ exportJsonKey: {
5299
+ "@body-inline": {
5300
+ type: "highlight",
5301
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5302
+ }
5303
+ },
5304
+ key: ConfigKey.tag_false,
5305
+ description: "Highlighted span in the body (incorrect)",
5306
+ maxCount: Count.infinity,
5307
+ format: TagFormat.plainText,
5308
+ chain: [
5309
+ {
5310
+ key: ConfigKey.tag_true,
5311
+ exportJsonKey: {},
5312
+ description: "Highlighted span values (correct)",
5313
+ maxCount: Count.infinity,
5314
+ format: TagFormat.plainText
5315
+ },
5316
+ {
5317
+ key: ConfigKey.tag_false,
5318
+ exportJsonKey: {},
5319
+ description: "Highlighted span values (incorrect)",
5320
+ maxCount: Count.infinity,
5321
+ format: TagFormat.plainText
5322
+ },
5323
+ {
5324
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5325
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5326
+ },
5327
+ {
5328
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5329
+ // `@example` on a `-` (incorrect highlight) emits nothing
5330
+ // (highlight-text fixture has no `isExample` on incorrect
5331
+ // entries; firstCorrectOnly cascade skips them entirely).
5332
+ // No `@absent` rule.
5333
+ // PLAN-083: identical to the `+` lead's chain @example —
5334
+ // cascade reaches the first correct option regardless of
5335
+ // which lead (+/-) opens the chain.
5336
+ key: ConfigKey.property_example,
5337
+ jsonKey: "example",
5338
+ exportJsonKey: [
5339
+ {
5340
+ "@keyonly": {
5341
+ isExample: true,
5342
+ example: "$parent.isCorrect",
5343
+ "@bit": { isExample: true },
5344
+ "@body-inline": { attrs: { isExample: true } }
5345
+ }
5346
+ },
5347
+ {
5348
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5349
+ maxEmits: 1,
5350
+ rule: {
5351
+ isExample: true,
5352
+ example: true,
5353
+ "@bit": { isExample: true },
5354
+ "@body-inline": { attrs: { isExample: true } }
5355
+ }
5356
+ },
5357
+ {
5358
+ isExample: true,
5359
+ example: "$",
5360
+ "@bit": { isExample: true },
5361
+ "@body-inline": { attrs: { isExample: true } }
5362
+ }
5363
+ ],
5364
+ description: "An example for the highlighted span (first correct only)",
5365
+ format: TagFormat.boolean,
5366
+ nullable: true
4100
5367
  }
4101
5368
  ]
4102
5369
  }
@@ -4109,7 +5376,7 @@ var GROUPS = {
4109
5376
  {
4110
5377
  key: ConfigKey.property_mark,
4111
5378
  jsonKey: "marks.mark",
4112
- exportJsonKey: { marks: { mark: ["$"] } },
5379
+ exportJsonKey: { marks: [{ mark: "$" }] },
4113
5380
  description: "The mark configuration",
4114
5381
  format: TagFormat.plainText,
4115
5382
  maxCount: Count.infinity,
@@ -4133,7 +5400,9 @@ var GROUPS = {
4133
5400
  description: "Mark chain",
4134
5401
  tags: [
4135
5402
  {
4136
- exportJsonKey: { solution: ["$"] },
5403
+ exportJsonKey: {
5404
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5405
+ },
4137
5406
  key: ConfigKey.tag_mark,
4138
5407
  description: "The marked content",
4139
5408
  maxCount: Count.infinity,
@@ -4142,6 +5411,7 @@ var GROUPS = {
4142
5411
  {
4143
5412
  key: ConfigKey.property_mark,
4144
5413
  jsonKey: "mark",
5414
+ exportJsonKey: { mark: "$" },
4145
5415
  description: "The mark configuration",
4146
5416
  format: TagFormat.plainText
4147
5417
  },
@@ -4154,7 +5424,8 @@ var GROUPS = {
4154
5424
  { isExample: true, example: "$", "@bit": { isExample: true } }
4155
5425
  ],
4156
5426
  description: "An example for the marked content",
4157
- format: TagFormat.bitmarkText
5427
+ format: TagFormat.bitmarkText,
5428
+ nullable: true
4158
5429
  }
4159
5430
  ]
4160
5431
  }
@@ -4583,6 +5854,42 @@ var GROUPS = {
4583
5854
  }
4584
5855
  ]
4585
5856
  },
5857
+ [ConfigKey.group_backgroundWallpaper]: {
5858
+ type: GroupConfigType.standard,
5859
+ description: "Background wallpaper tags",
5860
+ tags: [
5861
+ {
5862
+ key: ConfigKey.resource_backgroundWallpaper,
5863
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
5864
+ description: "Background wallpaper for the image, used to set a background for the image",
5865
+ chain: [
5866
+ {
5867
+ key: ConfigKey.group_resourceImageCommon,
5868
+ description: "Common resource image tags for images"
5869
+ }
5870
+ ]
5871
+ }
5872
+ ]
5873
+ },
5874
+ [ConfigKey.group_imageNoZoom]: {
5875
+ type: GroupConfigType.standard,
5876
+ description: "Image bit tags",
5877
+ tags: [
5878
+ {
5879
+ key: ConfigKey.group_backgroundWallpaper,
5880
+ description: "Background wallpaper tags for images, used to define background properties for images"
5881
+ },
5882
+ {
5883
+ key: ConfigKey.group_resourceBitTags,
5884
+ description: "Resource bit tags for images, used to define additional properties for images"
5885
+ },
5886
+ {
5887
+ key: ConfigKey.group_resourceImageNoZoom,
5888
+ description: "Resource image tags for images, used to attach images to the bit",
5889
+ minCount: 1
5890
+ }
5891
+ ]
5892
+ },
4586
5893
  //
4587
5894
  // Resource groups
4588
5895
  //
@@ -4702,6 +6009,58 @@ var GROUPS = {
4702
6009
  }
4703
6010
  ]
4704
6011
  },
6012
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
6013
+ type: GroupConfigType.standard,
6014
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
6015
+ tags: [
6016
+ {
6017
+ key: ConfigKey.group_resourceCommon,
6018
+ description: "Common resource properties"
6019
+ },
6020
+ {
6021
+ key: ConfigKey.property_src1x,
6022
+ description: "The source URL for the image at 1x resolution",
6023
+ format: TagFormat.plainText
6024
+ },
6025
+ {
6026
+ key: ConfigKey.property_src2x,
6027
+ description: "The source URL for the image at 2x resolution",
6028
+ format: TagFormat.plainText
6029
+ },
6030
+ {
6031
+ key: ConfigKey.property_src3x,
6032
+ description: "The source URL for the image at 3x resolution",
6033
+ format: TagFormat.plainText
6034
+ },
6035
+ {
6036
+ key: ConfigKey.property_src4x,
6037
+ description: "The source URL for the image at 4x resolution",
6038
+ format: TagFormat.plainText
6039
+ },
6040
+ {
6041
+ key: ConfigKey.property_width,
6042
+ description: "The width of the image",
6043
+ format: TagFormat.number
6044
+ },
6045
+ {
6046
+ key: ConfigKey.property_height,
6047
+ description: "The height of the image",
6048
+ format: TagFormat.number
6049
+ },
6050
+ {
6051
+ key: ConfigKey.property_alt,
6052
+ description: "The alternative text for the image",
6053
+ format: TagFormat.plainText
6054
+ },
6055
+ // @zoomDisabled with a true default
6056
+ {
6057
+ key: ConfigKey.property_zoomDisabled,
6058
+ description: "If true, zooming is disabled for the image (defaults to true)",
6059
+ format: TagFormat.boolean,
6060
+ defaultValue: "true"
6061
+ }
6062
+ ]
6063
+ },
4705
6064
  [ConfigKey.group_resourceAudioCommon]: {
4706
6065
  type: GroupConfigType.standard,
4707
6066
  description: "Common properties for audio resources",
@@ -4791,22 +6150,26 @@ var GROUPS = {
4791
6150
  {
4792
6151
  key: ConfigKey.property_src1x,
4793
6152
  description: "The source URL for the video at 1x resolution",
4794
- format: TagFormat.plainText
6153
+ format: TagFormat.plainText,
6154
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4795
6155
  },
4796
6156
  {
4797
6157
  key: ConfigKey.property_src2x,
4798
6158
  description: "The source URL for the video at 2x resolution",
4799
- format: TagFormat.plainText
6159
+ format: TagFormat.plainText,
6160
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4800
6161
  },
4801
6162
  {
4802
6163
  key: ConfigKey.property_src3x,
4803
6164
  description: "The source URL for the video at 3x resolution",
4804
- format: TagFormat.plainText
6165
+ format: TagFormat.plainText,
6166
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4805
6167
  },
4806
6168
  {
4807
6169
  key: ConfigKey.property_src4x,
4808
6170
  description: "The source URL for the video at 4x resolution",
4809
- format: TagFormat.plainText
6171
+ format: TagFormat.plainText,
6172
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4810
6173
  }
4811
6174
  ]
4812
6175
  },
@@ -4819,6 +6182,7 @@ var GROUPS = {
4819
6182
  tags: [
4820
6183
  {
4821
6184
  key: ConfigKey.resource_icon,
6185
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4822
6186
  description: "The icon resource",
4823
6187
  chain: [
4824
6188
  {
@@ -4846,12 +6210,30 @@ var GROUPS = {
4846
6210
  }
4847
6211
  ]
4848
6212
  },
6213
+ [ConfigKey.group_resourceImageNoZoom]: {
6214
+ type: GroupConfigType.resource,
6215
+ description: "Image resource where @zoomDisabled defaults to true",
6216
+ tags: [
6217
+ {
6218
+ key: ConfigKey.resource_image,
6219
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6220
+ description: "The image resource (no-zoom default)",
6221
+ chain: [
6222
+ {
6223
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6224
+ description: "Common image properties with @zoomDisabled defaulting to true"
6225
+ }
6226
+ ]
6227
+ }
6228
+ ]
6229
+ },
4849
6230
  [ConfigKey.group_resourceImagePortrait]: {
4850
6231
  type: GroupConfigType.resource,
4851
6232
  description: "Portrait image resource",
4852
6233
  tags: [
4853
6234
  {
4854
6235
  key: ConfigKey.resource_imagePortrait,
6236
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4855
6237
  description: "The portrait image resource",
4856
6238
  chain: [
4857
6239
  {
@@ -4868,6 +6250,7 @@ var GROUPS = {
4868
6250
  tags: [
4869
6251
  {
4870
6252
  key: ConfigKey.resource_imageLandscape,
6253
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4871
6254
  description: "The landscape image resource",
4872
6255
  chain: [
4873
6256
  {
@@ -4884,6 +6267,7 @@ var GROUPS = {
4884
6267
  tags: [
4885
6268
  {
4886
6269
  key: ConfigKey.resource_imageEmbed,
6270
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4887
6271
  description: "The embedded image resource",
4888
6272
  chain: [
4889
6273
  {
@@ -5057,6 +6441,7 @@ var GROUPS = {
5057
6441
  tags: [
5058
6442
  {
5059
6443
  key: ConfigKey.resource_article,
6444
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
5060
6445
  description: "The article resource",
5061
6446
  chain: [
5062
6447
  {
@@ -5073,6 +6458,7 @@ var GROUPS = {
5073
6458
  tags: [
5074
6459
  {
5075
6460
  key: ConfigKey.resource_articleEmbed,
6461
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
5076
6462
  description: "The embedded article resource",
5077
6463
  chain: [
5078
6464
  {
@@ -5089,6 +6475,7 @@ var GROUPS = {
5089
6475
  tags: [
5090
6476
  {
5091
6477
  key: ConfigKey.resource_articleLink,
6478
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
5092
6479
  description: "The link to the article resource",
5093
6480
  chain: [
5094
6481
  {
@@ -5359,7 +6746,12 @@ var ConfigHydrator = class {
5359
6746
  isDefault: v.isDefault,
5360
6747
  sideJsonKey: v.sideJsonKey,
5361
6748
  sideExportJsonKey: v.sideExportJsonKey,
5362
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6749
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6750
+ // PLAN-085: cardinality fields pass through; defaults applied
6751
+ // downstream by the config consumer (Rust treats `0` / undefined
6752
+ // as unbounded).
6753
+ minCount: v.minCount,
6754
+ maxCount: v.maxCount
5363
6755
  };
5364
6756
  }
5365
6757
  return out;
@@ -5574,7 +6966,8 @@ var BITS = {
5574
6966
  { isExample: true, example: "$" }
5575
6967
  ],
5576
6968
  description: "The example text for the bit",
5577
- format: TagFormat.plainText
6969
+ format: TagFormat.plainText,
6970
+ nullable: true
5578
6971
  }
5579
6972
  ],
5580
6973
  rootExampleType: ExampleType.string
@@ -5913,7 +7306,8 @@ var BITS = {
5913
7306
  { isExample: true, example: "$" }
5914
7307
  ],
5915
7308
  description: "The example text for the bit",
5916
- format: TagFormat.plainText
7309
+ format: TagFormat.plainText,
7310
+ nullable: true
5917
7311
  }
5918
7312
  ],
5919
7313
  rootExampleType: ExampleType.string
@@ -6076,7 +7470,8 @@ var BITS = {
6076
7470
  { isExample: true, example: "$" }
6077
7471
  ],
6078
7472
  description: "The example text for the bit",
6079
- format: TagFormat.plainText
7473
+ format: TagFormat.plainText,
7474
+ nullable: true
6080
7475
  }
6081
7476
  ],
6082
7477
  rootExampleType: ExampleType.string
@@ -6227,7 +7622,8 @@ var BITS = {
6227
7622
  { isExample: true, example: "$" }
6228
7623
  ],
6229
7624
  description: "The example text for the bit",
6230
- format: TagFormat.plainText
7625
+ format: TagFormat.plainText,
7626
+ nullable: true
6231
7627
  }
6232
7628
  ],
6233
7629
  rootExampleType: ExampleType.string
@@ -6822,8 +8218,31 @@ var BITS = {
6822
8218
  description: "Tags for gaps in cloze and multiple choice text bits"
6823
8219
  },
6824
8220
  {
6825
- key: ConfigKey.group_trueFalse,
8221
+ key: ConfigKey.group_trueFalseInlineSelect,
6826
8222
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8223
+ },
8224
+ {
8225
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8226
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8227
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8228
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8229
+ key: ConfigKey.property_isCaseSensitive,
8230
+ exportJsonKey: {},
8231
+ description: "If the cloze answers are case sensitive",
8232
+ format: TagFormat.boolean,
8233
+ defaultValue: "true"
8234
+ },
8235
+ {
8236
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8237
+ key: ConfigKey.property_example,
8238
+ exportJsonKey: [
8239
+ { "@keyonly": { isExample: true } },
8240
+ { "@absent": { isExample: true } },
8241
+ { isExample: true, example: "$" }
8242
+ ],
8243
+ description: "The example(s) for the bit",
8244
+ format: TagFormat.bitmarkText,
8245
+ nullable: true
6827
8246
  }
6828
8247
  ]
6829
8248
  },
@@ -6862,18 +8281,56 @@ var BITS = {
6862
8281
  {
6863
8282
  key: ConfigKey.group_gap,
6864
8283
  description: "Tags for gaps in cloze bits"
8284
+ },
8285
+ {
8286
+ // Override inherited @example: cloze is in OLD parser's
8287
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8288
+ // emits `isExample: true`.
8289
+ key: ConfigKey.property_example,
8290
+ exportJsonKey: [
8291
+ { "@keyonly": { isExample: true } },
8292
+ { "@absent": { isExample: true } },
8293
+ { isExample: true, example: "$" }
8294
+ ],
8295
+ description: "The example(s) for the bit",
8296
+ format: TagFormat.bitmarkText,
8297
+ nullable: true
6865
8298
  }
6866
8299
  ]
6867
8300
  },
6868
8301
  [BitType.clozeInstructionGrouped]: {
6869
8302
  since: "1.3.0",
6870
8303
  baseBitType: BitType.cloze,
6871
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8304
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8305
+ tags: [
8306
+ {
8307
+ key: ConfigKey.property_quizCountItems,
8308
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8309
+ format: TagFormat.boolean,
8310
+ defaultValue: "true"
8311
+ }
8312
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8313
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8314
+ ]
6872
8315
  },
6873
8316
  [BitType.clozeSolutionGrouped]: {
6874
8317
  since: "1.3.0",
6875
8318
  baseBitType: BitType.cloze,
6876
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8319
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8320
+ tags: [
8321
+ {
8322
+ key: ConfigKey.property_quizCountItems,
8323
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8324
+ format: TagFormat.boolean,
8325
+ defaultValue: "true"
8326
+ },
8327
+ {
8328
+ key: ConfigKey.property_quizStrikethroughSolutions,
8329
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8330
+ format: TagFormat.boolean,
8331
+ defaultValue: "true"
8332
+ }
8333
+ ]
6877
8334
  },
6878
8335
  [BitType.clozeSeveral]: {
6879
8336
  since: "3.5.0",
@@ -6909,6 +8366,17 @@ var BITS = {
6909
8366
  {
6910
8367
  key: ConfigKey.group_quizCommon,
6911
8368
  description: "Common tags for quiz bits"
8369
+ },
8370
+ {
8371
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8372
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8373
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8374
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8375
+ key: ConfigKey.property_isCaseSensitive,
8376
+ exportJsonKey: {},
8377
+ description: "If the cloze answers are case sensitive",
8378
+ format: TagFormat.boolean,
8379
+ defaultValue: "true"
6912
8380
  }
6913
8381
  ],
6914
8382
  cardSet: CardSetConfigKey.clozeList
@@ -7075,6 +8543,7 @@ var BITS = {
7075
8543
  tags: [
7076
8544
  {
7077
8545
  key: ConfigKey.group_person,
8546
+ exportJsonKey: { partner: { name: "$" } },
7078
8547
  description: "Tags for the person in the conversation"
7079
8548
  }
7080
8549
  ]
@@ -7513,7 +8982,8 @@ var BITS = {
7513
8982
  { isExample: true, example: "$" }
7514
8983
  ],
7515
8984
  description: "The example text for the essay bit",
7516
- format: TagFormat.plainText
8985
+ format: TagFormat.plainText,
8986
+ nullable: true
7517
8987
  }
7518
8988
  ],
7519
8989
  rootExampleType: ExampleType.string
@@ -7547,7 +9017,8 @@ var BITS = {
7547
9017
  { isExample: true, example: "$" }
7548
9018
  ],
7549
9019
  description: "The example text for the example bit",
7550
- format: TagFormat.plainText
9020
+ format: TagFormat.plainText,
9021
+ nullable: true
7551
9022
  }
7552
9023
  ],
7553
9024
  rootExampleType: ExampleType.string
@@ -7633,7 +9104,19 @@ var BITS = {
7633
9104
  [BitType.articleAi]: {
7634
9105
  since: "1.3.0",
7635
9106
  baseBitType: BitType.article,
7636
- description: "Article AI bit, used to create AI-generated articles"
9107
+ description: "Article AI bit, used to create AI-generated articles",
9108
+ tags: [
9109
+ {
9110
+ // Shadows the inherited @aiGenerated from the standard group: this
9111
+ // bit type forces aiGenerated:true regardless of source value or
9112
+ // absence. Literal `true` in the export pattern ignores `$`.
9113
+ key: ConfigKey.property_aiGenerated,
9114
+ description: "AI-generated flag, forced true for article-ai bits",
9115
+ format: TagFormat.boolean,
9116
+ defaultValue: "true",
9117
+ exportJsonKey: { aiGenerated: true }
9118
+ }
9119
+ ]
7637
9120
  },
7638
9121
  [BitType.articleAttachment]: {
7639
9122
  since: "1.3.0",
@@ -7958,7 +9441,19 @@ var BITS = {
7958
9441
  [BitType.noteAi]: {
7959
9442
  since: "1.3.0",
7960
9443
  baseBitType: BitType.note,
7961
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9444
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9445
+ tags: [
9446
+ {
9447
+ // Shadows the inherited @aiGenerated from the standard group: this
9448
+ // bit type forces aiGenerated:true regardless of source value or
9449
+ // absence. Literal `true` in the export pattern ignores `$`.
9450
+ key: ConfigKey.property_aiGenerated,
9451
+ description: "AI-generated flag, forced true for article-ai bits",
9452
+ format: TagFormat.boolean,
9453
+ defaultValue: "true",
9454
+ exportJsonKey: { aiGenerated: true }
9455
+ }
9456
+ ]
7962
9457
  },
7963
9458
  [BitType.notebookArticle]: {
7964
9459
  since: "1.3.0",
@@ -8083,7 +9578,19 @@ var BITS = {
8083
9578
  [BitType.summaryAi]: {
8084
9579
  since: "1.3.0",
8085
9580
  baseBitType: BitType.summary,
8086
- description: "AI-generated summary bit"
9581
+ description: "AI-generated summary bit",
9582
+ tags: [
9583
+ {
9584
+ // Shadows the inherited @aiGenerated from the standard group: this
9585
+ // bit type forces aiGenerated:true regardless of source value or
9586
+ // absence. Literal `true` in the export pattern ignores `$`.
9587
+ key: ConfigKey.property_aiGenerated,
9588
+ description: "AI-generated flag, forced true for article-ai bits",
9589
+ format: TagFormat.boolean,
9590
+ defaultValue: "true",
9591
+ exportJsonKey: { aiGenerated: true }
9592
+ }
9593
+ ]
8087
9594
  },
8088
9595
  [BitType.videoTranscript]: {
8089
9596
  since: "1.3.0",
@@ -8357,7 +9864,7 @@ var BITS = {
8357
9864
  description: "Product ID for the module product, used to link to a specific product",
8358
9865
  format: TagFormat.plainText,
8359
9866
  minCount: 1,
8360
- maxCount: Count.infinity
9867
+ maxCount: 1
8361
9868
  }
8362
9869
  ]
8363
9870
  },
@@ -8410,10 +9917,13 @@ var BITS = {
8410
9917
  description: "Resource bit tags for logo grave images, used to define additional properties"
8411
9918
  },
8412
9919
  {
8413
- // Image resource
8414
- key: ConfigKey.group_resourceImage,
9920
+ // Image resource — fully-expanded shape so each `&image` emission
9921
+ // produces a complete `{type, image: {src}}` object inside the
9922
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
9923
+ // `images[].image` via the `[]` last-element-merge semantic.
9924
+ key: ConfigKey.group_resourceImageNoZoom,
8415
9925
  description: "Resource image tags for logo grave images, used to attach images",
8416
- exportJsonKey: { images: "$" },
9926
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8417
9927
  minCount: 1,
8418
9928
  maxCount: Count.infinity
8419
9929
  }
@@ -8703,6 +10213,10 @@ var BITS = {
8703
10213
  tags: [
8704
10214
  {
8705
10215
  key: ConfigKey.property_book,
10216
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10217
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10218
+ // inside the most-recently-appended entry as `reference: $`.
10219
+ exportJsonKey: { book: [{ book: "$" }] },
8706
10220
  description: "Book reference for the page, used to link to a specific book",
8707
10221
  maxCount: Count.infinity,
8708
10222
  chain: [
@@ -8770,7 +10284,9 @@ var BITS = {
8770
10284
  format: TagFormat.plainText
8771
10285
  }
8772
10286
  ],
8773
- cardSet: CardSetConfigKey.exampleBitList
10287
+ // page-footer emits cards under `sections` (vs `listItems` for the
10288
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10289
+ cardSet: CardSetConfigKey.pageFooterSections
8774
10290
  },
8775
10291
  [BitType.legend]: {
8776
10292
  since: "3.12.0",
@@ -8812,6 +10328,20 @@ var BITS = {
8812
10328
  since: "1.34.0",
8813
10329
  baseBitType: BitType._standard,
8814
10330
  description: "Definition list bit, used to create lists of definitions in articles or books",
10331
+ tags: [
10332
+ {
10333
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10334
+ key: ConfigKey.property_example,
10335
+ exportJsonKey: [
10336
+ { "@keyonly": { isExample: true } },
10337
+ { "@absent": { isExample: true } },
10338
+ { isExample: true, example: "$" }
10339
+ ],
10340
+ description: "The example(s) for the bit",
10341
+ format: TagFormat.bitmarkText,
10342
+ nullable: true
10343
+ }
10344
+ ],
8815
10345
  cardSet: CardSetConfigKey.definitionList
8816
10346
  },
8817
10347
  [BitType.metaSearchDefaultTerms]: {
@@ -8834,6 +10364,18 @@ var BITS = {
8834
10364
  {
8835
10365
  key: ConfigKey.group_quizCommon,
8836
10366
  description: "Common quiz tags for flashcards"
10367
+ },
10368
+ {
10369
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10370
+ key: ConfigKey.property_example,
10371
+ exportJsonKey: [
10372
+ { "@keyonly": { isExample: true } },
10373
+ { "@absent": { isExample: true } },
10374
+ { isExample: true, example: "$" }
10375
+ ],
10376
+ description: "The example(s) for the bit",
10377
+ format: TagFormat.bitmarkText,
10378
+ nullable: true
8837
10379
  }
8838
10380
  ],
8839
10381
  cardSet: CardSetConfigKey.flashcard
@@ -8841,7 +10383,11 @@ var BITS = {
8841
10383
  [BitType.flashcard1]: {
8842
10384
  since: "1.3.0",
8843
10385
  baseBitType: BitType.flashcard,
8844
- description: "Flashcard 1 bit"
10386
+ description: "Flashcard 1 bit",
10387
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10388
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10389
+ // bit type structurally allows only a single card.
10390
+ cardSet: CardSetConfigKey.flashcard1
8845
10391
  },
8846
10392
  [BitType.qAndACard]: {
8847
10393
  since: "3.25.0",
@@ -8876,8 +10422,24 @@ var BITS = {
8876
10422
  description: "Common quiz tags for highlighted text"
8877
10423
  },
8878
10424
  {
8879
- key: ConfigKey.group_trueFalse,
8880
- description: "True/False quiz tags for highlighted text"
10425
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10426
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10427
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10428
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10429
+ key: ConfigKey.group_trueFalseInlineHighlight,
10430
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10431
+ },
10432
+ {
10433
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10434
+ key: ConfigKey.property_example,
10435
+ exportJsonKey: [
10436
+ { "@keyonly": { isExample: true } },
10437
+ { "@absent": { isExample: true } },
10438
+ { isExample: true, example: "$" }
10439
+ ],
10440
+ description: "The example(s) for the bit",
10441
+ format: TagFormat.bitmarkText,
10442
+ nullable: true
8881
10443
  }
8882
10444
  ]
8883
10445
  },
@@ -8887,15 +10449,8 @@ var BITS = {
8887
10449
  description: "Image bit, used to create images in articles or books",
8888
10450
  tags: [
8889
10451
  {
8890
- key: ConfigKey.resource_backgroundWallpaper,
8891
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8892
- description: "Background wallpaper for the image, used to set a background for the image",
8893
- chain: [
8894
- {
8895
- key: ConfigKey.group_resourceImageCommon,
8896
- description: "Common resource image tags for images"
8897
- }
8898
- ]
10452
+ key: ConfigKey.group_backgroundWallpaper,
10453
+ description: "Background wallpaper tags for images, used to define background properties for images"
8899
10454
  },
8900
10455
  {
8901
10456
  key: ConfigKey.group_resourceBitTags,
@@ -8998,8 +10553,15 @@ var BITS = {
8998
10553
  },
8999
10554
  [BitType.imageSeparator]: {
9000
10555
  since: "1.4.15",
9001
- baseBitType: BitType.image,
9002
- description: "Image separator bit, used to create separators in articles or books"
10556
+ baseBitType: BitType._standard,
10557
+ description: "Image separator bit, used to create separators in articles or books",
10558
+ tags: [
10559
+ {
10560
+ key: ConfigKey.group_imageNoZoom,
10561
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10562
+ }
10563
+ ],
10564
+ resourceAttachmentAllowed: false
9003
10565
  },
9004
10566
  [BitType.imageSeparatorAlt]: {
9005
10567
  since: "1.16.0",
@@ -9038,15 +10600,20 @@ var BITS = {
9038
10600
  },
9039
10601
  [BitType.pageBanner]: {
9040
10602
  since: "1.4.3",
9041
- baseBitType: BitType.image,
10603
+ baseBitType: BitType._standard,
9042
10604
  description: "Page banner bit, used to create banners for pages in articles or books",
9043
10605
  tags: [
9044
10606
  {
9045
10607
  key: ConfigKey.property_slug,
9046
10608
  description: "Slug for the page banner, used to identify the banner in the system",
9047
10609
  format: TagFormat.plainText
10610
+ },
10611
+ {
10612
+ key: ConfigKey.group_imageNoZoom,
10613
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
9048
10614
  }
9049
- ]
10615
+ ],
10616
+ resourceAttachmentAllowed: false
9050
10617
  },
9051
10618
  [BitType.pageHero]: {
9052
10619
  since: "1.11.0",
@@ -9061,7 +10628,7 @@ var BITS = {
9061
10628
  },
9062
10629
  [BitType.tableImage]: {
9063
10630
  since: "1.5.15",
9064
- baseBitType: BitType.table,
10631
+ baseBitType: BitType.tableExtended,
9065
10632
  description: "Table image bit, used to create images in tables in articles or books",
9066
10633
  tags: [
9067
10634
  {
@@ -9070,15 +10637,8 @@ var BITS = {
9070
10637
  format: TagFormat.bitmarkText
9071
10638
  },
9072
10639
  {
9073
- key: ConfigKey.resource_backgroundWallpaper,
9074
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
9075
- description: "Background wallpaper for the image, used to set a background for the image",
9076
- chain: [
9077
- {
9078
- key: ConfigKey.group_resourceImageCommon,
9079
- description: "Common resource image tags for images"
9080
- }
9081
- ]
10640
+ key: ConfigKey.group_backgroundWallpaper,
10641
+ description: "Background wallpaper tags for images, used to define background properties for images"
9082
10642
  },
9083
10643
  {
9084
10644
  key: ConfigKey.group_resourceBitTags,
@@ -9221,10 +10781,10 @@ var BITS = {
9221
10781
  description: "Resource bit tags for logo grave images, used to define additional properties"
9222
10782
  },
9223
10783
  {
9224
- // Image resource
9225
- key: ConfigKey.group_resourceImage,
10784
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10785
+ key: ConfigKey.group_resourceImageNoZoom,
9226
10786
  description: "Resource image tags for logo grave images, used to attach images",
9227
- exportJsonKey: { logos: "$" },
10787
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9228
10788
  minCount: 1,
9229
10789
  maxCount: Count.infinity
9230
10790
  }
@@ -9234,7 +10794,17 @@ var BITS = {
9234
10794
  [BitType.prototypeImages]: {
9235
10795
  since: "1.6.1",
9236
10796
  baseBitType: BitType.imagesLogoGrave,
9237
- description: "Prototype images bit, used to create prototype images in articles or books"
10797
+ description: "Prototype images bit, used to create prototype images in articles or books",
10798
+ tags: [
10799
+ {
10800
+ // Override inherited `logos` wrapper with `images` for this bit type.
10801
+ key: ConfigKey.group_resourceImageNoZoom,
10802
+ description: "Resource image tags for prototype images",
10803
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10804
+ minCount: 1,
10805
+ maxCount: Count.infinity
10806
+ }
10807
+ ]
9238
10808
  },
9239
10809
  [BitType.internalLink]: {
9240
10810
  since: "1.3.0",
@@ -9264,6 +10834,18 @@ var BITS = {
9264
10834
  exportJsonKey: {},
9265
10835
  description: "Reasonable number of characters for the interview, used to limit input size",
9266
10836
  format: TagFormat.number
10837
+ },
10838
+ {
10839
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10840
+ key: ConfigKey.property_example,
10841
+ exportJsonKey: [
10842
+ { "@keyonly": { isExample: true } },
10843
+ { "@absent": { isExample: true } },
10844
+ { isExample: true, example: "$" }
10845
+ ],
10846
+ description: "The example(s) for the bit",
10847
+ format: TagFormat.bitmarkText,
10848
+ nullable: true
9267
10849
  }
9268
10850
  ],
9269
10851
  cardSet: CardSetConfigKey.questions
@@ -9622,6 +11204,18 @@ var BITS = {
9622
11204
  {
9623
11205
  key: ConfigKey.group_mark,
9624
11206
  description: "Mark tags, used to define the content of marks"
11207
+ },
11208
+ {
11209
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11210
+ key: ConfigKey.property_example,
11211
+ exportJsonKey: [
11212
+ { "@keyonly": { isExample: true } },
11213
+ { "@absent": { isExample: true } },
11214
+ { isExample: true, example: "$" }
11215
+ ],
11216
+ description: "The example(s) for the bit",
11217
+ format: TagFormat.bitmarkText,
11218
+ nullable: true
9625
11219
  }
9626
11220
  ]
9627
11221
  },
@@ -9641,6 +11235,23 @@ var BITS = {
9641
11235
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9642
11236
  format: TagFormat.boolean,
9643
11237
  defaultValue: "true"
11238
+ },
11239
+ {
11240
+ // Override inherited @example: in `isTopLevelExample` allow-list
11241
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11242
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11243
+ // Bit-level fires emit only `isExample`; the @example value (when
11244
+ // provided) is recorded in the ancestor stack and consumed by
11245
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11246
+ key: ConfigKey.property_example,
11247
+ exportJsonKey: [
11248
+ { "@keyonly": { isExample: true } },
11249
+ { "@absent": { isExample: true } },
11250
+ { isExample: true }
11251
+ ],
11252
+ description: "The example(s) for the bit",
11253
+ format: TagFormat.bitmarkText,
11254
+ nullable: true
9644
11255
  }
9645
11256
  ],
9646
11257
  cardSet: CardSetConfigKey.matchPairs
@@ -9725,6 +11336,18 @@ var BITS = {
9725
11336
  {
9726
11337
  key: ConfigKey.group_trueFalse,
9727
11338
  description: "True/False tags for multiple choice 1 questions"
11339
+ },
11340
+ {
11341
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11342
+ key: ConfigKey.property_example,
11343
+ exportJsonKey: [
11344
+ { "@keyonly": { isExample: true } },
11345
+ { "@absent": { isExample: true } },
11346
+ { isExample: true, example: "$" }
11347
+ ],
11348
+ description: "The example(s) for the bit",
11349
+ format: TagFormat.bitmarkText,
11350
+ nullable: true
9728
11351
  }
9729
11352
  ]
9730
11353
  },
@@ -9746,8 +11369,20 @@ var BITS = {
9746
11369
  {
9747
11370
  key: ConfigKey.group_trueFalse,
9748
11371
  description: "True/False tags for multiple choice questions"
9749
- }
11372
+ },
9750
11373
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11374
+ {
11375
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11376
+ key: ConfigKey.property_example,
11377
+ exportJsonKey: [
11378
+ { "@keyonly": { isExample: true } },
11379
+ { "@absent": { isExample: true } },
11380
+ { isExample: true, example: "$" }
11381
+ ],
11382
+ description: "The example(s) for the bit",
11383
+ format: TagFormat.bitmarkText,
11384
+ nullable: true
11385
+ }
9751
11386
  ],
9752
11387
  cardSet: CardSetConfigKey.quiz
9753
11388
  },
@@ -9767,8 +11402,20 @@ var BITS = {
9767
11402
  description: "Common quiz tags for multiple choice text questions"
9768
11403
  },
9769
11404
  {
9770
- key: ConfigKey.group_trueFalse,
11405
+ key: ConfigKey.group_trueFalseInlineSelect,
9771
11406
  description: "True/False tags for multiple choice text questions"
11407
+ },
11408
+ {
11409
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11410
+ key: ConfigKey.property_example,
11411
+ exportJsonKey: [
11412
+ { "@keyonly": { isExample: true } },
11413
+ { "@absent": { isExample: true } },
11414
+ { isExample: true, example: "$" }
11415
+ ],
11416
+ description: "The example(s) for the bit",
11417
+ format: TagFormat.bitmarkText,
11418
+ nullable: true
9772
11419
  }
9773
11420
  ]
9774
11421
  },
@@ -9793,8 +11440,20 @@ var BITS = {
9793
11440
  description: "Common quiz tags for multiple response 1 questions"
9794
11441
  },
9795
11442
  {
9796
- key: ConfigKey.group_trueFalse,
11443
+ key: ConfigKey.group_trueFalseResponses,
9797
11444
  description: "True/False tags for multiple response 1 questions"
11445
+ },
11446
+ {
11447
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11448
+ key: ConfigKey.property_example,
11449
+ exportJsonKey: [
11450
+ { "@keyonly": { isExample: true } },
11451
+ { "@absent": { isExample: true } },
11452
+ { isExample: true, example: "$" }
11453
+ ],
11454
+ description: "The example(s) for the bit",
11455
+ format: TagFormat.bitmarkText,
11456
+ nullable: true
9798
11457
  }
9799
11458
  ]
9800
11459
  },
@@ -9814,12 +11473,24 @@ var BITS = {
9814
11473
  description: "Common quiz tags for multiple response questions"
9815
11474
  },
9816
11475
  {
9817
- key: ConfigKey.group_trueFalse,
11476
+ key: ConfigKey.group_trueFalseResponses,
9818
11477
  description: "True/False tags for multiple response questions"
9819
- }
11478
+ },
9820
11479
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11480
+ {
11481
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11482
+ key: ConfigKey.property_example,
11483
+ exportJsonKey: [
11484
+ { "@keyonly": { isExample: true } },
11485
+ { "@absent": { isExample: true } },
11486
+ { isExample: true, example: "$" }
11487
+ ],
11488
+ description: "The example(s) for the bit",
11489
+ format: TagFormat.bitmarkText,
11490
+ nullable: true
11491
+ }
9821
11492
  ],
9822
- cardSet: CardSetConfigKey.quiz
11493
+ cardSet: CardSetConfigKey.quizResponses
9823
11494
  },
9824
11495
  [BitType.coachSelfReflectionMultipleResponse]: {
9825
11496
  since: "1.3.0",
@@ -10069,6 +11740,8 @@ var BITS = {
10069
11740
  },
10070
11741
  {
10071
11742
  key: ConfigKey.resource_platformIcon,
11743
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11744
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
10072
11745
  description: "The platform icon",
10073
11746
  chain: [
10074
11747
  {
@@ -10104,6 +11777,8 @@ var BITS = {
10104
11777
  tags: [
10105
11778
  {
10106
11779
  key: ConfigKey.resource_platformLogo,
11780
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11781
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10107
11782
  description: "The platform logo",
10108
11783
  chain: [
10109
11784
  {
@@ -10315,7 +11990,8 @@ var BITS = {
10315
11990
  { isExample: true, example: "$" }
10316
11991
  ],
10317
11992
  description: "The example text for the review note bit",
10318
- format: TagFormat.plainText
11993
+ format: TagFormat.plainText,
11994
+ nullable: true
10319
11995
  }
10320
11996
  ],
10321
11997
  rootExampleType: ExampleType.string
@@ -10402,7 +12078,8 @@ var BITS = {
10402
12078
  { isExample: true, example: "$" }
10403
12079
  ],
10404
12080
  description: "The example flag for the bit (boolean)",
10405
- format: TagFormat.plainText
12081
+ format: TagFormat.boolean,
12082
+ nullable: true
10406
12083
  }
10407
12084
  ],
10408
12085
  cardSet: CardSetConfigKey.elements,
@@ -10676,15 +12353,8 @@ var BITS = {
10676
12353
  format: TagFormat.bitmarkText
10677
12354
  },
10678
12355
  {
10679
- key: ConfigKey.resource_backgroundWallpaper,
10680
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10681
- description: "Background wallpaper for the image, used to set a background for the image",
10682
- chain: [
10683
- {
10684
- key: ConfigKey.group_resourceImageCommon,
10685
- description: "Common resource image tags for images"
10686
- }
10687
- ]
12356
+ key: ConfigKey.group_backgroundWallpaper,
12357
+ description: "Background wallpaper tags for images, used to define background properties for images"
10688
12358
  },
10689
12359
  {
10690
12360
  key: ConfigKey.group_resourceBitTags,
@@ -11061,16 +12731,20 @@ var BITS = {
11061
12731
  format: TagFormat.plainText
11062
12732
  },
11063
12733
  {
11064
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12734
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12735
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12736
+ // not a `choices[]` array. Format is bitmarkText so the statement
12737
+ // body renders as a ProseMirror tree, matching reference fixtures.
12738
+ exportJsonKey: { statement: "$", isCorrect: true },
11065
12739
  key: ConfigKey.tag_true,
11066
12740
  description: "Tag for the true option in the true/false question",
11067
- format: TagFormat.plainText
12741
+ format: TagFormat.bitmarkText
11068
12742
  },
11069
12743
  {
11070
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12744
+ exportJsonKey: { statement: "$", isCorrect: false },
11071
12745
  key: ConfigKey.tag_false,
11072
12746
  description: "Tag for the false option in the true/false question",
11073
- format: TagFormat.plainText
12747
+ format: TagFormat.bitmarkText
11074
12748
  },
11075
12749
  {
11076
12750
  // Override inherited @example from group_standardTags: boolean root
@@ -11081,14 +12755,24 @@ var BITS = {
11081
12755
  // sufficient for `[+...]` (correct) statements which dominate the
11082
12756
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
11083
12757
  // need either a runtime extension or an explicit `[@example:false]`.
12758
+ //
12759
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12760
+ // (a natural default that gets stripped in optimised mode, matching
12761
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12762
+ // STRING `"false"` which doesn't strip.
11084
12763
  key: ConfigKey.property_example,
11085
12764
  exportJsonKey: [
11086
12765
  { "@keyonly": { isExample: true, example: true } },
11087
12766
  { "@absent": { isExample: true, example: true } },
12767
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12768
+ // is the natural default for boolean, stripped in optimised mode.
12769
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12770
+ { "@value=false": { isExample: true } },
11088
12771
  { isExample: true, example: "$" }
11089
12772
  ],
11090
12773
  description: "The example flag for the bit (boolean)",
11091
- format: TagFormat.plainText
12774
+ format: TagFormat.boolean,
12775
+ nullable: true
11092
12776
  }
11093
12777
  ],
11094
12778
  rootExampleType: ExampleType.boolean
@@ -11112,6 +12796,18 @@ var BITS = {
11112
12796
  key: ConfigKey.property_labelFalse,
11113
12797
  description: "Label for the false option in the true/false question",
11114
12798
  format: TagFormat.plainText
12799
+ },
12800
+ {
12801
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12802
+ key: ConfigKey.property_example,
12803
+ exportJsonKey: [
12804
+ { "@keyonly": { isExample: true } },
12805
+ { "@absent": { isExample: true } },
12806
+ { isExample: true, example: "$" }
12807
+ ],
12808
+ description: "The example(s) for the bit",
12809
+ format: TagFormat.bitmarkText,
12810
+ nullable: true
11115
12811
  }
11116
12812
  ],
11117
12813
  cardSet: CardSetConfigKey.statements
@@ -11301,6 +12997,11 @@ var BITS = {
11301
12997
  baseBitType: BitType.vendorStripePricingTable,
11302
12998
  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",
11303
12999
  tags: [
13000
+ {
13001
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
13002
+ description: "Descriptive name for the Stripe pricing table",
13003
+ format: TagFormat.plainText
13004
+ },
11304
13005
  {
11305
13006
  key: ConfigKey.property_printOnRequestCustomerId,
11306
13007
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11777,7 +13478,7 @@ var instance2 = new Config();
11777
13478
  // src/generated/package_info.ts
11778
13479
  var PACKAGE_INFO = {
11779
13480
  "name": "@gmb/bitmark-parser-generator",
11780
- "version": "5.23.0",
13481
+ "version": "5.24.0",
11781
13482
  "author": "Get More Brain Ltd",
11782
13483
  "license": "ISC",
11783
13484
  "description": "A bitmark parser and generator using Peggy.js"
@@ -12742,6 +14443,8 @@ var NodeType = {
12742
14443
  stillImageFilmLink: "stillImageFilmLink",
12743
14444
  stripePricingTableId: "stripePricingTableId",
12744
14445
  stripePricingTableIdValue: "stripePricingTableIdValue",
14446
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14447
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12745
14448
  stripePublishableKey: "stripePublishableKey",
12746
14449
  stripePublishableKeyValue: "stripePublishableKeyValue",
12747
14450
  subject: "subject",
@@ -26167,6 +27870,12 @@ var Builder = class extends BaseBuilder {
26167
27870
  data.stripePricingTableId,
26168
27871
  options
26169
27872
  ),
27873
+ stripePricingTableDescriptiveName: this.toAstProperty(
27874
+ bitType,
27875
+ ConfigKey.property_stripePricingTableDescriptiveName,
27876
+ data.stripePricingTableDescriptiveName,
27877
+ options
27878
+ ),
26170
27879
  stripePublishableKey: this.toAstProperty(
26171
27880
  bitType,
26172
27881
  ConfigKey.property_stripePublishableKey,
@@ -34412,6 +36121,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34412
36121
  const cardSetType = bitConfig.cardSet?.configKey;
34413
36122
  switch (cardSetType) {
34414
36123
  case CardSetConfigKey.flashcard:
36124
+ case CardSetConfigKey.flashcard1:
34415
36125
  case CardSetConfigKey.definitionList:
34416
36126
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34417
36127
  break;
@@ -34425,6 +36135,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34425
36135
  result = parseFeedback(context, bitType, processedCardSet);
34426
36136
  break;
34427
36137
  case CardSetConfigKey.quiz:
36138
+ case CardSetConfigKey.quizResponses:
34428
36139
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34429
36140
  break;
34430
36141
  case CardSetConfigKey.questions:
@@ -34451,6 +36162,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34451
36162
  break;
34452
36163
  case CardSetConfigKey.clozeList:
34453
36164
  case CardSetConfigKey.exampleBitList:
36165
+ case CardSetConfigKey.pageFooterSections:
34454
36166
  case CardSetConfigKey.bookReferenceList:
34455
36167
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34456
36168
  break;
@@ -41245,6 +42957,13 @@ var ConfigBuilder = class {
41245
42957
  const cardSetConfig = CARDSETS[cardSetKey];
41246
42958
  if (!cardSetConfig) return void 0;
41247
42959
  const normalizedKey = normalizeCardKey(cardSetKey);
42960
+ const variantTagEntriesTypeOrder = [
42961
+ BitTagConfigKeyType.group,
42962
+ BitTagConfigKeyType.tag,
42963
+ BitTagConfigKeyType.property,
42964
+ BitTagConfigKeyType.resource,
42965
+ BitTagConfigKeyType.unknown
42966
+ ];
41248
42967
  const sides = cardSetConfig.sides.map((side, sideIdx) => {
41249
42968
  const sidePath = `cardSets.${normalizedKey} / sides.${side.name ?? `[${sideIdx}]`}`;
41250
42969
  const variants = side.variants.map((variant, variantIdx) => {
@@ -41255,7 +42974,7 @@ var ConfigBuilder = class {
41255
42974
  const tagB = b[1];
41256
42975
  const typeA = typeFromConfigKey(tagA.key);
41257
42976
  const typeB = typeFromConfigKey(tagB.key);
41258
- const typeOrder = tagEntriesTypeOrder.indexOf(typeA) - tagEntriesTypeOrder.indexOf(typeB);
42977
+ const typeOrder = variantTagEntriesTypeOrder.indexOf(typeA) - variantTagEntriesTypeOrder.indexOf(typeB);
41259
42978
  return typeOrder;
41260
42979
  });
41261
42980
  for (const [, variantTag] of variantTagEntries) {
@@ -41295,6 +43014,10 @@ var ConfigBuilder = class {
41295
43014
  name: sectionName,
41296
43015
  ...section.isDefault ? { isDefault: true } : {},
41297
43016
  ...cardJsonKeyField(section, "jsonKey", "exportJsonKey", sectionPath),
43017
+ // PLAN-085: per-section cardinality. Emit only when non-default
43018
+ // (treat `0` / undefined as "unbounded — omit").
43019
+ ...section.minCount != null && section.minCount !== 0 ? { min: section.minCount } : {},
43020
+ ...section.maxCount != null && section.maxCount !== 0 ? { max: section.maxCount } : {},
41298
43021
  sides: cardSides
41299
43022
  };
41300
43023
  });
@@ -41364,10 +43087,20 @@ var ConfigBuilder = class {
41364
43087
  return typeOrder;
41365
43088
  });
41366
43089
  const isInheritedFrom = bitGroupConfigKeys.has(b.bitType);
43090
+ const indexByKey = /* @__PURE__ */ new Map();
43091
+ const pushOrReplace = (entry) => {
43092
+ const existing = indexByKey.get(entry.key);
43093
+ if (existing !== void 0) {
43094
+ tags2[existing] = entry;
43095
+ } else {
43096
+ indexByKey.set(entry.key, tags2.length);
43097
+ tags2.push(entry);
43098
+ }
43099
+ };
41367
43100
  if (isInheritedFrom) {
41368
43101
  const resolvedGroups = resolveGroupReferences(`group-${b.bitType}`);
41369
43102
  for (const ref of resolvedGroups) {
41370
- tags2.push({
43103
+ pushOrReplace({
41371
43104
  type: "group",
41372
43105
  key: ref.key,
41373
43106
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -41380,7 +43113,7 @@ var ConfigBuilder = class {
41380
43113
  if (b.baseBitType) {
41381
43114
  const resolvedGroups = resolveGroupReferences(`group-${b.baseBitType}`);
41382
43115
  for (const ref of resolvedGroups) {
41383
- tags2.push({
43116
+ pushOrReplace({
41384
43117
  type: "group",
41385
43118
  key: ref.key,
41386
43119
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -41391,7 +43124,9 @@ var ConfigBuilder = class {
41391
43124
  }
41392
43125
  }
41393
43126
  for (const [_tagKey, tag] of tagEntries) {
41394
- tags2.push(...processTagEntries(tag, []));
43127
+ for (const entry of processTagEntries(tag, [])) {
43128
+ pushOrReplace(entry);
43129
+ }
41395
43130
  }
41396
43131
  }
41397
43132
  let cardRef;