@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.
@@ -778,10 +778,12 @@ var TextFormat2 = {
778
778
  var import_superenum7 = require("@ncoderz/superenum");
779
779
  var CardSetConfigKey = {
780
780
  flashcard: "flashcard",
781
+ flashcard1: "flashcard1",
781
782
  definitionList: "definitionList",
782
783
  elements: "elements",
783
784
  statements: "statements",
784
785
  quiz: "quiz",
786
+ quizResponses: "quizResponses",
785
787
  feedback: "feedback",
786
788
  questions: "questions",
787
789
  matchPairs: "matchPairs",
@@ -793,6 +795,7 @@ var CardSetConfigKey = {
793
795
  pronunciationTable: "pronunciationTable",
794
796
  botActionResponses: "botActionResponses",
795
797
  exampleBitList: "exampleBitList",
798
+ pageFooterSections: "pageFooterSections",
796
799
  clozeList: "clozeList",
797
800
  ingredients: "ingredients",
798
801
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1209,12 +1212,17 @@ var groupKeys = {
1209
1212
  group_standardAllBits: "group_standardAllBits",
1210
1213
  group_standardItemLead: "group_standardItemLead",
1211
1214
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1215
+ group_standardInstructionHint: "group_standardInstructionHint",
1212
1216
  group_standardTags: "group_standardTags",
1217
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1213
1218
  group_imageSource: "group_imageSource",
1214
1219
  group_technicalTerm: "group_technicalTerm",
1215
1220
  group_person: "group_person",
1216
1221
  group_gap: "group_gap",
1217
1222
  group_trueFalse: "group_trueFalse",
1223
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1224
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1225
+ group_trueFalseResponses: "group_trueFalseResponses",
1218
1226
  group_markConfig: "group_markConfig",
1219
1227
  group_mark: "group_mark",
1220
1228
  group_bookCommon: "group_bookCommon",
@@ -1222,14 +1230,18 @@ var groupKeys = {
1222
1230
  group_advertisingCommon: "group_advertisingCommon",
1223
1231
  group_platformStylesCommon: "group_platformStylesCommon",
1224
1232
  group_quizCommon: "group_quizCommon",
1233
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1234
+ group_imageNoZoom: "group_imageNoZoom",
1225
1235
  group_resourceBitTags: "group_resourceBitTags",
1226
1236
  group_resourceCommon: "group_resourceCommon",
1227
1237
  group_resourceImageCommon: "group_resourceImageCommon",
1238
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1228
1239
  group_resourceAudioCommon: "group_resourceAudioCommon",
1229
1240
  group_resourceVideoCommon: "group_resourceVideoCommon",
1230
1241
  group_resourceIcon: "group_resourceIcon",
1231
1242
  // Alias for image
1232
1243
  group_resourceImage: "group_resourceImage",
1244
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1233
1245
  group_resourceImageEmbed: "group_resourceImageEmbed",
1234
1246
  group_resourceImageLink: "group_resourceImageLink",
1235
1247
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1506,6 +1518,7 @@ var propertyKeys = {
1506
1518
  property_src4x: "@src4x",
1507
1519
  property_srcAlt: "@srcAlt",
1508
1520
  property_stripePricingTableId: "@stripePricingTableId",
1521
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1509
1522
  property_stripePublishableKey: "@stripePublishableKey",
1510
1523
  property_subject: "@subject",
1511
1524
  property_subtype: "@subtype",
@@ -2257,7 +2270,7 @@ var CARDSETS = {
2257
2270
  variants: [
2258
2271
  {
2259
2272
  jsonKey: "question.text",
2260
- exportJsonKey: { question: { text: "$" } },
2273
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2261
2274
  tags: [
2262
2275
  {
2263
2276
  key: ConfigKey.group_standardTags,
@@ -2273,6 +2286,136 @@ var CARDSETS = {
2273
2286
  description: "Icon resource for the flashcard.",
2274
2287
  jsonKey: "question.icon|resource(type=image, key=image)",
2275
2288
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2289
+ },
2290
+ {
2291
+ key: ConfigKey.property_example,
2292
+ exportJsonKey: [
2293
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2294
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2295
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2296
+ ],
2297
+ description: "Example marker for the flashcard question.",
2298
+ format: TagFormat.bitmarkText,
2299
+ nullable: true
2300
+ }
2301
+ ]
2302
+ }
2303
+ ]
2304
+ },
2305
+ {
2306
+ name: "answer",
2307
+ variants: [
2308
+ {
2309
+ jsonKey: "answer.text",
2310
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2311
+ tags: [
2312
+ {
2313
+ key: ConfigKey.group_standardTags,
2314
+ description: "Standard tags for the flashcard."
2315
+ },
2316
+ {
2317
+ exportJsonKey: { title: "$" },
2318
+ key: ConfigKey.tag_title,
2319
+ description: "Title of the flashcard."
2320
+ },
2321
+ {
2322
+ key: ConfigKey.group_resourceIcon,
2323
+ description: "Icon resource for the flashcard.",
2324
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2325
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2326
+ },
2327
+ {
2328
+ key: ConfigKey.property_example,
2329
+ exportJsonKey: [
2330
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2331
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2332
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2333
+ ],
2334
+ description: "Example marker for the flashcard answer.",
2335
+ format: TagFormat.bitmarkText,
2336
+ nullable: true
2337
+ }
2338
+ ]
2339
+ },
2340
+ {
2341
+ jsonKey: "alternativeAnswers[].text",
2342
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2343
+ tags: [
2344
+ {
2345
+ key: ConfigKey.group_standardTags,
2346
+ description: "Standard tags for the flashcard."
2347
+ },
2348
+ {
2349
+ exportJsonKey: { title: "$" },
2350
+ key: ConfigKey.tag_title,
2351
+ description: "Title of the flashcard."
2352
+ },
2353
+ {
2354
+ key: ConfigKey.group_resourceIcon,
2355
+ description: "Icon resource for the flashcard.",
2356
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2357
+ exportJsonKey: {
2358
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2359
+ }
2360
+ }
2361
+ ],
2362
+ repeatCount: Count.infinity
2363
+ }
2364
+ ]
2365
+ }
2366
+ ]
2367
+ },
2368
+ //
2369
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2370
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2371
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2372
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2373
+ // cardset.
2374
+ //
2375
+ [CardSetConfigKey.flashcard1]: {
2376
+ jsonKey: "cards",
2377
+ exportJsonKey: { cards: "$" },
2378
+ sections: {
2379
+ default: {
2380
+ jsonKey: "cards",
2381
+ exportJsonKey: { cards: "$" },
2382
+ isDefault: true,
2383
+ maxCount: 1
2384
+ }
2385
+ },
2386
+ sides: [
2387
+ {
2388
+ name: "question",
2389
+ variants: [
2390
+ {
2391
+ jsonKey: "question.text",
2392
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2393
+ tags: [
2394
+ {
2395
+ key: ConfigKey.group_standardTags,
2396
+ description: "Standard tags for the flashcard."
2397
+ },
2398
+ {
2399
+ exportJsonKey: { title: "$" },
2400
+ key: ConfigKey.tag_title,
2401
+ description: "Title of the flashcard."
2402
+ },
2403
+ {
2404
+ key: ConfigKey.group_resourceIcon,
2405
+ description: "Icon resource for the flashcard.",
2406
+ jsonKey: "question.icon|resource(type=image, key=image)",
2407
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2408
+ },
2409
+ {
2410
+ key: ConfigKey.property_example,
2411
+ exportJsonKey: [
2412
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2413
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2414
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2415
+ ],
2416
+ description: "Example marker for the flashcard question.",
2417
+ format: TagFormat.bitmarkText,
2418
+ nullable: true
2276
2419
  }
2277
2420
  ]
2278
2421
  }
@@ -2283,7 +2426,7 @@ var CARDSETS = {
2283
2426
  variants: [
2284
2427
  {
2285
2428
  jsonKey: "answer.text",
2286
- exportJsonKey: { answer: { text: "$" } },
2429
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2287
2430
  tags: [
2288
2431
  {
2289
2432
  key: ConfigKey.group_standardTags,
@@ -2299,6 +2442,17 @@ var CARDSETS = {
2299
2442
  description: "Icon resource for the flashcard.",
2300
2443
  jsonKey: "answer.icon|resource(type=image, key=image)",
2301
2444
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2445
+ },
2446
+ {
2447
+ key: ConfigKey.property_example,
2448
+ exportJsonKey: [
2449
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2450
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2451
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2452
+ ],
2453
+ description: "Example marker for the flashcard answer.",
2454
+ format: TagFormat.bitmarkText,
2455
+ nullable: true
2302
2456
  }
2303
2457
  ]
2304
2458
  },
@@ -2342,7 +2496,7 @@ var CARDSETS = {
2342
2496
  variants: [
2343
2497
  {
2344
2498
  jsonKey: "term.text",
2345
- exportJsonKey: { term: { text: "$" } },
2499
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2346
2500
  tags: [
2347
2501
  {
2348
2502
  key: ConfigKey.group_standardTags,
@@ -2361,6 +2515,17 @@ var CARDSETS = {
2361
2515
  format: TagFormat.plainText,
2362
2516
  jsonKey: "term.icon|resource(type=image, key=image)",
2363
2517
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2518
+ },
2519
+ {
2520
+ key: ConfigKey.property_example,
2521
+ exportJsonKey: [
2522
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2523
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2524
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2525
+ ],
2526
+ description: "Example marker for the definition term.",
2527
+ format: TagFormat.bitmarkText,
2528
+ nullable: true
2364
2529
  }
2365
2530
  ]
2366
2531
  }
@@ -2371,7 +2536,7 @@ var CARDSETS = {
2371
2536
  variants: [
2372
2537
  {
2373
2538
  jsonKey: "definition.text",
2374
- exportJsonKey: { definition: { text: "$" } },
2539
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2375
2540
  tags: [
2376
2541
  {
2377
2542
  key: ConfigKey.group_standardTags,
@@ -2389,6 +2554,17 @@ var CARDSETS = {
2389
2554
  description: "Icon resource for the definition.",
2390
2555
  jsonKey: "definition.icon|resource(type=image, key=image)",
2391
2556
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2557
+ },
2558
+ {
2559
+ key: ConfigKey.property_example,
2560
+ exportJsonKey: [
2561
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2562
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2563
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2564
+ ],
2565
+ description: "Example marker for the definition.",
2566
+ format: TagFormat.bitmarkText,
2567
+ nullable: true
2392
2568
  }
2393
2569
  ]
2394
2570
  },
@@ -2433,11 +2609,51 @@ var CARDSETS = {
2433
2609
  {
2434
2610
  jsonKey: "key",
2435
2611
  exportJsonKey: { key: "$" },
2612
+ format: TextFormat2.plainText,
2436
2613
  tags: [
2437
2614
  {
2438
2615
  key: ConfigKey.group_standardTags,
2439
2616
  description: "Standard tags for the match pair."
2440
2617
  },
2618
+ {
2619
+ // Pair-level example: emit `isExample` alongside `example`
2620
+ // (matches OLD parser's per-pair allow-list behaviour).
2621
+ //
2622
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2623
+ // in the cardset) fills each pair's `example` with `V`
2624
+ // once per pair from the keys-side cascade fire (single
2625
+ // parsed_variant per pair — no per-value duplication).
2626
+ //
2627
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2628
+ // at the pair-object level. BPG `parseMatchPairs`
2629
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2630
+ // per PAIR (the `exampleCard` accumulator), placed at the
2631
+ // pair object. Unlike matchMatrix (per-cell example),
2632
+ // matchPairs has no nested per-cell layer, so the pair-key
2633
+ // `[@example:V]` must double-emit as the visible field.
2634
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2635
+ // expects `example: "packen"` at pair-object level from
2636
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2637
+ // draft applied to both keys-side tags; the matchPairs
2638
+ // half was reverted after fixture parity testing.
2639
+ key: ConfigKey.property_example,
2640
+ exportJsonKey: [
2641
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2642
+ {
2643
+ predicates: ["@absent", { $cascade: "*" }],
2644
+ rule: {
2645
+ isExample: true,
2646
+ example: "$cascade",
2647
+ "@bit": { isExample: true }
2648
+ }
2649
+ },
2650
+ { "@absent": { isExample: true } },
2651
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2652
+ ],
2653
+ description: "Example marker / value on the match pair.",
2654
+ format: TagFormat.bitmarkText,
2655
+ nullable: true
2656
+ },
2441
2657
  {
2442
2658
  key: ConfigKey.tag_title,
2443
2659
  description: "Title of the match pair.",
@@ -2446,9 +2662,19 @@ var CARDSETS = {
2446
2662
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2447
2663
  },
2448
2664
  {
2665
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2666
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2667
+ // only fires when NO variant in the active card explicitly
2668
+ // carries `[@isCaseSensitive]`. When a value-side variant
2669
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2670
+ // synth is skipped here; the value-side explicit fire owns
2671
+ // the pair-level emission.
2449
2672
  key: ConfigKey.property_isCaseSensitive,
2450
2673
  exportJsonKey: [
2451
- { "@absent": { isCaseSensitive: "$ancestor" } },
2674
+ {
2675
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2676
+ rule: { isCaseSensitive: "$ancestor" }
2677
+ },
2452
2678
  { isCaseSensitive: "$" }
2453
2679
  ],
2454
2680
  description: "Property to indicate if the match is case sensitive.",
@@ -2477,10 +2703,85 @@ var CARDSETS = {
2477
2703
  {
2478
2704
  jsonKey: "values[]",
2479
2705
  exportJsonKey: { values: ["$"] },
2706
+ format: TextFormat2.plainText,
2480
2707
  tags: [
2481
2708
  {
2482
- key: ConfigKey.group_standardTags,
2483
- description: "Standard tags for the match pair."
2709
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2710
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2711
+ // for sideIdx > 0; the schema-side equivalent is to use a
2712
+ // group_standardTags variant that omits group_standardItemLead.
2713
+ key: ConfigKey.group_standardTagsNoItemLead,
2714
+ description: "Standard tags for the match pair (values side, no item/lead)."
2715
+ },
2716
+ {
2717
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2718
+ // `@example` from the bit header fills each pair's
2719
+ // `example` with the pair's first value (mirrors BPG's
2720
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2721
+ //
2722
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2723
+ // TAG 415 (keys side) which fires once per pair — keeping
2724
+ // it here would emit `example` twice per pair (one per
2725
+ // values-side parsed_variant) and merge them into a
2726
+ // multi-paragraph array.
2727
+ //
2728
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2729
+ // source `$` value (the current `++`-iteration's value
2730
+ // text). For single-value pairs this matches the old
2731
+ // `$parent.values[0]`; for multi-value pairs with
2732
+ // `[@example]` attached to a non-first value, it correctly
2733
+ // resolves to that value.
2734
+ //
2735
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2736
+ // routes pair-key `[@example:V]` (a per-pair cascade
2737
+ // source, bound via per-card CascadeCounters scope) and
2738
+ // bit-header `[@example:V]` (cardset cascade) into the
2739
+ // pair-level `example` via `$cascade`. The aggregator in
2740
+ // `populate_variants` fires this rule ONCE per pair (BPG
2741
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2742
+ // per-iter pollution.
2743
+ key: ConfigKey.property_example,
2744
+ exportJsonKey: [
2745
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2746
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2747
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2748
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2749
+ // scope opened by `build_card_item`.
2750
+ {
2751
+ predicates: ["@keyonly"],
2752
+ maxEmits: 1,
2753
+ rule: {
2754
+ isExample: true,
2755
+ example: "$parent",
2756
+ "@bit": { isExample: true }
2757
+ }
2758
+ },
2759
+ {
2760
+ predicates: ["@absent", { $cascade: "*" }],
2761
+ rule: {
2762
+ isExample: true,
2763
+ example: "$cascade",
2764
+ "@bit": { isExample: true }
2765
+ }
2766
+ },
2767
+ {
2768
+ "@absent": {
2769
+ isExample: true,
2770
+ example: "$parent",
2771
+ "@bit": { isExample: true }
2772
+ }
2773
+ },
2774
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2775
+ // form so per-`++`-iter values-side fires also first-win.
2776
+ {
2777
+ predicates: [],
2778
+ maxEmits: 1,
2779
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2780
+ }
2781
+ ],
2782
+ description: "Example marker / value on the match pair (values side).",
2783
+ format: TagFormat.bitmarkText,
2784
+ nullable: true
2484
2785
  },
2485
2786
  {
2486
2787
  key: ConfigKey.tag_title,
@@ -2490,9 +2791,13 @@ var CARDSETS = {
2490
2791
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2491
2792
  },
2492
2793
  {
2794
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2493
2795
  key: ConfigKey.property_isCaseSensitive,
2494
2796
  exportJsonKey: [
2495
- { "@absent": { isCaseSensitive: "$ancestor" } },
2797
+ {
2798
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2799
+ rule: { isCaseSensitive: "$ancestor" }
2800
+ },
2496
2801
  { isCaseSensitive: "$" }
2497
2802
  ],
2498
2803
  description: "Property to indicate if the match is case sensitive.",
@@ -2518,6 +2823,7 @@ var CARDSETS = {
2518
2823
  {
2519
2824
  jsonKey: "key",
2520
2825
  exportJsonKey: { key: "$" },
2826
+ format: TextFormat2.plainText,
2521
2827
  tags: [
2522
2828
  {
2523
2829
  key: ConfigKey.group_standardTags,
@@ -2547,10 +2853,12 @@ var CARDSETS = {
2547
2853
  {
2548
2854
  jsonKey: "values[]",
2549
2855
  exportJsonKey: { values: ["$"] },
2856
+ format: TextFormat2.plainText,
2550
2857
  tags: [
2551
2858
  {
2552
- key: ConfigKey.group_standardTags,
2553
- description: "Standard tags for the audio match pair."
2859
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2860
+ key: ConfigKey.group_standardTagsNoItemLead,
2861
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2554
2862
  },
2555
2863
  {
2556
2864
  key: ConfigKey.tag_title,
@@ -2583,6 +2891,7 @@ var CARDSETS = {
2583
2891
  {
2584
2892
  jsonKey: "key",
2585
2893
  exportJsonKey: { key: "$" },
2894
+ format: TextFormat2.plainText,
2586
2895
  tags: [
2587
2896
  {
2588
2897
  key: ConfigKey.group_standardTags,
@@ -2612,10 +2921,12 @@ var CARDSETS = {
2612
2921
  {
2613
2922
  jsonKey: "values[]",
2614
2923
  exportJsonKey: { values: ["$"] },
2924
+ format: TextFormat2.plainText,
2615
2925
  tags: [
2616
2926
  {
2617
- key: ConfigKey.group_standardTags,
2618
- description: "Standard tags for the image match pair."
2927
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2928
+ key: ConfigKey.group_standardTagsNoItemLead,
2929
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2619
2930
  },
2620
2931
  {
2621
2932
  key: ConfigKey.tag_title,
@@ -2648,20 +2959,49 @@ var CARDSETS = {
2648
2959
  {
2649
2960
  jsonKey: "key",
2650
2961
  exportJsonKey: { key: "$" },
2962
+ format: TextFormat2.plainText,
2651
2963
  tags: [
2652
2964
  {
2653
2965
  key: ConfigKey.group_standardItemLeadInstructionHint,
2654
2966
  description: "Standard tags for lead, instruction, and hint."
2655
2967
  },
2656
2968
  {
2969
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
2970
+ // `[@example:V]` is consumed as the per-pair cascade source.
2971
+ // PLAN-077 R14 follow-up: investigation showed the fixture
2972
+ // reference is INCONSISTENT — bits without a cardset-level
2973
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
2974
+ // pair-level `example: V` at pair scope, but bits WITH a
2975
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
2976
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
2977
+ // example. Emitting `example: $` here satisfies bit 3 but
2978
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
2979
+ // 3. Conditional emission would require runtime gating on
2980
+ // the cardset cascade scope state. Left dropped pending a
2981
+ // dedicated plan — accepted divergence on bit 3's pair-
2982
+ // level emission (cells still get the per-cell `example`
2983
+ // correctly via PLAN-077 §4.2 + §4.3).
2657
2984
  key: ConfigKey.property_example,
2658
2985
  exportJsonKey: [
2659
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2660
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2661
- { isExample: true, example: "$", "@bit": { isExample: true } }
2986
+ {
2987
+ "@keyonly": {
2988
+ isExample: true,
2989
+ "@bit": { isExample: true },
2990
+ "@card": { isExample: true }
2991
+ }
2992
+ },
2993
+ {
2994
+ "@absent": {
2995
+ isExample: true,
2996
+ "@bit": { isExample: true },
2997
+ "@card": { isExample: true }
2998
+ }
2999
+ },
3000
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2662
3001
  ],
2663
3002
  description: "Example text for the match matrix.",
2664
- format: TagFormat.plainText
3003
+ format: TagFormat.plainText,
3004
+ nullable: true
2665
3005
  },
2666
3006
  {
2667
3007
  key: ConfigKey.tag_title,
@@ -2689,35 +3029,96 @@ var CARDSETS = {
2689
3029
  variants: [
2690
3030
  {
2691
3031
  jsonKey: "cells[{s}].values[]",
2692
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
3032
+ exportJsonKey: [
3033
+ // PLAN-085 / R14: the positional-blank emission for an
3034
+ // effectively-absent cell side fires ONLY when the active
3035
+ // card has at least one `[#]` variant fire. This matches
3036
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
3037
+ // `[#]` exists anywhere in the title card, BPG returns
3038
+ // undefined and no `heading` is emitted. The phantom
3039
+ // `forValues:['']` placeholder is appropriate only when
3040
+ // the card is being treated as a title-row.
3041
+ {
3042
+ predicates: ["@absent", { "@variantHasTag": "#" }],
3043
+ rule: { "@bit": { heading: { forValues: [""] } } }
3044
+ },
3045
+ { cells: { $s: { values: ["$"] } } }
3046
+ ],
3047
+ format: TextFormat2.plainText,
2693
3048
  tags: [
2694
3049
  {
2695
3050
  key: ConfigKey.group_standardItemLeadInstructionHint,
2696
3051
  description: "Standard tags for lead, instruction, and hint."
2697
3052
  },
2698
3053
  {
3054
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3055
+ // source `$` value (the current `++`-iteration's cell text).
3056
+ // Replaces `$parent.values[0]` which pinned to position 0
3057
+ // regardless of which `++`-value `[@example]` was on.
3058
+ //
3059
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3060
+ // routes pair-key `[@example:V]` (a per-pair cascade
3061
+ // source, bound via per-card CascadeCounters scope) and
3062
+ // bit-header `[@example:V]` (cardset cascade) into the
3063
+ // per-cell `example` via `$cascade`. The aggregator in
3064
+ // `populate_variants` fires this rule ONCE per cell (BPG
3065
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3066
+ // per-iter pollution.
3067
+ // @card scope-shift writes matrix-row container isExample.
2699
3068
  key: ConfigKey.property_example,
2700
3069
  exportJsonKey: [
2701
3070
  {
2702
3071
  "@keyonly": {
2703
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2704
- "@bit": { isExample: true }
3072
+ cells: { $s: { isExample: true, example: "$parent" } },
3073
+ "@bit": { isExample: true },
3074
+ "@card": { isExample: true }
2705
3075
  }
2706
3076
  },
2707
3077
  {
2708
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
3078
+ predicates: ["@absent", { $cascade: "*" }],
3079
+ rule: {
3080
+ cells: { $s: { isExample: true, example: "$cascade" } },
3081
+ "@bit": { isExample: true },
3082
+ "@card": { isExample: true }
3083
+ }
2709
3084
  },
2710
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
3085
+ {
3086
+ "@absent": {
3087
+ cells: { $s: { isExample: true, example: "$parent" } },
3088
+ "@bit": { isExample: true },
3089
+ "@card": { isExample: true }
3090
+ }
3091
+ },
3092
+ {
3093
+ cells: { $s: { isExample: true, example: "$" } },
3094
+ "@bit": { isExample: true },
3095
+ "@card": { isExample: true }
3096
+ }
2711
3097
  ],
2712
3098
  description: "Example text for the match matrix.",
2713
- format: TagFormat.plainText
3099
+ format: TagFormat.bitmarkText,
3100
+ nullable: true
2714
3101
  },
2715
3102
  {
3103
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3104
+ // empty-string positional blank into `forValues` so the
3105
+ // array stays aligned with the cell positions (matches BPG
3106
+ // `extractHeadingCard`'s "all value-side headings present"
3107
+ // shape). The `@keyonly` rule is required for two reasons:
3108
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3109
+ // in the unconditional rule has no source for bare); and
3110
+ // 2) the presence of any `@keyonly` rule disables the
3111
+ // optimised-mode `should_suppress_outcome` strip (see
3112
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3113
+ // survives the all-natural-defaults check.
2716
3114
  key: ConfigKey.tag_title,
2717
3115
  description: "Title of the match matrix.",
2718
3116
  format: TagFormat.plainText,
2719
3117
  jsonKey: "^heading.forValues",
2720
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3118
+ exportJsonKey: [
3119
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3120
+ { "@bit": { heading: { forValues: ["$"] } } }
3121
+ ]
2721
3122
  },
2722
3123
  {
2723
3124
  key: ConfigKey.property_isCaseSensitive,
@@ -2766,6 +3167,35 @@ var CARDSETS = {
2766
3167
  {
2767
3168
  key: ConfigKey.group_standardTags,
2768
3169
  description: "Standard tags for the statement."
3170
+ },
3171
+ {
3172
+ // PLAN-072: cascade-fired `@example` from the bit header
3173
+ // fills each statement entry's `example` with the entry's
3174
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3175
+ // for the statements cardset. Per-array-item strip removes
3176
+ // `example: false` from incorrect entries so only correct
3177
+ // entries retain `example: true`.
3178
+ key: ConfigKey.property_example,
3179
+ exportJsonKey: [
3180
+ {
3181
+ "@keyonly": {
3182
+ isExample: true,
3183
+ example: true,
3184
+ "@bit": { isExample: true }
3185
+ }
3186
+ },
3187
+ {
3188
+ "@absent": {
3189
+ isExample: true,
3190
+ example: "$parent.isCorrect",
3191
+ "@bit": { isExample: true }
3192
+ }
3193
+ },
3194
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3195
+ ],
3196
+ 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`.",
3197
+ format: TagFormat.boolean,
3198
+ nullable: true
2769
3199
  }
2770
3200
  ],
2771
3201
  bodyAllowed: false
@@ -2794,6 +3224,71 @@ var CARDSETS = {
2794
3224
  {
2795
3225
  key: ConfigKey.group_standardTags,
2796
3226
  description: "Standard tags for the quiz."
3227
+ },
3228
+ {
3229
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3230
+ // quiz choices is delegated to group_trueFalse's
3231
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3232
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3233
+ // would over-fire on subsequent correct choices and on
3234
+ // incorrect ones — drop it.
3235
+ key: ConfigKey.property_example,
3236
+ exportJsonKey: [
3237
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3238
+ { isExample: true, "@bit": { isExample: true } }
3239
+ ],
3240
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3241
+ format: TagFormat.boolean,
3242
+ nullable: true
3243
+ }
3244
+ ],
3245
+ bodyAllowed: false
3246
+ }
3247
+ ]
3248
+ }
3249
+ ]
3250
+ },
3251
+ //
3252
+ // quiz (responses)
3253
+ //
3254
+ [CardSetConfigKey.quizResponses]: {
3255
+ jsonKey: "quizzes",
3256
+ exportJsonKey: { quizzes: "$" },
3257
+ sides: [
3258
+ {
3259
+ name: "responses",
3260
+ variants: [
3261
+ {
3262
+ jsonKey: null,
3263
+ tags: [
3264
+ {
3265
+ key: ConfigKey.group_trueFalseResponses,
3266
+ description: "Group for true/false questions."
3267
+ },
3268
+ {
3269
+ key: ConfigKey.group_standardTags,
3270
+ description: "Standard tags for the quiz."
3271
+ },
3272
+ {
3273
+ // PLAN-072: cascade-fired `@example` from the bit header
3274
+ // fills each response entry's `example` with the entry's
3275
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3276
+ // for the quiz-responses cardset (statements-style).
3277
+ key: ConfigKey.property_example,
3278
+ exportJsonKey: [
3279
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3280
+ {
3281
+ "@absent": {
3282
+ isExample: true,
3283
+ example: "$parent.isCorrect",
3284
+ "@bit": { isExample: true }
3285
+ }
3286
+ },
3287
+ { isExample: true, "@bit": { isExample: true } }
3288
+ ],
3289
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3290
+ format: TagFormat.boolean,
3291
+ nullable: true
2797
3292
  }
2798
3293
  ],
2799
3294
  bodyAllowed: false
@@ -2819,6 +3314,73 @@ var CARDSETS = {
2819
3314
  key: ConfigKey.group_standardItemLeadInstructionHint,
2820
3315
  description: "Standard tags for lead, instruction, and hint."
2821
3316
  },
3317
+ {
3318
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3319
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3320
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3321
+ // feedback-specific shape.
3322
+ //
3323
+ // Chain @example: `[+ X][@example]` writes
3324
+ // `isExample/example` onto the X choice only. BPG's
3325
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3326
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3327
+ // there is no bubble to bit-level or feedbacks-level
3328
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3329
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3330
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3331
+ // no feedback dispatch, so bit-header `[@example]` does
3332
+ // not flow into feedback choices.
3333
+ key: ConfigKey.tag_true,
3334
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3335
+ description: "Reason-required choice for feedback (`[+]`).",
3336
+ maxCount: Count.infinity,
3337
+ format: TagFormat.plainText,
3338
+ chain: [
3339
+ {
3340
+ key: ConfigKey.property_example,
3341
+ exportJsonKey: [
3342
+ { "@keyonly": { isExample: true, example: true } },
3343
+ { isExample: true, example: "$" }
3344
+ ],
3345
+ description: "Per-choice example marker for feedback `[+]`.",
3346
+ format: TagFormat.boolean,
3347
+ maxCount: 1,
3348
+ nullable: true
3349
+ },
3350
+ {
3351
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3352
+ // choice; same for `[?]` (hint), `[%]` (item),
3353
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3354
+ // Mirrors group_trueFalse's `+` chain shape.
3355
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3356
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3357
+ }
3358
+ ]
3359
+ },
3360
+ {
3361
+ key: ConfigKey.tag_false,
3362
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3363
+ description: "Reason-not-required choice for feedback (`[-]`).",
3364
+ maxCount: Count.infinity,
3365
+ format: TagFormat.plainText,
3366
+ chain: [
3367
+ {
3368
+ key: ConfigKey.property_example,
3369
+ exportJsonKey: [
3370
+ { "@keyonly": { isExample: true, example: true } },
3371
+ { isExample: true, example: "$" }
3372
+ ],
3373
+ description: "Per-choice example marker for feedback `[-]`.",
3374
+ format: TagFormat.boolean,
3375
+ maxCount: 1,
3376
+ nullable: true
3377
+ },
3378
+ {
3379
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3380
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3381
+ }
3382
+ ]
3383
+ },
2822
3384
  {
2823
3385
  key: ConfigKey.group_trueFalse,
2824
3386
  description: "Group for true/false feedback."
@@ -2839,31 +3401,71 @@ var CARDSETS = {
2839
3401
  name: "reason",
2840
3402
  variants: [
2841
3403
  {
3404
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3405
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3406
+ // under `reason.*`; each tag here spells out the full path.
3407
+ // Body is a plain string (BPG `parseFeedback` uses
3408
+ // `cardBodyStr ?? ''` for `reason.text`).
2842
3409
  jsonKey: "reason.text",
2843
3410
  exportJsonKey: { reason: { text: "$" } },
3411
+ format: TextFormat2.plainText,
2844
3412
  tags: [
2845
3413
  {
2846
- key: ConfigKey.group_standardItemLeadInstructionHint,
2847
- description: "Standard tags for lead, instruction, and hint."
3414
+ // Instruction lands under reason. Hint/item/lead/etc.
3415
+ // omitted: not exercised by any fixture and BPG's
3416
+ // `parseFeedback` spreads only the tags actually present
3417
+ // — adding the rest can be done lazily as fixtures
3418
+ // require them, with the same `{reason: {…}}` wrapper.
3419
+ key: ConfigKey.tag_instruction,
3420
+ exportJsonKey: { reason: { instruction: "$" } },
3421
+ description: "Instruction for the feedback reason.",
3422
+ format: TagFormat.bitmarkText
2848
3423
  },
2849
3424
  {
2850
3425
  key: ConfigKey.property_reasonableNumOfChars,
2851
3426
  exportJsonKey: [
2852
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2853
- { reasonableNumOfChars: "$" }
3427
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3428
+ { reason: { reasonableNumOfChars: "$" } }
2854
3429
  ],
2855
3430
  description: "Property for reasonable number of characters.",
2856
3431
  format: TagFormat.number
2857
3432
  },
2858
3433
  {
3434
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3435
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3436
+ // cascade. Local emission only.
3437
+ //
3438
+ // Both `reason.example` and `feedbacks[].example` are
3439
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3440
+ // here is `bitmarkText` so `$` coerces to a paragraph
3441
+ // array at both write sites.
2859
3442
  key: ConfigKey.property_example,
2860
3443
  exportJsonKey: [
2861
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2862
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2863
- { isExample: true, example: "$", "@bit": { isExample: true } }
3444
+ {
3445
+ // Bare `[@example]`: reason.example is TextAst BPG
3446
+ // coerces the boolean default to a paragraph
3447
+ // containing the string "true". feedbacks[].example
3448
+ // is not TextAst, stays a boolean. Two literal writes.
3449
+ "@keyonly": {
3450
+ reason: {
3451
+ isExample: true,
3452
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3453
+ },
3454
+ isExample: true,
3455
+ example: true,
3456
+ "@bit": { isExample: true }
3457
+ }
3458
+ },
3459
+ {
3460
+ reason: { isExample: true, example: "$" },
3461
+ isExample: true,
3462
+ example: "$",
3463
+ "@bit": { isExample: true }
3464
+ }
2864
3465
  ],
2865
- description: "Example text for the feedback.",
2866
- format: TagFormat.plainText
3466
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3467
+ format: TagFormat.bitmarkText,
3468
+ nullable: true
2867
3469
  },
2868
3470
  {
2869
3471
  key: ConfigKey.tag_title,
@@ -2927,6 +3529,22 @@ var CARDSETS = {
2927
3529
  {
2928
3530
  key: ConfigKey.group_standardTags,
2929
3531
  description: "Standard tags for the question."
3532
+ },
3533
+ {
3534
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3535
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3536
+ // does not propagate `example`/`isExample` onto question
3537
+ // entries. Only entry-local `[@example]` fires (handled by
3538
+ // `@keyonly` and the unconditional fallback). No `@absent`
3539
+ // rule.
3540
+ key: ConfigKey.property_example,
3541
+ exportJsonKey: [
3542
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3543
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3544
+ ],
3545
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3546
+ format: TagFormat.bitmarkText,
3547
+ nullable: true
2930
3548
  }
2931
3549
  ]
2932
3550
  }
@@ -2953,14 +3571,16 @@ var CARDSETS = {
2953
3571
  description: "Standard tags for lead, instruction, and hint."
2954
3572
  },
2955
3573
  {
3574
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3575
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2956
3576
  key: ConfigKey.property_example,
2957
3577
  exportJsonKey: [
2958
3578
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2959
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2960
3579
  { isExample: true, example: "$", "@bit": { isExample: true } }
2961
3580
  ],
2962
- description: "Example text for the element.",
2963
- format: TagFormat.plainText
3581
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3582
+ format: TagFormat.plainText,
3583
+ nullable: true
2964
3584
  }
2965
3585
  ],
2966
3586
  repeatCount: Count.infinity
@@ -2972,9 +3592,57 @@ var CARDSETS = {
2972
3592
  //
2973
3593
  // table
2974
3594
  //
3595
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3596
+ // output (`{ columns?, data, columnWidths? }`):
3597
+ //
3598
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3599
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3600
+ // no qualifier, so they route to the `table-body` (default) section.
3601
+ //
3602
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3603
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3604
+ // the first header card's cells to `columns` and demote remaining
3605
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3606
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3607
+ // here (no per-cell metadata in basic shape) — declared on the cell
3608
+ // variant below.
3609
+ //
2975
3610
  [CardSetConfigKey.table]: {
2976
- jsonKey: "table.data",
2977
- exportJsonKey: { table: { data: "$" } },
3611
+ jsonKey: null,
3612
+ exportJsonKey: null,
3613
+ sections: {
3614
+ "table-header": {
3615
+ // First header card's row of cells → `columns`. Subsequent header
3616
+ // cards' rows → `data` (per-card append; `[$]` shape because
3617
+ // per-card emission fires once per card, multiple fires deep_merge-
3618
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3619
+ // both ROUTE cards to this section (when no explicit `==== table-
3620
+ // header ====` qualifier) and SELECT the rule body once routed.
3621
+ // First rule = first card with any `[#]` → `columns`; second rule
3622
+ // = subsequent cards in this section → `data` (per-row append).
3623
+ jsonKey: "table.columns",
3624
+ exportJsonKey: [
3625
+ {
3626
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3627
+ rule: { "@bit": { table: { columns: "$" } } }
3628
+ },
3629
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3630
+ { "@bit": { table: { data: ["$"] } } }
3631
+ ]
3632
+ },
3633
+ "table-body": {
3634
+ // Batched emission: `$` is the entire 2D array of body card rows.
3635
+ // This is the default section (no qualifier → cards land here).
3636
+ jsonKey: "table.data",
3637
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3638
+ isDefault: true
3639
+ },
3640
+ "table-footer": {
3641
+ // Batched emission: `$` is the entire 2D array of footer rows.
3642
+ jsonKey: "table.data",
3643
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3644
+ }
3645
+ },
2978
3646
  sides: [
2979
3647
  {
2980
3648
  name: "cell",
@@ -3015,6 +3683,8 @@ var CARDSETS = {
3015
3683
  },
3016
3684
  {
3017
3685
  key: ConfigKey.property_tableColWidth,
3686
+ jsonKey: "^table.columnWidths[{s}]",
3687
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
3018
3688
  description: "Width for the column.",
3019
3689
  format: TagFormat.number
3020
3690
  }
@@ -3033,7 +3703,22 @@ var CARDSETS = {
3033
3703
  sections: {
3034
3704
  "table-header": {
3035
3705
  jsonKey: "table.header.rows",
3036
- exportJsonKey: { table: { header: { rows: "$" } } },
3706
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3707
+ // Cards with no explicit `==== table-header ====` qualifier
3708
+ // route here if FIRST CARD contains any `[#]` tag in any
3709
+ // variant. Explicit-qualifier cards fall through to the
3710
+ // unconditional fallback rule. Both rules use the per-card
3711
+ // append shape `rows: ['$']` — the runtime triggers per-card
3712
+ // emission whenever the pattern carries predicates, and each
3713
+ // fire appends a single-element array (multi-fire deep-merge
3714
+ // concatenates).
3715
+ exportJsonKey: [
3716
+ {
3717
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3718
+ rule: { table: { header: { rows: ["$"] } } }
3719
+ },
3720
+ { table: { header: { rows: ["$"] } } }
3721
+ ],
3037
3722
  sideJsonKey: "cells[{s}]|set(title=true)",
3038
3723
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
3039
3724
  },
@@ -3066,13 +3751,13 @@ var CARDSETS = {
3066
3751
  },
3067
3752
  {
3068
3753
  key: ConfigKey.tag_title,
3069
- jsonKey: ".",
3070
- exportJsonKey: {
3071
- "@bit": {
3072
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
3073
- }
3074
- },
3075
- description: "Title of the table cell."
3754
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3755
+ // `content` field. When the card is in the `table-header`
3756
+ // section, the section's sideExportJsonKey wraps each
3757
+ // cell with `title: true` automatically.
3758
+ jsonKey: "content",
3759
+ exportJsonKey: { content: "$" },
3760
+ description: "Title text of the table cell (header rows)."
3076
3761
  },
3077
3762
  {
3078
3763
  key: ConfigKey.property_tableCellType,
@@ -3104,6 +3789,9 @@ var CARDSETS = {
3104
3789
  },
3105
3790
  {
3106
3791
  key: ConfigKey.property_tableColWidth,
3792
+ // PLAN-080: write to the cell's `colwidth` field.
3793
+ jsonKey: "colwidth",
3794
+ exportJsonKey: { colwidth: "$" },
3107
3795
  description: "Width for the column.",
3108
3796
  format: TagFormat.number
3109
3797
  }
@@ -3141,7 +3829,8 @@ var CARDSETS = {
3141
3829
  {
3142
3830
  key: ConfigKey.resource_audio,
3143
3831
  description: "Audio resource for the pronunciation table.",
3144
- jsonKey: "audio"
3832
+ jsonKey: "audio",
3833
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3145
3834
  }
3146
3835
  ],
3147
3836
  repeatCount: Count.infinity
@@ -3212,14 +3901,17 @@ var CARDSETS = {
3212
3901
  format: TagFormat.plainText
3213
3902
  },
3214
3903
  {
3904
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
3905
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
3906
+ // header cascade.
3215
3907
  key: ConfigKey.property_example,
3216
3908
  exportJsonKey: [
3217
3909
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3218
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3219
3910
  { isExample: true, example: "$", "@bit": { isExample: true } }
3220
3911
  ],
3221
- description: "Example text for the bot action response.",
3222
- format: TagFormat.plainText
3912
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
3913
+ format: TagFormat.plainText,
3914
+ nullable: true
3223
3915
  }
3224
3916
  ]
3225
3917
  }
@@ -3285,6 +3977,38 @@ var CARDSETS = {
3285
3977
  ]
3286
3978
  },
3287
3979
  //
3980
+ // page-footer-sections — mirrors example-bit-list but emits cards under
3981
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
3982
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
3983
+ // routing as pure config so the Rust serializer can pick it up.
3984
+ //
3985
+ [CardSetConfigKey.pageFooterSections]: {
3986
+ jsonKey: "sections",
3987
+ exportJsonKey: { sections: "$" },
3988
+ sides: [
3989
+ {
3990
+ name: "item",
3991
+ variants: [
3992
+ {
3993
+ jsonKey: "body",
3994
+ exportJsonKey: { body: "$" },
3995
+ tags: [
3996
+ {
3997
+ key: ConfigKey.group_standardTags,
3998
+ description: "Standard tags for page-footer section bits."
3999
+ },
4000
+ {
4001
+ exportJsonKey: { title: "$" },
4002
+ key: ConfigKey.tag_title,
4003
+ description: "Title of the page-footer section."
4004
+ }
4005
+ ]
4006
+ }
4007
+ ]
4008
+ }
4009
+ ]
4010
+ },
4011
+ //
3288
4012
  // ingredients
3289
4013
  //
3290
4014
  [CardSetConfigKey.ingredients]: {
@@ -3615,7 +4339,11 @@ var GROUPS = {
3615
4339
  {
3616
4340
  key: ConfigKey.property_groupTag,
3617
4341
  jsonKey: "groupTag.name",
3618
- exportJsonKey: { groupTag: { name: "$" } },
4342
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4343
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4344
+ // same value fold into one entry; chained `@tag` children's
4345
+ // arrays merge as sets.
4346
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3619
4347
  description: "The group tag(s) for the bit",
3620
4348
  format: TagFormat.plainText,
3621
4349
  maxCount: Count.infinity,
@@ -3788,6 +4516,24 @@ var GROUPS = {
3788
4516
  }
3789
4517
  ]
3790
4518
  },
4519
+ [ConfigKey.group_standardInstructionHint]: {
4520
+ type: GroupConfigType.standard,
4521
+ 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).",
4522
+ tags: [
4523
+ {
4524
+ exportJsonKey: { instruction: "$" },
4525
+ key: ConfigKey.tag_instruction,
4526
+ name: "Instruction",
4527
+ description: "The instruction for the bit"
4528
+ },
4529
+ {
4530
+ exportJsonKey: { hint: "$" },
4531
+ key: ConfigKey.tag_hint,
4532
+ name: "Hint",
4533
+ description: "The hint for the bit"
4534
+ }
4535
+ ]
4536
+ },
3791
4537
  [ConfigKey.group_standardTags]: {
3792
4538
  type: GroupConfigType.standard,
3793
4539
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3801,17 +4547,45 @@ var GROUPS = {
3801
4547
  description: "Item, lead, page number, margin number, instruction and hint tags"
3802
4548
  },
3803
4549
  {
3804
- // Bit-level example tag: bare/synthesized records the marker only;
3805
- // valued [@example:text] emits `isExample: true` and `example` as
3806
- // PM-rendered rich text (bitmark+ format).
4550
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4551
+ // - bare `[@example]`: consume only (records cascade marker, emits
4552
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4553
+ // for bits NOT in its `isTopLevelExample` allow-list).
4554
+ // - synthesized (descendant cascade): still emits `isExample: true`
4555
+ // at the descendant scope.
4556
+ // - valued `[@example:text]`: emit the example value only; bit-level
4557
+ // `isExample` is left to bubble from descendants.
4558
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4559
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4560
+ // matchMatrix, match, essay, etc.) override this entry with the
4561
+ // legacy emit-isExample shape.
3807
4562
  key: ConfigKey.property_example,
3808
- exportJsonKey: [
3809
- { "@keyonly": { isExample: true } },
3810
- { "@absent": { isExample: true } },
3811
- { isExample: true, example: "$" }
3812
- ],
4563
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3813
4564
  description: "The example(s) for the bit",
3814
- format: TagFormat.bitmarkText
4565
+ format: TagFormat.bitmarkText,
4566
+ nullable: true
4567
+ }
4568
+ ]
4569
+ },
4570
+ [ConfigKey.group_standardTagsNoItemLead]: {
4571
+ type: GroupConfigType.standard,
4572
+ 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).",
4573
+ tags: [
4574
+ {
4575
+ key: ConfigKey.group_standardAllBits,
4576
+ description: "All standard tags which apply to all bits"
4577
+ },
4578
+ {
4579
+ key: ConfigKey.group_standardInstructionHint,
4580
+ description: "Instruction and hint tags only (no item/lead chain)."
4581
+ },
4582
+ {
4583
+ // Mirror of group_standardTags property_example entry.
4584
+ key: ConfigKey.property_example,
4585
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4586
+ description: "The example(s) for the bit",
4587
+ format: TagFormat.bitmarkText,
4588
+ nullable: true
3815
4589
  }
3816
4590
  ]
3817
4591
  },
@@ -3876,8 +4650,8 @@ var GROUPS = {
3876
4650
  tags: [
3877
4651
  {
3878
4652
  key: ConfigKey.property_person,
3879
- jsonKey: "partner.name",
3880
- exportJsonKey: { partner: { name: "$" } },
4653
+ jsonKey: "person.name",
4654
+ exportJsonKey: { person: { name: "$" } },
3881
4655
  description: "A person",
3882
4656
  format: TagFormat.plainText,
3883
4657
  chain: [
@@ -3897,8 +4671,8 @@ var GROUPS = {
3897
4671
  {
3898
4672
  // Deprecated (parter renamed to person)
3899
4673
  key: ConfigKey.property_partner,
3900
- jsonKey: "partner.name",
3901
- exportJsonKey: { partner: { name: "$" } },
4674
+ jsonKey: "person.name",
4675
+ exportJsonKey: { person: { name: "$" } },
3902
4676
  description: "A partner",
3903
4677
  format: TagFormat.plainText,
3904
4678
  chain: [
@@ -3922,7 +4696,9 @@ var GROUPS = {
3922
4696
  description: "Gap chain",
3923
4697
  tags: [
3924
4698
  {
3925
- exportJsonKey: { solutions: [["$"]] },
4699
+ exportJsonKey: {
4700
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4701
+ },
3926
4702
  key: ConfigKey.tag_gap,
3927
4703
  description: "The value of a gap in the content",
3928
4704
  maxCount: Count.infinity,
@@ -3941,27 +4717,37 @@ var GROUPS = {
3941
4717
  description: "Item, lead, page number, margin number, instruction and hint tags"
3942
4718
  },
3943
4719
  {
4720
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4721
+ // `__solutionsAst[length - 1]` — the solution closest to the
4722
+ // `[@example]` tag at parse time. Our chain accumulator is built
4723
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4724
+ // the moment `[@example]` fires, `solutions` only contains the
4725
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4726
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4727
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4728
+ // solutions=["solution"], `[-1]`="solution".
3944
4729
  key: ConfigKey.property_example,
3945
4730
  jsonKey: "example",
3946
4731
  exportJsonKey: [
3947
4732
  {
3948
4733
  "@keyonly": {
3949
4734
  isExample: true,
3950
- example: "$parent.solutions[0]",
4735
+ example: "$parent.solutions[-1]",
3951
4736
  "@bit": { isExample: true }
3952
4737
  }
3953
4738
  },
3954
4739
  {
3955
4740
  "@absent": {
3956
4741
  isExample: true,
3957
- example: "$parent.solutions[0]",
4742
+ example: "$parent.solutions[-1]",
3958
4743
  "@bit": { isExample: true }
3959
4744
  }
3960
4745
  },
3961
4746
  { isExample: true, example: "$", "@bit": { isExample: true } }
3962
4747
  ],
3963
4748
  description: "An example for the gap",
3964
- format: TagFormat.bitmarkText
4749
+ format: TagFormat.bitmarkText,
4750
+ nullable: true
3965
4751
  },
3966
4752
  {
3967
4753
  key: ConfigKey.property_isCaseSensitive,
@@ -4010,6 +4796,24 @@ var GROUPS = {
4010
4796
  description: "Item, lead, page number, margin number, instruction and hint tags"
4011
4797
  },
4012
4798
  {
4799
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4800
+ // `@example` from the bit header marks EVERY choice with
4801
+ // `isExample: true` (matches BPG output), but only the
4802
+ // FIRST correct choice gets `example: true` (BPG
4803
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4804
+ //
4805
+ // Two `@absent` rules, picked first-match-wins:
4806
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4807
+ // `maxEmits:1` — fires once for the first correct.
4808
+ // 2. Plain `@absent` — fires on every remaining entry
4809
+ // (subsequent corrects and all incorrects), emitting
4810
+ // `isExample` only.
4811
+ // The `serialize_cards` cascade-counter scope holds the cap
4812
+ // across the whole cardset.
4813
+ // @card scope-shift writes choice-container isExample (e.g.
4814
+ // `quizzes[i].isExample`) — was previously provided by the
4815
+ // universal `bubble_is_example` post-walk, which was removed
4816
+ // in favour of explicit per-tag config. SYNTAX.md §8.
4013
4817
  key: ConfigKey.property_example,
4014
4818
  jsonKey: "example",
4015
4819
  exportJsonKey: [
@@ -4017,20 +4821,37 @@ var GROUPS = {
4017
4821
  "@keyonly": {
4018
4822
  isExample: true,
4019
4823
  example: "$parent.isCorrect",
4020
- "@bit": { isExample: true }
4824
+ "@bit": { isExample: true },
4825
+ "@card": { isExample: true }
4826
+ }
4827
+ },
4828
+ {
4829
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4830
+ maxEmits: 1,
4831
+ rule: {
4832
+ isExample: true,
4833
+ example: true,
4834
+ "@bit": { isExample: true },
4835
+ "@card": { isExample: true }
4021
4836
  }
4022
4837
  },
4023
4838
  {
4024
4839
  "@absent": {
4025
4840
  isExample: true,
4026
- example: "$parent.isCorrect",
4027
- "@bit": { isExample: true }
4841
+ "@bit": { isExample: true },
4842
+ "@card": { isExample: true }
4028
4843
  }
4029
4844
  },
4030
- { isExample: true, example: "$", "@bit": { isExample: true } }
4845
+ {
4846
+ isExample: true,
4847
+ example: "$",
4848
+ "@bit": { isExample: true },
4849
+ "@card": { isExample: true }
4850
+ }
4031
4851
  ],
4032
4852
  description: "An example for the true/false statement/question",
4033
- format: TagFormat.bitmarkText
4853
+ format: TagFormat.boolean,
4854
+ nullable: true
4034
4855
  }
4035
4856
  ]
4036
4857
  },
@@ -4062,6 +4883,12 @@ var GROUPS = {
4062
4883
  description: "Item, lead, page number, margin number, instruction and hint tags"
4063
4884
  },
4064
4885
  {
4886
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4887
+ // `@example` on a `-` (incorrect choice) emits
4888
+ // `isExample: true` only — no `example` (BPG fixtures show
4889
+ // `isExample` on every choice, but `example` only on the
4890
+ // first correct via firstCorrectOnly cascade).
4891
+ // @card scope-shift writes choice-container isExample.
4065
4892
  key: ConfigKey.property_example,
4066
4893
  jsonKey: "example",
4067
4894
  exportJsonKey: [
@@ -4069,20 +4896,460 @@ var GROUPS = {
4069
4896
  "@keyonly": {
4070
4897
  isExample: true,
4071
4898
  example: "$parent.isCorrect",
4072
- "@bit": { isExample: true }
4899
+ "@bit": { isExample: true },
4900
+ "@card": { isExample: true }
4073
4901
  }
4074
4902
  },
4075
4903
  {
4076
4904
  "@absent": {
4905
+ isExample: true,
4906
+ "@bit": { isExample: true },
4907
+ "@card": { isExample: true }
4908
+ }
4909
+ },
4910
+ {
4911
+ isExample: true,
4912
+ example: "$",
4913
+ "@bit": { isExample: true },
4914
+ "@card": { isExample: true }
4915
+ }
4916
+ ],
4917
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
4918
+ format: TagFormat.boolean,
4919
+ nullable: true
4920
+ }
4921
+ ]
4922
+ }
4923
+ ]
4924
+ },
4925
+ [ConfigKey.group_trueFalseResponses]: {
4926
+ type: GroupConfigType.standard,
4927
+ description: "True/False Responses chain",
4928
+ tags: [
4929
+ {
4930
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4931
+ key: ConfigKey.tag_true,
4932
+ description: "True value for a true/false statement/question",
4933
+ maxCount: Count.infinity,
4934
+ format: TagFormat.plainText,
4935
+ chain: [
4936
+ {
4937
+ key: ConfigKey.tag_true,
4938
+ jsonKey: "responses[]|set(isCorrect=true)",
4939
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4940
+ description: "True values for a true/false statement/question",
4941
+ maxCount: Count.infinity,
4942
+ format: TagFormat.plainText
4943
+ },
4944
+ {
4945
+ key: ConfigKey.tag_false,
4946
+ jsonKey: "responses[]|set(isCorrect=false)",
4947
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4948
+ description: "False values for a true/false statement/question",
4949
+ maxCount: Count.infinity,
4950
+ format: TagFormat.plainText
4951
+ },
4952
+ {
4953
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4954
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4955
+ },
4956
+ {
4957
+ // @card scope-shift writes response-container isExample.
4958
+ key: ConfigKey.property_example,
4959
+ jsonKey: "example",
4960
+ exportJsonKey: [
4961
+ {
4962
+ "@keyonly": {
4077
4963
  isExample: true,
4078
4964
  example: "$parent.isCorrect",
4079
- "@bit": { isExample: true }
4965
+ "@bit": { isExample: true },
4966
+ "@card": { isExample: true }
4080
4967
  }
4081
4968
  },
4082
- { isExample: true, example: "$", "@bit": { isExample: true } }
4969
+ {
4970
+ "@absent": {
4971
+ isExample: "$parent.isCorrect",
4972
+ example: "$parent.isCorrect",
4973
+ "@bit": { isExample: "$parent.isCorrect" },
4974
+ "@card": { isExample: "$parent.isCorrect" }
4975
+ }
4976
+ },
4977
+ {
4978
+ isExample: true,
4979
+ example: "$",
4980
+ "@bit": { isExample: true },
4981
+ "@card": { isExample: true }
4982
+ }
4083
4983
  ],
4084
4984
  description: "An example for the true/false statement/question",
4085
- format: TagFormat.bitmarkText
4985
+ format: TagFormat.boolean,
4986
+ nullable: true
4987
+ }
4988
+ ]
4989
+ },
4990
+ {
4991
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4992
+ key: ConfigKey.tag_false,
4993
+ description: "False value for a true/false statement/question",
4994
+ maxCount: Count.infinity,
4995
+ format: TagFormat.plainText,
4996
+ chain: [
4997
+ {
4998
+ key: ConfigKey.tag_true,
4999
+ jsonKey: "responses[]|set(isCorrect=true)",
5000
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5001
+ description: "True values for a true/false statement/question",
5002
+ maxCount: Count.infinity,
5003
+ format: TagFormat.plainText
5004
+ },
5005
+ {
5006
+ key: ConfigKey.tag_false,
5007
+ jsonKey: "responses[]|set(isCorrect=false)",
5008
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
5009
+ description: "False values for a true/false statement/question",
5010
+ maxCount: Count.infinity,
5011
+ format: TagFormat.plainText
5012
+ },
5013
+ {
5014
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5015
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5016
+ },
5017
+ {
5018
+ // @card scope-shift writes response-container isExample.
5019
+ key: ConfigKey.property_example,
5020
+ jsonKey: "example",
5021
+ exportJsonKey: [
5022
+ {
5023
+ "@keyonly": {
5024
+ isExample: true,
5025
+ example: "$parent.isCorrect",
5026
+ "@bit": { isExample: true },
5027
+ "@card": { isExample: true }
5028
+ }
5029
+ },
5030
+ {
5031
+ "@absent": {
5032
+ isExample: true,
5033
+ example: "$parent.isCorrect",
5034
+ "@bit": { isExample: true },
5035
+ "@card": { isExample: true }
5036
+ }
5037
+ },
5038
+ {
5039
+ isExample: true,
5040
+ example: "$",
5041
+ "@bit": { isExample: true },
5042
+ "@card": { isExample: true }
5043
+ }
5044
+ ],
5045
+ description: "An example for the true/false statement/question",
5046
+ format: TagFormat.boolean,
5047
+ nullable: true
5048
+ }
5049
+ ]
5050
+ }
5051
+ ]
5052
+ },
5053
+ [ConfigKey.group_trueFalseInlineSelect]: {
5054
+ type: GroupConfigType.standard,
5055
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5056
+ tags: [
5057
+ {
5058
+ exportJsonKey: {
5059
+ "@body-inline": {
5060
+ type: "select",
5061
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5062
+ }
5063
+ },
5064
+ key: ConfigKey.tag_true,
5065
+ description: "True value for an inline select body bit",
5066
+ maxCount: Count.infinity,
5067
+ format: TagFormat.plainText,
5068
+ chain: [
5069
+ {
5070
+ key: ConfigKey.tag_true,
5071
+ exportJsonKey: {},
5072
+ description: "True values for an inline select body bit",
5073
+ maxCount: Count.infinity,
5074
+ format: TagFormat.plainText
5075
+ },
5076
+ {
5077
+ key: ConfigKey.tag_false,
5078
+ exportJsonKey: {},
5079
+ description: "False values for an inline select body bit",
5080
+ maxCount: Count.infinity,
5081
+ format: TagFormat.plainText
5082
+ },
5083
+ {
5084
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5085
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5086
+ },
5087
+ {
5088
+ // PLAN-083: cascade-fired `@example` from bit header fires
5089
+ // `isExample:true, example:true` on the FIRST CORRECT option
5090
+ // only (BPG `fillBooleanExample(select.options,
5091
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5092
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5093
+ // OR — any cascade ⇒ bit marked) and
5094
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5095
+ // on the select node).
5096
+ key: ConfigKey.property_example,
5097
+ jsonKey: "example",
5098
+ exportJsonKey: [
5099
+ {
5100
+ "@keyonly": {
5101
+ isExample: true,
5102
+ example: "$parent.isCorrect",
5103
+ "@bit": { isExample: true },
5104
+ "@body-inline": { attrs: { isExample: true } }
5105
+ }
5106
+ },
5107
+ {
5108
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5109
+ maxEmits: 1,
5110
+ rule: {
5111
+ isExample: true,
5112
+ example: true,
5113
+ "@bit": { isExample: true },
5114
+ "@body-inline": { attrs: { isExample: true } }
5115
+ }
5116
+ },
5117
+ {
5118
+ isExample: true,
5119
+ example: "$",
5120
+ "@bit": { isExample: true },
5121
+ "@body-inline": { attrs: { isExample: true } }
5122
+ }
5123
+ ],
5124
+ description: "An example for the inline select option (first correct only)",
5125
+ format: TagFormat.boolean,
5126
+ nullable: true
5127
+ }
5128
+ ]
5129
+ },
5130
+ {
5131
+ exportJsonKey: {
5132
+ "@body-inline": {
5133
+ type: "select",
5134
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5135
+ }
5136
+ },
5137
+ key: ConfigKey.tag_false,
5138
+ description: "False value for an inline select body bit",
5139
+ maxCount: Count.infinity,
5140
+ format: TagFormat.plainText,
5141
+ chain: [
5142
+ {
5143
+ key: ConfigKey.tag_true,
5144
+ exportJsonKey: {},
5145
+ description: "True values for an inline select body bit",
5146
+ maxCount: Count.infinity,
5147
+ format: TagFormat.plainText
5148
+ },
5149
+ {
5150
+ key: ConfigKey.tag_false,
5151
+ exportJsonKey: {},
5152
+ description: "False values for an inline select body bit",
5153
+ maxCount: Count.infinity,
5154
+ format: TagFormat.plainText
5155
+ },
5156
+ {
5157
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5158
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5159
+ },
5160
+ {
5161
+ // PLAN-083: identical to the `+` lead's chain @example —
5162
+ // cascade reaches the first correct option regardless of
5163
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5164
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5165
+ key: ConfigKey.property_example,
5166
+ jsonKey: "example",
5167
+ exportJsonKey: [
5168
+ {
5169
+ "@keyonly": {
5170
+ isExample: true,
5171
+ example: "$parent.isCorrect",
5172
+ "@bit": { isExample: true },
5173
+ "@body-inline": { attrs: { isExample: true } }
5174
+ }
5175
+ },
5176
+ {
5177
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5178
+ maxEmits: 1,
5179
+ rule: {
5180
+ isExample: true,
5181
+ example: true,
5182
+ "@bit": { isExample: true },
5183
+ "@body-inline": { attrs: { isExample: true } }
5184
+ }
5185
+ },
5186
+ {
5187
+ isExample: true,
5188
+ example: "$",
5189
+ "@bit": { isExample: true },
5190
+ "@body-inline": { attrs: { isExample: true } }
5191
+ }
5192
+ ],
5193
+ description: "An example for the inline select option (first correct only)",
5194
+ format: TagFormat.boolean,
5195
+ nullable: true
5196
+ }
5197
+ ]
5198
+ }
5199
+ ]
5200
+ },
5201
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5202
+ type: GroupConfigType.standard,
5203
+ 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.',
5204
+ tags: [
5205
+ {
5206
+ exportJsonKey: {
5207
+ "@body-inline": {
5208
+ type: "highlight",
5209
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5210
+ }
5211
+ },
5212
+ key: ConfigKey.tag_true,
5213
+ description: "Highlighted span in the body (correct)",
5214
+ maxCount: Count.infinity,
5215
+ format: TagFormat.plainText,
5216
+ chain: [
5217
+ {
5218
+ key: ConfigKey.tag_true,
5219
+ exportJsonKey: {},
5220
+ description: "Highlighted span values (correct)",
5221
+ maxCount: Count.infinity,
5222
+ format: TagFormat.plainText
5223
+ },
5224
+ {
5225
+ key: ConfigKey.tag_false,
5226
+ exportJsonKey: {},
5227
+ description: "Highlighted span values (incorrect)",
5228
+ maxCount: Count.infinity,
5229
+ format: TagFormat.plainText
5230
+ },
5231
+ {
5232
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5233
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5234
+ },
5235
+ {
5236
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5237
+ // `@example` from the bit header fires `example: true` +
5238
+ // `isExample: true` on the FIRST correct highlight only
5239
+ // (BPG `fillBooleanExample(highlight.texts,
5240
+ // firstCorrectOnly=true)`). Incorrect highlights get
5241
+ // nothing (unlike choices, the highlight-text fixture
5242
+ // shows no `isExample` on incorrect entries).
5243
+ key: ConfigKey.property_example,
5244
+ jsonKey: "example",
5245
+ exportJsonKey: [
5246
+ {
5247
+ "@keyonly": {
5248
+ isExample: true,
5249
+ example: "$parent.isCorrect",
5250
+ "@bit": { isExample: true },
5251
+ "@body-inline": { attrs: { isExample: true } }
5252
+ }
5253
+ },
5254
+ {
5255
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5256
+ maxEmits: 1,
5257
+ rule: {
5258
+ isExample: true,
5259
+ example: true,
5260
+ "@bit": { isExample: true },
5261
+ "@body-inline": { attrs: { isExample: true } }
5262
+ }
5263
+ },
5264
+ {
5265
+ isExample: true,
5266
+ example: "$",
5267
+ "@bit": { isExample: true },
5268
+ "@body-inline": { attrs: { isExample: true } }
5269
+ }
5270
+ ],
5271
+ description: "An example for the highlighted span (first correct only)",
5272
+ format: TagFormat.boolean,
5273
+ nullable: true
5274
+ }
5275
+ ]
5276
+ },
5277
+ {
5278
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5279
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5280
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5281
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5282
+ // through and nothing fires. The natural-default strip removes
5283
+ // `isCorrect: false` from the final texts entries (matches fixture).
5284
+ exportJsonKey: {
5285
+ "@body-inline": {
5286
+ type: "highlight",
5287
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5288
+ }
5289
+ },
5290
+ key: ConfigKey.tag_false,
5291
+ description: "Highlighted span in the body (incorrect)",
5292
+ maxCount: Count.infinity,
5293
+ format: TagFormat.plainText,
5294
+ chain: [
5295
+ {
5296
+ key: ConfigKey.tag_true,
5297
+ exportJsonKey: {},
5298
+ description: "Highlighted span values (correct)",
5299
+ maxCount: Count.infinity,
5300
+ format: TagFormat.plainText
5301
+ },
5302
+ {
5303
+ key: ConfigKey.tag_false,
5304
+ exportJsonKey: {},
5305
+ description: "Highlighted span values (incorrect)",
5306
+ maxCount: Count.infinity,
5307
+ format: TagFormat.plainText
5308
+ },
5309
+ {
5310
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5311
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5312
+ },
5313
+ {
5314
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5315
+ // `@example` on a `-` (incorrect highlight) emits nothing
5316
+ // (highlight-text fixture has no `isExample` on incorrect
5317
+ // entries; firstCorrectOnly cascade skips them entirely).
5318
+ // No `@absent` rule.
5319
+ // PLAN-083: identical to the `+` lead's chain @example —
5320
+ // cascade reaches the first correct option regardless of
5321
+ // which lead (+/-) opens the chain.
5322
+ key: ConfigKey.property_example,
5323
+ jsonKey: "example",
5324
+ exportJsonKey: [
5325
+ {
5326
+ "@keyonly": {
5327
+ isExample: true,
5328
+ example: "$parent.isCorrect",
5329
+ "@bit": { isExample: true },
5330
+ "@body-inline": { attrs: { isExample: true } }
5331
+ }
5332
+ },
5333
+ {
5334
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5335
+ maxEmits: 1,
5336
+ rule: {
5337
+ isExample: true,
5338
+ example: true,
5339
+ "@bit": { isExample: true },
5340
+ "@body-inline": { attrs: { isExample: true } }
5341
+ }
5342
+ },
5343
+ {
5344
+ isExample: true,
5345
+ example: "$",
5346
+ "@bit": { isExample: true },
5347
+ "@body-inline": { attrs: { isExample: true } }
5348
+ }
5349
+ ],
5350
+ description: "An example for the highlighted span (first correct only)",
5351
+ format: TagFormat.boolean,
5352
+ nullable: true
4086
5353
  }
4087
5354
  ]
4088
5355
  }
@@ -4095,7 +5362,7 @@ var GROUPS = {
4095
5362
  {
4096
5363
  key: ConfigKey.property_mark,
4097
5364
  jsonKey: "marks.mark",
4098
- exportJsonKey: { marks: { mark: ["$"] } },
5365
+ exportJsonKey: { marks: [{ mark: "$" }] },
4099
5366
  description: "The mark configuration",
4100
5367
  format: TagFormat.plainText,
4101
5368
  maxCount: Count.infinity,
@@ -4119,7 +5386,9 @@ var GROUPS = {
4119
5386
  description: "Mark chain",
4120
5387
  tags: [
4121
5388
  {
4122
- exportJsonKey: { solution: ["$"] },
5389
+ exportJsonKey: {
5390
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5391
+ },
4123
5392
  key: ConfigKey.tag_mark,
4124
5393
  description: "The marked content",
4125
5394
  maxCount: Count.infinity,
@@ -4128,6 +5397,7 @@ var GROUPS = {
4128
5397
  {
4129
5398
  key: ConfigKey.property_mark,
4130
5399
  jsonKey: "mark",
5400
+ exportJsonKey: { mark: "$" },
4131
5401
  description: "The mark configuration",
4132
5402
  format: TagFormat.plainText
4133
5403
  },
@@ -4140,7 +5410,8 @@ var GROUPS = {
4140
5410
  { isExample: true, example: "$", "@bit": { isExample: true } }
4141
5411
  ],
4142
5412
  description: "An example for the marked content",
4143
- format: TagFormat.bitmarkText
5413
+ format: TagFormat.bitmarkText,
5414
+ nullable: true
4144
5415
  }
4145
5416
  ]
4146
5417
  }
@@ -4569,6 +5840,42 @@ var GROUPS = {
4569
5840
  }
4570
5841
  ]
4571
5842
  },
5843
+ [ConfigKey.group_backgroundWallpaper]: {
5844
+ type: GroupConfigType.standard,
5845
+ description: "Background wallpaper tags",
5846
+ tags: [
5847
+ {
5848
+ key: ConfigKey.resource_backgroundWallpaper,
5849
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
5850
+ description: "Background wallpaper for the image, used to set a background for the image",
5851
+ chain: [
5852
+ {
5853
+ key: ConfigKey.group_resourceImageCommon,
5854
+ description: "Common resource image tags for images"
5855
+ }
5856
+ ]
5857
+ }
5858
+ ]
5859
+ },
5860
+ [ConfigKey.group_imageNoZoom]: {
5861
+ type: GroupConfigType.standard,
5862
+ description: "Image bit tags",
5863
+ tags: [
5864
+ {
5865
+ key: ConfigKey.group_backgroundWallpaper,
5866
+ description: "Background wallpaper tags for images, used to define background properties for images"
5867
+ },
5868
+ {
5869
+ key: ConfigKey.group_resourceBitTags,
5870
+ description: "Resource bit tags for images, used to define additional properties for images"
5871
+ },
5872
+ {
5873
+ key: ConfigKey.group_resourceImageNoZoom,
5874
+ description: "Resource image tags for images, used to attach images to the bit",
5875
+ minCount: 1
5876
+ }
5877
+ ]
5878
+ },
4572
5879
  //
4573
5880
  // Resource groups
4574
5881
  //
@@ -4688,6 +5995,58 @@ var GROUPS = {
4688
5995
  }
4689
5996
  ]
4690
5997
  },
5998
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
5999
+ type: GroupConfigType.standard,
6000
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
6001
+ tags: [
6002
+ {
6003
+ key: ConfigKey.group_resourceCommon,
6004
+ description: "Common resource properties"
6005
+ },
6006
+ {
6007
+ key: ConfigKey.property_src1x,
6008
+ description: "The source URL for the image at 1x resolution",
6009
+ format: TagFormat.plainText
6010
+ },
6011
+ {
6012
+ key: ConfigKey.property_src2x,
6013
+ description: "The source URL for the image at 2x resolution",
6014
+ format: TagFormat.plainText
6015
+ },
6016
+ {
6017
+ key: ConfigKey.property_src3x,
6018
+ description: "The source URL for the image at 3x resolution",
6019
+ format: TagFormat.plainText
6020
+ },
6021
+ {
6022
+ key: ConfigKey.property_src4x,
6023
+ description: "The source URL for the image at 4x resolution",
6024
+ format: TagFormat.plainText
6025
+ },
6026
+ {
6027
+ key: ConfigKey.property_width,
6028
+ description: "The width of the image",
6029
+ format: TagFormat.number
6030
+ },
6031
+ {
6032
+ key: ConfigKey.property_height,
6033
+ description: "The height of the image",
6034
+ format: TagFormat.number
6035
+ },
6036
+ {
6037
+ key: ConfigKey.property_alt,
6038
+ description: "The alternative text for the image",
6039
+ format: TagFormat.plainText
6040
+ },
6041
+ // @zoomDisabled with a true default
6042
+ {
6043
+ key: ConfigKey.property_zoomDisabled,
6044
+ description: "If true, zooming is disabled for the image (defaults to true)",
6045
+ format: TagFormat.boolean,
6046
+ defaultValue: "true"
6047
+ }
6048
+ ]
6049
+ },
4691
6050
  [ConfigKey.group_resourceAudioCommon]: {
4692
6051
  type: GroupConfigType.standard,
4693
6052
  description: "Common properties for audio resources",
@@ -4777,22 +6136,26 @@ var GROUPS = {
4777
6136
  {
4778
6137
  key: ConfigKey.property_src1x,
4779
6138
  description: "The source URL for the video at 1x resolution",
4780
- format: TagFormat.plainText
6139
+ format: TagFormat.plainText,
6140
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4781
6141
  },
4782
6142
  {
4783
6143
  key: ConfigKey.property_src2x,
4784
6144
  description: "The source URL for the video at 2x resolution",
4785
- format: TagFormat.plainText
6145
+ format: TagFormat.plainText,
6146
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4786
6147
  },
4787
6148
  {
4788
6149
  key: ConfigKey.property_src3x,
4789
6150
  description: "The source URL for the video at 3x resolution",
4790
- format: TagFormat.plainText
6151
+ format: TagFormat.plainText,
6152
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4791
6153
  },
4792
6154
  {
4793
6155
  key: ConfigKey.property_src4x,
4794
6156
  description: "The source URL for the video at 4x resolution",
4795
- format: TagFormat.plainText
6157
+ format: TagFormat.plainText,
6158
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4796
6159
  }
4797
6160
  ]
4798
6161
  },
@@ -4805,6 +6168,7 @@ var GROUPS = {
4805
6168
  tags: [
4806
6169
  {
4807
6170
  key: ConfigKey.resource_icon,
6171
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4808
6172
  description: "The icon resource",
4809
6173
  chain: [
4810
6174
  {
@@ -4832,12 +6196,30 @@ var GROUPS = {
4832
6196
  }
4833
6197
  ]
4834
6198
  },
6199
+ [ConfigKey.group_resourceImageNoZoom]: {
6200
+ type: GroupConfigType.resource,
6201
+ description: "Image resource where @zoomDisabled defaults to true",
6202
+ tags: [
6203
+ {
6204
+ key: ConfigKey.resource_image,
6205
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6206
+ description: "The image resource (no-zoom default)",
6207
+ chain: [
6208
+ {
6209
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6210
+ description: "Common image properties with @zoomDisabled defaulting to true"
6211
+ }
6212
+ ]
6213
+ }
6214
+ ]
6215
+ },
4835
6216
  [ConfigKey.group_resourceImagePortrait]: {
4836
6217
  type: GroupConfigType.resource,
4837
6218
  description: "Portrait image resource",
4838
6219
  tags: [
4839
6220
  {
4840
6221
  key: ConfigKey.resource_imagePortrait,
6222
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4841
6223
  description: "The portrait image resource",
4842
6224
  chain: [
4843
6225
  {
@@ -4854,6 +6236,7 @@ var GROUPS = {
4854
6236
  tags: [
4855
6237
  {
4856
6238
  key: ConfigKey.resource_imageLandscape,
6239
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4857
6240
  description: "The landscape image resource",
4858
6241
  chain: [
4859
6242
  {
@@ -4870,6 +6253,7 @@ var GROUPS = {
4870
6253
  tags: [
4871
6254
  {
4872
6255
  key: ConfigKey.resource_imageEmbed,
6256
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4873
6257
  description: "The embedded image resource",
4874
6258
  chain: [
4875
6259
  {
@@ -5043,6 +6427,7 @@ var GROUPS = {
5043
6427
  tags: [
5044
6428
  {
5045
6429
  key: ConfigKey.resource_article,
6430
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
5046
6431
  description: "The article resource",
5047
6432
  chain: [
5048
6433
  {
@@ -5059,6 +6444,7 @@ var GROUPS = {
5059
6444
  tags: [
5060
6445
  {
5061
6446
  key: ConfigKey.resource_articleEmbed,
6447
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
5062
6448
  description: "The embedded article resource",
5063
6449
  chain: [
5064
6450
  {
@@ -5075,6 +6461,7 @@ var GROUPS = {
5075
6461
  tags: [
5076
6462
  {
5077
6463
  key: ConfigKey.resource_articleLink,
6464
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
5078
6465
  description: "The link to the article resource",
5079
6466
  chain: [
5080
6467
  {
@@ -5345,7 +6732,12 @@ var ConfigHydrator = class {
5345
6732
  isDefault: v.isDefault,
5346
6733
  sideJsonKey: v.sideJsonKey,
5347
6734
  sideExportJsonKey: v.sideExportJsonKey,
5348
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6735
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6736
+ // PLAN-085: cardinality fields pass through; defaults applied
6737
+ // downstream by the config consumer (Rust treats `0` / undefined
6738
+ // as unbounded).
6739
+ minCount: v.minCount,
6740
+ maxCount: v.maxCount
5349
6741
  };
5350
6742
  }
5351
6743
  return out;
@@ -5560,7 +6952,8 @@ var BITS = {
5560
6952
  { isExample: true, example: "$" }
5561
6953
  ],
5562
6954
  description: "The example text for the bit",
5563
- format: TagFormat.plainText
6955
+ format: TagFormat.plainText,
6956
+ nullable: true
5564
6957
  }
5565
6958
  ],
5566
6959
  rootExampleType: ExampleType.string
@@ -5899,7 +7292,8 @@ var BITS = {
5899
7292
  { isExample: true, example: "$" }
5900
7293
  ],
5901
7294
  description: "The example text for the bit",
5902
- format: TagFormat.plainText
7295
+ format: TagFormat.plainText,
7296
+ nullable: true
5903
7297
  }
5904
7298
  ],
5905
7299
  rootExampleType: ExampleType.string
@@ -6062,7 +7456,8 @@ var BITS = {
6062
7456
  { isExample: true, example: "$" }
6063
7457
  ],
6064
7458
  description: "The example text for the bit",
6065
- format: TagFormat.plainText
7459
+ format: TagFormat.plainText,
7460
+ nullable: true
6066
7461
  }
6067
7462
  ],
6068
7463
  rootExampleType: ExampleType.string
@@ -6213,7 +7608,8 @@ var BITS = {
6213
7608
  { isExample: true, example: "$" }
6214
7609
  ],
6215
7610
  description: "The example text for the bit",
6216
- format: TagFormat.plainText
7611
+ format: TagFormat.plainText,
7612
+ nullable: true
6217
7613
  }
6218
7614
  ],
6219
7615
  rootExampleType: ExampleType.string
@@ -6808,8 +8204,31 @@ var BITS = {
6808
8204
  description: "Tags for gaps in cloze and multiple choice text bits"
6809
8205
  },
6810
8206
  {
6811
- key: ConfigKey.group_trueFalse,
8207
+ key: ConfigKey.group_trueFalseInlineSelect,
6812
8208
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8209
+ },
8210
+ {
8211
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8212
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8213
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8214
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8215
+ key: ConfigKey.property_isCaseSensitive,
8216
+ exportJsonKey: {},
8217
+ description: "If the cloze answers are case sensitive",
8218
+ format: TagFormat.boolean,
8219
+ defaultValue: "true"
8220
+ },
8221
+ {
8222
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8223
+ key: ConfigKey.property_example,
8224
+ exportJsonKey: [
8225
+ { "@keyonly": { isExample: true } },
8226
+ { "@absent": { isExample: true } },
8227
+ { isExample: true, example: "$" }
8228
+ ],
8229
+ description: "The example(s) for the bit",
8230
+ format: TagFormat.bitmarkText,
8231
+ nullable: true
6813
8232
  }
6814
8233
  ]
6815
8234
  },
@@ -6848,18 +8267,56 @@ var BITS = {
6848
8267
  {
6849
8268
  key: ConfigKey.group_gap,
6850
8269
  description: "Tags for gaps in cloze bits"
8270
+ },
8271
+ {
8272
+ // Override inherited @example: cloze is in OLD parser's
8273
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8274
+ // emits `isExample: true`.
8275
+ key: ConfigKey.property_example,
8276
+ exportJsonKey: [
8277
+ { "@keyonly": { isExample: true } },
8278
+ { "@absent": { isExample: true } },
8279
+ { isExample: true, example: "$" }
8280
+ ],
8281
+ description: "The example(s) for the bit",
8282
+ format: TagFormat.bitmarkText,
8283
+ nullable: true
6851
8284
  }
6852
8285
  ]
6853
8286
  },
6854
8287
  [BitType2.clozeInstructionGrouped]: {
6855
8288
  since: "1.3.0",
6856
8289
  baseBitType: BitType2.cloze,
6857
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8290
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8291
+ tags: [
8292
+ {
8293
+ key: ConfigKey.property_quizCountItems,
8294
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8295
+ format: TagFormat.boolean,
8296
+ defaultValue: "true"
8297
+ }
8298
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8299
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8300
+ ]
6858
8301
  },
6859
8302
  [BitType2.clozeSolutionGrouped]: {
6860
8303
  since: "1.3.0",
6861
8304
  baseBitType: BitType2.cloze,
6862
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8305
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8306
+ tags: [
8307
+ {
8308
+ key: ConfigKey.property_quizCountItems,
8309
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8310
+ format: TagFormat.boolean,
8311
+ defaultValue: "true"
8312
+ },
8313
+ {
8314
+ key: ConfigKey.property_quizStrikethroughSolutions,
8315
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8316
+ format: TagFormat.boolean,
8317
+ defaultValue: "true"
8318
+ }
8319
+ ]
6863
8320
  },
6864
8321
  [BitType2.clozeSeveral]: {
6865
8322
  since: "3.5.0",
@@ -6895,6 +8352,17 @@ var BITS = {
6895
8352
  {
6896
8353
  key: ConfigKey.group_quizCommon,
6897
8354
  description: "Common tags for quiz bits"
8355
+ },
8356
+ {
8357
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8358
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8359
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8360
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8361
+ key: ConfigKey.property_isCaseSensitive,
8362
+ exportJsonKey: {},
8363
+ description: "If the cloze answers are case sensitive",
8364
+ format: TagFormat.boolean,
8365
+ defaultValue: "true"
6898
8366
  }
6899
8367
  ],
6900
8368
  cardSet: CardSetConfigKey.clozeList
@@ -7061,6 +8529,7 @@ var BITS = {
7061
8529
  tags: [
7062
8530
  {
7063
8531
  key: ConfigKey.group_person,
8532
+ exportJsonKey: { partner: { name: "$" } },
7064
8533
  description: "Tags for the person in the conversation"
7065
8534
  }
7066
8535
  ]
@@ -7499,7 +8968,8 @@ var BITS = {
7499
8968
  { isExample: true, example: "$" }
7500
8969
  ],
7501
8970
  description: "The example text for the essay bit",
7502
- format: TagFormat.plainText
8971
+ format: TagFormat.plainText,
8972
+ nullable: true
7503
8973
  }
7504
8974
  ],
7505
8975
  rootExampleType: ExampleType.string
@@ -7533,7 +9003,8 @@ var BITS = {
7533
9003
  { isExample: true, example: "$" }
7534
9004
  ],
7535
9005
  description: "The example text for the example bit",
7536
- format: TagFormat.plainText
9006
+ format: TagFormat.plainText,
9007
+ nullable: true
7537
9008
  }
7538
9009
  ],
7539
9010
  rootExampleType: ExampleType.string
@@ -7619,7 +9090,19 @@ var BITS = {
7619
9090
  [BitType2.articleAi]: {
7620
9091
  since: "1.3.0",
7621
9092
  baseBitType: BitType2.article,
7622
- description: "Article AI bit, used to create AI-generated articles"
9093
+ description: "Article AI bit, used to create AI-generated articles",
9094
+ tags: [
9095
+ {
9096
+ // Shadows the inherited @aiGenerated from the standard group: this
9097
+ // bit type forces aiGenerated:true regardless of source value or
9098
+ // absence. Literal `true` in the export pattern ignores `$`.
9099
+ key: ConfigKey.property_aiGenerated,
9100
+ description: "AI-generated flag, forced true for article-ai bits",
9101
+ format: TagFormat.boolean,
9102
+ defaultValue: "true",
9103
+ exportJsonKey: { aiGenerated: true }
9104
+ }
9105
+ ]
7623
9106
  },
7624
9107
  [BitType2.articleAttachment]: {
7625
9108
  since: "1.3.0",
@@ -7944,7 +9427,19 @@ var BITS = {
7944
9427
  [BitType2.noteAi]: {
7945
9428
  since: "1.3.0",
7946
9429
  baseBitType: BitType2.note,
7947
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9430
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9431
+ tags: [
9432
+ {
9433
+ // Shadows the inherited @aiGenerated from the standard group: this
9434
+ // bit type forces aiGenerated:true regardless of source value or
9435
+ // absence. Literal `true` in the export pattern ignores `$`.
9436
+ key: ConfigKey.property_aiGenerated,
9437
+ description: "AI-generated flag, forced true for article-ai bits",
9438
+ format: TagFormat.boolean,
9439
+ defaultValue: "true",
9440
+ exportJsonKey: { aiGenerated: true }
9441
+ }
9442
+ ]
7948
9443
  },
7949
9444
  [BitType2.notebookArticle]: {
7950
9445
  since: "1.3.0",
@@ -8069,7 +9564,19 @@ var BITS = {
8069
9564
  [BitType2.summaryAi]: {
8070
9565
  since: "1.3.0",
8071
9566
  baseBitType: BitType2.summary,
8072
- description: "AI-generated summary bit"
9567
+ description: "AI-generated summary bit",
9568
+ tags: [
9569
+ {
9570
+ // Shadows the inherited @aiGenerated from the standard group: this
9571
+ // bit type forces aiGenerated:true regardless of source value or
9572
+ // absence. Literal `true` in the export pattern ignores `$`.
9573
+ key: ConfigKey.property_aiGenerated,
9574
+ description: "AI-generated flag, forced true for article-ai bits",
9575
+ format: TagFormat.boolean,
9576
+ defaultValue: "true",
9577
+ exportJsonKey: { aiGenerated: true }
9578
+ }
9579
+ ]
8073
9580
  },
8074
9581
  [BitType2.videoTranscript]: {
8075
9582
  since: "1.3.0",
@@ -8343,7 +9850,7 @@ var BITS = {
8343
9850
  description: "Product ID for the module product, used to link to a specific product",
8344
9851
  format: TagFormat.plainText,
8345
9852
  minCount: 1,
8346
- maxCount: Count.infinity
9853
+ maxCount: 1
8347
9854
  }
8348
9855
  ]
8349
9856
  },
@@ -8396,10 +9903,13 @@ var BITS = {
8396
9903
  description: "Resource bit tags for logo grave images, used to define additional properties"
8397
9904
  },
8398
9905
  {
8399
- // Image resource
8400
- key: ConfigKey.group_resourceImage,
9906
+ // Image resource — fully-expanded shape so each `&image` emission
9907
+ // produces a complete `{type, image: {src}}` object inside the
9908
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
9909
+ // `images[].image` via the `[]` last-element-merge semantic.
9910
+ key: ConfigKey.group_resourceImageNoZoom,
8401
9911
  description: "Resource image tags for logo grave images, used to attach images",
8402
- exportJsonKey: { images: "$" },
9912
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8403
9913
  minCount: 1,
8404
9914
  maxCount: Count.infinity
8405
9915
  }
@@ -8689,6 +10199,10 @@ var BITS = {
8689
10199
  tags: [
8690
10200
  {
8691
10201
  key: ConfigKey.property_book,
10202
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10203
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10204
+ // inside the most-recently-appended entry as `reference: $`.
10205
+ exportJsonKey: { book: [{ book: "$" }] },
8692
10206
  description: "Book reference for the page, used to link to a specific book",
8693
10207
  maxCount: Count.infinity,
8694
10208
  chain: [
@@ -8756,7 +10270,9 @@ var BITS = {
8756
10270
  format: TagFormat.plainText
8757
10271
  }
8758
10272
  ],
8759
- cardSet: CardSetConfigKey.exampleBitList
10273
+ // page-footer emits cards under `sections` (vs `listItems` for the
10274
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10275
+ cardSet: CardSetConfigKey.pageFooterSections
8760
10276
  },
8761
10277
  [BitType2.legend]: {
8762
10278
  since: "3.12.0",
@@ -8798,6 +10314,20 @@ var BITS = {
8798
10314
  since: "1.34.0",
8799
10315
  baseBitType: BitType2._standard,
8800
10316
  description: "Definition list bit, used to create lists of definitions in articles or books",
10317
+ tags: [
10318
+ {
10319
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10320
+ key: ConfigKey.property_example,
10321
+ exportJsonKey: [
10322
+ { "@keyonly": { isExample: true } },
10323
+ { "@absent": { isExample: true } },
10324
+ { isExample: true, example: "$" }
10325
+ ],
10326
+ description: "The example(s) for the bit",
10327
+ format: TagFormat.bitmarkText,
10328
+ nullable: true
10329
+ }
10330
+ ],
8801
10331
  cardSet: CardSetConfigKey.definitionList
8802
10332
  },
8803
10333
  [BitType2.metaSearchDefaultTerms]: {
@@ -8820,6 +10350,18 @@ var BITS = {
8820
10350
  {
8821
10351
  key: ConfigKey.group_quizCommon,
8822
10352
  description: "Common quiz tags for flashcards"
10353
+ },
10354
+ {
10355
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10356
+ key: ConfigKey.property_example,
10357
+ exportJsonKey: [
10358
+ { "@keyonly": { isExample: true } },
10359
+ { "@absent": { isExample: true } },
10360
+ { isExample: true, example: "$" }
10361
+ ],
10362
+ description: "The example(s) for the bit",
10363
+ format: TagFormat.bitmarkText,
10364
+ nullable: true
8823
10365
  }
8824
10366
  ],
8825
10367
  cardSet: CardSetConfigKey.flashcard
@@ -8827,7 +10369,11 @@ var BITS = {
8827
10369
  [BitType2.flashcard1]: {
8828
10370
  since: "1.3.0",
8829
10371
  baseBitType: BitType2.flashcard,
8830
- description: "Flashcard 1 bit"
10372
+ description: "Flashcard 1 bit",
10373
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10374
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10375
+ // bit type structurally allows only a single card.
10376
+ cardSet: CardSetConfigKey.flashcard1
8831
10377
  },
8832
10378
  [BitType2.qAndACard]: {
8833
10379
  since: "3.25.0",
@@ -8862,8 +10408,24 @@ var BITS = {
8862
10408
  description: "Common quiz tags for highlighted text"
8863
10409
  },
8864
10410
  {
8865
- key: ConfigKey.group_trueFalse,
8866
- description: "True/False quiz tags for highlighted text"
10411
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10412
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10413
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10414
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10415
+ key: ConfigKey.group_trueFalseInlineHighlight,
10416
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10417
+ },
10418
+ {
10419
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10420
+ key: ConfigKey.property_example,
10421
+ exportJsonKey: [
10422
+ { "@keyonly": { isExample: true } },
10423
+ { "@absent": { isExample: true } },
10424
+ { isExample: true, example: "$" }
10425
+ ],
10426
+ description: "The example(s) for the bit",
10427
+ format: TagFormat.bitmarkText,
10428
+ nullable: true
8867
10429
  }
8868
10430
  ]
8869
10431
  },
@@ -8873,15 +10435,8 @@ var BITS = {
8873
10435
  description: "Image bit, used to create images in articles or books",
8874
10436
  tags: [
8875
10437
  {
8876
- key: ConfigKey.resource_backgroundWallpaper,
8877
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8878
- description: "Background wallpaper for the image, used to set a background for the image",
8879
- chain: [
8880
- {
8881
- key: ConfigKey.group_resourceImageCommon,
8882
- description: "Common resource image tags for images"
8883
- }
8884
- ]
10438
+ key: ConfigKey.group_backgroundWallpaper,
10439
+ description: "Background wallpaper tags for images, used to define background properties for images"
8885
10440
  },
8886
10441
  {
8887
10442
  key: ConfigKey.group_resourceBitTags,
@@ -8984,8 +10539,15 @@ var BITS = {
8984
10539
  },
8985
10540
  [BitType2.imageSeparator]: {
8986
10541
  since: "1.4.15",
8987
- baseBitType: BitType2.image,
8988
- description: "Image separator bit, used to create separators in articles or books"
10542
+ baseBitType: BitType2._standard,
10543
+ description: "Image separator bit, used to create separators in articles or books",
10544
+ tags: [
10545
+ {
10546
+ key: ConfigKey.group_imageNoZoom,
10547
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10548
+ }
10549
+ ],
10550
+ resourceAttachmentAllowed: false
8989
10551
  },
8990
10552
  [BitType2.imageSeparatorAlt]: {
8991
10553
  since: "1.16.0",
@@ -9024,15 +10586,20 @@ var BITS = {
9024
10586
  },
9025
10587
  [BitType2.pageBanner]: {
9026
10588
  since: "1.4.3",
9027
- baseBitType: BitType2.image,
10589
+ baseBitType: BitType2._standard,
9028
10590
  description: "Page banner bit, used to create banners for pages in articles or books",
9029
10591
  tags: [
9030
10592
  {
9031
10593
  key: ConfigKey.property_slug,
9032
10594
  description: "Slug for the page banner, used to identify the banner in the system",
9033
10595
  format: TagFormat.plainText
10596
+ },
10597
+ {
10598
+ key: ConfigKey.group_imageNoZoom,
10599
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
9034
10600
  }
9035
- ]
10601
+ ],
10602
+ resourceAttachmentAllowed: false
9036
10603
  },
9037
10604
  [BitType2.pageHero]: {
9038
10605
  since: "1.11.0",
@@ -9047,7 +10614,7 @@ var BITS = {
9047
10614
  },
9048
10615
  [BitType2.tableImage]: {
9049
10616
  since: "1.5.15",
9050
- baseBitType: BitType2.table,
10617
+ baseBitType: BitType2.tableExtended,
9051
10618
  description: "Table image bit, used to create images in tables in articles or books",
9052
10619
  tags: [
9053
10620
  {
@@ -9056,15 +10623,8 @@ var BITS = {
9056
10623
  format: TagFormat.bitmarkText
9057
10624
  },
9058
10625
  {
9059
- key: ConfigKey.resource_backgroundWallpaper,
9060
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
9061
- description: "Background wallpaper for the image, used to set a background for the image",
9062
- chain: [
9063
- {
9064
- key: ConfigKey.group_resourceImageCommon,
9065
- description: "Common resource image tags for images"
9066
- }
9067
- ]
10626
+ key: ConfigKey.group_backgroundWallpaper,
10627
+ description: "Background wallpaper tags for images, used to define background properties for images"
9068
10628
  },
9069
10629
  {
9070
10630
  key: ConfigKey.group_resourceBitTags,
@@ -9207,10 +10767,10 @@ var BITS = {
9207
10767
  description: "Resource bit tags for logo grave images, used to define additional properties"
9208
10768
  },
9209
10769
  {
9210
- // Image resource
9211
- key: ConfigKey.group_resourceImage,
10770
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10771
+ key: ConfigKey.group_resourceImageNoZoom,
9212
10772
  description: "Resource image tags for logo grave images, used to attach images",
9213
- exportJsonKey: { logos: "$" },
10773
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9214
10774
  minCount: 1,
9215
10775
  maxCount: Count.infinity
9216
10776
  }
@@ -9220,7 +10780,17 @@ var BITS = {
9220
10780
  [BitType2.prototypeImages]: {
9221
10781
  since: "1.6.1",
9222
10782
  baseBitType: BitType2.imagesLogoGrave,
9223
- description: "Prototype images bit, used to create prototype images in articles or books"
10783
+ description: "Prototype images bit, used to create prototype images in articles or books",
10784
+ tags: [
10785
+ {
10786
+ // Override inherited `logos` wrapper with `images` for this bit type.
10787
+ key: ConfigKey.group_resourceImageNoZoom,
10788
+ description: "Resource image tags for prototype images",
10789
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10790
+ minCount: 1,
10791
+ maxCount: Count.infinity
10792
+ }
10793
+ ]
9224
10794
  },
9225
10795
  [BitType2.internalLink]: {
9226
10796
  since: "1.3.0",
@@ -9250,6 +10820,18 @@ var BITS = {
9250
10820
  exportJsonKey: {},
9251
10821
  description: "Reasonable number of characters for the interview, used to limit input size",
9252
10822
  format: TagFormat.number
10823
+ },
10824
+ {
10825
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10826
+ key: ConfigKey.property_example,
10827
+ exportJsonKey: [
10828
+ { "@keyonly": { isExample: true } },
10829
+ { "@absent": { isExample: true } },
10830
+ { isExample: true, example: "$" }
10831
+ ],
10832
+ description: "The example(s) for the bit",
10833
+ format: TagFormat.bitmarkText,
10834
+ nullable: true
9253
10835
  }
9254
10836
  ],
9255
10837
  cardSet: CardSetConfigKey.questions
@@ -9608,6 +11190,18 @@ var BITS = {
9608
11190
  {
9609
11191
  key: ConfigKey.group_mark,
9610
11192
  description: "Mark tags, used to define the content of marks"
11193
+ },
11194
+ {
11195
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11196
+ key: ConfigKey.property_example,
11197
+ exportJsonKey: [
11198
+ { "@keyonly": { isExample: true } },
11199
+ { "@absent": { isExample: true } },
11200
+ { isExample: true, example: "$" }
11201
+ ],
11202
+ description: "The example(s) for the bit",
11203
+ format: TagFormat.bitmarkText,
11204
+ nullable: true
9611
11205
  }
9612
11206
  ]
9613
11207
  },
@@ -9627,6 +11221,23 @@ var BITS = {
9627
11221
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9628
11222
  format: TagFormat.boolean,
9629
11223
  defaultValue: "true"
11224
+ },
11225
+ {
11226
+ // Override inherited @example: in `isTopLevelExample` allow-list
11227
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11228
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11229
+ // Bit-level fires emit only `isExample`; the @example value (when
11230
+ // provided) is recorded in the ancestor stack and consumed by
11231
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11232
+ key: ConfigKey.property_example,
11233
+ exportJsonKey: [
11234
+ { "@keyonly": { isExample: true } },
11235
+ { "@absent": { isExample: true } },
11236
+ { isExample: true }
11237
+ ],
11238
+ description: "The example(s) for the bit",
11239
+ format: TagFormat.bitmarkText,
11240
+ nullable: true
9630
11241
  }
9631
11242
  ],
9632
11243
  cardSet: CardSetConfigKey.matchPairs
@@ -9711,6 +11322,18 @@ var BITS = {
9711
11322
  {
9712
11323
  key: ConfigKey.group_trueFalse,
9713
11324
  description: "True/False tags for multiple choice 1 questions"
11325
+ },
11326
+ {
11327
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11328
+ key: ConfigKey.property_example,
11329
+ exportJsonKey: [
11330
+ { "@keyonly": { isExample: true } },
11331
+ { "@absent": { isExample: true } },
11332
+ { isExample: true, example: "$" }
11333
+ ],
11334
+ description: "The example(s) for the bit",
11335
+ format: TagFormat.bitmarkText,
11336
+ nullable: true
9714
11337
  }
9715
11338
  ]
9716
11339
  },
@@ -9732,8 +11355,20 @@ var BITS = {
9732
11355
  {
9733
11356
  key: ConfigKey.group_trueFalse,
9734
11357
  description: "True/False tags for multiple choice questions"
9735
- }
11358
+ },
9736
11359
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11360
+ {
11361
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11362
+ key: ConfigKey.property_example,
11363
+ exportJsonKey: [
11364
+ { "@keyonly": { isExample: true } },
11365
+ { "@absent": { isExample: true } },
11366
+ { isExample: true, example: "$" }
11367
+ ],
11368
+ description: "The example(s) for the bit",
11369
+ format: TagFormat.bitmarkText,
11370
+ nullable: true
11371
+ }
9737
11372
  ],
9738
11373
  cardSet: CardSetConfigKey.quiz
9739
11374
  },
@@ -9753,8 +11388,20 @@ var BITS = {
9753
11388
  description: "Common quiz tags for multiple choice text questions"
9754
11389
  },
9755
11390
  {
9756
- key: ConfigKey.group_trueFalse,
11391
+ key: ConfigKey.group_trueFalseInlineSelect,
9757
11392
  description: "True/False tags for multiple choice text questions"
11393
+ },
11394
+ {
11395
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11396
+ key: ConfigKey.property_example,
11397
+ exportJsonKey: [
11398
+ { "@keyonly": { isExample: true } },
11399
+ { "@absent": { isExample: true } },
11400
+ { isExample: true, example: "$" }
11401
+ ],
11402
+ description: "The example(s) for the bit",
11403
+ format: TagFormat.bitmarkText,
11404
+ nullable: true
9758
11405
  }
9759
11406
  ]
9760
11407
  },
@@ -9779,8 +11426,20 @@ var BITS = {
9779
11426
  description: "Common quiz tags for multiple response 1 questions"
9780
11427
  },
9781
11428
  {
9782
- key: ConfigKey.group_trueFalse,
11429
+ key: ConfigKey.group_trueFalseResponses,
9783
11430
  description: "True/False tags for multiple response 1 questions"
11431
+ },
11432
+ {
11433
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11434
+ key: ConfigKey.property_example,
11435
+ exportJsonKey: [
11436
+ { "@keyonly": { isExample: true } },
11437
+ { "@absent": { isExample: true } },
11438
+ { isExample: true, example: "$" }
11439
+ ],
11440
+ description: "The example(s) for the bit",
11441
+ format: TagFormat.bitmarkText,
11442
+ nullable: true
9784
11443
  }
9785
11444
  ]
9786
11445
  },
@@ -9800,12 +11459,24 @@ var BITS = {
9800
11459
  description: "Common quiz tags for multiple response questions"
9801
11460
  },
9802
11461
  {
9803
- key: ConfigKey.group_trueFalse,
11462
+ key: ConfigKey.group_trueFalseResponses,
9804
11463
  description: "True/False tags for multiple response questions"
9805
- }
11464
+ },
9806
11465
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11466
+ {
11467
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11468
+ key: ConfigKey.property_example,
11469
+ exportJsonKey: [
11470
+ { "@keyonly": { isExample: true } },
11471
+ { "@absent": { isExample: true } },
11472
+ { isExample: true, example: "$" }
11473
+ ],
11474
+ description: "The example(s) for the bit",
11475
+ format: TagFormat.bitmarkText,
11476
+ nullable: true
11477
+ }
9807
11478
  ],
9808
- cardSet: CardSetConfigKey.quiz
11479
+ cardSet: CardSetConfigKey.quizResponses
9809
11480
  },
9810
11481
  [BitType2.coachSelfReflectionMultipleResponse]: {
9811
11482
  since: "1.3.0",
@@ -10055,6 +11726,8 @@ var BITS = {
10055
11726
  },
10056
11727
  {
10057
11728
  key: ConfigKey.resource_platformIcon,
11729
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11730
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
10058
11731
  description: "The platform icon",
10059
11732
  chain: [
10060
11733
  {
@@ -10090,6 +11763,8 @@ var BITS = {
10090
11763
  tags: [
10091
11764
  {
10092
11765
  key: ConfigKey.resource_platformLogo,
11766
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11767
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10093
11768
  description: "The platform logo",
10094
11769
  chain: [
10095
11770
  {
@@ -10301,7 +11976,8 @@ var BITS = {
10301
11976
  { isExample: true, example: "$" }
10302
11977
  ],
10303
11978
  description: "The example text for the review note bit",
10304
- format: TagFormat.plainText
11979
+ format: TagFormat.plainText,
11980
+ nullable: true
10305
11981
  }
10306
11982
  ],
10307
11983
  rootExampleType: ExampleType.string
@@ -10388,7 +12064,8 @@ var BITS = {
10388
12064
  { isExample: true, example: "$" }
10389
12065
  ],
10390
12066
  description: "The example flag for the bit (boolean)",
10391
- format: TagFormat.plainText
12067
+ format: TagFormat.boolean,
12068
+ nullable: true
10392
12069
  }
10393
12070
  ],
10394
12071
  cardSet: CardSetConfigKey.elements,
@@ -10662,15 +12339,8 @@ var BITS = {
10662
12339
  format: TagFormat.bitmarkText
10663
12340
  },
10664
12341
  {
10665
- key: ConfigKey.resource_backgroundWallpaper,
10666
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10667
- description: "Background wallpaper for the image, used to set a background for the image",
10668
- chain: [
10669
- {
10670
- key: ConfigKey.group_resourceImageCommon,
10671
- description: "Common resource image tags for images"
10672
- }
10673
- ]
12342
+ key: ConfigKey.group_backgroundWallpaper,
12343
+ description: "Background wallpaper tags for images, used to define background properties for images"
10674
12344
  },
10675
12345
  {
10676
12346
  key: ConfigKey.group_resourceBitTags,
@@ -11047,16 +12717,20 @@ var BITS = {
11047
12717
  format: TagFormat.plainText
11048
12718
  },
11049
12719
  {
11050
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12720
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12721
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12722
+ // not a `choices[]` array. Format is bitmarkText so the statement
12723
+ // body renders as a ProseMirror tree, matching reference fixtures.
12724
+ exportJsonKey: { statement: "$", isCorrect: true },
11051
12725
  key: ConfigKey.tag_true,
11052
12726
  description: "Tag for the true option in the true/false question",
11053
- format: TagFormat.plainText
12727
+ format: TagFormat.bitmarkText
11054
12728
  },
11055
12729
  {
11056
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12730
+ exportJsonKey: { statement: "$", isCorrect: false },
11057
12731
  key: ConfigKey.tag_false,
11058
12732
  description: "Tag for the false option in the true/false question",
11059
- format: TagFormat.plainText
12733
+ format: TagFormat.bitmarkText
11060
12734
  },
11061
12735
  {
11062
12736
  // Override inherited @example from group_standardTags: boolean root
@@ -11067,14 +12741,24 @@ var BITS = {
11067
12741
  // sufficient for `[+...]` (correct) statements which dominate the
11068
12742
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
11069
12743
  // need either a runtime extension or an explicit `[@example:false]`.
12744
+ //
12745
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12746
+ // (a natural default that gets stripped in optimised mode, matching
12747
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12748
+ // STRING `"false"` which doesn't strip.
11070
12749
  key: ConfigKey.property_example,
11071
12750
  exportJsonKey: [
11072
12751
  { "@keyonly": { isExample: true, example: true } },
11073
12752
  { "@absent": { isExample: true, example: true } },
12753
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12754
+ // is the natural default for boolean, stripped in optimised mode.
12755
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12756
+ { "@value=false": { isExample: true } },
11074
12757
  { isExample: true, example: "$" }
11075
12758
  ],
11076
12759
  description: "The example flag for the bit (boolean)",
11077
- format: TagFormat.plainText
12760
+ format: TagFormat.boolean,
12761
+ nullable: true
11078
12762
  }
11079
12763
  ],
11080
12764
  rootExampleType: ExampleType.boolean
@@ -11098,6 +12782,18 @@ var BITS = {
11098
12782
  key: ConfigKey.property_labelFalse,
11099
12783
  description: "Label for the false option in the true/false question",
11100
12784
  format: TagFormat.plainText
12785
+ },
12786
+ {
12787
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12788
+ key: ConfigKey.property_example,
12789
+ exportJsonKey: [
12790
+ { "@keyonly": { isExample: true } },
12791
+ { "@absent": { isExample: true } },
12792
+ { isExample: true, example: "$" }
12793
+ ],
12794
+ description: "The example(s) for the bit",
12795
+ format: TagFormat.bitmarkText,
12796
+ nullable: true
11101
12797
  }
11102
12798
  ],
11103
12799
  cardSet: CardSetConfigKey.statements
@@ -11287,6 +12983,11 @@ var BITS = {
11287
12983
  baseBitType: BitType2.vendorStripePricingTable,
11288
12984
  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",
11289
12985
  tags: [
12986
+ {
12987
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
12988
+ description: "Descriptive name for the Stripe pricing table",
12989
+ format: TagFormat.plainText
12990
+ },
11290
12991
  {
11291
12992
  key: ConfigKey.property_printOnRequestCustomerId,
11292
12993
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11763,7 +13464,7 @@ var instance2 = new Config();
11763
13464
  // src/generated/package_info.ts
11764
13465
  var PACKAGE_INFO = {
11765
13466
  "name": "@gmb/bitmark-parser-generator",
11766
- "version": "5.23.0",
13467
+ "version": "5.24.0",
11767
13468
  "author": "Get More Brain Ltd",
11768
13469
  "license": "ISC",
11769
13470
  "description": "A bitmark parser and generator using Peggy.js"
@@ -12728,6 +14429,8 @@ var NodeType = {
12728
14429
  stillImageFilmLink: "stillImageFilmLink",
12729
14430
  stripePricingTableId: "stripePricingTableId",
12730
14431
  stripePricingTableIdValue: "stripePricingTableIdValue",
14432
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14433
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12731
14434
  stripePublishableKey: "stripePublishableKey",
12732
14435
  stripePublishableKeyValue: "stripePublishableKeyValue",
12733
14436
  subject: "subject",
@@ -26153,6 +27856,12 @@ var Builder = class extends BaseBuilder {
26153
27856
  data.stripePricingTableId,
26154
27857
  options
26155
27858
  ),
27859
+ stripePricingTableDescriptiveName: this.toAstProperty(
27860
+ bitType,
27861
+ ConfigKey.property_stripePricingTableDescriptiveName,
27862
+ data.stripePricingTableDescriptiveName,
27863
+ options
27864
+ ),
26156
27865
  stripePublishableKey: this.toAstProperty(
26157
27866
  bitType,
26158
27867
  ConfigKey.property_stripePublishableKey,
@@ -34380,6 +36089,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34380
36089
  const cardSetType = bitConfig.cardSet?.configKey;
34381
36090
  switch (cardSetType) {
34382
36091
  case CardSetConfigKey.flashcard:
36092
+ case CardSetConfigKey.flashcard1:
34383
36093
  case CardSetConfigKey.definitionList:
34384
36094
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34385
36095
  break;
@@ -34393,6 +36103,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34393
36103
  result = parseFeedback(context, bitType, processedCardSet);
34394
36104
  break;
34395
36105
  case CardSetConfigKey.quiz:
36106
+ case CardSetConfigKey.quizResponses:
34396
36107
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34397
36108
  break;
34398
36109
  case CardSetConfigKey.questions:
@@ -34419,6 +36130,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34419
36130
  break;
34420
36131
  case CardSetConfigKey.clozeList:
34421
36132
  case CardSetConfigKey.exampleBitList:
36133
+ case CardSetConfigKey.pageFooterSections:
34422
36134
  case CardSetConfigKey.bookReferenceList:
34423
36135
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34424
36136
  break;