rebly-sections 1.0.1 → 1.2.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.
Files changed (161) hide show
  1. package/assets/data/block-patterns.csv +160 -0
  2. package/assets/data/component-library/GDPR-modal.liquid +183 -0
  3. package/assets/data/component-library/Ishi_parallaxblockstyle1.liquid +331 -0
  4. package/assets/data/component-library/_index.csv +157 -19
  5. package/assets/data/component-library/about.liquid +1557 -0
  6. package/assets/data/component-library/adv-header.liquid +344 -0
  7. package/assets/data/component-library/adv-navigation.liquid +542 -0
  8. package/assets/data/component-library/announcement-bar.liquid +42 -60
  9. package/assets/data/component-library/article.liquid +242 -0
  10. package/assets/data/component-library/axeo-perfume-cosmetics-store-shopify-theme-about.liquid +1557 -0
  11. package/assets/data/component-library/basel-gl_newsletter_pets.liquid +612 -0
  12. package/assets/data/component-library/bixbang-fullpackage-collection-template.liquid +990 -0
  13. package/assets/data/component-library/blog-sidebar-article.liquid +51 -0
  14. package/assets/data/component-library/blog-sidebar-deals.liquid +189 -0
  15. package/assets/data/component-library/blog-sidebar-instagram.liquid +126 -0
  16. package/assets/data/component-library/blog-sidebar-tags.liquid +30 -0
  17. package/assets/data/component-library/blog.liquid +371 -0
  18. package/assets/data/component-library/brands-page.liquid +114 -0
  19. package/assets/data/component-library/cake-shop-shopify-theme-for-bakery-and-cafe-home-support-blo.liquid +780 -0
  20. package/assets/data/component-library/collection-template-promotion.liquid +1139 -0
  21. package/assets/data/component-library/collection-template.liquid +146 -0
  22. package/assets/data/component-library/contact-us.liquid +663 -0
  23. package/assets/data/component-library/contact.liquid +256 -0
  24. package/assets/data/component-library/copyright_payment.liquid +95 -0
  25. package/assets/data/component-library/custom-content.liquid +832 -0
  26. package/assets/data/component-library/faq-template-3.liquid +1014 -0
  27. package/assets/data/component-library/footer-model-1.liquid +503 -0
  28. package/assets/data/component-library/footer-model-10.liquid +210 -0
  29. package/assets/data/component-library/footer-model-2.liquid +460 -0
  30. package/assets/data/component-library/footer-model-3.liquid +548 -0
  31. package/assets/data/component-library/footer-model-4.liquid +455 -0
  32. package/assets/data/component-library/footer-model-5.liquid +407 -0
  33. package/assets/data/component-library/footer-model-6.liquid +543 -0
  34. package/assets/data/component-library/footer-model-7.liquid +345 -0
  35. package/assets/data/component-library/footer-model-8.liquid +279 -0
  36. package/assets/data/component-library/footer-model-9.liquid +376 -0
  37. package/assets/data/component-library/gallery.liquid +236 -0
  38. package/assets/data/component-library/gecko-shopify-v5-7-6-nulled-manual_blog.liquid +720 -0
  39. package/assets/data/component-library/gl_newsletter_pets.liquid +612 -0
  40. package/assets/data/component-library/gp-logo-list.liquid +362 -0
  41. package/assets/data/component-library/grid-banner-type-3-b.liquid +655 -0
  42. package/assets/data/component-library/header-model-1.liquid +427 -0
  43. package/assets/data/component-library/header-model-10.liquid +599 -0
  44. package/assets/data/component-library/header-model-2.liquid +633 -0
  45. package/assets/data/component-library/header-model-3.liquid +415 -0
  46. package/assets/data/component-library/header-model-4.liquid +754 -0
  47. package/assets/data/component-library/header-model-5.liquid +562 -0
  48. package/assets/data/component-library/header-model-6.liquid +713 -0
  49. package/assets/data/component-library/header-model-7.liquid +743 -0
  50. package/assets/data/component-library/header-model-8.liquid +500 -0
  51. package/assets/data/component-library/header-model-9.liquid +506 -0
  52. package/assets/data/component-library/home-blog-posts-1.liquid +399 -0
  53. package/assets/data/component-library/home-blog-posts-2.liquid +393 -0
  54. package/assets/data/component-library/home-blog-posts-3.liquid +545 -0
  55. package/assets/data/component-library/home-brand-slider.liquid +224 -0
  56. package/assets/data/component-library/home-circled-block.liquid +332 -0
  57. package/assets/data/component-library/home-contact-block-1.liquid +395 -0
  58. package/assets/data/component-library/home-contact-block-2.liquid +372 -0
  59. package/assets/data/component-library/home-content-block-1.liquid +320 -0
  60. package/assets/data/component-library/home-donut-chart.liquid +335 -0
  61. package/assets/data/component-library/home-fade-in-banner.liquid +277 -0
  62. package/assets/data/component-library/home-faq-model.liquid +323 -0
  63. package/assets/data/component-library/home-featured-blog.liquid +1462 -0
  64. package/assets/data/component-library/home-featured-collections.liquid +484 -0
  65. package/assets/data/component-library/home-gallery-block1.liquid +276 -0
  66. package/assets/data/component-library/home-gallery-block2.liquid +396 -0
  67. package/assets/data/component-library/home-grid-banner-type-1.liquid +371 -0
  68. package/assets/data/component-library/home-grid-banner-type-2.liquid +362 -0
  69. package/assets/data/component-library/home-grid-banner-type-3.liquid +374 -0
  70. package/assets/data/component-library/home-grid-banner-type-4.liquid +900 -0
  71. package/assets/data/component-library/home-grid-banner-type-5.liquid +368 -0
  72. package/assets/data/component-library/home-grid-banner-type-6.liquid +382 -0
  73. package/assets/data/component-library/home-grid-banner-type-7.liquid +371 -0
  74. package/assets/data/component-library/home-hotspot-with-product-carousel.liquid +1425 -0
  75. package/assets/data/component-library/home-image-gallery.liquid +1087 -0
  76. package/assets/data/component-library/home-instagram.liquid +356 -0
  77. package/assets/data/component-library/home-newsletter.liquid +246 -0
  78. package/assets/data/component-library/home-number-counter.liquid +790 -0
  79. package/assets/data/component-library/home-price-table.liquid +416 -0
  80. package/assets/data/component-library/home-pricing-table.liquid +1076 -0
  81. package/assets/data/component-library/home-product-grid.liquid +413 -0
  82. package/assets/data/component-library/home-product-tab-1.liquid +528 -0
  83. package/assets/data/component-library/home-product-tab-2.liquid +342 -0
  84. package/assets/data/component-library/home-product-tab-3.liquid +357 -0
  85. package/assets/data/component-library/home-product-vertical-carousel.liquid +477 -0
  86. package/assets/data/component-library/home-quotes-1.liquid +274 -0
  87. package/assets/data/component-library/home-quotes-2.liquid +239 -0
  88. package/assets/data/component-library/home-quotes-3.liquid +244 -0
  89. package/assets/data/component-library/home-quotes-4.liquid +258 -0
  90. package/assets/data/component-library/home-slider-width-promo-images.liquid +1377 -0
  91. package/assets/data/component-library/home-slideshow-type-1.liquid +656 -0
  92. package/assets/data/component-library/home-slideshow-type-2.liquid +570 -0
  93. package/assets/data/component-library/home-specification-block-1.liquid +468 -0
  94. package/assets/data/component-library/home-specification-block-2.liquid +291 -0
  95. package/assets/data/component-library/home-specification-block-3.liquid +429 -0
  96. package/assets/data/component-library/home-support-block.liquid +392 -0
  97. package/assets/data/component-library/home-testimonial.liquid +1348 -0
  98. package/assets/data/component-library/home-video-banner.liquid +317 -0
  99. package/assets/data/component-library/home-wide-banner.liquid +327 -0
  100. package/assets/data/component-library/icon-with-content.liquid +478 -0
  101. package/assets/data/component-library/instafeed.liquid +1 -0
  102. package/assets/data/component-library/kea-ecommerce-interior-furniture-shopify-theme-about.liquid +1300 -0
  103. package/assets/data/component-library/kidslife-responsive-shopify-theme-home-number-counter.liquid +729 -0
  104. package/assets/data/component-library/logo-bar.liquid +314 -0
  105. package/assets/data/component-library/lookbook.liquid +367 -0
  106. package/assets/data/component-library/manual_blog.liquid +724 -0
  107. package/assets/data/component-library/navigation-etc.liquid +642 -0
  108. package/assets/data/component-library/newsletter.liquid +246 -0
  109. package/assets/data/component-library/order-form.liquid +96 -0
  110. package/assets/data/component-library/page-catev1-template.liquid +344 -0
  111. package/assets/data/component-library/popup_video.liquid +396 -0
  112. package/assets/data/component-library/product-sidebar-bestsellers.liquid +99 -0
  113. package/assets/data/component-library/product-sidebar-deals.liquid +158 -0
  114. package/assets/data/component-library/product-template-2.liquid +629 -0
  115. package/assets/data/component-library/product-template-3.liquid +670 -0
  116. package/assets/data/component-library/product-template-4.liquid +627 -0
  117. package/assets/data/component-library/product-template-5.liquid +652 -0
  118. package/assets/data/component-library/product-template.liquid +698 -0
  119. package/assets/data/component-library/rich-text.liquid +541 -0
  120. package/assets/data/component-library/section-countdown-v2.liquid +215 -0
  121. package/assets/data/component-library/services.liquid +596 -0
  122. package/assets/data/component-library/shipping_info.liquid +327 -0
  123. package/assets/data/component-library/sidebar-bestsellers.liquid +109 -0
  124. package/assets/data/component-library/sidebar-category.liquid +105 -0
  125. package/assets/data/component-library/sidebar-colors.liquid +104 -0
  126. package/assets/data/component-library/single_product_feature.liquid +1892 -0
  127. package/assets/data/component-library/social-links-menu.liquid +244 -0
  128. package/assets/data/component-library/someone-purchased.liquid +190 -0
  129. package/assets/data/component-library/special-offer-area.liquid +530 -0
  130. package/assets/data/component-library/theno-minimal-clean-watch-store-shopify-theme-page-catev1-te.liquid +344 -0
  131. package/assets/data/component-library/top-bar-type-1.liquid +200 -0
  132. package/assets/data/component-library/top-bar-type-10.liquid +395 -0
  133. package/assets/data/component-library/top-bar-type-11.liquid +395 -0
  134. package/assets/data/component-library/top-bar-type-2.liquid +106 -0
  135. package/assets/data/component-library/top-bar-type-3.liquid +205 -0
  136. package/assets/data/component-library/top-countdown-bar.liquid +116 -0
  137. package/assets/data/component-library/trixe-solar-responsive-shopify-template-home-image-gallery.liquid +783 -0
  138. package/assets/data/component-library/trixe-solar-responsive-shopify-template-home-pricing-table.liquid +1043 -0
  139. package/assets/data/component-library/trixe-solar-responsive-shopify-template-home-testimonial.liquid +1338 -0
  140. package/assets/data/component-library/video.liquid +511 -0
  141. package/assets/data/component-library/waffy-spices-dry-fruits-store-shopify-theme-v-1-1-contact-us.liquid +523 -0
  142. package/assets/data/design-tokens.csv +93 -57
  143. package/assets/data/schema-library.csv +48 -46
  144. package/assets/data/settings-profiles.csv +235 -0
  145. package/assets/data/shopify-best-practices.csv +58 -36
  146. package/assets/scripts/backfill-component-index.py +102 -0
  147. package/assets/scripts/core.py +30 -8
  148. package/assets/scripts/fix-schema-library.py +42 -0
  149. package/assets/scripts/kb-analyzer-helpers.py +136 -0
  150. package/assets/scripts/kb-analyzer.py +186 -0
  151. package/assets/scripts/kb-builder.py +32 -63
  152. package/assets/scripts/kb-constants.py +62 -0
  153. package/assets/scripts/kb-extractor-helpers.py +178 -0
  154. package/assets/scripts/kb-extractor.py +106 -170
  155. package/assets/scripts/kb-synthesizer.py +251 -0
  156. package/assets/scripts/quality-gate-checks.py +55 -0
  157. package/assets/scripts/quality-gate.py +56 -2
  158. package/assets/scripts/section-generator-helpers.py +74 -0
  159. package/assets/scripts/section-generator.py +59 -49
  160. package/assets/templates/generation-prompt.md +78 -14
  161. package/package.json +1 -1
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ KB Synthesizer — generate block-patterns.csv, settings-profiles.csv from aggregate-stats.json,
4
+ and curate top components from analysis.jsonl into component-library.
5
+
6
+ Usage:
7
+ python3 kb-synthesizer.py --stats themes/aggregate-stats.json \
8
+ --analysis themes/analysis.jsonl --extracted themes/extracted/ \
9
+ --kb src/rebly-sections/data/
10
+ """
11
+
12
+ import argparse
13
+ import csv
14
+ import json
15
+ import shutil
16
+ import sys
17
+ from pathlib import Path
18
+
19
+
20
+ MIN_CATEGORY_SECTIONS = 5 # skip categories with fewer than this
21
+
22
+
23
+ def generate_block_patterns(stats: dict, analysis_path: Path, out_path: Path):
24
+ """Generate block-patterns.csv from aggregate stats + analysis examples."""
25
+ rows = []
26
+ # Build lookup: find first example of each block combo per category
27
+ examples = {} # (category, combo) -> first JSON example from JSONL
28
+ with open(analysis_path, 'r', encoding='utf-8') as f:
29
+ for line in f:
30
+ rec = json.loads(line.strip())
31
+ cat = rec.get('category', '')
32
+ combo = '|'.join(sorted(rec.get('block_types', [])))
33
+ if combo and (cat, combo) not in examples:
34
+ examples[(cat, combo)] = rec
35
+
36
+ for cat, data in sorted(stats['categories'].items()):
37
+ if data['count'] < MIN_CATEGORY_SECTIONS:
38
+ continue
39
+ for rank, bc in enumerate(data.get('top_block_combos', [])[:8], 1):
40
+ combo = bc['combo']
41
+ rec = examples.get((cat, combo), {})
42
+ # Build a minimal JSON example from the record
43
+ block_types = combo.split('|')
44
+ json_ex = json.dumps([{"type": bt, "name": bt.replace('-', ' ').title()}
45
+ for bt in block_types], ensure_ascii=False)
46
+ common_settings = '|'.join(rec.get('setting_ids', [])[:6])
47
+ use_case = f"{cat} section with {' + '.join(block_types)} blocks"
48
+ rows.append({
49
+ 'Category': cat,
50
+ 'Block Type': combo,
51
+ 'Common Settings': common_settings,
52
+ 'Use Case': use_case,
53
+ 'JSON Example': json_ex[:300],
54
+ 'Frequency Rank': rank,
55
+ })
56
+
57
+ fieldnames = ['Category', 'Block Type', 'Common Settings', 'Use Case', 'JSON Example', 'Frequency Rank']
58
+ with open(out_path, 'w', newline='', encoding='utf-8') as f:
59
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
60
+ writer.writeheader()
61
+ writer.writerows(rows)
62
+ print(f"block-patterns.csv: {len(rows)} rows")
63
+
64
+
65
+ def generate_settings_profiles(stats: dict, out_path: Path):
66
+ """Generate settings-profiles.csv from aggregate stats top setting IDs."""
67
+ rows = []
68
+ for cat, data in sorted(stats['categories'].items()):
69
+ if data['count'] < MIN_CATEGORY_SECTIONS:
70
+ continue
71
+ # Build type lookup from top_setting_types
72
+ type_lookup = {t['type']: t for t in data.get('top_setting_types', [])}
73
+
74
+ for rank, sid_entry in enumerate(data.get('top_setting_ids', [])[:10], 1):
75
+ sid = sid_entry['id']
76
+ # Infer setting type from ID name patterns
77
+ stype = _infer_setting_type(sid, type_lookup)
78
+ label = sid.replace('_', ' ').replace('-', ' ').title()
79
+ required = rank <= 3 # top 3 are "required"
80
+ rows.append({
81
+ 'Section Type': cat,
82
+ 'Setting ID': sid,
83
+ 'Setting Type': stype,
84
+ 'Label': label,
85
+ 'Default': '',
86
+ 'Required': str(required),
87
+ 'Notes': f"Appears in {sid_entry['count']}/{data['count']} {cat} sections",
88
+ 'Frequency Rank': rank,
89
+ })
90
+
91
+ fieldnames = ['Section Type', 'Setting ID', 'Setting Type', 'Label',
92
+ 'Default', 'Required', 'Notes', 'Frequency Rank']
93
+ with open(out_path, 'w', newline='', encoding='utf-8') as f:
94
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
95
+ writer.writeheader()
96
+ writer.writerows(rows)
97
+ print(f"settings-profiles.csv: {len(rows)} rows")
98
+
99
+
100
+ def _infer_setting_type(setting_id: str, type_lookup: dict) -> str:
101
+ """Infer Shopify setting type from ID name."""
102
+ sid = setting_id.lower()
103
+ if 'image' in sid or 'photo' in sid or 'logo' in sid:
104
+ return 'image_picker'
105
+ if 'color' in sid and 'scheme' in sid:
106
+ return 'color_scheme'
107
+ if 'color' in sid:
108
+ return 'color'
109
+ if 'url' in sid or 'link' in sid:
110
+ return 'url'
111
+ if 'video' in sid:
112
+ return 'video_url'
113
+ if 'collection' in sid:
114
+ return 'collection'
115
+ if 'product' in sid:
116
+ return 'product'
117
+ if 'blog' in sid:
118
+ return 'blog'
119
+ if 'padding' in sid or 'margin' in sid or 'spacing' in sid or 'width' in sid or 'height' in sid:
120
+ return 'range'
121
+ if 'columns' in sid or 'count' in sid or 'limit' in sid or 'per_row' in sid:
122
+ return 'range'
123
+ if 'show' in sid or 'enable' in sid or 'hide' in sid:
124
+ return 'checkbox'
125
+ if 'layout' in sid or 'style' in sid or 'alignment' in sid or 'position' in sid:
126
+ return 'select'
127
+ if 'content' in sid or 'description' in sid or ('text' in sid and len(sid) > 6):
128
+ return 'richtext'
129
+ if 'heading' in sid or 'title' in sid or 'label' in sid or 'button' in sid:
130
+ return 'text'
131
+ if 'font' in sid:
132
+ return 'font_picker'
133
+ # Check if type_lookup has a dominant type
134
+ if type_lookup:
135
+ top = max(type_lookup.values(), key=lambda t: t.get('count', 0))
136
+ return top.get('type', 'text')
137
+ return 'text'
138
+
139
+
140
+ def select_best_components(analysis_path: Path, extracted_dir: Path,
141
+ component_dir: Path, index_path: Path):
142
+ """Pick top-scoring sections per category, copy to component library."""
143
+ # Score each section
144
+ scores = {} # (category, theme_slug, filename) -> score
145
+ records = {}
146
+ with open(analysis_path, 'r', encoding='utf-8') as f:
147
+ for line in f:
148
+ rec = json.loads(line.strip())
149
+ cat = rec.get('category', 'other')
150
+ key = (cat, rec['theme_slug'], rec['filename'])
151
+ score = rec.get('settings_count', 0) * 2
152
+ if rec.get('has_presets'):
153
+ score += 10
154
+ if rec.get('has_app_block'):
155
+ score += 5
156
+ if rec.get('blocks_count', 0) > 0:
157
+ score += 5
158
+ scores[key] = score
159
+ records[key] = rec
160
+
161
+ # Group by category, pick top 2 per category
162
+ from collections import defaultdict
163
+ by_cat = defaultdict(list)
164
+ for key, score in scores.items():
165
+ by_cat[key[0]].append((score, key))
166
+
167
+ # Load existing index
168
+ existing_slugs = set()
169
+ if index_path.exists():
170
+ with open(index_path, newline='', encoding='utf-8') as f:
171
+ for row in csv.DictReader(f):
172
+ existing_slugs.add(row.get('Slug', ''))
173
+
174
+ added = 0
175
+ new_rows = []
176
+ max_no = len(existing_slugs) + 1
177
+
178
+ for cat, entries in sorted(by_cat.items()):
179
+ if cat in ('other', 'header', 'footer', 'sidebar'):
180
+ continue # skip non-section categories
181
+ entries.sort(reverse=True)
182
+ for score, (_, theme_slug, filename) in entries[:2]:
183
+ slug = filename.replace('.liquid', '')
184
+ # Avoid slug collisions with existing files
185
+ if slug in existing_slugs:
186
+ slug = f"{theme_slug}-{slug}"[:60]
187
+ if slug in existing_slugs:
188
+ continue
189
+
190
+ src = extracted_dir / theme_slug / 'sections' / filename
191
+ if not src.exists():
192
+ continue
193
+ dst = component_dir / f"{slug}.liquid"
194
+ shutil.copy2(src, dst)
195
+ existing_slugs.add(slug)
196
+
197
+ rec = records[(cat, theme_slug, filename)]
198
+ new_rows.append({
199
+ 'No': max_no,
200
+ 'Name': rec.get('schema_name', slug),
201
+ 'Slug': slug,
202
+ 'Category': cat,
203
+ 'Keywords': f"{slug.replace('-', ' ')} {cat}",
204
+ 'Difficulty': 'intermediate',
205
+ 'Blocks Used': '|'.join(rec.get('block_types', []))[:80],
206
+ 'Settings Count': rec.get('settings_count', 0),
207
+ 'ui_style_keywords': f"{slug.replace('-', ' ')} {cat}",
208
+ 'File': f"{slug}.liquid",
209
+ 'Description': f"{rec.get('schema_name', slug)} section from {theme_slug}",
210
+ 'Completeness': score,
211
+ 'Has Presets': str(rec.get('has_presets', False)),
212
+ 'Has App Block': str(rec.get('has_app_block', False)),
213
+ })
214
+ max_no += 1
215
+ added += 1
216
+
217
+ # Append to index
218
+ if new_rows:
219
+ fieldnames = list(new_rows[0].keys())
220
+ with open(index_path, 'a', newline='', encoding='utf-8') as f:
221
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
222
+ for row in new_rows:
223
+ writer.writerow(row)
224
+
225
+ print(f"Components: {added} new curated files added to library")
226
+
227
+
228
+ def main():
229
+ parser = argparse.ArgumentParser(description="Synthesize KB from analysis data")
230
+ parser.add_argument('--stats', default='themes/aggregate-stats.json')
231
+ parser.add_argument('--analysis', default='themes/analysis.jsonl')
232
+ parser.add_argument('--extracted', default='themes/extracted/')
233
+ parser.add_argument('--kb', default='src/rebly-sections/data/')
234
+ parser.add_argument('--mode', default='all', choices=['all', 'blocks', 'settings', 'components'])
235
+ args = parser.parse_args()
236
+
237
+ stats = json.loads(Path(args.stats).read_text(encoding='utf-8'))
238
+ kb_dir = Path(args.kb)
239
+
240
+ if args.mode in ('all', 'blocks'):
241
+ generate_block_patterns(stats, Path(args.analysis), kb_dir / 'block-patterns.csv')
242
+ if args.mode in ('all', 'settings'):
243
+ generate_settings_profiles(stats, kb_dir / 'settings-profiles.csv')
244
+ if args.mode in ('all', 'components'):
245
+ select_best_components(
246
+ Path(args.analysis), Path(args.extracted),
247
+ kb_dir / 'component-library', kb_dir / 'component-library' / '_index.csv')
248
+
249
+
250
+ if __name__ == '__main__':
251
+ main()
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ """Additional quality gate checks — WARN-level advisory checks for section quality."""
3
+
4
+ import re
5
+
6
+
7
+ def check_has_color_scheme(content, schema_json):
8
+ """Check if section schema includes color_scheme setting."""
9
+ if not schema_json:
10
+ return ("WARN", "No schema to check for color_scheme")
11
+ settings = schema_json.get("settings", [])
12
+ has_cs = any(s.get("type") == "color_scheme" for s in settings)
13
+ if has_cs:
14
+ return ("PASS", "color_scheme setting present")
15
+ return ("WARN", "Missing color_scheme setting — recommended for theme consistency")
16
+
17
+
18
+ def check_has_font_picker(content, schema_json):
19
+ """Check if section schema includes at least one font_picker."""
20
+ if not schema_json:
21
+ return ("WARN", "No schema to check for font_picker")
22
+ settings = schema_json.get("settings", [])
23
+ has_fp = any(s.get("type") == "font_picker" for s in settings)
24
+ if has_fp:
25
+ return ("PASS", "font_picker setting present")
26
+ return ("WARN", "Missing font_picker — sections with headings should include typography control")
27
+
28
+
29
+ def check_has_heading_tag(content, schema_json):
30
+ """Check if section has configurable heading tag for SEO."""
31
+ if not schema_json:
32
+ return ("WARN", "No schema to check for heading_tag")
33
+ settings = schema_json.get("settings", [])
34
+ has_ht = any(
35
+ s.get("id") in ("heading_tag", "heading_element", "tag")
36
+ and s.get("type") == "select"
37
+ for s in settings
38
+ )
39
+ if has_ht:
40
+ return ("PASS", "Heading tag configurable")
41
+ return ("WARN", "Missing heading_tag select — heading level should be merchant-configurable")
42
+
43
+
44
+ def check_no_hardcoded_colors(content, schema_json):
45
+ """Check CSS doesn't contain hardcoded hex colors (outside Liquid variables)."""
46
+ style_match = re.search(r'<style[^>]*>(.*?)</style>', content, re.DOTALL)
47
+ if not style_match:
48
+ return ("PASS", "No style block found")
49
+ css = style_match.group(1)
50
+ # Strip Liquid variable references ({{ ... }}) before scanning
51
+ css_no_liquid = re.sub(r'\{\{.*?\}\}', '', css)
52
+ hex_colors = re.findall(r'#[0-9a-fA-F]{3,8}\b', css_no_liquid)
53
+ if hex_colors:
54
+ return ("WARN", f"Hardcoded colors in CSS: {', '.join(hex_colors[:5])} — use CSS variables instead")
55
+ return ("PASS", "No hardcoded colors in CSS")
@@ -6,12 +6,23 @@ Usage: python3 quality-gate.py path/to/section.liquid [--json]
6
6
  """
7
7
 
8
8
  import argparse
9
+ import importlib.util
9
10
  import json
10
11
  import re
11
12
  import sys
12
13
  from pathlib import Path
13
14
 
14
-
15
+ # Import advisory checks from helper module (graceful fallback if missing)
16
+ _checks_mod = None
17
+ try:
18
+ _checks_path = Path(__file__).parent / 'quality-gate-checks.py'
19
+ _spec = importlib.util.spec_from_file_location('qg_checks', _checks_path)
20
+ _checks_mod = importlib.util.module_from_spec(_spec)
21
+ _spec.loader.exec_module(_checks_mod)
22
+ except (FileNotFoundError, AttributeError):
23
+ pass
24
+
25
+ # Core checks (PASS/FAIL)
15
26
  QUALITY_CHECKS = [
16
27
  ("schema_valid_json", "Schema block contains valid JSON"),
17
28
  ("has_presets", "Schema has non-empty presets array"),
@@ -26,6 +37,14 @@ QUALITY_CHECKS = [
26
37
  ("text_elements_explicit_color", "Text elements have explicit color in CSS"),
27
38
  ]
28
39
 
40
+ # Advisory checks (PASS/WARN) — imported from quality-gate-checks.py
41
+ ADVISORY_CHECKS = [
42
+ ("has_color_scheme", "color_scheme setting present"),
43
+ ("has_font_picker", "font_picker setting present"),
44
+ ("has_heading_tag", "Heading tag configurable via select"),
45
+ ("no_hardcoded_colors", "No hardcoded hex colors in CSS"),
46
+ ]
47
+
29
48
 
30
49
  def extract_schema_json(content):
31
50
  """Extract JSON from {% schema %} block"""
@@ -161,10 +180,13 @@ def run_quality_gate(filepath):
161
180
  return f"Error: File not found: {filepath}"
162
181
 
163
182
  content = path.read_text(encoding="utf-8")
183
+ schema_json = extract_schema_json(content)
164
184
  results = []
165
185
  passed = 0
166
186
  failed = 0
187
+ warned = 0
167
188
 
189
+ # Core checks (PASS/FAIL)
168
190
  for check_id, description in QUALITY_CHECKS:
169
191
  check_fn = CHECK_FUNCTIONS[check_id]
170
192
  try:
@@ -179,12 +201,44 @@ def run_quality_gate(filepath):
179
201
  failed += 1
180
202
  results.append(f"✗ {description}: ERROR ({e})")
181
203
 
204
+ # Advisory checks (PASS/WARN) — skip if helper module unavailable
205
+ if not _checks_mod:
206
+ advisory_fns = {}
207
+ else:
208
+ advisory_fns = {
209
+ "has_color_scheme": _checks_mod.check_has_color_scheme,
210
+ "has_font_picker": _checks_mod.check_has_font_picker,
211
+ "has_heading_tag": _checks_mod.check_has_heading_tag,
212
+ "no_hardcoded_colors": _checks_mod.check_no_hardcoded_colors,
213
+ }
214
+ for check_id, description in ADVISORY_CHECKS:
215
+ if check_id not in advisory_fns:
216
+ continue
217
+ check_fn = advisory_fns[check_id]
218
+ try:
219
+ status, message = check_fn(content, schema_json)
220
+ if status == "PASS":
221
+ passed += 1
222
+ else:
223
+ warned += 1
224
+ icon = "✓" if status == "PASS" else "⚠"
225
+ results.append(f"{icon} {message}: {status}")
226
+ except Exception as e:
227
+ warned += 1
228
+ results.append(f"⚠ {description}: ERROR ({e})")
229
+
230
+ total = passed + failed + warned
182
231
  summary = f"\n## Quality Gate: {path.name}\n"
183
- summary += f"**Score:** {passed}/{passed + failed} checks passed\n\n"
232
+ summary += f"**Score:** {passed}/{total} checks passed"
233
+ if warned:
234
+ summary += f" ({warned} warnings)"
235
+ summary += "\n\n"
184
236
  summary += "\n".join(results)
185
237
 
186
238
  if failed == 0:
187
239
  summary += "\n\n**Result: PASS** ✓"
240
+ if warned:
241
+ summary += f" (with {warned} advisory warnings)"
188
242
  else:
189
243
  summary += f"\n\n**Result: FAIL** — {failed} issue(s) found"
190
244
 
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """Helper search functions for section-generator.py — block patterns, settings profiles, theme DNA."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _scripts_dir = str(Path(__file__).parent)
8
+ if _scripts_dir not in sys.path:
9
+ sys.path.append(_scripts_dir)
10
+ from core import search
11
+
12
+
13
+ def search_block_patterns(description):
14
+ """Search block-patterns.csv for relevant block configurations."""
15
+ try:
16
+ result = search(description, domain="block-patterns", max_results=5)
17
+ if "error" in result or not result.get("results"):
18
+ return ""
19
+ lines = ["**Relevant Block Patterns:**"]
20
+ for r in result["results"]:
21
+ lines.append(f"- [{r.get('Category', '')} / {r.get('Block Type', '')}] {r.get('Use Case', '')}")
22
+ lines.append(f" Settings: {r.get('Common Settings', '')}")
23
+ ex = r.get('JSON Example', '')
24
+ if ex:
25
+ lines.append(f" Example: `{ex[:200]}`")
26
+ return "\n".join(lines)
27
+ except (FileNotFoundError, KeyError):
28
+ return ""
29
+
30
+
31
+ def search_settings_profile(section_name):
32
+ """Search settings-profiles.csv for recommended settings for this section type."""
33
+ try:
34
+ result = search(section_name, domain="settings-profiles", max_results=8)
35
+ if "error" in result or not result.get("results"):
36
+ return ""
37
+ lines = ["**Recommended Settings Profile:**",
38
+ "| Setting ID | Type | Label | Default | Required |",
39
+ "|------------|------|-------|---------|----------|"]
40
+ for r in result["results"]:
41
+ lines.append(f"| {r.get('Setting ID', '')} | {r.get('Setting Type', '')} | "
42
+ f"{r.get('Label', '')} | {r.get('Default', '')} | {r.get('Required', '')} |")
43
+ return "\n".join(lines)
44
+ except (FileNotFoundError, KeyError):
45
+ return ""
46
+
47
+
48
+ def search_theme_dna(theme_name):
49
+ """Search theme-dna.csv for theme-specific conventions."""
50
+ try:
51
+ result = search(theme_name, domain="themes", max_results=1)
52
+ if "error" in result or not result.get("results"):
53
+ return ""
54
+ r = result["results"][0]
55
+ return (f"**Theme DNA:** {r.get('Theme', '')} — "
56
+ f"CSS Prefix: {r.get('CSS Variable Prefix', '')} | "
57
+ f"Naming: {r.get('Section Naming', '')} | "
58
+ f"Blocks: {r.get('Block Conventions', '')}")
59
+ except (FileNotFoundError, KeyError):
60
+ return ""
61
+
62
+
63
+ def search_design_tokens(description):
64
+ """Search design-tokens.csv for relevant CSS tokens."""
65
+ try:
66
+ result = search(description, domain="tokens", max_results=5, min_score=0.3)
67
+ if "error" in result or not result.get("results"):
68
+ return ""
69
+ lines = ["**Relevant Design Tokens:**"]
70
+ for r in result["results"]:
71
+ lines.append(f"- `{r.get('CSS Variable', '')}`: {r.get('Value', '')} — {r.get('Usage', '')}")
72
+ return "\n".join(lines)
73
+ except (FileNotFoundError, KeyError):
74
+ return ""
@@ -6,6 +6,7 @@ Usage: python3 section-generator.py "name" "description" [--theme-profile path]
6
6
  """
7
7
 
8
8
  import argparse
9
+ import re
9
10
  import sys
10
11
  import io
11
12
  from pathlib import Path
@@ -16,6 +17,17 @@ if _scripts_dir not in sys.path:
16
17
  sys.path.append(_scripts_dir)
17
18
  from core import search, search_component, DATA_DIR
18
19
 
20
+ # Import helpers (kebab-case filename requires importlib)
21
+ import importlib.util as _ilu
22
+ _hp = Path(__file__).parent / 'section-generator-helpers.py'
23
+ _hs = _ilu.spec_from_file_location('sg_helpers', _hp)
24
+ _hm = _ilu.module_from_spec(_hs)
25
+ _hs.loader.exec_module(_hm)
26
+ search_block_patterns = _hm.search_block_patterns
27
+ search_settings_profile = _hm.search_settings_profile
28
+ search_theme_dna = _hm.search_theme_dna
29
+ search_design_tokens = _hm.search_design_tokens
30
+
19
31
  TEMPLATE_DIR = Path(__file__).parent.parent / "templates"
20
32
 
21
33
  # UTF-8 stdout
@@ -38,8 +50,8 @@ def load_template(name):
38
50
  return ""
39
51
 
40
52
 
41
- def search_components(description, max_results=2):
42
- """Search component library for closest pattern"""
53
+ def search_components(description, max_results=1):
54
+ """Search component library for closest pattern — 1 full untruncated example"""
43
55
  result = search_component(description, max_results=max_results)
44
56
  if "error" in result or not result.get("results"):
45
57
  return "No matching component patterns found."
@@ -49,14 +61,14 @@ def search_components(description, max_results=2):
49
61
  output.append(f"**{r.get('Name', 'Unknown')}** ({r.get('Category', '')}, {r.get('Difficulty', '')})")
50
62
  output.append(f"Blocks: {r.get('Blocks Used', '')} | Settings: {r.get('Settings Count', '')}")
51
63
  if 'liquid_content' in r:
52
- output.append(f"```liquid\n{r['liquid_content'][:2000]}\n```")
64
+ output.append(f"```liquid\n{r['liquid_content'][:6000]}\n```")
53
65
  output.append("")
54
66
  return "\n".join(output)
55
67
 
56
68
 
57
69
  def search_schema_settings(description, max_results=5):
58
70
  """Search schema-library for relevant setting types"""
59
- result = search(description, domain="schema", max_results=max_results)
71
+ result = search(description, domain="schema", max_results=max_results, min_score=0.3)
60
72
  if "error" in result or not result.get("results"):
61
73
  return "No matching schema settings found."
62
74
 
@@ -71,7 +83,7 @@ def search_schema_settings(description, max_results=5):
71
83
 
72
84
  def search_best_practices(description, max_results=5):
73
85
  """Search best practices for applicable rules"""
74
- result = search(description, domain="practices", max_results=max_results)
86
+ result = search(description, domain="practices", max_results=max_results, min_score=0.3)
75
87
  if "error" in result or not result.get("results"):
76
88
  return "No matching best practices found."
77
89
 
@@ -88,7 +100,7 @@ def search_best_practices(description, max_results=5):
88
100
 
89
101
 
90
102
  def try_ui_ux_bridge(ui_style_keywords):
91
- """Optional: query ui-ux-pro-max if available"""
103
+ """Optional: query ui-ux-pro-max if available. Returns empty string if N/A."""
92
104
  try:
93
105
  ux_search_path = Path(__file__).parent.parent.parent.parent / ".claude" / "skills" / "ui-ux-pro-max" / "scripts" / "core.py"
94
106
  if not ux_search_path.exists():
@@ -101,17 +113,27 @@ def try_ui_ux_bridge(ui_style_keywords):
101
113
  spec.loader.exec_module(ux_module)
102
114
  result = ux_module.search(ui_style_keywords, domain="style", max_results=2)
103
115
  if result.get("results"):
104
- lines = ["**UI/UX Style Recommendations (from ui-ux-pro-max):**"]
116
+ lines = ["<design_recommendations>", "**UI/UX Style Recommendations:**"]
105
117
  for r in result["results"]:
106
118
  lines.append(f"- {r.get('Style Category', '')}: {r.get('Keywords', '')}")
107
119
  lines.append(f" Effects: {r.get('Effects & Animation', '')}")
120
+ lines.append("</design_recommendations>")
108
121
  return "\n".join(lines)
109
122
  except Exception:
110
123
  pass
111
- return "N/A ui-ux-pro-max not available"
124
+ # Return empty string instead of N/A to avoid wasting prompt tokens
125
+ return ""
126
+
127
+
128
+ def _strip_empty_xml_tags(content):
129
+ """Remove XML tags whose body is empty or whitespace-only"""
130
+ return re.sub(r'<([\w-]+)>\s*</\1>', '', content)
112
131
 
113
132
 
114
- def assemble_context(name, description, theme_profile, component, schema, practices, design_recs):
133
+ def assemble_context(name, description, theme_profile, component, schema,
134
+ practices, design_recs, block_patterns="",
135
+ settings_profile="", theme_dna="",
136
+ design_tokens="", refinement=""):
115
137
  """Merge all context into generation prompt"""
116
138
  template = load_template("generation-prompt.md")
117
139
  base_template = load_template("section-base.liquid")
@@ -125,44 +147,19 @@ def assemble_context(name, description, theme_profile, component, schema, practi
125
147
  content = content.replace("{{ schema_search_results }}", schema)
126
148
  content = content.replace("{{ best_practices_results }}", practices)
127
149
  content = content.replace("{{ ui_ux_recommendations }}", design_recs)
150
+ content = content.replace("{{ block_patterns_results }}", block_patterns)
151
+ content = content.replace("{{ settings_profile_results }}", settings_profile)
152
+ content = content.replace("{{ theme_dna_context }}", theme_dna)
153
+ content = content.replace("{{ design_tokens_results }}", design_tokens)
154
+ content = content.replace("{{ refinement_context }}", refinement)
128
155
  base_block = f"```liquid\n{base_template}\n```" if base_template else "See standard OS2.0 section structure"
129
156
  content = content.replace("{{ section_base_content }}", base_block)
157
+ # Clean up empty XML tags from unused sections
158
+ content = _strip_empty_xml_tags(content)
130
159
  return content
131
160
 
132
- # Fallback: manual assembly
133
- return f"""# Generate Shopify Section: {name}
134
-
135
- ## Description
136
- {description}
137
-
138
- ## Theme Context
139
- {theme_profile}
140
-
141
- ## Closest Pattern Template
142
- {component}
143
-
144
- ## Relevant Schema Settings
145
- {schema}
146
-
147
- ## Applicable Best Practices
148
- {practices}
149
-
150
- ## Design Recommendations
151
- {design_recs}
152
-
153
- ## Base Template
154
- ```liquid
155
- {base_template}
156
- ```
157
-
158
- ## Requirements
159
- - OS2.0 compliant (presets, @app blocks, block.shopify_attributes)
160
- - Scoped CSS via #section-{{{{ section.id }}}}
161
- - Responsive (mobile-first)
162
- - Accessible (semantic HTML, aria-labels, alt text)
163
- - Performance (lazy-load non-hero images, no Liquid in CSS/JS)
164
- - Use {{% render %}} not {{% include %}}
165
- """
161
+ # Fallback: manual assembly (shouldn't reach here with template present)
162
+ return f"# Generate Shopify Section: {name}\n\n{description}\n\n{component}\n\n{schema}\n\n{practices}"
166
163
 
167
164
 
168
165
  def main():
@@ -174,11 +171,18 @@ def main():
174
171
  args = parser.parse_args()
175
172
 
176
173
  theme = load_theme_profile(args.theme_profile)
174
+ # Tailored BM25 queries per domain for better relevance
177
175
  component = search_components(f"{args.name} {args.description}")
178
- schema = search_schema_settings(args.description)
179
- practices = search_best_practices(args.description)
176
+ schema = search_schema_settings(f"shopify section settings {args.description}")
177
+ practices = search_best_practices(f"shopify section {args.name} best practices")
180
178
  design_recs = try_ui_ux_bridge(args.name + " " + args.description)
179
+ blocks = search_block_patterns(f"{args.name} {args.description}")
180
+ profile = search_settings_profile(args.name)
181
+ dna = search_theme_dna(args.name)
182
+ tokens = search_design_tokens(f"css typography spacing color {args.description}")
181
183
 
184
+ # Build refinement context as dedicated section (not appended to component)
185
+ refinement = ""
182
186
  if args.refine:
183
187
  refine_path = Path(args.refine)
184
188
  try:
@@ -189,12 +193,18 @@ def main():
189
193
  try:
190
194
  from quality_gate import run_quality_gate
191
195
  gate_result = run_quality_gate(args.refine)
192
- refine_context = f"\n## Existing Section (for refinement)\n```liquid\n{existing[:3000]}\n```\n\n## Quality Gate Results\n{gate_result}\n\nPlease fix the issues identified above while preserving the section's functionality."
193
- component += refine_context
196
+ refinement = (f"<refinement>\n## Existing Section (for refinement)\n"
197
+ f"```liquid\n{existing[:3000]}\n```\n\n"
198
+ f"## Quality Gate Results\n{gate_result}\n\n"
199
+ f"Please fix the issues identified above while preserving functionality.\n"
200
+ f"</refinement>")
194
201
  except ImportError:
195
- component += f"\n## Existing Section (for refinement)\n```liquid\n{existing[:3000]}\n```"
202
+ refinement = (f"<refinement>\n## Existing Section (for refinement)\n"
203
+ f"```liquid\n{existing[:3000]}\n```\n</refinement>")
196
204
 
197
- context = assemble_context(args.name, args.description, theme, component, schema, practices, design_recs)
205
+ context = assemble_context(args.name, args.description, theme, component, schema,
206
+ practices, design_recs, blocks, profile, dna,
207
+ design_tokens=tokens, refinement=refinement)
198
208
  print(context)
199
209
 
200
210