@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.
@@ -729,10 +729,12 @@ var TextFormat2 = {
729
729
  import "@ncoderz/superenum";
730
730
  var CardSetConfigKey = {
731
731
  flashcard: "flashcard",
732
+ flashcard1: "flashcard1",
732
733
  definitionList: "definitionList",
733
734
  elements: "elements",
734
735
  statements: "statements",
735
736
  quiz: "quiz",
737
+ quizResponses: "quizResponses",
736
738
  feedback: "feedback",
737
739
  questions: "questions",
738
740
  matchPairs: "matchPairs",
@@ -744,6 +746,7 @@ var CardSetConfigKey = {
744
746
  pronunciationTable: "pronunciationTable",
745
747
  botActionResponses: "botActionResponses",
746
748
  exampleBitList: "exampleBitList",
749
+ pageFooterSections: "pageFooterSections",
747
750
  clozeList: "clozeList",
748
751
  ingredients: "ingredients",
749
752
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1160,12 +1163,17 @@ var groupKeys = {
1160
1163
  group_standardAllBits: "group_standardAllBits",
1161
1164
  group_standardItemLead: "group_standardItemLead",
1162
1165
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1166
+ group_standardInstructionHint: "group_standardInstructionHint",
1163
1167
  group_standardTags: "group_standardTags",
1168
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1164
1169
  group_imageSource: "group_imageSource",
1165
1170
  group_technicalTerm: "group_technicalTerm",
1166
1171
  group_person: "group_person",
1167
1172
  group_gap: "group_gap",
1168
1173
  group_trueFalse: "group_trueFalse",
1174
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1175
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1176
+ group_trueFalseResponses: "group_trueFalseResponses",
1169
1177
  group_markConfig: "group_markConfig",
1170
1178
  group_mark: "group_mark",
1171
1179
  group_bookCommon: "group_bookCommon",
@@ -1173,14 +1181,18 @@ var groupKeys = {
1173
1181
  group_advertisingCommon: "group_advertisingCommon",
1174
1182
  group_platformStylesCommon: "group_platformStylesCommon",
1175
1183
  group_quizCommon: "group_quizCommon",
1184
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1185
+ group_imageNoZoom: "group_imageNoZoom",
1176
1186
  group_resourceBitTags: "group_resourceBitTags",
1177
1187
  group_resourceCommon: "group_resourceCommon",
1178
1188
  group_resourceImageCommon: "group_resourceImageCommon",
1189
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1179
1190
  group_resourceAudioCommon: "group_resourceAudioCommon",
1180
1191
  group_resourceVideoCommon: "group_resourceVideoCommon",
1181
1192
  group_resourceIcon: "group_resourceIcon",
1182
1193
  // Alias for image
1183
1194
  group_resourceImage: "group_resourceImage",
1195
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1184
1196
  group_resourceImageEmbed: "group_resourceImageEmbed",
1185
1197
  group_resourceImageLink: "group_resourceImageLink",
1186
1198
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1457,6 +1469,7 @@ var propertyKeys = {
1457
1469
  property_src4x: "@src4x",
1458
1470
  property_srcAlt: "@srcAlt",
1459
1471
  property_stripePricingTableId: "@stripePricingTableId",
1472
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1460
1473
  property_stripePublishableKey: "@stripePublishableKey",
1461
1474
  property_subject: "@subject",
1462
1475
  property_subtype: "@subtype",
@@ -2208,7 +2221,7 @@ var CARDSETS = {
2208
2221
  variants: [
2209
2222
  {
2210
2223
  jsonKey: "question.text",
2211
- exportJsonKey: { question: { text: "$" } },
2224
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2212
2225
  tags: [
2213
2226
  {
2214
2227
  key: ConfigKey.group_standardTags,
@@ -2224,6 +2237,136 @@ var CARDSETS = {
2224
2237
  description: "Icon resource for the flashcard.",
2225
2238
  jsonKey: "question.icon|resource(type=image, key=image)",
2226
2239
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2240
+ },
2241
+ {
2242
+ key: ConfigKey.property_example,
2243
+ exportJsonKey: [
2244
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2245
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2246
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2247
+ ],
2248
+ description: "Example marker for the flashcard question.",
2249
+ format: TagFormat.bitmarkText,
2250
+ nullable: true
2251
+ }
2252
+ ]
2253
+ }
2254
+ ]
2255
+ },
2256
+ {
2257
+ name: "answer",
2258
+ variants: [
2259
+ {
2260
+ jsonKey: "answer.text",
2261
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2262
+ tags: [
2263
+ {
2264
+ key: ConfigKey.group_standardTags,
2265
+ description: "Standard tags for the flashcard."
2266
+ },
2267
+ {
2268
+ exportJsonKey: { title: "$" },
2269
+ key: ConfigKey.tag_title,
2270
+ description: "Title of the flashcard."
2271
+ },
2272
+ {
2273
+ key: ConfigKey.group_resourceIcon,
2274
+ description: "Icon resource for the flashcard.",
2275
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2276
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2277
+ },
2278
+ {
2279
+ key: ConfigKey.property_example,
2280
+ exportJsonKey: [
2281
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2282
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2283
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2284
+ ],
2285
+ description: "Example marker for the flashcard answer.",
2286
+ format: TagFormat.bitmarkText,
2287
+ nullable: true
2288
+ }
2289
+ ]
2290
+ },
2291
+ {
2292
+ jsonKey: "alternativeAnswers[].text",
2293
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2294
+ tags: [
2295
+ {
2296
+ key: ConfigKey.group_standardTags,
2297
+ description: "Standard tags for the flashcard."
2298
+ },
2299
+ {
2300
+ exportJsonKey: { title: "$" },
2301
+ key: ConfigKey.tag_title,
2302
+ description: "Title of the flashcard."
2303
+ },
2304
+ {
2305
+ key: ConfigKey.group_resourceIcon,
2306
+ description: "Icon resource for the flashcard.",
2307
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2308
+ exportJsonKey: {
2309
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2310
+ }
2311
+ }
2312
+ ],
2313
+ repeatCount: Count.infinity
2314
+ }
2315
+ ]
2316
+ }
2317
+ ]
2318
+ },
2319
+ //
2320
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2321
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2322
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2323
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2324
+ // cardset.
2325
+ //
2326
+ [CardSetConfigKey.flashcard1]: {
2327
+ jsonKey: "cards",
2328
+ exportJsonKey: { cards: "$" },
2329
+ sections: {
2330
+ default: {
2331
+ jsonKey: "cards",
2332
+ exportJsonKey: { cards: "$" },
2333
+ isDefault: true,
2334
+ maxCount: 1
2335
+ }
2336
+ },
2337
+ sides: [
2338
+ {
2339
+ name: "question",
2340
+ variants: [
2341
+ {
2342
+ jsonKey: "question.text",
2343
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2344
+ tags: [
2345
+ {
2346
+ key: ConfigKey.group_standardTags,
2347
+ description: "Standard tags for the flashcard."
2348
+ },
2349
+ {
2350
+ exportJsonKey: { title: "$" },
2351
+ key: ConfigKey.tag_title,
2352
+ description: "Title of the flashcard."
2353
+ },
2354
+ {
2355
+ key: ConfigKey.group_resourceIcon,
2356
+ description: "Icon resource for the flashcard.",
2357
+ jsonKey: "question.icon|resource(type=image, key=image)",
2358
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2359
+ },
2360
+ {
2361
+ key: ConfigKey.property_example,
2362
+ exportJsonKey: [
2363
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2364
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2365
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2366
+ ],
2367
+ description: "Example marker for the flashcard question.",
2368
+ format: TagFormat.bitmarkText,
2369
+ nullable: true
2227
2370
  }
2228
2371
  ]
2229
2372
  }
@@ -2234,7 +2377,7 @@ var CARDSETS = {
2234
2377
  variants: [
2235
2378
  {
2236
2379
  jsonKey: "answer.text",
2237
- exportJsonKey: { answer: { text: "$" } },
2380
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2238
2381
  tags: [
2239
2382
  {
2240
2383
  key: ConfigKey.group_standardTags,
@@ -2250,6 +2393,17 @@ var CARDSETS = {
2250
2393
  description: "Icon resource for the flashcard.",
2251
2394
  jsonKey: "answer.icon|resource(type=image, key=image)",
2252
2395
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2396
+ },
2397
+ {
2398
+ key: ConfigKey.property_example,
2399
+ exportJsonKey: [
2400
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2401
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2402
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2403
+ ],
2404
+ description: "Example marker for the flashcard answer.",
2405
+ format: TagFormat.bitmarkText,
2406
+ nullable: true
2253
2407
  }
2254
2408
  ]
2255
2409
  },
@@ -2293,7 +2447,7 @@ var CARDSETS = {
2293
2447
  variants: [
2294
2448
  {
2295
2449
  jsonKey: "term.text",
2296
- exportJsonKey: { term: { text: "$" } },
2450
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2297
2451
  tags: [
2298
2452
  {
2299
2453
  key: ConfigKey.group_standardTags,
@@ -2312,6 +2466,17 @@ var CARDSETS = {
2312
2466
  format: TagFormat.plainText,
2313
2467
  jsonKey: "term.icon|resource(type=image, key=image)",
2314
2468
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2469
+ },
2470
+ {
2471
+ key: ConfigKey.property_example,
2472
+ exportJsonKey: [
2473
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2474
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2475
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2476
+ ],
2477
+ description: "Example marker for the definition term.",
2478
+ format: TagFormat.bitmarkText,
2479
+ nullable: true
2315
2480
  }
2316
2481
  ]
2317
2482
  }
@@ -2322,7 +2487,7 @@ var CARDSETS = {
2322
2487
  variants: [
2323
2488
  {
2324
2489
  jsonKey: "definition.text",
2325
- exportJsonKey: { definition: { text: "$" } },
2490
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2326
2491
  tags: [
2327
2492
  {
2328
2493
  key: ConfigKey.group_standardTags,
@@ -2340,6 +2505,17 @@ var CARDSETS = {
2340
2505
  description: "Icon resource for the definition.",
2341
2506
  jsonKey: "definition.icon|resource(type=image, key=image)",
2342
2507
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2508
+ },
2509
+ {
2510
+ key: ConfigKey.property_example,
2511
+ exportJsonKey: [
2512
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2513
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2514
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2515
+ ],
2516
+ description: "Example marker for the definition.",
2517
+ format: TagFormat.bitmarkText,
2518
+ nullable: true
2343
2519
  }
2344
2520
  ]
2345
2521
  },
@@ -2384,11 +2560,51 @@ var CARDSETS = {
2384
2560
  {
2385
2561
  jsonKey: "key",
2386
2562
  exportJsonKey: { key: "$" },
2563
+ format: TextFormat2.plainText,
2387
2564
  tags: [
2388
2565
  {
2389
2566
  key: ConfigKey.group_standardTags,
2390
2567
  description: "Standard tags for the match pair."
2391
2568
  },
2569
+ {
2570
+ // Pair-level example: emit `isExample` alongside `example`
2571
+ // (matches OLD parser's per-pair allow-list behaviour).
2572
+ //
2573
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2574
+ // in the cardset) fills each pair's `example` with `V`
2575
+ // once per pair from the keys-side cascade fire (single
2576
+ // parsed_variant per pair — no per-value duplication).
2577
+ //
2578
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2579
+ // at the pair-object level. BPG `parseMatchPairs`
2580
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2581
+ // per PAIR (the `exampleCard` accumulator), placed at the
2582
+ // pair object. Unlike matchMatrix (per-cell example),
2583
+ // matchPairs has no nested per-cell layer, so the pair-key
2584
+ // `[@example:V]` must double-emit as the visible field.
2585
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2586
+ // expects `example: "packen"` at pair-object level from
2587
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2588
+ // draft applied to both keys-side tags; the matchPairs
2589
+ // half was reverted after fixture parity testing.
2590
+ key: ConfigKey.property_example,
2591
+ exportJsonKey: [
2592
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2593
+ {
2594
+ predicates: ["@absent", { $cascade: "*" }],
2595
+ rule: {
2596
+ isExample: true,
2597
+ example: "$cascade",
2598
+ "@bit": { isExample: true }
2599
+ }
2600
+ },
2601
+ { "@absent": { isExample: true } },
2602
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2603
+ ],
2604
+ description: "Example marker / value on the match pair.",
2605
+ format: TagFormat.bitmarkText,
2606
+ nullable: true
2607
+ },
2392
2608
  {
2393
2609
  key: ConfigKey.tag_title,
2394
2610
  description: "Title of the match pair.",
@@ -2397,9 +2613,19 @@ var CARDSETS = {
2397
2613
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2398
2614
  },
2399
2615
  {
2616
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2617
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2618
+ // only fires when NO variant in the active card explicitly
2619
+ // carries `[@isCaseSensitive]`. When a value-side variant
2620
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2621
+ // synth is skipped here; the value-side explicit fire owns
2622
+ // the pair-level emission.
2400
2623
  key: ConfigKey.property_isCaseSensitive,
2401
2624
  exportJsonKey: [
2402
- { "@absent": { isCaseSensitive: "$ancestor" } },
2625
+ {
2626
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2627
+ rule: { isCaseSensitive: "$ancestor" }
2628
+ },
2403
2629
  { isCaseSensitive: "$" }
2404
2630
  ],
2405
2631
  description: "Property to indicate if the match is case sensitive.",
@@ -2428,10 +2654,85 @@ var CARDSETS = {
2428
2654
  {
2429
2655
  jsonKey: "values[]",
2430
2656
  exportJsonKey: { values: ["$"] },
2657
+ format: TextFormat2.plainText,
2431
2658
  tags: [
2432
2659
  {
2433
- key: ConfigKey.group_standardTags,
2434
- description: "Standard tags for the match pair."
2660
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2661
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2662
+ // for sideIdx > 0; the schema-side equivalent is to use a
2663
+ // group_standardTags variant that omits group_standardItemLead.
2664
+ key: ConfigKey.group_standardTagsNoItemLead,
2665
+ description: "Standard tags for the match pair (values side, no item/lead)."
2666
+ },
2667
+ {
2668
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2669
+ // `@example` from the bit header fills each pair's
2670
+ // `example` with the pair's first value (mirrors BPG's
2671
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2672
+ //
2673
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2674
+ // TAG 415 (keys side) which fires once per pair — keeping
2675
+ // it here would emit `example` twice per pair (one per
2676
+ // values-side parsed_variant) and merge them into a
2677
+ // multi-paragraph array.
2678
+ //
2679
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2680
+ // source `$` value (the current `++`-iteration's value
2681
+ // text). For single-value pairs this matches the old
2682
+ // `$parent.values[0]`; for multi-value pairs with
2683
+ // `[@example]` attached to a non-first value, it correctly
2684
+ // resolves to that value.
2685
+ //
2686
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2687
+ // routes pair-key `[@example:V]` (a per-pair cascade
2688
+ // source, bound via per-card CascadeCounters scope) and
2689
+ // bit-header `[@example:V]` (cardset cascade) into the
2690
+ // pair-level `example` via `$cascade`. The aggregator in
2691
+ // `populate_variants` fires this rule ONCE per pair (BPG
2692
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2693
+ // per-iter pollution.
2694
+ key: ConfigKey.property_example,
2695
+ exportJsonKey: [
2696
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2697
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2698
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2699
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2700
+ // scope opened by `build_card_item`.
2701
+ {
2702
+ predicates: ["@keyonly"],
2703
+ maxEmits: 1,
2704
+ rule: {
2705
+ isExample: true,
2706
+ example: "$parent",
2707
+ "@bit": { isExample: true }
2708
+ }
2709
+ },
2710
+ {
2711
+ predicates: ["@absent", { $cascade: "*" }],
2712
+ rule: {
2713
+ isExample: true,
2714
+ example: "$cascade",
2715
+ "@bit": { isExample: true }
2716
+ }
2717
+ },
2718
+ {
2719
+ "@absent": {
2720
+ isExample: true,
2721
+ example: "$parent",
2722
+ "@bit": { isExample: true }
2723
+ }
2724
+ },
2725
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2726
+ // form so per-`++`-iter values-side fires also first-win.
2727
+ {
2728
+ predicates: [],
2729
+ maxEmits: 1,
2730
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2731
+ }
2732
+ ],
2733
+ description: "Example marker / value on the match pair (values side).",
2734
+ format: TagFormat.bitmarkText,
2735
+ nullable: true
2435
2736
  },
2436
2737
  {
2437
2738
  key: ConfigKey.tag_title,
@@ -2441,9 +2742,13 @@ var CARDSETS = {
2441
2742
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2442
2743
  },
2443
2744
  {
2745
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2444
2746
  key: ConfigKey.property_isCaseSensitive,
2445
2747
  exportJsonKey: [
2446
- { "@absent": { isCaseSensitive: "$ancestor" } },
2748
+ {
2749
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2750
+ rule: { isCaseSensitive: "$ancestor" }
2751
+ },
2447
2752
  { isCaseSensitive: "$" }
2448
2753
  ],
2449
2754
  description: "Property to indicate if the match is case sensitive.",
@@ -2469,6 +2774,7 @@ var CARDSETS = {
2469
2774
  {
2470
2775
  jsonKey: "key",
2471
2776
  exportJsonKey: { key: "$" },
2777
+ format: TextFormat2.plainText,
2472
2778
  tags: [
2473
2779
  {
2474
2780
  key: ConfigKey.group_standardTags,
@@ -2498,10 +2804,12 @@ var CARDSETS = {
2498
2804
  {
2499
2805
  jsonKey: "values[]",
2500
2806
  exportJsonKey: { values: ["$"] },
2807
+ format: TextFormat2.plainText,
2501
2808
  tags: [
2502
2809
  {
2503
- key: ConfigKey.group_standardTags,
2504
- description: "Standard tags for the audio match pair."
2810
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2811
+ key: ConfigKey.group_standardTagsNoItemLead,
2812
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2505
2813
  },
2506
2814
  {
2507
2815
  key: ConfigKey.tag_title,
@@ -2534,6 +2842,7 @@ var CARDSETS = {
2534
2842
  {
2535
2843
  jsonKey: "key",
2536
2844
  exportJsonKey: { key: "$" },
2845
+ format: TextFormat2.plainText,
2537
2846
  tags: [
2538
2847
  {
2539
2848
  key: ConfigKey.group_standardTags,
@@ -2563,10 +2872,12 @@ var CARDSETS = {
2563
2872
  {
2564
2873
  jsonKey: "values[]",
2565
2874
  exportJsonKey: { values: ["$"] },
2875
+ format: TextFormat2.plainText,
2566
2876
  tags: [
2567
2877
  {
2568
- key: ConfigKey.group_standardTags,
2569
- description: "Standard tags for the image match pair."
2878
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2879
+ key: ConfigKey.group_standardTagsNoItemLead,
2880
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2570
2881
  },
2571
2882
  {
2572
2883
  key: ConfigKey.tag_title,
@@ -2599,20 +2910,49 @@ var CARDSETS = {
2599
2910
  {
2600
2911
  jsonKey: "key",
2601
2912
  exportJsonKey: { key: "$" },
2913
+ format: TextFormat2.plainText,
2602
2914
  tags: [
2603
2915
  {
2604
2916
  key: ConfigKey.group_standardItemLeadInstructionHint,
2605
2917
  description: "Standard tags for lead, instruction, and hint."
2606
2918
  },
2607
2919
  {
2920
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
2921
+ // `[@example:V]` is consumed as the per-pair cascade source.
2922
+ // PLAN-077 R14 follow-up: investigation showed the fixture
2923
+ // reference is INCONSISTENT — bits without a cardset-level
2924
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
2925
+ // pair-level `example: V` at pair scope, but bits WITH a
2926
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
2927
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
2928
+ // example. Emitting `example: $` here satisfies bit 3 but
2929
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
2930
+ // 3. Conditional emission would require runtime gating on
2931
+ // the cardset cascade scope state. Left dropped pending a
2932
+ // dedicated plan — accepted divergence on bit 3's pair-
2933
+ // level emission (cells still get the per-cell `example`
2934
+ // correctly via PLAN-077 §4.2 + §4.3).
2608
2935
  key: ConfigKey.property_example,
2609
2936
  exportJsonKey: [
2610
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2611
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2612
- { isExample: true, example: "$", "@bit": { isExample: true } }
2937
+ {
2938
+ "@keyonly": {
2939
+ isExample: true,
2940
+ "@bit": { isExample: true },
2941
+ "@card": { isExample: true }
2942
+ }
2943
+ },
2944
+ {
2945
+ "@absent": {
2946
+ isExample: true,
2947
+ "@bit": { isExample: true },
2948
+ "@card": { isExample: true }
2949
+ }
2950
+ },
2951
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2613
2952
  ],
2614
2953
  description: "Example text for the match matrix.",
2615
- format: TagFormat.plainText
2954
+ format: TagFormat.plainText,
2955
+ nullable: true
2616
2956
  },
2617
2957
  {
2618
2958
  key: ConfigKey.tag_title,
@@ -2640,35 +2980,96 @@ var CARDSETS = {
2640
2980
  variants: [
2641
2981
  {
2642
2982
  jsonKey: "cells[{s}].values[]",
2643
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
2983
+ exportJsonKey: [
2984
+ // PLAN-085 / R14: the positional-blank emission for an
2985
+ // effectively-absent cell side fires ONLY when the active
2986
+ // card has at least one `[#]` variant fire. This matches
2987
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
2988
+ // `[#]` exists anywhere in the title card, BPG returns
2989
+ // undefined and no `heading` is emitted. The phantom
2990
+ // `forValues:['']` placeholder is appropriate only when
2991
+ // the card is being treated as a title-row.
2992
+ {
2993
+ predicates: ["@absent", { "@variantHasTag": "#" }],
2994
+ rule: { "@bit": { heading: { forValues: [""] } } }
2995
+ },
2996
+ { cells: { $s: { values: ["$"] } } }
2997
+ ],
2998
+ format: TextFormat2.plainText,
2644
2999
  tags: [
2645
3000
  {
2646
3001
  key: ConfigKey.group_standardItemLeadInstructionHint,
2647
3002
  description: "Standard tags for lead, instruction, and hint."
2648
3003
  },
2649
3004
  {
3005
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3006
+ // source `$` value (the current `++`-iteration's cell text).
3007
+ // Replaces `$parent.values[0]` which pinned to position 0
3008
+ // regardless of which `++`-value `[@example]` was on.
3009
+ //
3010
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3011
+ // routes pair-key `[@example:V]` (a per-pair cascade
3012
+ // source, bound via per-card CascadeCounters scope) and
3013
+ // bit-header `[@example:V]` (cardset cascade) into the
3014
+ // per-cell `example` via `$cascade`. The aggregator in
3015
+ // `populate_variants` fires this rule ONCE per cell (BPG
3016
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3017
+ // per-iter pollution.
3018
+ // @card scope-shift writes matrix-row container isExample.
2650
3019
  key: ConfigKey.property_example,
2651
3020
  exportJsonKey: [
2652
3021
  {
2653
3022
  "@keyonly": {
2654
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2655
- "@bit": { isExample: true }
3023
+ cells: { $s: { isExample: true, example: "$parent" } },
3024
+ "@bit": { isExample: true },
3025
+ "@card": { isExample: true }
2656
3026
  }
2657
3027
  },
2658
3028
  {
2659
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
3029
+ predicates: ["@absent", { $cascade: "*" }],
3030
+ rule: {
3031
+ cells: { $s: { isExample: true, example: "$cascade" } },
3032
+ "@bit": { isExample: true },
3033
+ "@card": { isExample: true }
3034
+ }
2660
3035
  },
2661
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
3036
+ {
3037
+ "@absent": {
3038
+ cells: { $s: { isExample: true, example: "$parent" } },
3039
+ "@bit": { isExample: true },
3040
+ "@card": { isExample: true }
3041
+ }
3042
+ },
3043
+ {
3044
+ cells: { $s: { isExample: true, example: "$" } },
3045
+ "@bit": { isExample: true },
3046
+ "@card": { isExample: true }
3047
+ }
2662
3048
  ],
2663
3049
  description: "Example text for the match matrix.",
2664
- format: TagFormat.plainText
3050
+ format: TagFormat.bitmarkText,
3051
+ nullable: true
2665
3052
  },
2666
3053
  {
3054
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3055
+ // empty-string positional blank into `forValues` so the
3056
+ // array stays aligned with the cell positions (matches BPG
3057
+ // `extractHeadingCard`'s "all value-side headings present"
3058
+ // shape). The `@keyonly` rule is required for two reasons:
3059
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3060
+ // in the unconditional rule has no source for bare); and
3061
+ // 2) the presence of any `@keyonly` rule disables the
3062
+ // optimised-mode `should_suppress_outcome` strip (see
3063
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3064
+ // survives the all-natural-defaults check.
2667
3065
  key: ConfigKey.tag_title,
2668
3066
  description: "Title of the match matrix.",
2669
3067
  format: TagFormat.plainText,
2670
3068
  jsonKey: "^heading.forValues",
2671
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3069
+ exportJsonKey: [
3070
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3071
+ { "@bit": { heading: { forValues: ["$"] } } }
3072
+ ]
2672
3073
  },
2673
3074
  {
2674
3075
  key: ConfigKey.property_isCaseSensitive,
@@ -2717,6 +3118,35 @@ var CARDSETS = {
2717
3118
  {
2718
3119
  key: ConfigKey.group_standardTags,
2719
3120
  description: "Standard tags for the statement."
3121
+ },
3122
+ {
3123
+ // PLAN-072: cascade-fired `@example` from the bit header
3124
+ // fills each statement entry's `example` with the entry's
3125
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3126
+ // for the statements cardset. Per-array-item strip removes
3127
+ // `example: false` from incorrect entries so only correct
3128
+ // entries retain `example: true`.
3129
+ key: ConfigKey.property_example,
3130
+ exportJsonKey: [
3131
+ {
3132
+ "@keyonly": {
3133
+ isExample: true,
3134
+ example: true,
3135
+ "@bit": { isExample: true }
3136
+ }
3137
+ },
3138
+ {
3139
+ "@absent": {
3140
+ isExample: true,
3141
+ example: "$parent.isCorrect",
3142
+ "@bit": { isExample: true }
3143
+ }
3144
+ },
3145
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3146
+ ],
3147
+ 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`.",
3148
+ format: TagFormat.boolean,
3149
+ nullable: true
2720
3150
  }
2721
3151
  ],
2722
3152
  bodyAllowed: false
@@ -2745,6 +3175,71 @@ var CARDSETS = {
2745
3175
  {
2746
3176
  key: ConfigKey.group_standardTags,
2747
3177
  description: "Standard tags for the quiz."
3178
+ },
3179
+ {
3180
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3181
+ // quiz choices is delegated to group_trueFalse's
3182
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3183
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3184
+ // would over-fire on subsequent correct choices and on
3185
+ // incorrect ones — drop it.
3186
+ key: ConfigKey.property_example,
3187
+ exportJsonKey: [
3188
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3189
+ { isExample: true, "@bit": { isExample: true } }
3190
+ ],
3191
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3192
+ format: TagFormat.boolean,
3193
+ nullable: true
3194
+ }
3195
+ ],
3196
+ bodyAllowed: false
3197
+ }
3198
+ ]
3199
+ }
3200
+ ]
3201
+ },
3202
+ //
3203
+ // quiz (responses)
3204
+ //
3205
+ [CardSetConfigKey.quizResponses]: {
3206
+ jsonKey: "quizzes",
3207
+ exportJsonKey: { quizzes: "$" },
3208
+ sides: [
3209
+ {
3210
+ name: "responses",
3211
+ variants: [
3212
+ {
3213
+ jsonKey: null,
3214
+ tags: [
3215
+ {
3216
+ key: ConfigKey.group_trueFalseResponses,
3217
+ description: "Group for true/false questions."
3218
+ },
3219
+ {
3220
+ key: ConfigKey.group_standardTags,
3221
+ description: "Standard tags for the quiz."
3222
+ },
3223
+ {
3224
+ // PLAN-072: cascade-fired `@example` from the bit header
3225
+ // fills each response entry's `example` with the entry's
3226
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3227
+ // for the quiz-responses cardset (statements-style).
3228
+ key: ConfigKey.property_example,
3229
+ exportJsonKey: [
3230
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3231
+ {
3232
+ "@absent": {
3233
+ isExample: true,
3234
+ example: "$parent.isCorrect",
3235
+ "@bit": { isExample: true }
3236
+ }
3237
+ },
3238
+ { isExample: true, "@bit": { isExample: true } }
3239
+ ],
3240
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3241
+ format: TagFormat.boolean,
3242
+ nullable: true
2748
3243
  }
2749
3244
  ],
2750
3245
  bodyAllowed: false
@@ -2770,6 +3265,73 @@ var CARDSETS = {
2770
3265
  key: ConfigKey.group_standardItemLeadInstructionHint,
2771
3266
  description: "Standard tags for lead, instruction, and hint."
2772
3267
  },
3268
+ {
3269
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3270
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3271
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3272
+ // feedback-specific shape.
3273
+ //
3274
+ // Chain @example: `[+ X][@example]` writes
3275
+ // `isExample/example` onto the X choice only. BPG's
3276
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3277
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3278
+ // there is no bubble to bit-level or feedbacks-level
3279
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3280
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3281
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3282
+ // no feedback dispatch, so bit-header `[@example]` does
3283
+ // not flow into feedback choices.
3284
+ key: ConfigKey.tag_true,
3285
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3286
+ description: "Reason-required choice for feedback (`[+]`).",
3287
+ maxCount: Count.infinity,
3288
+ format: TagFormat.plainText,
3289
+ chain: [
3290
+ {
3291
+ key: ConfigKey.property_example,
3292
+ exportJsonKey: [
3293
+ { "@keyonly": { isExample: true, example: true } },
3294
+ { isExample: true, example: "$" }
3295
+ ],
3296
+ description: "Per-choice example marker for feedback `[+]`.",
3297
+ format: TagFormat.boolean,
3298
+ maxCount: 1,
3299
+ nullable: true
3300
+ },
3301
+ {
3302
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3303
+ // choice; same for `[?]` (hint), `[%]` (item),
3304
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3305
+ // Mirrors group_trueFalse's `+` chain shape.
3306
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3307
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3308
+ }
3309
+ ]
3310
+ },
3311
+ {
3312
+ key: ConfigKey.tag_false,
3313
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3314
+ description: "Reason-not-required choice for feedback (`[-]`).",
3315
+ maxCount: Count.infinity,
3316
+ format: TagFormat.plainText,
3317
+ chain: [
3318
+ {
3319
+ key: ConfigKey.property_example,
3320
+ exportJsonKey: [
3321
+ { "@keyonly": { isExample: true, example: true } },
3322
+ { isExample: true, example: "$" }
3323
+ ],
3324
+ description: "Per-choice example marker for feedback `[-]`.",
3325
+ format: TagFormat.boolean,
3326
+ maxCount: 1,
3327
+ nullable: true
3328
+ },
3329
+ {
3330
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3331
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3332
+ }
3333
+ ]
3334
+ },
2773
3335
  {
2774
3336
  key: ConfigKey.group_trueFalse,
2775
3337
  description: "Group for true/false feedback."
@@ -2790,31 +3352,71 @@ var CARDSETS = {
2790
3352
  name: "reason",
2791
3353
  variants: [
2792
3354
  {
3355
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3356
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3357
+ // under `reason.*`; each tag here spells out the full path.
3358
+ // Body is a plain string (BPG `parseFeedback` uses
3359
+ // `cardBodyStr ?? ''` for `reason.text`).
2793
3360
  jsonKey: "reason.text",
2794
3361
  exportJsonKey: { reason: { text: "$" } },
3362
+ format: TextFormat2.plainText,
2795
3363
  tags: [
2796
3364
  {
2797
- key: ConfigKey.group_standardItemLeadInstructionHint,
2798
- description: "Standard tags for lead, instruction, and hint."
3365
+ // Instruction lands under reason. Hint/item/lead/etc.
3366
+ // omitted: not exercised by any fixture and BPG's
3367
+ // `parseFeedback` spreads only the tags actually present
3368
+ // — adding the rest can be done lazily as fixtures
3369
+ // require them, with the same `{reason: {…}}` wrapper.
3370
+ key: ConfigKey.tag_instruction,
3371
+ exportJsonKey: { reason: { instruction: "$" } },
3372
+ description: "Instruction for the feedback reason.",
3373
+ format: TagFormat.bitmarkText
2799
3374
  },
2800
3375
  {
2801
3376
  key: ConfigKey.property_reasonableNumOfChars,
2802
3377
  exportJsonKey: [
2803
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2804
- { reasonableNumOfChars: "$" }
3378
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3379
+ { reason: { reasonableNumOfChars: "$" } }
2805
3380
  ],
2806
3381
  description: "Property for reasonable number of characters.",
2807
3382
  format: TagFormat.number
2808
3383
  },
2809
3384
  {
3385
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3386
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3387
+ // cascade. Local emission only.
3388
+ //
3389
+ // Both `reason.example` and `feedbacks[].example` are
3390
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3391
+ // here is `bitmarkText` so `$` coerces to a paragraph
3392
+ // array at both write sites.
2810
3393
  key: ConfigKey.property_example,
2811
3394
  exportJsonKey: [
2812
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2813
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2814
- { isExample: true, example: "$", "@bit": { isExample: true } }
3395
+ {
3396
+ // Bare `[@example]`: reason.example is TextAst BPG
3397
+ // coerces the boolean default to a paragraph
3398
+ // containing the string "true". feedbacks[].example
3399
+ // is not TextAst, stays a boolean. Two literal writes.
3400
+ "@keyonly": {
3401
+ reason: {
3402
+ isExample: true,
3403
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3404
+ },
3405
+ isExample: true,
3406
+ example: true,
3407
+ "@bit": { isExample: true }
3408
+ }
3409
+ },
3410
+ {
3411
+ reason: { isExample: true, example: "$" },
3412
+ isExample: true,
3413
+ example: "$",
3414
+ "@bit": { isExample: true }
3415
+ }
2815
3416
  ],
2816
- description: "Example text for the feedback.",
2817
- format: TagFormat.plainText
3417
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3418
+ format: TagFormat.bitmarkText,
3419
+ nullable: true
2818
3420
  },
2819
3421
  {
2820
3422
  key: ConfigKey.tag_title,
@@ -2878,6 +3480,22 @@ var CARDSETS = {
2878
3480
  {
2879
3481
  key: ConfigKey.group_standardTags,
2880
3482
  description: "Standard tags for the question."
3483
+ },
3484
+ {
3485
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3486
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3487
+ // does not propagate `example`/`isExample` onto question
3488
+ // entries. Only entry-local `[@example]` fires (handled by
3489
+ // `@keyonly` and the unconditional fallback). No `@absent`
3490
+ // rule.
3491
+ key: ConfigKey.property_example,
3492
+ exportJsonKey: [
3493
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3494
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3495
+ ],
3496
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3497
+ format: TagFormat.bitmarkText,
3498
+ nullable: true
2881
3499
  }
2882
3500
  ]
2883
3501
  }
@@ -2904,14 +3522,16 @@ var CARDSETS = {
2904
3522
  description: "Standard tags for lead, instruction, and hint."
2905
3523
  },
2906
3524
  {
3525
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3526
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2907
3527
  key: ConfigKey.property_example,
2908
3528
  exportJsonKey: [
2909
3529
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2910
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2911
3530
  { isExample: true, example: "$", "@bit": { isExample: true } }
2912
3531
  ],
2913
- description: "Example text for the element.",
2914
- format: TagFormat.plainText
3532
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3533
+ format: TagFormat.plainText,
3534
+ nullable: true
2915
3535
  }
2916
3536
  ],
2917
3537
  repeatCount: Count.infinity
@@ -2923,9 +3543,57 @@ var CARDSETS = {
2923
3543
  //
2924
3544
  // table
2925
3545
  //
3546
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3547
+ // output (`{ columns?, data, columnWidths? }`):
3548
+ //
3549
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3550
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3551
+ // no qualifier, so they route to the `table-body` (default) section.
3552
+ //
3553
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3554
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3555
+ // the first header card's cells to `columns` and demote remaining
3556
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3557
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3558
+ // here (no per-cell metadata in basic shape) — declared on the cell
3559
+ // variant below.
3560
+ //
2926
3561
  [CardSetConfigKey.table]: {
2927
- jsonKey: "table.data",
2928
- exportJsonKey: { table: { data: "$" } },
3562
+ jsonKey: null,
3563
+ exportJsonKey: null,
3564
+ sections: {
3565
+ "table-header": {
3566
+ // First header card's row of cells → `columns`. Subsequent header
3567
+ // cards' rows → `data` (per-card append; `[$]` shape because
3568
+ // per-card emission fires once per card, multiple fires deep_merge-
3569
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3570
+ // both ROUTE cards to this section (when no explicit `==== table-
3571
+ // header ====` qualifier) and SELECT the rule body once routed.
3572
+ // First rule = first card with any `[#]` → `columns`; second rule
3573
+ // = subsequent cards in this section → `data` (per-row append).
3574
+ jsonKey: "table.columns",
3575
+ exportJsonKey: [
3576
+ {
3577
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3578
+ rule: { "@bit": { table: { columns: "$" } } }
3579
+ },
3580
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3581
+ { "@bit": { table: { data: ["$"] } } }
3582
+ ]
3583
+ },
3584
+ "table-body": {
3585
+ // Batched emission: `$` is the entire 2D array of body card rows.
3586
+ // This is the default section (no qualifier → cards land here).
3587
+ jsonKey: "table.data",
3588
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3589
+ isDefault: true
3590
+ },
3591
+ "table-footer": {
3592
+ // Batched emission: `$` is the entire 2D array of footer rows.
3593
+ jsonKey: "table.data",
3594
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3595
+ }
3596
+ },
2929
3597
  sides: [
2930
3598
  {
2931
3599
  name: "cell",
@@ -2966,6 +3634,8 @@ var CARDSETS = {
2966
3634
  },
2967
3635
  {
2968
3636
  key: ConfigKey.property_tableColWidth,
3637
+ jsonKey: "^table.columnWidths[{s}]",
3638
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
2969
3639
  description: "Width for the column.",
2970
3640
  format: TagFormat.number
2971
3641
  }
@@ -2984,7 +3654,22 @@ var CARDSETS = {
2984
3654
  sections: {
2985
3655
  "table-header": {
2986
3656
  jsonKey: "table.header.rows",
2987
- exportJsonKey: { table: { header: { rows: "$" } } },
3657
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3658
+ // Cards with no explicit `==== table-header ====` qualifier
3659
+ // route here if FIRST CARD contains any `[#]` tag in any
3660
+ // variant. Explicit-qualifier cards fall through to the
3661
+ // unconditional fallback rule. Both rules use the per-card
3662
+ // append shape `rows: ['$']` — the runtime triggers per-card
3663
+ // emission whenever the pattern carries predicates, and each
3664
+ // fire appends a single-element array (multi-fire deep-merge
3665
+ // concatenates).
3666
+ exportJsonKey: [
3667
+ {
3668
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3669
+ rule: { table: { header: { rows: ["$"] } } }
3670
+ },
3671
+ { table: { header: { rows: ["$"] } } }
3672
+ ],
2988
3673
  sideJsonKey: "cells[{s}]|set(title=true)",
2989
3674
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
2990
3675
  },
@@ -3017,13 +3702,13 @@ var CARDSETS = {
3017
3702
  },
3018
3703
  {
3019
3704
  key: ConfigKey.tag_title,
3020
- jsonKey: ".",
3021
- exportJsonKey: {
3022
- "@bit": {
3023
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
3024
- }
3025
- },
3026
- description: "Title of the table cell."
3705
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3706
+ // `content` field. When the card is in the `table-header`
3707
+ // section, the section's sideExportJsonKey wraps each
3708
+ // cell with `title: true` automatically.
3709
+ jsonKey: "content",
3710
+ exportJsonKey: { content: "$" },
3711
+ description: "Title text of the table cell (header rows)."
3027
3712
  },
3028
3713
  {
3029
3714
  key: ConfigKey.property_tableCellType,
@@ -3055,6 +3740,9 @@ var CARDSETS = {
3055
3740
  },
3056
3741
  {
3057
3742
  key: ConfigKey.property_tableColWidth,
3743
+ // PLAN-080: write to the cell's `colwidth` field.
3744
+ jsonKey: "colwidth",
3745
+ exportJsonKey: { colwidth: "$" },
3058
3746
  description: "Width for the column.",
3059
3747
  format: TagFormat.number
3060
3748
  }
@@ -3092,7 +3780,8 @@ var CARDSETS = {
3092
3780
  {
3093
3781
  key: ConfigKey.resource_audio,
3094
3782
  description: "Audio resource for the pronunciation table.",
3095
- jsonKey: "audio"
3783
+ jsonKey: "audio",
3784
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3096
3785
  }
3097
3786
  ],
3098
3787
  repeatCount: Count.infinity
@@ -3163,14 +3852,17 @@ var CARDSETS = {
3163
3852
  format: TagFormat.plainText
3164
3853
  },
3165
3854
  {
3855
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
3856
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
3857
+ // header cascade.
3166
3858
  key: ConfigKey.property_example,
3167
3859
  exportJsonKey: [
3168
3860
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3169
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3170
3861
  { isExample: true, example: "$", "@bit": { isExample: true } }
3171
3862
  ],
3172
- description: "Example text for the bot action response.",
3173
- format: TagFormat.plainText
3863
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
3864
+ format: TagFormat.plainText,
3865
+ nullable: true
3174
3866
  }
3175
3867
  ]
3176
3868
  }
@@ -3236,6 +3928,38 @@ var CARDSETS = {
3236
3928
  ]
3237
3929
  },
3238
3930
  //
3931
+ // page-footer-sections — mirrors example-bit-list but emits cards under
3932
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
3933
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
3934
+ // routing as pure config so the Rust serializer can pick it up.
3935
+ //
3936
+ [CardSetConfigKey.pageFooterSections]: {
3937
+ jsonKey: "sections",
3938
+ exportJsonKey: { sections: "$" },
3939
+ sides: [
3940
+ {
3941
+ name: "item",
3942
+ variants: [
3943
+ {
3944
+ jsonKey: "body",
3945
+ exportJsonKey: { body: "$" },
3946
+ tags: [
3947
+ {
3948
+ key: ConfigKey.group_standardTags,
3949
+ description: "Standard tags for page-footer section bits."
3950
+ },
3951
+ {
3952
+ exportJsonKey: { title: "$" },
3953
+ key: ConfigKey.tag_title,
3954
+ description: "Title of the page-footer section."
3955
+ }
3956
+ ]
3957
+ }
3958
+ ]
3959
+ }
3960
+ ]
3961
+ },
3962
+ //
3239
3963
  // ingredients
3240
3964
  //
3241
3965
  [CardSetConfigKey.ingredients]: {
@@ -3566,7 +4290,11 @@ var GROUPS = {
3566
4290
  {
3567
4291
  key: ConfigKey.property_groupTag,
3568
4292
  jsonKey: "groupTag.name",
3569
- exportJsonKey: { groupTag: { name: "$" } },
4293
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4294
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4295
+ // same value fold into one entry; chained `@tag` children's
4296
+ // arrays merge as sets.
4297
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3570
4298
  description: "The group tag(s) for the bit",
3571
4299
  format: TagFormat.plainText,
3572
4300
  maxCount: Count.infinity,
@@ -3739,6 +4467,24 @@ var GROUPS = {
3739
4467
  }
3740
4468
  ]
3741
4469
  },
4470
+ [ConfigKey.group_standardInstructionHint]: {
4471
+ type: GroupConfigType.standard,
4472
+ 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).",
4473
+ tags: [
4474
+ {
4475
+ exportJsonKey: { instruction: "$" },
4476
+ key: ConfigKey.tag_instruction,
4477
+ name: "Instruction",
4478
+ description: "The instruction for the bit"
4479
+ },
4480
+ {
4481
+ exportJsonKey: { hint: "$" },
4482
+ key: ConfigKey.tag_hint,
4483
+ name: "Hint",
4484
+ description: "The hint for the bit"
4485
+ }
4486
+ ]
4487
+ },
3742
4488
  [ConfigKey.group_standardTags]: {
3743
4489
  type: GroupConfigType.standard,
3744
4490
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3752,17 +4498,45 @@ var GROUPS = {
3752
4498
  description: "Item, lead, page number, margin number, instruction and hint tags"
3753
4499
  },
3754
4500
  {
3755
- // Bit-level example tag: bare/synthesized records the marker only;
3756
- // valued [@example:text] emits `isExample: true` and `example` as
3757
- // PM-rendered rich text (bitmark+ format).
4501
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4502
+ // - bare `[@example]`: consume only (records cascade marker, emits
4503
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4504
+ // for bits NOT in its `isTopLevelExample` allow-list).
4505
+ // - synthesized (descendant cascade): still emits `isExample: true`
4506
+ // at the descendant scope.
4507
+ // - valued `[@example:text]`: emit the example value only; bit-level
4508
+ // `isExample` is left to bubble from descendants.
4509
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4510
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4511
+ // matchMatrix, match, essay, etc.) override this entry with the
4512
+ // legacy emit-isExample shape.
3758
4513
  key: ConfigKey.property_example,
3759
- exportJsonKey: [
3760
- { "@keyonly": { isExample: true } },
3761
- { "@absent": { isExample: true } },
3762
- { isExample: true, example: "$" }
3763
- ],
4514
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3764
4515
  description: "The example(s) for the bit",
3765
- format: TagFormat.bitmarkText
4516
+ format: TagFormat.bitmarkText,
4517
+ nullable: true
4518
+ }
4519
+ ]
4520
+ },
4521
+ [ConfigKey.group_standardTagsNoItemLead]: {
4522
+ type: GroupConfigType.standard,
4523
+ 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).",
4524
+ tags: [
4525
+ {
4526
+ key: ConfigKey.group_standardAllBits,
4527
+ description: "All standard tags which apply to all bits"
4528
+ },
4529
+ {
4530
+ key: ConfigKey.group_standardInstructionHint,
4531
+ description: "Instruction and hint tags only (no item/lead chain)."
4532
+ },
4533
+ {
4534
+ // Mirror of group_standardTags property_example entry.
4535
+ key: ConfigKey.property_example,
4536
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4537
+ description: "The example(s) for the bit",
4538
+ format: TagFormat.bitmarkText,
4539
+ nullable: true
3766
4540
  }
3767
4541
  ]
3768
4542
  },
@@ -3827,8 +4601,8 @@ var GROUPS = {
3827
4601
  tags: [
3828
4602
  {
3829
4603
  key: ConfigKey.property_person,
3830
- jsonKey: "partner.name",
3831
- exportJsonKey: { partner: { name: "$" } },
4604
+ jsonKey: "person.name",
4605
+ exportJsonKey: { person: { name: "$" } },
3832
4606
  description: "A person",
3833
4607
  format: TagFormat.plainText,
3834
4608
  chain: [
@@ -3848,8 +4622,8 @@ var GROUPS = {
3848
4622
  {
3849
4623
  // Deprecated (parter renamed to person)
3850
4624
  key: ConfigKey.property_partner,
3851
- jsonKey: "partner.name",
3852
- exportJsonKey: { partner: { name: "$" } },
4625
+ jsonKey: "person.name",
4626
+ exportJsonKey: { person: { name: "$" } },
3853
4627
  description: "A partner",
3854
4628
  format: TagFormat.plainText,
3855
4629
  chain: [
@@ -3873,7 +4647,9 @@ var GROUPS = {
3873
4647
  description: "Gap chain",
3874
4648
  tags: [
3875
4649
  {
3876
- exportJsonKey: { solutions: [["$"]] },
4650
+ exportJsonKey: {
4651
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4652
+ },
3877
4653
  key: ConfigKey.tag_gap,
3878
4654
  description: "The value of a gap in the content",
3879
4655
  maxCount: Count.infinity,
@@ -3892,27 +4668,37 @@ var GROUPS = {
3892
4668
  description: "Item, lead, page number, margin number, instruction and hint tags"
3893
4669
  },
3894
4670
  {
4671
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4672
+ // `__solutionsAst[length - 1]` — the solution closest to the
4673
+ // `[@example]` tag at parse time. Our chain accumulator is built
4674
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4675
+ // the moment `[@example]` fires, `solutions` only contains the
4676
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4677
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4678
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4679
+ // solutions=["solution"], `[-1]`="solution".
3895
4680
  key: ConfigKey.property_example,
3896
4681
  jsonKey: "example",
3897
4682
  exportJsonKey: [
3898
4683
  {
3899
4684
  "@keyonly": {
3900
4685
  isExample: true,
3901
- example: "$parent.solutions[0]",
4686
+ example: "$parent.solutions[-1]",
3902
4687
  "@bit": { isExample: true }
3903
4688
  }
3904
4689
  },
3905
4690
  {
3906
4691
  "@absent": {
3907
4692
  isExample: true,
3908
- example: "$parent.solutions[0]",
4693
+ example: "$parent.solutions[-1]",
3909
4694
  "@bit": { isExample: true }
3910
4695
  }
3911
4696
  },
3912
4697
  { isExample: true, example: "$", "@bit": { isExample: true } }
3913
4698
  ],
3914
4699
  description: "An example for the gap",
3915
- format: TagFormat.bitmarkText
4700
+ format: TagFormat.bitmarkText,
4701
+ nullable: true
3916
4702
  },
3917
4703
  {
3918
4704
  key: ConfigKey.property_isCaseSensitive,
@@ -3961,6 +4747,24 @@ var GROUPS = {
3961
4747
  description: "Item, lead, page number, margin number, instruction and hint tags"
3962
4748
  },
3963
4749
  {
4750
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4751
+ // `@example` from the bit header marks EVERY choice with
4752
+ // `isExample: true` (matches BPG output), but only the
4753
+ // FIRST correct choice gets `example: true` (BPG
4754
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4755
+ //
4756
+ // Two `@absent` rules, picked first-match-wins:
4757
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4758
+ // `maxEmits:1` — fires once for the first correct.
4759
+ // 2. Plain `@absent` — fires on every remaining entry
4760
+ // (subsequent corrects and all incorrects), emitting
4761
+ // `isExample` only.
4762
+ // The `serialize_cards` cascade-counter scope holds the cap
4763
+ // across the whole cardset.
4764
+ // @card scope-shift writes choice-container isExample (e.g.
4765
+ // `quizzes[i].isExample`) — was previously provided by the
4766
+ // universal `bubble_is_example` post-walk, which was removed
4767
+ // in favour of explicit per-tag config. SYNTAX.md §8.
3964
4768
  key: ConfigKey.property_example,
3965
4769
  jsonKey: "example",
3966
4770
  exportJsonKey: [
@@ -3968,20 +4772,37 @@ var GROUPS = {
3968
4772
  "@keyonly": {
3969
4773
  isExample: true,
3970
4774
  example: "$parent.isCorrect",
3971
- "@bit": { isExample: true }
4775
+ "@bit": { isExample: true },
4776
+ "@card": { isExample: true }
4777
+ }
4778
+ },
4779
+ {
4780
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4781
+ maxEmits: 1,
4782
+ rule: {
4783
+ isExample: true,
4784
+ example: true,
4785
+ "@bit": { isExample: true },
4786
+ "@card": { isExample: true }
3972
4787
  }
3973
4788
  },
3974
4789
  {
3975
4790
  "@absent": {
3976
4791
  isExample: true,
3977
- example: "$parent.isCorrect",
3978
- "@bit": { isExample: true }
4792
+ "@bit": { isExample: true },
4793
+ "@card": { isExample: true }
3979
4794
  }
3980
4795
  },
3981
- { isExample: true, example: "$", "@bit": { isExample: true } }
4796
+ {
4797
+ isExample: true,
4798
+ example: "$",
4799
+ "@bit": { isExample: true },
4800
+ "@card": { isExample: true }
4801
+ }
3982
4802
  ],
3983
4803
  description: "An example for the true/false statement/question",
3984
- format: TagFormat.bitmarkText
4804
+ format: TagFormat.boolean,
4805
+ nullable: true
3985
4806
  }
3986
4807
  ]
3987
4808
  },
@@ -4013,6 +4834,12 @@ var GROUPS = {
4013
4834
  description: "Item, lead, page number, margin number, instruction and hint tags"
4014
4835
  },
4015
4836
  {
4837
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4838
+ // `@example` on a `-` (incorrect choice) emits
4839
+ // `isExample: true` only — no `example` (BPG fixtures show
4840
+ // `isExample` on every choice, but `example` only on the
4841
+ // first correct via firstCorrectOnly cascade).
4842
+ // @card scope-shift writes choice-container isExample.
4016
4843
  key: ConfigKey.property_example,
4017
4844
  jsonKey: "example",
4018
4845
  exportJsonKey: [
@@ -4020,20 +4847,460 @@ var GROUPS = {
4020
4847
  "@keyonly": {
4021
4848
  isExample: true,
4022
4849
  example: "$parent.isCorrect",
4023
- "@bit": { isExample: true }
4850
+ "@bit": { isExample: true },
4851
+ "@card": { isExample: true }
4024
4852
  }
4025
4853
  },
4026
4854
  {
4027
4855
  "@absent": {
4856
+ isExample: true,
4857
+ "@bit": { isExample: true },
4858
+ "@card": { isExample: true }
4859
+ }
4860
+ },
4861
+ {
4862
+ isExample: true,
4863
+ example: "$",
4864
+ "@bit": { isExample: true },
4865
+ "@card": { isExample: true }
4866
+ }
4867
+ ],
4868
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
4869
+ format: TagFormat.boolean,
4870
+ nullable: true
4871
+ }
4872
+ ]
4873
+ }
4874
+ ]
4875
+ },
4876
+ [ConfigKey.group_trueFalseResponses]: {
4877
+ type: GroupConfigType.standard,
4878
+ description: "True/False Responses chain",
4879
+ tags: [
4880
+ {
4881
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4882
+ key: ConfigKey.tag_true,
4883
+ description: "True value for a true/false statement/question",
4884
+ maxCount: Count.infinity,
4885
+ format: TagFormat.plainText,
4886
+ chain: [
4887
+ {
4888
+ key: ConfigKey.tag_true,
4889
+ jsonKey: "responses[]|set(isCorrect=true)",
4890
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4891
+ description: "True values for a true/false statement/question",
4892
+ maxCount: Count.infinity,
4893
+ format: TagFormat.plainText
4894
+ },
4895
+ {
4896
+ key: ConfigKey.tag_false,
4897
+ jsonKey: "responses[]|set(isCorrect=false)",
4898
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4899
+ description: "False values for a true/false statement/question",
4900
+ maxCount: Count.infinity,
4901
+ format: TagFormat.plainText
4902
+ },
4903
+ {
4904
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4905
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4906
+ },
4907
+ {
4908
+ // @card scope-shift writes response-container isExample.
4909
+ key: ConfigKey.property_example,
4910
+ jsonKey: "example",
4911
+ exportJsonKey: [
4912
+ {
4913
+ "@keyonly": {
4028
4914
  isExample: true,
4029
4915
  example: "$parent.isCorrect",
4030
- "@bit": { isExample: true }
4916
+ "@bit": { isExample: true },
4917
+ "@card": { isExample: true }
4031
4918
  }
4032
4919
  },
4033
- { isExample: true, example: "$", "@bit": { isExample: true } }
4920
+ {
4921
+ "@absent": {
4922
+ isExample: "$parent.isCorrect",
4923
+ example: "$parent.isCorrect",
4924
+ "@bit": { isExample: "$parent.isCorrect" },
4925
+ "@card": { isExample: "$parent.isCorrect" }
4926
+ }
4927
+ },
4928
+ {
4929
+ isExample: true,
4930
+ example: "$",
4931
+ "@bit": { isExample: true },
4932
+ "@card": { isExample: true }
4933
+ }
4034
4934
  ],
4035
4935
  description: "An example for the true/false statement/question",
4036
- format: TagFormat.bitmarkText
4936
+ format: TagFormat.boolean,
4937
+ nullable: true
4938
+ }
4939
+ ]
4940
+ },
4941
+ {
4942
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4943
+ key: ConfigKey.tag_false,
4944
+ description: "False value for a true/false statement/question",
4945
+ maxCount: Count.infinity,
4946
+ format: TagFormat.plainText,
4947
+ chain: [
4948
+ {
4949
+ key: ConfigKey.tag_true,
4950
+ jsonKey: "responses[]|set(isCorrect=true)",
4951
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4952
+ description: "True values for a true/false statement/question",
4953
+ maxCount: Count.infinity,
4954
+ format: TagFormat.plainText
4955
+ },
4956
+ {
4957
+ key: ConfigKey.tag_false,
4958
+ jsonKey: "responses[]|set(isCorrect=false)",
4959
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4960
+ description: "False values for a true/false statement/question",
4961
+ maxCount: Count.infinity,
4962
+ format: TagFormat.plainText
4963
+ },
4964
+ {
4965
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4966
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4967
+ },
4968
+ {
4969
+ // @card scope-shift writes response-container isExample.
4970
+ key: ConfigKey.property_example,
4971
+ jsonKey: "example",
4972
+ exportJsonKey: [
4973
+ {
4974
+ "@keyonly": {
4975
+ isExample: true,
4976
+ example: "$parent.isCorrect",
4977
+ "@bit": { isExample: true },
4978
+ "@card": { isExample: true }
4979
+ }
4980
+ },
4981
+ {
4982
+ "@absent": {
4983
+ isExample: true,
4984
+ example: "$parent.isCorrect",
4985
+ "@bit": { isExample: true },
4986
+ "@card": { isExample: true }
4987
+ }
4988
+ },
4989
+ {
4990
+ isExample: true,
4991
+ example: "$",
4992
+ "@bit": { isExample: true },
4993
+ "@card": { isExample: true }
4994
+ }
4995
+ ],
4996
+ description: "An example for the true/false statement/question",
4997
+ format: TagFormat.boolean,
4998
+ nullable: true
4999
+ }
5000
+ ]
5001
+ }
5002
+ ]
5003
+ },
5004
+ [ConfigKey.group_trueFalseInlineSelect]: {
5005
+ type: GroupConfigType.standard,
5006
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5007
+ tags: [
5008
+ {
5009
+ exportJsonKey: {
5010
+ "@body-inline": {
5011
+ type: "select",
5012
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5013
+ }
5014
+ },
5015
+ key: ConfigKey.tag_true,
5016
+ description: "True value for an inline select body bit",
5017
+ maxCount: Count.infinity,
5018
+ format: TagFormat.plainText,
5019
+ chain: [
5020
+ {
5021
+ key: ConfigKey.tag_true,
5022
+ exportJsonKey: {},
5023
+ description: "True values for an inline select body bit",
5024
+ maxCount: Count.infinity,
5025
+ format: TagFormat.plainText
5026
+ },
5027
+ {
5028
+ key: ConfigKey.tag_false,
5029
+ exportJsonKey: {},
5030
+ description: "False values for an inline select body bit",
5031
+ maxCount: Count.infinity,
5032
+ format: TagFormat.plainText
5033
+ },
5034
+ {
5035
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5036
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5037
+ },
5038
+ {
5039
+ // PLAN-083: cascade-fired `@example` from bit header fires
5040
+ // `isExample:true, example:true` on the FIRST CORRECT option
5041
+ // only (BPG `fillBooleanExample(select.options,
5042
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5043
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5044
+ // OR — any cascade ⇒ bit marked) and
5045
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5046
+ // on the select node).
5047
+ key: ConfigKey.property_example,
5048
+ jsonKey: "example",
5049
+ exportJsonKey: [
5050
+ {
5051
+ "@keyonly": {
5052
+ isExample: true,
5053
+ example: "$parent.isCorrect",
5054
+ "@bit": { isExample: true },
5055
+ "@body-inline": { attrs: { isExample: true } }
5056
+ }
5057
+ },
5058
+ {
5059
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5060
+ maxEmits: 1,
5061
+ rule: {
5062
+ isExample: true,
5063
+ example: true,
5064
+ "@bit": { isExample: true },
5065
+ "@body-inline": { attrs: { isExample: true } }
5066
+ }
5067
+ },
5068
+ {
5069
+ isExample: true,
5070
+ example: "$",
5071
+ "@bit": { isExample: true },
5072
+ "@body-inline": { attrs: { isExample: true } }
5073
+ }
5074
+ ],
5075
+ description: "An example for the inline select option (first correct only)",
5076
+ format: TagFormat.boolean,
5077
+ nullable: true
5078
+ }
5079
+ ]
5080
+ },
5081
+ {
5082
+ exportJsonKey: {
5083
+ "@body-inline": {
5084
+ type: "select",
5085
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5086
+ }
5087
+ },
5088
+ key: ConfigKey.tag_false,
5089
+ description: "False value for an inline select body bit",
5090
+ maxCount: Count.infinity,
5091
+ format: TagFormat.plainText,
5092
+ chain: [
5093
+ {
5094
+ key: ConfigKey.tag_true,
5095
+ exportJsonKey: {},
5096
+ description: "True values for an inline select body bit",
5097
+ maxCount: Count.infinity,
5098
+ format: TagFormat.plainText
5099
+ },
5100
+ {
5101
+ key: ConfigKey.tag_false,
5102
+ exportJsonKey: {},
5103
+ description: "False values for an inline select body bit",
5104
+ maxCount: Count.infinity,
5105
+ format: TagFormat.plainText
5106
+ },
5107
+ {
5108
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5109
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5110
+ },
5111
+ {
5112
+ // PLAN-083: identical to the `+` lead's chain @example —
5113
+ // cascade reaches the first correct option regardless of
5114
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5115
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5116
+ key: ConfigKey.property_example,
5117
+ jsonKey: "example",
5118
+ exportJsonKey: [
5119
+ {
5120
+ "@keyonly": {
5121
+ isExample: true,
5122
+ example: "$parent.isCorrect",
5123
+ "@bit": { isExample: true },
5124
+ "@body-inline": { attrs: { isExample: true } }
5125
+ }
5126
+ },
5127
+ {
5128
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5129
+ maxEmits: 1,
5130
+ rule: {
5131
+ isExample: true,
5132
+ example: true,
5133
+ "@bit": { isExample: true },
5134
+ "@body-inline": { attrs: { isExample: true } }
5135
+ }
5136
+ },
5137
+ {
5138
+ isExample: true,
5139
+ example: "$",
5140
+ "@bit": { isExample: true },
5141
+ "@body-inline": { attrs: { isExample: true } }
5142
+ }
5143
+ ],
5144
+ description: "An example for the inline select option (first correct only)",
5145
+ format: TagFormat.boolean,
5146
+ nullable: true
5147
+ }
5148
+ ]
5149
+ }
5150
+ ]
5151
+ },
5152
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5153
+ type: GroupConfigType.standard,
5154
+ 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.',
5155
+ tags: [
5156
+ {
5157
+ exportJsonKey: {
5158
+ "@body-inline": {
5159
+ type: "highlight",
5160
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5161
+ }
5162
+ },
5163
+ key: ConfigKey.tag_true,
5164
+ description: "Highlighted span in the body (correct)",
5165
+ maxCount: Count.infinity,
5166
+ format: TagFormat.plainText,
5167
+ chain: [
5168
+ {
5169
+ key: ConfigKey.tag_true,
5170
+ exportJsonKey: {},
5171
+ description: "Highlighted span values (correct)",
5172
+ maxCount: Count.infinity,
5173
+ format: TagFormat.plainText
5174
+ },
5175
+ {
5176
+ key: ConfigKey.tag_false,
5177
+ exportJsonKey: {},
5178
+ description: "Highlighted span values (incorrect)",
5179
+ maxCount: Count.infinity,
5180
+ format: TagFormat.plainText
5181
+ },
5182
+ {
5183
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5184
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5185
+ },
5186
+ {
5187
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5188
+ // `@example` from the bit header fires `example: true` +
5189
+ // `isExample: true` on the FIRST correct highlight only
5190
+ // (BPG `fillBooleanExample(highlight.texts,
5191
+ // firstCorrectOnly=true)`). Incorrect highlights get
5192
+ // nothing (unlike choices, the highlight-text fixture
5193
+ // shows no `isExample` on incorrect entries).
5194
+ key: ConfigKey.property_example,
5195
+ jsonKey: "example",
5196
+ exportJsonKey: [
5197
+ {
5198
+ "@keyonly": {
5199
+ isExample: true,
5200
+ example: "$parent.isCorrect",
5201
+ "@bit": { isExample: true },
5202
+ "@body-inline": { attrs: { isExample: true } }
5203
+ }
5204
+ },
5205
+ {
5206
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5207
+ maxEmits: 1,
5208
+ rule: {
5209
+ isExample: true,
5210
+ example: true,
5211
+ "@bit": { isExample: true },
5212
+ "@body-inline": { attrs: { isExample: true } }
5213
+ }
5214
+ },
5215
+ {
5216
+ isExample: true,
5217
+ example: "$",
5218
+ "@bit": { isExample: true },
5219
+ "@body-inline": { attrs: { isExample: true } }
5220
+ }
5221
+ ],
5222
+ description: "An example for the highlighted span (first correct only)",
5223
+ format: TagFormat.boolean,
5224
+ nullable: true
5225
+ }
5226
+ ]
5227
+ },
5228
+ {
5229
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5230
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5231
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5232
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5233
+ // through and nothing fires. The natural-default strip removes
5234
+ // `isCorrect: false` from the final texts entries (matches fixture).
5235
+ exportJsonKey: {
5236
+ "@body-inline": {
5237
+ type: "highlight",
5238
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5239
+ }
5240
+ },
5241
+ key: ConfigKey.tag_false,
5242
+ description: "Highlighted span in the body (incorrect)",
5243
+ maxCount: Count.infinity,
5244
+ format: TagFormat.plainText,
5245
+ chain: [
5246
+ {
5247
+ key: ConfigKey.tag_true,
5248
+ exportJsonKey: {},
5249
+ description: "Highlighted span values (correct)",
5250
+ maxCount: Count.infinity,
5251
+ format: TagFormat.plainText
5252
+ },
5253
+ {
5254
+ key: ConfigKey.tag_false,
5255
+ exportJsonKey: {},
5256
+ description: "Highlighted span values (incorrect)",
5257
+ maxCount: Count.infinity,
5258
+ format: TagFormat.plainText
5259
+ },
5260
+ {
5261
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5262
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5263
+ },
5264
+ {
5265
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5266
+ // `@example` on a `-` (incorrect highlight) emits nothing
5267
+ // (highlight-text fixture has no `isExample` on incorrect
5268
+ // entries; firstCorrectOnly cascade skips them entirely).
5269
+ // No `@absent` rule.
5270
+ // PLAN-083: identical to the `+` lead's chain @example —
5271
+ // cascade reaches the first correct option regardless of
5272
+ // which lead (+/-) opens the chain.
5273
+ key: ConfigKey.property_example,
5274
+ jsonKey: "example",
5275
+ exportJsonKey: [
5276
+ {
5277
+ "@keyonly": {
5278
+ isExample: true,
5279
+ example: "$parent.isCorrect",
5280
+ "@bit": { isExample: true },
5281
+ "@body-inline": { attrs: { isExample: true } }
5282
+ }
5283
+ },
5284
+ {
5285
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5286
+ maxEmits: 1,
5287
+ rule: {
5288
+ isExample: true,
5289
+ example: true,
5290
+ "@bit": { isExample: true },
5291
+ "@body-inline": { attrs: { isExample: true } }
5292
+ }
5293
+ },
5294
+ {
5295
+ isExample: true,
5296
+ example: "$",
5297
+ "@bit": { isExample: true },
5298
+ "@body-inline": { attrs: { isExample: true } }
5299
+ }
5300
+ ],
5301
+ description: "An example for the highlighted span (first correct only)",
5302
+ format: TagFormat.boolean,
5303
+ nullable: true
4037
5304
  }
4038
5305
  ]
4039
5306
  }
@@ -4046,7 +5313,7 @@ var GROUPS = {
4046
5313
  {
4047
5314
  key: ConfigKey.property_mark,
4048
5315
  jsonKey: "marks.mark",
4049
- exportJsonKey: { marks: { mark: ["$"] } },
5316
+ exportJsonKey: { marks: [{ mark: "$" }] },
4050
5317
  description: "The mark configuration",
4051
5318
  format: TagFormat.plainText,
4052
5319
  maxCount: Count.infinity,
@@ -4070,7 +5337,9 @@ var GROUPS = {
4070
5337
  description: "Mark chain",
4071
5338
  tags: [
4072
5339
  {
4073
- exportJsonKey: { solution: ["$"] },
5340
+ exportJsonKey: {
5341
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5342
+ },
4074
5343
  key: ConfigKey.tag_mark,
4075
5344
  description: "The marked content",
4076
5345
  maxCount: Count.infinity,
@@ -4079,6 +5348,7 @@ var GROUPS = {
4079
5348
  {
4080
5349
  key: ConfigKey.property_mark,
4081
5350
  jsonKey: "mark",
5351
+ exportJsonKey: { mark: "$" },
4082
5352
  description: "The mark configuration",
4083
5353
  format: TagFormat.plainText
4084
5354
  },
@@ -4091,7 +5361,8 @@ var GROUPS = {
4091
5361
  { isExample: true, example: "$", "@bit": { isExample: true } }
4092
5362
  ],
4093
5363
  description: "An example for the marked content",
4094
- format: TagFormat.bitmarkText
5364
+ format: TagFormat.bitmarkText,
5365
+ nullable: true
4095
5366
  }
4096
5367
  ]
4097
5368
  }
@@ -4520,6 +5791,42 @@ var GROUPS = {
4520
5791
  }
4521
5792
  ]
4522
5793
  },
5794
+ [ConfigKey.group_backgroundWallpaper]: {
5795
+ type: GroupConfigType.standard,
5796
+ description: "Background wallpaper tags",
5797
+ tags: [
5798
+ {
5799
+ key: ConfigKey.resource_backgroundWallpaper,
5800
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
5801
+ description: "Background wallpaper for the image, used to set a background for the image",
5802
+ chain: [
5803
+ {
5804
+ key: ConfigKey.group_resourceImageCommon,
5805
+ description: "Common resource image tags for images"
5806
+ }
5807
+ ]
5808
+ }
5809
+ ]
5810
+ },
5811
+ [ConfigKey.group_imageNoZoom]: {
5812
+ type: GroupConfigType.standard,
5813
+ description: "Image bit tags",
5814
+ tags: [
5815
+ {
5816
+ key: ConfigKey.group_backgroundWallpaper,
5817
+ description: "Background wallpaper tags for images, used to define background properties for images"
5818
+ },
5819
+ {
5820
+ key: ConfigKey.group_resourceBitTags,
5821
+ description: "Resource bit tags for images, used to define additional properties for images"
5822
+ },
5823
+ {
5824
+ key: ConfigKey.group_resourceImageNoZoom,
5825
+ description: "Resource image tags for images, used to attach images to the bit",
5826
+ minCount: 1
5827
+ }
5828
+ ]
5829
+ },
4523
5830
  //
4524
5831
  // Resource groups
4525
5832
  //
@@ -4639,6 +5946,58 @@ var GROUPS = {
4639
5946
  }
4640
5947
  ]
4641
5948
  },
5949
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
5950
+ type: GroupConfigType.standard,
5951
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
5952
+ tags: [
5953
+ {
5954
+ key: ConfigKey.group_resourceCommon,
5955
+ description: "Common resource properties"
5956
+ },
5957
+ {
5958
+ key: ConfigKey.property_src1x,
5959
+ description: "The source URL for the image at 1x resolution",
5960
+ format: TagFormat.plainText
5961
+ },
5962
+ {
5963
+ key: ConfigKey.property_src2x,
5964
+ description: "The source URL for the image at 2x resolution",
5965
+ format: TagFormat.plainText
5966
+ },
5967
+ {
5968
+ key: ConfigKey.property_src3x,
5969
+ description: "The source URL for the image at 3x resolution",
5970
+ format: TagFormat.plainText
5971
+ },
5972
+ {
5973
+ key: ConfigKey.property_src4x,
5974
+ description: "The source URL for the image at 4x resolution",
5975
+ format: TagFormat.plainText
5976
+ },
5977
+ {
5978
+ key: ConfigKey.property_width,
5979
+ description: "The width of the image",
5980
+ format: TagFormat.number
5981
+ },
5982
+ {
5983
+ key: ConfigKey.property_height,
5984
+ description: "The height of the image",
5985
+ format: TagFormat.number
5986
+ },
5987
+ {
5988
+ key: ConfigKey.property_alt,
5989
+ description: "The alternative text for the image",
5990
+ format: TagFormat.plainText
5991
+ },
5992
+ // @zoomDisabled with a true default
5993
+ {
5994
+ key: ConfigKey.property_zoomDisabled,
5995
+ description: "If true, zooming is disabled for the image (defaults to true)",
5996
+ format: TagFormat.boolean,
5997
+ defaultValue: "true"
5998
+ }
5999
+ ]
6000
+ },
4642
6001
  [ConfigKey.group_resourceAudioCommon]: {
4643
6002
  type: GroupConfigType.standard,
4644
6003
  description: "Common properties for audio resources",
@@ -4728,22 +6087,26 @@ var GROUPS = {
4728
6087
  {
4729
6088
  key: ConfigKey.property_src1x,
4730
6089
  description: "The source URL for the video at 1x resolution",
4731
- format: TagFormat.plainText
6090
+ format: TagFormat.plainText,
6091
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4732
6092
  },
4733
6093
  {
4734
6094
  key: ConfigKey.property_src2x,
4735
6095
  description: "The source URL for the video at 2x resolution",
4736
- format: TagFormat.plainText
6096
+ format: TagFormat.plainText,
6097
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4737
6098
  },
4738
6099
  {
4739
6100
  key: ConfigKey.property_src3x,
4740
6101
  description: "The source URL for the video at 3x resolution",
4741
- format: TagFormat.plainText
6102
+ format: TagFormat.plainText,
6103
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4742
6104
  },
4743
6105
  {
4744
6106
  key: ConfigKey.property_src4x,
4745
6107
  description: "The source URL for the video at 4x resolution",
4746
- format: TagFormat.plainText
6108
+ format: TagFormat.plainText,
6109
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4747
6110
  }
4748
6111
  ]
4749
6112
  },
@@ -4756,6 +6119,7 @@ var GROUPS = {
4756
6119
  tags: [
4757
6120
  {
4758
6121
  key: ConfigKey.resource_icon,
6122
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4759
6123
  description: "The icon resource",
4760
6124
  chain: [
4761
6125
  {
@@ -4783,12 +6147,30 @@ var GROUPS = {
4783
6147
  }
4784
6148
  ]
4785
6149
  },
6150
+ [ConfigKey.group_resourceImageNoZoom]: {
6151
+ type: GroupConfigType.resource,
6152
+ description: "Image resource where @zoomDisabled defaults to true",
6153
+ tags: [
6154
+ {
6155
+ key: ConfigKey.resource_image,
6156
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6157
+ description: "The image resource (no-zoom default)",
6158
+ chain: [
6159
+ {
6160
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6161
+ description: "Common image properties with @zoomDisabled defaulting to true"
6162
+ }
6163
+ ]
6164
+ }
6165
+ ]
6166
+ },
4786
6167
  [ConfigKey.group_resourceImagePortrait]: {
4787
6168
  type: GroupConfigType.resource,
4788
6169
  description: "Portrait image resource",
4789
6170
  tags: [
4790
6171
  {
4791
6172
  key: ConfigKey.resource_imagePortrait,
6173
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4792
6174
  description: "The portrait image resource",
4793
6175
  chain: [
4794
6176
  {
@@ -4805,6 +6187,7 @@ var GROUPS = {
4805
6187
  tags: [
4806
6188
  {
4807
6189
  key: ConfigKey.resource_imageLandscape,
6190
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4808
6191
  description: "The landscape image resource",
4809
6192
  chain: [
4810
6193
  {
@@ -4821,6 +6204,7 @@ var GROUPS = {
4821
6204
  tags: [
4822
6205
  {
4823
6206
  key: ConfigKey.resource_imageEmbed,
6207
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4824
6208
  description: "The embedded image resource",
4825
6209
  chain: [
4826
6210
  {
@@ -4994,6 +6378,7 @@ var GROUPS = {
4994
6378
  tags: [
4995
6379
  {
4996
6380
  key: ConfigKey.resource_article,
6381
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
4997
6382
  description: "The article resource",
4998
6383
  chain: [
4999
6384
  {
@@ -5010,6 +6395,7 @@ var GROUPS = {
5010
6395
  tags: [
5011
6396
  {
5012
6397
  key: ConfigKey.resource_articleEmbed,
6398
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
5013
6399
  description: "The embedded article resource",
5014
6400
  chain: [
5015
6401
  {
@@ -5026,6 +6412,7 @@ var GROUPS = {
5026
6412
  tags: [
5027
6413
  {
5028
6414
  key: ConfigKey.resource_articleLink,
6415
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
5029
6416
  description: "The link to the article resource",
5030
6417
  chain: [
5031
6418
  {
@@ -5296,7 +6683,12 @@ var ConfigHydrator = class {
5296
6683
  isDefault: v.isDefault,
5297
6684
  sideJsonKey: v.sideJsonKey,
5298
6685
  sideExportJsonKey: v.sideExportJsonKey,
5299
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6686
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6687
+ // PLAN-085: cardinality fields pass through; defaults applied
6688
+ // downstream by the config consumer (Rust treats `0` / undefined
6689
+ // as unbounded).
6690
+ minCount: v.minCount,
6691
+ maxCount: v.maxCount
5300
6692
  };
5301
6693
  }
5302
6694
  return out;
@@ -5511,7 +6903,8 @@ var BITS = {
5511
6903
  { isExample: true, example: "$" }
5512
6904
  ],
5513
6905
  description: "The example text for the bit",
5514
- format: TagFormat.plainText
6906
+ format: TagFormat.plainText,
6907
+ nullable: true
5515
6908
  }
5516
6909
  ],
5517
6910
  rootExampleType: ExampleType.string
@@ -5850,7 +7243,8 @@ var BITS = {
5850
7243
  { isExample: true, example: "$" }
5851
7244
  ],
5852
7245
  description: "The example text for the bit",
5853
- format: TagFormat.plainText
7246
+ format: TagFormat.plainText,
7247
+ nullable: true
5854
7248
  }
5855
7249
  ],
5856
7250
  rootExampleType: ExampleType.string
@@ -6013,7 +7407,8 @@ var BITS = {
6013
7407
  { isExample: true, example: "$" }
6014
7408
  ],
6015
7409
  description: "The example text for the bit",
6016
- format: TagFormat.plainText
7410
+ format: TagFormat.plainText,
7411
+ nullable: true
6017
7412
  }
6018
7413
  ],
6019
7414
  rootExampleType: ExampleType.string
@@ -6164,7 +7559,8 @@ var BITS = {
6164
7559
  { isExample: true, example: "$" }
6165
7560
  ],
6166
7561
  description: "The example text for the bit",
6167
- format: TagFormat.plainText
7562
+ format: TagFormat.plainText,
7563
+ nullable: true
6168
7564
  }
6169
7565
  ],
6170
7566
  rootExampleType: ExampleType.string
@@ -6759,8 +8155,31 @@ var BITS = {
6759
8155
  description: "Tags for gaps in cloze and multiple choice text bits"
6760
8156
  },
6761
8157
  {
6762
- key: ConfigKey.group_trueFalse,
8158
+ key: ConfigKey.group_trueFalseInlineSelect,
6763
8159
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8160
+ },
8161
+ {
8162
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8163
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8164
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8165
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8166
+ key: ConfigKey.property_isCaseSensitive,
8167
+ exportJsonKey: {},
8168
+ description: "If the cloze answers are case sensitive",
8169
+ format: TagFormat.boolean,
8170
+ defaultValue: "true"
8171
+ },
8172
+ {
8173
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8174
+ key: ConfigKey.property_example,
8175
+ exportJsonKey: [
8176
+ { "@keyonly": { isExample: true } },
8177
+ { "@absent": { isExample: true } },
8178
+ { isExample: true, example: "$" }
8179
+ ],
8180
+ description: "The example(s) for the bit",
8181
+ format: TagFormat.bitmarkText,
8182
+ nullable: true
6764
8183
  }
6765
8184
  ]
6766
8185
  },
@@ -6799,18 +8218,56 @@ var BITS = {
6799
8218
  {
6800
8219
  key: ConfigKey.group_gap,
6801
8220
  description: "Tags for gaps in cloze bits"
8221
+ },
8222
+ {
8223
+ // Override inherited @example: cloze is in OLD parser's
8224
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8225
+ // emits `isExample: true`.
8226
+ key: ConfigKey.property_example,
8227
+ exportJsonKey: [
8228
+ { "@keyonly": { isExample: true } },
8229
+ { "@absent": { isExample: true } },
8230
+ { isExample: true, example: "$" }
8231
+ ],
8232
+ description: "The example(s) for the bit",
8233
+ format: TagFormat.bitmarkText,
8234
+ nullable: true
6802
8235
  }
6803
8236
  ]
6804
8237
  },
6805
8238
  [BitType2.clozeInstructionGrouped]: {
6806
8239
  since: "1.3.0",
6807
8240
  baseBitType: BitType2.cloze,
6808
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8241
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8242
+ tags: [
8243
+ {
8244
+ key: ConfigKey.property_quizCountItems,
8245
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8246
+ format: TagFormat.boolean,
8247
+ defaultValue: "true"
8248
+ }
8249
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8250
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8251
+ ]
6809
8252
  },
6810
8253
  [BitType2.clozeSolutionGrouped]: {
6811
8254
  since: "1.3.0",
6812
8255
  baseBitType: BitType2.cloze,
6813
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8256
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8257
+ tags: [
8258
+ {
8259
+ key: ConfigKey.property_quizCountItems,
8260
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8261
+ format: TagFormat.boolean,
8262
+ defaultValue: "true"
8263
+ },
8264
+ {
8265
+ key: ConfigKey.property_quizStrikethroughSolutions,
8266
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8267
+ format: TagFormat.boolean,
8268
+ defaultValue: "true"
8269
+ }
8270
+ ]
6814
8271
  },
6815
8272
  [BitType2.clozeSeveral]: {
6816
8273
  since: "3.5.0",
@@ -6846,6 +8303,17 @@ var BITS = {
6846
8303
  {
6847
8304
  key: ConfigKey.group_quizCommon,
6848
8305
  description: "Common tags for quiz bits"
8306
+ },
8307
+ {
8308
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8309
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8310
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8311
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8312
+ key: ConfigKey.property_isCaseSensitive,
8313
+ exportJsonKey: {},
8314
+ description: "If the cloze answers are case sensitive",
8315
+ format: TagFormat.boolean,
8316
+ defaultValue: "true"
6849
8317
  }
6850
8318
  ],
6851
8319
  cardSet: CardSetConfigKey.clozeList
@@ -7012,6 +8480,7 @@ var BITS = {
7012
8480
  tags: [
7013
8481
  {
7014
8482
  key: ConfigKey.group_person,
8483
+ exportJsonKey: { partner: { name: "$" } },
7015
8484
  description: "Tags for the person in the conversation"
7016
8485
  }
7017
8486
  ]
@@ -7450,7 +8919,8 @@ var BITS = {
7450
8919
  { isExample: true, example: "$" }
7451
8920
  ],
7452
8921
  description: "The example text for the essay bit",
7453
- format: TagFormat.plainText
8922
+ format: TagFormat.plainText,
8923
+ nullable: true
7454
8924
  }
7455
8925
  ],
7456
8926
  rootExampleType: ExampleType.string
@@ -7484,7 +8954,8 @@ var BITS = {
7484
8954
  { isExample: true, example: "$" }
7485
8955
  ],
7486
8956
  description: "The example text for the example bit",
7487
- format: TagFormat.plainText
8957
+ format: TagFormat.plainText,
8958
+ nullable: true
7488
8959
  }
7489
8960
  ],
7490
8961
  rootExampleType: ExampleType.string
@@ -7570,7 +9041,19 @@ var BITS = {
7570
9041
  [BitType2.articleAi]: {
7571
9042
  since: "1.3.0",
7572
9043
  baseBitType: BitType2.article,
7573
- description: "Article AI bit, used to create AI-generated articles"
9044
+ description: "Article AI bit, used to create AI-generated articles",
9045
+ tags: [
9046
+ {
9047
+ // Shadows the inherited @aiGenerated from the standard group: this
9048
+ // bit type forces aiGenerated:true regardless of source value or
9049
+ // absence. Literal `true` in the export pattern ignores `$`.
9050
+ key: ConfigKey.property_aiGenerated,
9051
+ description: "AI-generated flag, forced true for article-ai bits",
9052
+ format: TagFormat.boolean,
9053
+ defaultValue: "true",
9054
+ exportJsonKey: { aiGenerated: true }
9055
+ }
9056
+ ]
7574
9057
  },
7575
9058
  [BitType2.articleAttachment]: {
7576
9059
  since: "1.3.0",
@@ -7895,7 +9378,19 @@ var BITS = {
7895
9378
  [BitType2.noteAi]: {
7896
9379
  since: "1.3.0",
7897
9380
  baseBitType: BitType2.note,
7898
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9381
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9382
+ tags: [
9383
+ {
9384
+ // Shadows the inherited @aiGenerated from the standard group: this
9385
+ // bit type forces aiGenerated:true regardless of source value or
9386
+ // absence. Literal `true` in the export pattern ignores `$`.
9387
+ key: ConfigKey.property_aiGenerated,
9388
+ description: "AI-generated flag, forced true for article-ai bits",
9389
+ format: TagFormat.boolean,
9390
+ defaultValue: "true",
9391
+ exportJsonKey: { aiGenerated: true }
9392
+ }
9393
+ ]
7899
9394
  },
7900
9395
  [BitType2.notebookArticle]: {
7901
9396
  since: "1.3.0",
@@ -8020,7 +9515,19 @@ var BITS = {
8020
9515
  [BitType2.summaryAi]: {
8021
9516
  since: "1.3.0",
8022
9517
  baseBitType: BitType2.summary,
8023
- description: "AI-generated summary bit"
9518
+ description: "AI-generated summary bit",
9519
+ tags: [
9520
+ {
9521
+ // Shadows the inherited @aiGenerated from the standard group: this
9522
+ // bit type forces aiGenerated:true regardless of source value or
9523
+ // absence. Literal `true` in the export pattern ignores `$`.
9524
+ key: ConfigKey.property_aiGenerated,
9525
+ description: "AI-generated flag, forced true for article-ai bits",
9526
+ format: TagFormat.boolean,
9527
+ defaultValue: "true",
9528
+ exportJsonKey: { aiGenerated: true }
9529
+ }
9530
+ ]
8024
9531
  },
8025
9532
  [BitType2.videoTranscript]: {
8026
9533
  since: "1.3.0",
@@ -8294,7 +9801,7 @@ var BITS = {
8294
9801
  description: "Product ID for the module product, used to link to a specific product",
8295
9802
  format: TagFormat.plainText,
8296
9803
  minCount: 1,
8297
- maxCount: Count.infinity
9804
+ maxCount: 1
8298
9805
  }
8299
9806
  ]
8300
9807
  },
@@ -8347,10 +9854,13 @@ var BITS = {
8347
9854
  description: "Resource bit tags for logo grave images, used to define additional properties"
8348
9855
  },
8349
9856
  {
8350
- // Image resource
8351
- key: ConfigKey.group_resourceImage,
9857
+ // Image resource — fully-expanded shape so each `&image` emission
9858
+ // produces a complete `{type, image: {src}}` object inside the
9859
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
9860
+ // `images[].image` via the `[]` last-element-merge semantic.
9861
+ key: ConfigKey.group_resourceImageNoZoom,
8352
9862
  description: "Resource image tags for logo grave images, used to attach images",
8353
- exportJsonKey: { images: "$" },
9863
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8354
9864
  minCount: 1,
8355
9865
  maxCount: Count.infinity
8356
9866
  }
@@ -8640,6 +10150,10 @@ var BITS = {
8640
10150
  tags: [
8641
10151
  {
8642
10152
  key: ConfigKey.property_book,
10153
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10154
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10155
+ // inside the most-recently-appended entry as `reference: $`.
10156
+ exportJsonKey: { book: [{ book: "$" }] },
8643
10157
  description: "Book reference for the page, used to link to a specific book",
8644
10158
  maxCount: Count.infinity,
8645
10159
  chain: [
@@ -8707,7 +10221,9 @@ var BITS = {
8707
10221
  format: TagFormat.plainText
8708
10222
  }
8709
10223
  ],
8710
- cardSet: CardSetConfigKey.exampleBitList
10224
+ // page-footer emits cards under `sections` (vs `listItems` for the
10225
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10226
+ cardSet: CardSetConfigKey.pageFooterSections
8711
10227
  },
8712
10228
  [BitType2.legend]: {
8713
10229
  since: "3.12.0",
@@ -8749,6 +10265,20 @@ var BITS = {
8749
10265
  since: "1.34.0",
8750
10266
  baseBitType: BitType2._standard,
8751
10267
  description: "Definition list bit, used to create lists of definitions in articles or books",
10268
+ tags: [
10269
+ {
10270
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10271
+ key: ConfigKey.property_example,
10272
+ exportJsonKey: [
10273
+ { "@keyonly": { isExample: true } },
10274
+ { "@absent": { isExample: true } },
10275
+ { isExample: true, example: "$" }
10276
+ ],
10277
+ description: "The example(s) for the bit",
10278
+ format: TagFormat.bitmarkText,
10279
+ nullable: true
10280
+ }
10281
+ ],
8752
10282
  cardSet: CardSetConfigKey.definitionList
8753
10283
  },
8754
10284
  [BitType2.metaSearchDefaultTerms]: {
@@ -8771,6 +10301,18 @@ var BITS = {
8771
10301
  {
8772
10302
  key: ConfigKey.group_quizCommon,
8773
10303
  description: "Common quiz tags for flashcards"
10304
+ },
10305
+ {
10306
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10307
+ key: ConfigKey.property_example,
10308
+ exportJsonKey: [
10309
+ { "@keyonly": { isExample: true } },
10310
+ { "@absent": { isExample: true } },
10311
+ { isExample: true, example: "$" }
10312
+ ],
10313
+ description: "The example(s) for the bit",
10314
+ format: TagFormat.bitmarkText,
10315
+ nullable: true
8774
10316
  }
8775
10317
  ],
8776
10318
  cardSet: CardSetConfigKey.flashcard
@@ -8778,7 +10320,11 @@ var BITS = {
8778
10320
  [BitType2.flashcard1]: {
8779
10321
  since: "1.3.0",
8780
10322
  baseBitType: BitType2.flashcard,
8781
- description: "Flashcard 1 bit"
10323
+ description: "Flashcard 1 bit",
10324
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10325
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10326
+ // bit type structurally allows only a single card.
10327
+ cardSet: CardSetConfigKey.flashcard1
8782
10328
  },
8783
10329
  [BitType2.qAndACard]: {
8784
10330
  since: "3.25.0",
@@ -8813,8 +10359,24 @@ var BITS = {
8813
10359
  description: "Common quiz tags for highlighted text"
8814
10360
  },
8815
10361
  {
8816
- key: ConfigKey.group_trueFalse,
8817
- description: "True/False quiz tags for highlighted text"
10362
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10363
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10364
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10365
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10366
+ key: ConfigKey.group_trueFalseInlineHighlight,
10367
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10368
+ },
10369
+ {
10370
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10371
+ key: ConfigKey.property_example,
10372
+ exportJsonKey: [
10373
+ { "@keyonly": { isExample: true } },
10374
+ { "@absent": { isExample: true } },
10375
+ { isExample: true, example: "$" }
10376
+ ],
10377
+ description: "The example(s) for the bit",
10378
+ format: TagFormat.bitmarkText,
10379
+ nullable: true
8818
10380
  }
8819
10381
  ]
8820
10382
  },
@@ -8824,15 +10386,8 @@ var BITS = {
8824
10386
  description: "Image bit, used to create images in articles or books",
8825
10387
  tags: [
8826
10388
  {
8827
- key: ConfigKey.resource_backgroundWallpaper,
8828
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8829
- description: "Background wallpaper for the image, used to set a background for the image",
8830
- chain: [
8831
- {
8832
- key: ConfigKey.group_resourceImageCommon,
8833
- description: "Common resource image tags for images"
8834
- }
8835
- ]
10389
+ key: ConfigKey.group_backgroundWallpaper,
10390
+ description: "Background wallpaper tags for images, used to define background properties for images"
8836
10391
  },
8837
10392
  {
8838
10393
  key: ConfigKey.group_resourceBitTags,
@@ -8935,8 +10490,15 @@ var BITS = {
8935
10490
  },
8936
10491
  [BitType2.imageSeparator]: {
8937
10492
  since: "1.4.15",
8938
- baseBitType: BitType2.image,
8939
- description: "Image separator bit, used to create separators in articles or books"
10493
+ baseBitType: BitType2._standard,
10494
+ description: "Image separator bit, used to create separators in articles or books",
10495
+ tags: [
10496
+ {
10497
+ key: ConfigKey.group_imageNoZoom,
10498
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10499
+ }
10500
+ ],
10501
+ resourceAttachmentAllowed: false
8940
10502
  },
8941
10503
  [BitType2.imageSeparatorAlt]: {
8942
10504
  since: "1.16.0",
@@ -8975,15 +10537,20 @@ var BITS = {
8975
10537
  },
8976
10538
  [BitType2.pageBanner]: {
8977
10539
  since: "1.4.3",
8978
- baseBitType: BitType2.image,
10540
+ baseBitType: BitType2._standard,
8979
10541
  description: "Page banner bit, used to create banners for pages in articles or books",
8980
10542
  tags: [
8981
10543
  {
8982
10544
  key: ConfigKey.property_slug,
8983
10545
  description: "Slug for the page banner, used to identify the banner in the system",
8984
10546
  format: TagFormat.plainText
10547
+ },
10548
+ {
10549
+ key: ConfigKey.group_imageNoZoom,
10550
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
8985
10551
  }
8986
- ]
10552
+ ],
10553
+ resourceAttachmentAllowed: false
8987
10554
  },
8988
10555
  [BitType2.pageHero]: {
8989
10556
  since: "1.11.0",
@@ -8998,7 +10565,7 @@ var BITS = {
8998
10565
  },
8999
10566
  [BitType2.tableImage]: {
9000
10567
  since: "1.5.15",
9001
- baseBitType: BitType2.table,
10568
+ baseBitType: BitType2.tableExtended,
9002
10569
  description: "Table image bit, used to create images in tables in articles or books",
9003
10570
  tags: [
9004
10571
  {
@@ -9007,15 +10574,8 @@ var BITS = {
9007
10574
  format: TagFormat.bitmarkText
9008
10575
  },
9009
10576
  {
9010
- key: ConfigKey.resource_backgroundWallpaper,
9011
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
9012
- description: "Background wallpaper for the image, used to set a background for the image",
9013
- chain: [
9014
- {
9015
- key: ConfigKey.group_resourceImageCommon,
9016
- description: "Common resource image tags for images"
9017
- }
9018
- ]
10577
+ key: ConfigKey.group_backgroundWallpaper,
10578
+ description: "Background wallpaper tags for images, used to define background properties for images"
9019
10579
  },
9020
10580
  {
9021
10581
  key: ConfigKey.group_resourceBitTags,
@@ -9158,10 +10718,10 @@ var BITS = {
9158
10718
  description: "Resource bit tags for logo grave images, used to define additional properties"
9159
10719
  },
9160
10720
  {
9161
- // Image resource
9162
- key: ConfigKey.group_resourceImage,
10721
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10722
+ key: ConfigKey.group_resourceImageNoZoom,
9163
10723
  description: "Resource image tags for logo grave images, used to attach images",
9164
- exportJsonKey: { logos: "$" },
10724
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9165
10725
  minCount: 1,
9166
10726
  maxCount: Count.infinity
9167
10727
  }
@@ -9171,7 +10731,17 @@ var BITS = {
9171
10731
  [BitType2.prototypeImages]: {
9172
10732
  since: "1.6.1",
9173
10733
  baseBitType: BitType2.imagesLogoGrave,
9174
- description: "Prototype images bit, used to create prototype images in articles or books"
10734
+ description: "Prototype images bit, used to create prototype images in articles or books",
10735
+ tags: [
10736
+ {
10737
+ // Override inherited `logos` wrapper with `images` for this bit type.
10738
+ key: ConfigKey.group_resourceImageNoZoom,
10739
+ description: "Resource image tags for prototype images",
10740
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10741
+ minCount: 1,
10742
+ maxCount: Count.infinity
10743
+ }
10744
+ ]
9175
10745
  },
9176
10746
  [BitType2.internalLink]: {
9177
10747
  since: "1.3.0",
@@ -9201,6 +10771,18 @@ var BITS = {
9201
10771
  exportJsonKey: {},
9202
10772
  description: "Reasonable number of characters for the interview, used to limit input size",
9203
10773
  format: TagFormat.number
10774
+ },
10775
+ {
10776
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10777
+ key: ConfigKey.property_example,
10778
+ exportJsonKey: [
10779
+ { "@keyonly": { isExample: true } },
10780
+ { "@absent": { isExample: true } },
10781
+ { isExample: true, example: "$" }
10782
+ ],
10783
+ description: "The example(s) for the bit",
10784
+ format: TagFormat.bitmarkText,
10785
+ nullable: true
9204
10786
  }
9205
10787
  ],
9206
10788
  cardSet: CardSetConfigKey.questions
@@ -9559,6 +11141,18 @@ var BITS = {
9559
11141
  {
9560
11142
  key: ConfigKey.group_mark,
9561
11143
  description: "Mark tags, used to define the content of marks"
11144
+ },
11145
+ {
11146
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11147
+ key: ConfigKey.property_example,
11148
+ exportJsonKey: [
11149
+ { "@keyonly": { isExample: true } },
11150
+ { "@absent": { isExample: true } },
11151
+ { isExample: true, example: "$" }
11152
+ ],
11153
+ description: "The example(s) for the bit",
11154
+ format: TagFormat.bitmarkText,
11155
+ nullable: true
9562
11156
  }
9563
11157
  ]
9564
11158
  },
@@ -9578,6 +11172,23 @@ var BITS = {
9578
11172
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9579
11173
  format: TagFormat.boolean,
9580
11174
  defaultValue: "true"
11175
+ },
11176
+ {
11177
+ // Override inherited @example: in `isTopLevelExample` allow-list
11178
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11179
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11180
+ // Bit-level fires emit only `isExample`; the @example value (when
11181
+ // provided) is recorded in the ancestor stack and consumed by
11182
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11183
+ key: ConfigKey.property_example,
11184
+ exportJsonKey: [
11185
+ { "@keyonly": { isExample: true } },
11186
+ { "@absent": { isExample: true } },
11187
+ { isExample: true }
11188
+ ],
11189
+ description: "The example(s) for the bit",
11190
+ format: TagFormat.bitmarkText,
11191
+ nullable: true
9581
11192
  }
9582
11193
  ],
9583
11194
  cardSet: CardSetConfigKey.matchPairs
@@ -9662,6 +11273,18 @@ var BITS = {
9662
11273
  {
9663
11274
  key: ConfigKey.group_trueFalse,
9664
11275
  description: "True/False tags for multiple choice 1 questions"
11276
+ },
11277
+ {
11278
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11279
+ key: ConfigKey.property_example,
11280
+ exportJsonKey: [
11281
+ { "@keyonly": { isExample: true } },
11282
+ { "@absent": { isExample: true } },
11283
+ { isExample: true, example: "$" }
11284
+ ],
11285
+ description: "The example(s) for the bit",
11286
+ format: TagFormat.bitmarkText,
11287
+ nullable: true
9665
11288
  }
9666
11289
  ]
9667
11290
  },
@@ -9683,8 +11306,20 @@ var BITS = {
9683
11306
  {
9684
11307
  key: ConfigKey.group_trueFalse,
9685
11308
  description: "True/False tags for multiple choice questions"
9686
- }
11309
+ },
9687
11310
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11311
+ {
11312
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11313
+ key: ConfigKey.property_example,
11314
+ exportJsonKey: [
11315
+ { "@keyonly": { isExample: true } },
11316
+ { "@absent": { isExample: true } },
11317
+ { isExample: true, example: "$" }
11318
+ ],
11319
+ description: "The example(s) for the bit",
11320
+ format: TagFormat.bitmarkText,
11321
+ nullable: true
11322
+ }
9688
11323
  ],
9689
11324
  cardSet: CardSetConfigKey.quiz
9690
11325
  },
@@ -9704,8 +11339,20 @@ var BITS = {
9704
11339
  description: "Common quiz tags for multiple choice text questions"
9705
11340
  },
9706
11341
  {
9707
- key: ConfigKey.group_trueFalse,
11342
+ key: ConfigKey.group_trueFalseInlineSelect,
9708
11343
  description: "True/False tags for multiple choice text questions"
11344
+ },
11345
+ {
11346
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11347
+ key: ConfigKey.property_example,
11348
+ exportJsonKey: [
11349
+ { "@keyonly": { isExample: true } },
11350
+ { "@absent": { isExample: true } },
11351
+ { isExample: true, example: "$" }
11352
+ ],
11353
+ description: "The example(s) for the bit",
11354
+ format: TagFormat.bitmarkText,
11355
+ nullable: true
9709
11356
  }
9710
11357
  ]
9711
11358
  },
@@ -9730,8 +11377,20 @@ var BITS = {
9730
11377
  description: "Common quiz tags for multiple response 1 questions"
9731
11378
  },
9732
11379
  {
9733
- key: ConfigKey.group_trueFalse,
11380
+ key: ConfigKey.group_trueFalseResponses,
9734
11381
  description: "True/False tags for multiple response 1 questions"
11382
+ },
11383
+ {
11384
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11385
+ key: ConfigKey.property_example,
11386
+ exportJsonKey: [
11387
+ { "@keyonly": { isExample: true } },
11388
+ { "@absent": { isExample: true } },
11389
+ { isExample: true, example: "$" }
11390
+ ],
11391
+ description: "The example(s) for the bit",
11392
+ format: TagFormat.bitmarkText,
11393
+ nullable: true
9735
11394
  }
9736
11395
  ]
9737
11396
  },
@@ -9751,12 +11410,24 @@ var BITS = {
9751
11410
  description: "Common quiz tags for multiple response questions"
9752
11411
  },
9753
11412
  {
9754
- key: ConfigKey.group_trueFalse,
11413
+ key: ConfigKey.group_trueFalseResponses,
9755
11414
  description: "True/False tags for multiple response questions"
9756
- }
11415
+ },
9757
11416
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11417
+ {
11418
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11419
+ key: ConfigKey.property_example,
11420
+ exportJsonKey: [
11421
+ { "@keyonly": { isExample: true } },
11422
+ { "@absent": { isExample: true } },
11423
+ { isExample: true, example: "$" }
11424
+ ],
11425
+ description: "The example(s) for the bit",
11426
+ format: TagFormat.bitmarkText,
11427
+ nullable: true
11428
+ }
9758
11429
  ],
9759
- cardSet: CardSetConfigKey.quiz
11430
+ cardSet: CardSetConfigKey.quizResponses
9760
11431
  },
9761
11432
  [BitType2.coachSelfReflectionMultipleResponse]: {
9762
11433
  since: "1.3.0",
@@ -10006,6 +11677,8 @@ var BITS = {
10006
11677
  },
10007
11678
  {
10008
11679
  key: ConfigKey.resource_platformIcon,
11680
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11681
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
10009
11682
  description: "The platform icon",
10010
11683
  chain: [
10011
11684
  {
@@ -10041,6 +11714,8 @@ var BITS = {
10041
11714
  tags: [
10042
11715
  {
10043
11716
  key: ConfigKey.resource_platformLogo,
11717
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11718
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10044
11719
  description: "The platform logo",
10045
11720
  chain: [
10046
11721
  {
@@ -10252,7 +11927,8 @@ var BITS = {
10252
11927
  { isExample: true, example: "$" }
10253
11928
  ],
10254
11929
  description: "The example text for the review note bit",
10255
- format: TagFormat.plainText
11930
+ format: TagFormat.plainText,
11931
+ nullable: true
10256
11932
  }
10257
11933
  ],
10258
11934
  rootExampleType: ExampleType.string
@@ -10339,7 +12015,8 @@ var BITS = {
10339
12015
  { isExample: true, example: "$" }
10340
12016
  ],
10341
12017
  description: "The example flag for the bit (boolean)",
10342
- format: TagFormat.plainText
12018
+ format: TagFormat.boolean,
12019
+ nullable: true
10343
12020
  }
10344
12021
  ],
10345
12022
  cardSet: CardSetConfigKey.elements,
@@ -10613,15 +12290,8 @@ var BITS = {
10613
12290
  format: TagFormat.bitmarkText
10614
12291
  },
10615
12292
  {
10616
- key: ConfigKey.resource_backgroundWallpaper,
10617
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10618
- description: "Background wallpaper for the image, used to set a background for the image",
10619
- chain: [
10620
- {
10621
- key: ConfigKey.group_resourceImageCommon,
10622
- description: "Common resource image tags for images"
10623
- }
10624
- ]
12293
+ key: ConfigKey.group_backgroundWallpaper,
12294
+ description: "Background wallpaper tags for images, used to define background properties for images"
10625
12295
  },
10626
12296
  {
10627
12297
  key: ConfigKey.group_resourceBitTags,
@@ -10998,16 +12668,20 @@ var BITS = {
10998
12668
  format: TagFormat.plainText
10999
12669
  },
11000
12670
  {
11001
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12671
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12672
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12673
+ // not a `choices[]` array. Format is bitmarkText so the statement
12674
+ // body renders as a ProseMirror tree, matching reference fixtures.
12675
+ exportJsonKey: { statement: "$", isCorrect: true },
11002
12676
  key: ConfigKey.tag_true,
11003
12677
  description: "Tag for the true option in the true/false question",
11004
- format: TagFormat.plainText
12678
+ format: TagFormat.bitmarkText
11005
12679
  },
11006
12680
  {
11007
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12681
+ exportJsonKey: { statement: "$", isCorrect: false },
11008
12682
  key: ConfigKey.tag_false,
11009
12683
  description: "Tag for the false option in the true/false question",
11010
- format: TagFormat.plainText
12684
+ format: TagFormat.bitmarkText
11011
12685
  },
11012
12686
  {
11013
12687
  // Override inherited @example from group_standardTags: boolean root
@@ -11018,14 +12692,24 @@ var BITS = {
11018
12692
  // sufficient for `[+...]` (correct) statements which dominate the
11019
12693
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
11020
12694
  // need either a runtime extension or an explicit `[@example:false]`.
12695
+ //
12696
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12697
+ // (a natural default that gets stripped in optimised mode, matching
12698
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12699
+ // STRING `"false"` which doesn't strip.
11021
12700
  key: ConfigKey.property_example,
11022
12701
  exportJsonKey: [
11023
12702
  { "@keyonly": { isExample: true, example: true } },
11024
12703
  { "@absent": { isExample: true, example: true } },
12704
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12705
+ // is the natural default for boolean, stripped in optimised mode.
12706
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12707
+ { "@value=false": { isExample: true } },
11025
12708
  { isExample: true, example: "$" }
11026
12709
  ],
11027
12710
  description: "The example flag for the bit (boolean)",
11028
- format: TagFormat.plainText
12711
+ format: TagFormat.boolean,
12712
+ nullable: true
11029
12713
  }
11030
12714
  ],
11031
12715
  rootExampleType: ExampleType.boolean
@@ -11049,6 +12733,18 @@ var BITS = {
11049
12733
  key: ConfigKey.property_labelFalse,
11050
12734
  description: "Label for the false option in the true/false question",
11051
12735
  format: TagFormat.plainText
12736
+ },
12737
+ {
12738
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12739
+ key: ConfigKey.property_example,
12740
+ exportJsonKey: [
12741
+ { "@keyonly": { isExample: true } },
12742
+ { "@absent": { isExample: true } },
12743
+ { isExample: true, example: "$" }
12744
+ ],
12745
+ description: "The example(s) for the bit",
12746
+ format: TagFormat.bitmarkText,
12747
+ nullable: true
11052
12748
  }
11053
12749
  ],
11054
12750
  cardSet: CardSetConfigKey.statements
@@ -11238,6 +12934,11 @@ var BITS = {
11238
12934
  baseBitType: BitType2.vendorStripePricingTable,
11239
12935
  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",
11240
12936
  tags: [
12937
+ {
12938
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
12939
+ description: "Descriptive name for the Stripe pricing table",
12940
+ format: TagFormat.plainText
12941
+ },
11241
12942
  {
11242
12943
  key: ConfigKey.property_printOnRequestCustomerId,
11243
12944
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11714,7 +13415,7 @@ var instance2 = new Config();
11714
13415
  // src/generated/package_info.ts
11715
13416
  var PACKAGE_INFO = {
11716
13417
  "name": "@gmb/bitmark-parser-generator",
11717
- "version": "5.23.0",
13418
+ "version": "5.24.0",
11718
13419
  "author": "Get More Brain Ltd",
11719
13420
  "license": "ISC",
11720
13421
  "description": "A bitmark parser and generator using Peggy.js"
@@ -12679,6 +14380,8 @@ var NodeType = {
12679
14380
  stillImageFilmLink: "stillImageFilmLink",
12680
14381
  stripePricingTableId: "stripePricingTableId",
12681
14382
  stripePricingTableIdValue: "stripePricingTableIdValue",
14383
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14384
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12682
14385
  stripePublishableKey: "stripePublishableKey",
12683
14386
  stripePublishableKeyValue: "stripePublishableKeyValue",
12684
14387
  subject: "subject",
@@ -26104,6 +27807,12 @@ var Builder = class extends BaseBuilder {
26104
27807
  data.stripePricingTableId,
26105
27808
  options
26106
27809
  ),
27810
+ stripePricingTableDescriptiveName: this.toAstProperty(
27811
+ bitType,
27812
+ ConfigKey.property_stripePricingTableDescriptiveName,
27813
+ data.stripePricingTableDescriptiveName,
27814
+ options
27815
+ ),
26107
27816
  stripePublishableKey: this.toAstProperty(
26108
27817
  bitType,
26109
27818
  ConfigKey.property_stripePublishableKey,
@@ -34331,6 +36040,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34331
36040
  const cardSetType = bitConfig.cardSet?.configKey;
34332
36041
  switch (cardSetType) {
34333
36042
  case CardSetConfigKey.flashcard:
36043
+ case CardSetConfigKey.flashcard1:
34334
36044
  case CardSetConfigKey.definitionList:
34335
36045
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34336
36046
  break;
@@ -34344,6 +36054,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34344
36054
  result = parseFeedback(context, bitType, processedCardSet);
34345
36055
  break;
34346
36056
  case CardSetConfigKey.quiz:
36057
+ case CardSetConfigKey.quizResponses:
34347
36058
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34348
36059
  break;
34349
36060
  case CardSetConfigKey.questions:
@@ -34370,6 +36081,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34370
36081
  break;
34371
36082
  case CardSetConfigKey.clozeList:
34372
36083
  case CardSetConfigKey.exampleBitList:
36084
+ case CardSetConfigKey.pageFooterSections:
34373
36085
  case CardSetConfigKey.bookReferenceList:
34374
36086
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34375
36087
  break;