spec-to-ui 0.1.0 → 0.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 (143) hide show
  1. package/LICENSE +2 -0
  2. package/README.ko.md +129 -0
  3. package/README.md +96 -29
  4. package/RELEASE.md +26 -0
  5. package/THIRD-PARTY.md +14 -0
  6. package/bin/cli.mjs +70 -91
  7. package/bundle-manifest.json +140 -0
  8. package/package.json +40 -8
  9. package/plugin.json +16 -0
  10. package/skills/design-taste-frontend/LICENSE +21 -0
  11. package/skills/design-taste-frontend/README.md +81 -0
  12. package/skills/design-taste-frontend/SKILL.md +546 -0
  13. package/skills/design-taste-frontend/assets/app.css +216 -0
  14. package/skills/design-taste-frontend/assets/base.css +105 -0
  15. package/skills/design-taste-frontend/assets/base.js +37 -0
  16. package/skills/design-taste-frontend/assets/charts.js +619 -0
  17. package/skills/design-taste-frontend/assets/scenes/stage.css +28 -0
  18. package/skills/design-taste-frontend/assets/scenes/stage.js +134 -0
  19. package/skills/design-taste-frontend/evals/evals.json +76 -0
  20. package/skills/design-taste-frontend/library/app.json +1327 -0
  21. package/skills/design-taste-frontend/library/directions.json +1078 -0
  22. package/skills/design-taste-frontend/library/heroes.json +1086 -0
  23. package/skills/design-taste-frontend/library/motions.json +92 -0
  24. package/skills/design-taste-frontend/library/palettes.json +1485 -0
  25. package/skills/design-taste-frontend/library/photos.json +7240 -0
  26. package/skills/design-taste-frontend/library/sections.json +4429 -0
  27. package/skills/design-taste-frontend/library/sites.json +8350 -0
  28. package/skills/design-taste-frontend/library/type.json +2024 -0
  29. package/skills/design-taste-frontend/references/app-screens.md +1199 -0
  30. package/skills/design-taste-frontend/references/colour.md +141 -0
  31. package/skills/design-taste-frontend/references/copy.md +177 -0
  32. package/skills/design-taste-frontend/references/critique.md +182 -0
  33. package/skills/design-taste-frontend/references/imagery.md +791 -0
  34. package/skills/design-taste-frontend/references/layout.md +292 -0
  35. package/skills/design-taste-frontend/references/motion.md +980 -0
  36. package/skills/design-taste-frontend/references/research.md +356 -0
  37. package/skills/design-taste-frontend/references/scenes.md +174 -0
  38. package/skills/design-taste-frontend/references/tells.md +115 -0
  39. package/skills/design-taste-frontend/references/type.md +170 -0
  40. package/skills/design-taste-frontend/scripts/assets.mjs +865 -0
  41. package/skills/design-taste-frontend/scripts/draw.mjs +584 -0
  42. package/skills/design-taste-frontend/scripts/inspect.mjs +308 -0
  43. package/skills/design-taste-frontend/scripts/lib/app-draw.mjs +203 -0
  44. package/skills/design-taste-frontend/scripts/lib/cdp.mjs +248 -0
  45. package/skills/design-taste-frontend/scripts/lib/inspection-files.mjs +9 -0
  46. package/skills/design-taste-frontend/scripts/lib/probe.mjs +929 -0
  47. package/skills/design-taste-frontend/scripts/lib/rules.mjs +243 -0
  48. package/skills/design-taste-frontend/scripts/lint.mjs +119 -0
  49. package/skills/design-taste-frontend/scripts/scenes.mjs +124 -0
  50. package/skills/spec-to-ui/SKILL.md +61 -0
  51. package/{skill → skills/spec-to-ui}/references/composition-and-validation.md +6 -0
  52. package/{skill → skills/spec-to-ui}/references/drafts-and-feedback.md +10 -0
  53. package/skills/spec-to-ui/references/requirements-supplement.md +21 -0
  54. package/{skill → skills/spec-to-ui}/references/review-and-handoff.md +9 -0
  55. package/skills/spec-to-ui/references/skill-composition.md +21 -0
  56. package/{skill → skills/spec-to-ui}/references/structure-and-decisions.md +4 -0
  57. package/skills/specframe/NOTICE.md +7 -0
  58. package/skills/specframe/SKILL.md +58 -0
  59. package/skills/specframe/agents/openai.yaml +3 -0
  60. package/skills/specframe/references/operating-contract.md +137 -0
  61. package/skills/specframe/references/visual.md +3 -0
  62. package/skills/specframe/references/web.md +15 -0
  63. package/skills/specframe/scripts/state.py +182 -0
  64. package/skills/ui-ux-pro-max/LICENSE +21 -0
  65. package/skills/ui-ux-pro-max/SKILL.md +218 -0
  66. package/skills/ui-ux-pro-max/data/app-interface.csv +33 -0
  67. package/skills/ui-ux-pro-max/data/catalog-summary.json +78 -0
  68. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  69. package/skills/ui-ux-pro-max/data/colors.csv +193 -0
  70. package/skills/ui-ux-pro-max/data/data-provenance.json +1348 -0
  71. package/skills/ui-ux-pro-max/data/google-font-licenses.json +20185 -0
  72. package/skills/ui-ux-pro-max/data/google-fonts.csv +1935 -0
  73. package/skills/ui-ux-pro-max/data/icons.csv +106 -0
  74. package/skills/ui-ux-pro-max/data/landing.csv +35 -0
  75. package/skills/ui-ux-pro-max/data/motion.csv +18 -0
  76. package/skills/ui-ux-pro-max/data/phosphor-icons-upstream.json +32582 -0
  77. package/skills/ui-ux-pro-max/data/products.csv +193 -0
  78. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  79. package/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
  80. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  81. package/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
  82. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  83. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +60 -0
  84. package/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
  85. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  86. package/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
  87. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +62 -0
  88. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +84 -0
  89. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +68 -0
  90. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  91. package/skills/ui-ux-pro-max/data/stacks/react.csv +67 -0
  92. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +69 -0
  93. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +56 -0
  94. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  95. package/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
  96. package/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
  97. package/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
  98. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  99. package/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
  100. package/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
  101. package/skills/ui-ux-pro-max/data/styles.csv +89 -0
  102. package/skills/ui-ux-pro-max/data/typography.csv +75 -0
  103. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +193 -0
  104. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +120 -0
  105. package/skills/ui-ux-pro-max/references/pro-rules.md +117 -0
  106. package/skills/ui-ux-pro-max/references/quick-reference.md +256 -0
  107. package/skills/ui-ux-pro-max/scripts/core.py +993 -0
  108. package/skills/ui-ux-pro-max/scripts/design_system.py +1668 -0
  109. package/skills/ui-ux-pro-max/scripts/reasoning_contract.py +123 -0
  110. package/skills/ui-ux-pro-max/scripts/search.py +184 -0
  111. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json +36 -0
  112. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-catalog.json +33 -0
  113. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv +3 -0
  114. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json +17 -0
  115. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json +7 -0
  116. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv +4 -0
  117. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json +23 -0
  118. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json +13 -0
  119. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json +4 -0
  120. package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json +5 -0
  121. package/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-baseline.json +3366 -0
  122. package/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-cases.json +111 -0
  123. package/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-thresholds.json +100 -0
  124. package/skills/ui-ux-pro-max/scripts/tests/test_catalog_refresh.py +354 -0
  125. package/skills/ui-ux-pro-max/scripts/tests/test_catalog_summary_line_endings.py +78 -0
  126. package/skills/ui-ux-pro-max/scripts/tests/test_core.py +343 -0
  127. package/skills/ui-ux-pro-max/scripts/tests/test_core_data_quality.py +198 -0
  128. package/skills/ui-ux-pro-max/scripts/tests/test_data_contracts.py +421 -0
  129. package/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +233 -0
  130. package/skills/ui-ux-pro-max/scripts/tests/test_design_system_stack.py +61 -0
  131. package/skills/ui-ux-pro-max/scripts/tests/test_native_desktop_stack_freshness.py +179 -0
  132. package/skills/ui-ux-pro-max/scripts/tests/test_relevance_evaluator.py +182 -0
  133. package/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py +82 -0
  134. package/skills/ui-ux-pro-max/scripts/tests/test_style_taxonomy.py +175 -0
  135. package/skills/ui-ux-pro-max/scripts/tests/test_text_layout_resilience.py +120 -0
  136. package/skills/ui-ux-pro-max/scripts/tests/test_web_stack_freshness.py +191 -0
  137. package/skills/ui-ux-pro-max/scripts/validate_data.py +1095 -0
  138. package/sources.json +58 -0
  139. package/skill/SKILL.md +0 -68
  140. package/skill-manifest.json +0 -10
  141. /package/{skill → skills/spec-to-ui}/LICENSE +0 -0
  142. /package/{skill → skills/spec-to-ui}/agents/openai.yaml +0 -0
  143. /package/{skill → skills/spec-to-ui}/references/reference-selection.md +0 -0
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for the public style taxonomy and search contract."""
3
+
4
+ import csv
5
+ import json
6
+ import statistics
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ SCRIPTS_DIR = Path(__file__).resolve().parent.parent
12
+ DATA_DIR = SCRIPTS_DIR.parent / "data"
13
+ sys.path.insert(0, str(SCRIPTS_DIR))
14
+
15
+ from core import search # noqa: E402
16
+ from design_system import _style_is_dark_primary # noqa: E402
17
+
18
+
19
+ def read_rows(name):
20
+ with (DATA_DIR / name).open(encoding="utf-8", newline="") as handle:
21
+ return list(csv.DictReader(handle))
22
+
23
+
24
+ class TestStyleTaxonomy(unittest.TestCase):
25
+ @classmethod
26
+ def setUpClass(cls):
27
+ cls.styles = read_rows("styles.csv")
28
+ cls.by_id = {row["Style ID"]: row for row in cls.styles}
29
+
30
+ def test_curated_state_distribution_is_explicit(self):
31
+ counts = {
32
+ status: sum(row["Status"] == status for row in self.styles)
33
+ for status in ("active", "supplemental", "deprecated")
34
+ }
35
+ self.assertEqual(
36
+ {"active": 50, "supplemental": 29, "deprecated": 9}, counts
37
+ )
38
+
39
+ def test_every_style_name_and_alias_has_a_deterministic_destination(self):
40
+ for row in self.styles:
41
+ queries = [row["Style Category"]]
42
+ queries.extend(alias for alias in row["Aliases"].split("|") if alias)
43
+ for query in queries:
44
+ with self.subTest(style=row["Style ID"], query=query):
45
+ result = search(query, max_results=1)
46
+ if (row["Status"] == "deprecated"
47
+ and row["Replacement Domain"] == "landing"):
48
+ self.assertEqual(0, result["count"])
49
+ self.assertEqual(
50
+ {
51
+ "domain": row["Replacement Domain"],
52
+ "id": row["Replacement ID"],
53
+ },
54
+ result.get("redirect"),
55
+ )
56
+ else:
57
+ expected_id = (
58
+ row["Replacement ID"]
59
+ if row["Status"] == "deprecated"
60
+ else row["Style ID"]
61
+ )
62
+ self.assertEqual(expected_id, result["results"][0]["Style ID"])
63
+
64
+ def test_deprecated_rows_never_appear_in_generic_results(self):
65
+ for query in ("modern interface", "marketing page", "trust design"):
66
+ with self.subTest(query=query):
67
+ result = search(query, domain="style", max_results=20)
68
+ self.assertLessEqual(
69
+ {row["Status"] for row in result["results"]}, {"active"}
70
+ )
71
+
72
+ def test_style_arbitration_does_not_steal_product_intent(self):
73
+ result = search("design a financial dashboard for my bank", max_results=1)
74
+ self.assertEqual("product", result["domain"])
75
+
76
+ def test_family_variants_and_mobile_intent_remain_distinct(self):
77
+ expected_parents = {
78
+ "gradient-mesh-aurora-evolved": "aurora-ui",
79
+ "swiss-modernism-2-0": "minimalism-and-swiss-style",
80
+ "neumorphism-mobile": "neumorphism",
81
+ "claymorphism-mobile": "claymorphism",
82
+ "spectrum-2": "spectrum-design-system",
83
+ }
84
+ for style_id, parent_id in expected_parents.items():
85
+ with self.subTest(style=style_id):
86
+ row = self.by_id[style_id]
87
+ self.assertEqual(parent_id, row["Parent Style ID"])
88
+ self.assertIn(row["Status"], {"supplemental", "deprecated"})
89
+
90
+ self.assertEqual("style", self.by_id["bento-grids"]["Replacement Domain"])
91
+ self.assertEqual(
92
+ "bento-box-grid", self.by_id["bento-grids"]["Replacement ID"]
93
+ )
94
+
95
+ self.assertEqual(
96
+ "neumorphism-mobile",
97
+ search("Neumorphism (Mobile)", "style", 1)["results"][0]["Style ID"],
98
+ )
99
+ self.assertEqual(
100
+ "claymorphism-mobile",
101
+ search("Claymorphism (Mobile)", "style", 1)["results"][0]["Style ID"],
102
+ )
103
+ self.assertEqual(
104
+ "material-you-md3-mobile",
105
+ search("M3 Expressive", "style", 1)["results"][0]["Style ID"],
106
+ )
107
+ self.assertEqual(
108
+ "neumorphism-mobile",
109
+ search("mobile neumorphism UI", "style", 1)["results"][0]["Style ID"],
110
+ )
111
+ self.assertEqual(
112
+ "claymorphism-mobile",
113
+ search("mobile app with claymorphism", "style", 1)["results"][0]["Style ID"],
114
+ )
115
+ self.assertEqual(
116
+ "spectrum-2",
117
+ search("design system for Spectrum 2", "style", 1)["results"][0]["Style ID"],
118
+ )
119
+
120
+ def test_claim_fields_use_controlled_non_guarantee_language(self):
121
+ allowed_performance = {"cost:low", "cost:moderate", "cost:high"}
122
+ allowed_accessibility = {"risk:low", "risk:conditional", "risk:high"}
123
+ allowed_mode = {"supported", "conditional", "not-recommended"}
124
+ for row in self.styles:
125
+ with self.subTest(style=row["Style ID"]):
126
+ self.assertIn(row["Performance"].split("|", 1)[0], allowed_performance)
127
+ self.assertIn(row["Accessibility"].split("|", 1)[0], allowed_accessibility)
128
+ self.assertIn(row["Light Mode ✓"], allowed_mode)
129
+ self.assertIn(row["Dark Mode ✓"], allowed_mode)
130
+ self.assertIn(row["Preferred Mode"], {"auto", "light", "dark"})
131
+ claim_text = " ".join(row.values())
132
+ self.assertNotRegex(claim_text, r"(?i)\bWCAG\s+A{2,3}\+?\b")
133
+ self.assertNotRegex(
134
+ claim_text,
135
+ r"(?i)\bWCAG\b.{0,40}\b(?:compliant|compliance)\b",
136
+ )
137
+ self.assertNotRegex(row["Framework Compatibility"], r"\d+/10")
138
+
139
+ self.assertTrue(_style_is_dark_primary(self.by_id["dark-mode-oled"]))
140
+ self.assertFalse(
141
+ _style_is_dark_primary(self.by_id["minimalism-and-swiss-style"])
142
+ )
143
+
144
+ def test_searchable_prompt_lengths_are_balanced(self):
145
+ lengths_by_type = {}
146
+ for row in self.styles:
147
+ length = len(row["AI Prompt Keywords"].split())
148
+ self.assertLessEqual(length, 40, row["Style ID"])
149
+ lengths_by_type.setdefault(row["Type"], []).append(length)
150
+ general_median = statistics.median(lengths_by_type["General"])
151
+ mobile_median = statistics.median(lengths_by_type["Mobile"])
152
+ self.assertLessEqual(mobile_median, general_median * 1.6)
153
+
154
+ def test_new_rows_have_first_party_provenance(self):
155
+ payload = json.loads(
156
+ (DATA_DIR / "data-provenance.json").read_text(encoding="utf-8")
157
+ )
158
+ records = {
159
+ record["entityId"]: record
160
+ for record in payload["records"]
161
+ if record["entityKind"] == "style"
162
+ }
163
+ new_rows = [row for row in self.styles if int(row["No"]) > 85]
164
+ self.assertGreaterEqual(len(new_rows), 4)
165
+ for row in new_rows:
166
+ with self.subTest(style=row["Style ID"]):
167
+ record = records[row["Style ID"]]
168
+ self.assertTrue(record["sources"])
169
+ self.assertTrue(
170
+ any(source["type"] == "official" for source in record["sources"])
171
+ )
172
+
173
+
174
+ if __name__ == "__main__":
175
+ unittest.main(verbosity=2)
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Canonical regression contracts for resilient UI text layouts."""
3
+
4
+ import csv
5
+ import sys
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ SCRIPTS_DIR = Path(__file__).resolve().parent.parent
10
+ DATA_DIR = SCRIPTS_DIR.parent / "data"
11
+ sys.path.insert(0, str(SCRIPTS_DIR))
12
+
13
+ from core import search, search_stack # noqa: E402
14
+
15
+
16
+ def read_rows(relative_path):
17
+ with (DATA_DIR / relative_path).open(encoding="utf-8", newline="") as handle:
18
+ return list(csv.DictReader(handle))
19
+
20
+
21
+ UX_PHRASES = {
22
+ "Heading Line Balance": ("progressive visual heuristic", "natural-wrap fallback"),
23
+ "Long Token Wrapping": ("overflow-wrap anywhere", "text children shrink"),
24
+ "Text Reflow and Spacing": ("narrow widths zoom", "content-driven height"),
25
+ "Essential Text Truncation": ("complete access", "visible full-detail path"),
26
+ "Compact Label Semantics": ("badges communicate state", "meaning and ownership"),
27
+ "Chip Collection Reflow": ("filter chips", "operable +n disclosure"),
28
+ "Compact Label Overflow": ("stay whole on one line", "keyboard pointer and touch"),
29
+ "Compact Control Semantics": ("native role", "pressed or selected state"),
30
+ "Contextual Live Badge Updates": ("meaningful contextual status", "atomic status"),
31
+ "Cancellable State Transitions": ("interrupt an in-flight transition", "final semantic state"),
32
+ }
33
+
34
+ TAILWIND_PHRASES = {
35
+ "Balanced heading wrapping": ("text-balance", "natural wrapping fallback"),
36
+ "Long token resilience": ("wrap-anywhere", "min-w-0"),
37
+ "Compact label layout": ("flex flex-wrap gap-2", "whitespace-nowrap", "shrink-0"),
38
+ }
39
+
40
+
41
+ class TestTextLayoutRetrieval(unittest.TestCase):
42
+ def test_locked_queries_return_the_canonical_identity_first(self):
43
+ cases = (
44
+ ("orphan heading line balance", "Heading Line Balance"),
45
+ ("long url token breaks layout", "Long Token Wrapping"),
46
+ ("badge chip label wraps to second line", "Compact Label Overflow"),
47
+ ("filter chip collection reflow hidden values", "Chip Collection Reflow"),
48
+ ("live badge count screen reader", "Contextual Live Badge Updates"),
49
+ ("rapid chip animation interrupted", "Cancellable State Transitions"),
50
+ )
51
+ for query, expected in cases:
52
+ with self.subTest(query=query):
53
+ result = search(query, domain="ux", max_results=3, diagnostics=True)
54
+ actual = [row.get("Issue") for row in result["results"]]
55
+ self.assertTrue(actual, result.get("diagnostics"))
56
+ self.assertEqual(expected, actual[0], f"ranking={actual!r}")
57
+
58
+ def test_tailwind_query_returns_compact_label_layout_first(self):
59
+ result = search_stack(
60
+ "chip badge overflow nowrap", "html-tailwind",
61
+ max_results=3, diagnostics=True,
62
+ )
63
+ actual = [row.get("Guideline") for row in result["results"]]
64
+ self.assertTrue(actual, result.get("diagnostics"))
65
+ self.assertEqual("Compact label layout", actual[0], f"ranking={actual!r}")
66
+
67
+
68
+ class TestTextLayoutDataContracts(unittest.TestCase):
69
+ @classmethod
70
+ def setUpClass(cls):
71
+ cls.ux = read_rows("ux-guidelines.csv")
72
+ cls.tailwind = read_rows("stacks/html-tailwind.csv")
73
+
74
+ def test_new_ux_rows_are_unique_sequential_and_keep_critical_guidance(self):
75
+ matches = [row for row in self.ux if row["Issue"] in UX_PHRASES]
76
+ self.assertEqual(len(UX_PHRASES), len(matches))
77
+ self.assertEqual(len(matches), len({row["Issue"] for row in matches}))
78
+ self.assertEqual(list(range(110, 120)), [int(row["No"]) for row in matches])
79
+ for row in matches:
80
+ with self.subTest(issue=row["Issue"]):
81
+ text = " ".join(row.values()).casefold()
82
+ for phrase in UX_PHRASES[row["Issue"]]:
83
+ self.assertIn(phrase.casefold(), text)
84
+ self.assertIn(row["Severity"], {"Medium", "High", "Critical"})
85
+
86
+ def test_new_tailwind_rows_are_unique_current_and_keep_required_utilities(self):
87
+ matches = [row for row in self.tailwind if row["Guideline"] in TAILWIND_PHRASES]
88
+ self.assertEqual(len(TAILWIND_PHRASES), len(matches))
89
+ self.assertEqual(len(matches), len({row["Guideline"] for row in matches}))
90
+ self.assertEqual([57, 58, 59], [int(row["No"]) for row in matches])
91
+ for row in matches:
92
+ with self.subTest(guideline=row["Guideline"]):
93
+ text = " ".join(row.values()).casefold()
94
+ for phrase in TAILWIND_PHRASES[row["Guideline"]]:
95
+ self.assertIn(phrase.casefold(), text)
96
+ self.assertEqual("active", row["Status"])
97
+ self.assertEqual("html-tailwind 4.3", row["Applies To"])
98
+ self.assertEqual("2026-08-13", row["Verified At"])
99
+
100
+ def test_refined_rows_are_context_sensitive_not_universal_recipes(self):
101
+ expected = {
102
+ "8": ("depends on distance complexity platform", "shared motion tokens"),
103
+ "14": ("match how an element changes speed", "linear for constant-rate progress"),
104
+ "19": ("badges validation text", "stable content-driven container"),
105
+ "78": ("avoid flashing for near-instant work", "platform and component guidance"),
106
+ }
107
+ forbidden = ("use 150-300ms", "operations > 300ms", "linear motion feels robotic")
108
+ by_number = {row["No"]: row for row in self.ux}
109
+ for number, phrases in expected.items():
110
+ with self.subTest(row=number):
111
+ row = by_number[number]
112
+ guidance = " ".join((row["Description"], row["Do"])).casefold()
113
+ for phrase in phrases:
114
+ self.assertIn(phrase, guidance)
115
+ for claim in forbidden:
116
+ self.assertNotIn(claim, guidance)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ unittest.main()
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """Freshness and generation-isolation contracts for web stack guidance."""
3
+
4
+ import csv
5
+ import sys
6
+ import unittest
7
+ from pathlib import Path
8
+ from urllib.parse import urlsplit
9
+
10
+ SCRIPTS_DIR = Path(__file__).resolve().parents[1]
11
+ if str(SCRIPTS_DIR) not in sys.path:
12
+ sys.path.insert(0, str(SCRIPTS_DIR))
13
+
14
+ from core import DATA_DIR, STACK_CONFIG, WEB_STACKS, search_stack # noqa: E402
15
+ from validate_data import STACK_OFFICIAL_HOSTS # noqa: E402
16
+
17
+ CURRENT_APPLICABILITY = {
18
+ "react": "react 19.2.x",
19
+ "nextjs": "nextjs 16.2",
20
+ "vue": "vue 3.5.x",
21
+ "svelte": "svelte 5",
22
+ "astro": "astro 7.1.6",
23
+ "angular": "angular 22.x",
24
+ "html-tailwind": "html-tailwind 4.3",
25
+ "shadcn": "shadcn cli 4",
26
+ "nuxtjs": "nuxtjs 4.5",
27
+ "nuxt-ui": "nuxt-ui 4.12.0",
28
+ }
29
+
30
+
31
+ def _rows(stack):
32
+ path = DATA_DIR / STACK_CONFIG[stack]["file"]
33
+ with path.open(encoding="utf-8", newline="") as handle:
34
+ return list(csv.DictReader(handle))
35
+
36
+
37
+ class TestWebStackFreshness(unittest.TestCase):
38
+ def test_web_rows_have_explicit_freshness_metadata(self):
39
+ for stack in WEB_STACKS:
40
+ for row in _rows(stack):
41
+ with self.subTest(stack=stack, row=row["No"]):
42
+ self.assertIn(row["Status"], {"active", "deprecated"})
43
+ self.assertTrue(row["Applies To"].startswith(stack))
44
+ self.assertRegex(row["Verified At"], r"^\d{4}-\d{2}-\d{2}$")
45
+ self.assertEqual(
46
+ "legacy" in row["Applies To"].casefold(),
47
+ row["Status"] == "deprecated",
48
+ )
49
+
50
+ def test_high_impact_rows_use_official_sources(self):
51
+ for stack in WEB_STACKS:
52
+ for row in _rows(stack):
53
+ if row["Severity"] not in {"Critical", "High"}:
54
+ continue
55
+ with self.subTest(stack=stack, row=row["No"]):
56
+ parsed = urlsplit(row["Docs URL"])
57
+ self.assertEqual(parsed.scheme, "https")
58
+ self.assertIn(parsed.hostname, STACK_OFFICIAL_HOSTS[stack])
59
+
60
+ def test_active_rows_use_the_verified_current_applicability(self):
61
+ for stack, applicability in CURRENT_APPLICABILITY.items():
62
+ for row in _rows(stack):
63
+ if row["Status"] != "active":
64
+ continue
65
+ with self.subTest(stack=stack, row=row["No"]):
66
+ self.assertTrue(row["Applies To"].startswith(applicability))
67
+
68
+ def test_svelte_current_and_legacy_queries_do_not_mix_generations(self):
69
+ current = search_stack("Svelte state props and events", "svelte")
70
+ legacy = search_stack("Svelte 4 legacy props and events", "svelte")
71
+ self.assertGreater(current["count"], 0)
72
+ self.assertGreater(legacy["count"], 0)
73
+ self.assertEqual({row["Status"] for row in current["results"]}, {"active"})
74
+ self.assertEqual({row["Status"] for row in legacy["results"]}, {"deprecated"})
75
+ self.assertTrue(all("legacy" in row["Applies To"] for row in legacy["results"]))
76
+
77
+ def test_explicit_old_major_uses_only_curated_legacy_rows(self):
78
+ cases = {
79
+ "nextjs": "Next.js 15 middleware auth matcher",
80
+ "html-tailwind": "Tailwind 3 JIT content configuration",
81
+ "nuxtjs": "Nuxt 3 app config runtime config migration",
82
+ }
83
+ for stack, query in cases.items():
84
+ with self.subTest(stack=stack):
85
+ result = search_stack(query, stack)
86
+ self.assertGreater(result["count"], 0)
87
+ self.assertEqual(
88
+ {row["Status"] for row in result["results"]}, {"deprecated"}
89
+ )
90
+
91
+ def test_current_major_migration_query_stays_on_current_guidance(self):
92
+ result = search_stack("Next.js 16 migration to proxy", "nextjs")
93
+ self.assertGreater(result["count"], 0)
94
+ self.assertEqual({row["Status"] for row in result["results"]}, {"active"})
95
+
96
+ def test_shadcn_named_base_excludes_incompatible_composition_apis(self):
97
+ result = search_stack("shadcn Base UI asChild composition", "shadcn")
98
+ self.assertGreater(result["count"], 0)
99
+ self.assertEqual(result["results"][0]["Guideline"],
100
+ "Use render for Base UI composition")
101
+ self.assertTrue(all("base=radix" not in row["Applies To"]
102
+ for row in result["results"]))
103
+
104
+ def test_common_old_major_syntaxes_select_legacy_guidance(self):
105
+ cases = {
106
+ "svelte": "svelte@4 props events",
107
+ "nextjs": "Next.js (v15) middleware",
108
+ "html-tailwind": "tailwindcss@3 JIT",
109
+ "nuxtjs": "nuxt@3 app config",
110
+ }
111
+ for stack, query in cases.items():
112
+ with self.subTest(stack=stack, query=query):
113
+ result = search_stack(query, stack)
114
+ self.assertGreater(result["count"], 0)
115
+ self.assertEqual(
116
+ {row["Status"] for row in result["results"]}, {"deprecated"}
117
+ )
118
+
119
+ def test_number_starting_a_word_is_not_a_version(self):
120
+ cases = {
121
+ "react": "react 3d canvas effect cleanup",
122
+ "html-tailwind": "tailwind 2xl breakpoint",
123
+ "angular": "angular 2fa login form",
124
+ }
125
+ for stack, query in cases.items():
126
+ with self.subTest(stack=stack, query=query):
127
+ result = search_stack(query, stack)
128
+ self.assertGreater(result["count"], 0)
129
+ self.assertNotIn(
130
+ "deprecated", {row["Status"] for row in result["results"]}
131
+ )
132
+
133
+ def test_old_major_without_curated_legacy_guidance_abstains(self):
134
+ result = search_stack("Astro 5 content collections", "astro")
135
+ self.assertEqual(result["count"], 0)
136
+ self.assertEqual(result["results"], [])
137
+
138
+ def test_current_high_drift_queries_return_current_contracts(self):
139
+ cases = {
140
+ ("nextjs", "Next.js 16 request interception proxy"): "proxy",
141
+ ("html-tailwind", "Tailwind 4 CSS-first source detection"): "source",
142
+ ("react", "React Effect Event latest values inside an Effect"): "effect event",
143
+ }
144
+ for (stack, query), expected in cases.items():
145
+ with self.subTest(stack=stack):
146
+ result = search_stack(query, stack, max_results=1)
147
+ self.assertEqual(result["count"], 1)
148
+ row = result["results"][0]
149
+ self.assertEqual(row["Status"], "active")
150
+ self.assertIn(expected, row["Guideline"].casefold())
151
+
152
+ def test_stack_without_curated_legacy_rows_keeps_nonlegacy_fallback(self):
153
+ result = search_stack(
154
+ "which Windows UI framework should a new app choose instead of legacy UWP",
155
+ "uwp",
156
+ )
157
+ self.assertGreater(result["count"], 0)
158
+
159
+ def test_stale_apis_are_not_recommended_by_active_rows(self):
160
+ forbidden = {
161
+ "svelte": ("$: ", "export let ", "on:click", "createeventdispatcher"),
162
+ "nextjs": ("middleware.ts", "function middleware", "skipmiddleware"),
163
+ "html-tailwind": (
164
+ "content: [", "mode: 'jit'", "@tailwindcss/aspect-ratio",
165
+ "tailwindcss-container-queries",
166
+ ),
167
+ "nuxt-ui": ("#cell-status", "sortable: true", "v-model:content"),
168
+ "astro": (
169
+ "output: 'hybrid'", "viewtransitions", "@astrojs/tailwind",
170
+ "astro add prefetch",
171
+ ),
172
+ }
173
+ for stack, tokens in forbidden.items():
174
+ for row in _rows(stack):
175
+ if row["Status"] != "active":
176
+ continue
177
+ recommended = " ".join(
178
+ row[field] for field in ("Guideline", "Do", "Code Good")
179
+ ).casefold()
180
+ for token in tokens:
181
+ with self.subTest(stack=stack, row=row["No"], token=token):
182
+ self.assertNotIn(token, recommended)
183
+
184
+ self.assertNotIn(
185
+ "fetch(url {", _rows("nextjs")[12]["Code Good"].casefold()
186
+ )
187
+ self.assertNotIn("catch(e) {}", _rows("react")[39]["Code Good"].casefold())
188
+
189
+
190
+ if __name__ == "__main__":
191
+ unittest.main()