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.
- package/LICENSE +2 -0
- package/README.ko.md +129 -0
- package/README.md +96 -29
- package/RELEASE.md +26 -0
- package/THIRD-PARTY.md +14 -0
- package/bin/cli.mjs +70 -91
- package/bundle-manifest.json +140 -0
- package/package.json +40 -8
- package/plugin.json +16 -0
- package/skills/design-taste-frontend/LICENSE +21 -0
- package/skills/design-taste-frontend/README.md +81 -0
- package/skills/design-taste-frontend/SKILL.md +546 -0
- package/skills/design-taste-frontend/assets/app.css +216 -0
- package/skills/design-taste-frontend/assets/base.css +105 -0
- package/skills/design-taste-frontend/assets/base.js +37 -0
- package/skills/design-taste-frontend/assets/charts.js +619 -0
- package/skills/design-taste-frontend/assets/scenes/stage.css +28 -0
- package/skills/design-taste-frontend/assets/scenes/stage.js +134 -0
- package/skills/design-taste-frontend/evals/evals.json +76 -0
- package/skills/design-taste-frontend/library/app.json +1327 -0
- package/skills/design-taste-frontend/library/directions.json +1078 -0
- package/skills/design-taste-frontend/library/heroes.json +1086 -0
- package/skills/design-taste-frontend/library/motions.json +92 -0
- package/skills/design-taste-frontend/library/palettes.json +1485 -0
- package/skills/design-taste-frontend/library/photos.json +7240 -0
- package/skills/design-taste-frontend/library/sections.json +4429 -0
- package/skills/design-taste-frontend/library/sites.json +8350 -0
- package/skills/design-taste-frontend/library/type.json +2024 -0
- package/skills/design-taste-frontend/references/app-screens.md +1199 -0
- package/skills/design-taste-frontend/references/colour.md +141 -0
- package/skills/design-taste-frontend/references/copy.md +177 -0
- package/skills/design-taste-frontend/references/critique.md +182 -0
- package/skills/design-taste-frontend/references/imagery.md +791 -0
- package/skills/design-taste-frontend/references/layout.md +292 -0
- package/skills/design-taste-frontend/references/motion.md +980 -0
- package/skills/design-taste-frontend/references/research.md +356 -0
- package/skills/design-taste-frontend/references/scenes.md +174 -0
- package/skills/design-taste-frontend/references/tells.md +115 -0
- package/skills/design-taste-frontend/references/type.md +170 -0
- package/skills/design-taste-frontend/scripts/assets.mjs +865 -0
- package/skills/design-taste-frontend/scripts/draw.mjs +584 -0
- package/skills/design-taste-frontend/scripts/inspect.mjs +308 -0
- package/skills/design-taste-frontend/scripts/lib/app-draw.mjs +203 -0
- package/skills/design-taste-frontend/scripts/lib/cdp.mjs +248 -0
- package/skills/design-taste-frontend/scripts/lib/inspection-files.mjs +9 -0
- package/skills/design-taste-frontend/scripts/lib/probe.mjs +929 -0
- package/skills/design-taste-frontend/scripts/lib/rules.mjs +243 -0
- package/skills/design-taste-frontend/scripts/lint.mjs +119 -0
- package/skills/design-taste-frontend/scripts/scenes.mjs +124 -0
- package/skills/spec-to-ui/SKILL.md +61 -0
- package/{skill → skills/spec-to-ui}/references/composition-and-validation.md +6 -0
- package/{skill → skills/spec-to-ui}/references/drafts-and-feedback.md +10 -0
- package/skills/spec-to-ui/references/requirements-supplement.md +21 -0
- package/{skill → skills/spec-to-ui}/references/review-and-handoff.md +9 -0
- package/skills/spec-to-ui/references/skill-composition.md +21 -0
- package/{skill → skills/spec-to-ui}/references/structure-and-decisions.md +4 -0
- package/skills/specframe/NOTICE.md +7 -0
- package/skills/specframe/SKILL.md +58 -0
- package/skills/specframe/agents/openai.yaml +3 -0
- package/skills/specframe/references/operating-contract.md +137 -0
- package/skills/specframe/references/visual.md +3 -0
- package/skills/specframe/references/web.md +15 -0
- package/skills/specframe/scripts/state.py +182 -0
- package/skills/ui-ux-pro-max/LICENSE +21 -0
- package/skills/ui-ux-pro-max/SKILL.md +218 -0
- package/skills/ui-ux-pro-max/data/app-interface.csv +33 -0
- package/skills/ui-ux-pro-max/data/catalog-summary.json +78 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +193 -0
- package/skills/ui-ux-pro-max/data/data-provenance.json +1348 -0
- package/skills/ui-ux-pro-max/data/google-font-licenses.json +20185 -0
- package/skills/ui-ux-pro-max/data/google-fonts.csv +1935 -0
- package/skills/ui-ux-pro-max/data/icons.csv +106 -0
- package/skills/ui-ux-pro-max/data/landing.csv +35 -0
- package/skills/ui-ux-pro-max/data/motion.csv +18 -0
- package/skills/ui-ux-pro-max/data/phosphor-icons-upstream.json +32582 -0
- package/skills/ui-ux-pro-max/data/products.csv +193 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +60 -0
- package/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +62 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +84 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +68 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +67 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +69 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
- package/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
- package/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
- package/skills/ui-ux-pro-max/data/styles.csv +89 -0
- package/skills/ui-ux-pro-max/data/typography.csv +75 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +193 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +120 -0
- package/skills/ui-ux-pro-max/references/pro-rules.md +117 -0
- package/skills/ui-ux-pro-max/references/quick-reference.md +256 -0
- package/skills/ui-ux-pro-max/scripts/core.py +993 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1668 -0
- package/skills/ui-ux-pro-max/scripts/reasoning_contract.py +123 -0
- package/skills/ui-ux-pro-max/scripts/search.py +184 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-api.json +36 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-catalog.json +33 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-existing.csv +3 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-metadata.json +17 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/google-overrides.json +7 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/icons-curated.csv +4 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-core.json +23 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-package.json +13 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-exports.json +4 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/catalogs/phosphor-react-package.json +5 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-baseline.json +3366 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-cases.json +111 -0
- package/skills/ui-ux-pro-max/scripts/tests/fixtures/relevance-thresholds.json +100 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_catalog_refresh.py +354 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_catalog_summary_line_endings.py +78 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_core.py +343 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_core_data_quality.py +198 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_data_contracts.py +421 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +233 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_design_system_stack.py +61 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_native_desktop_stack_freshness.py +179 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_relevance_evaluator.py +182 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_skill_script_paths.py +82 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_style_taxonomy.py +175 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_text_layout_resilience.py +120 -0
- package/skills/ui-ux-pro-max/scripts/tests/test_web_stack_freshness.py +191 -0
- package/skills/ui-ux-pro-max/scripts/validate_data.py +1095 -0
- package/sources.json +58 -0
- package/skill/SKILL.md +0 -68
- package/skill-manifest.json +0 -10
- /package/{skill → skills/spec-to-ui}/LICENSE +0 -0
- /package/{skill → skills/spec-to-ui}/agents/openai.yaml +0 -0
- /package/{skill → skills/spec-to-ui}/references/reference-selection.md +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Regression tests for the dropped --stack flag in --design-system mode (issue #484).
|
|
5
|
+
|
|
6
|
+
`search.py "<query>" --design-system --stack nextjs` used to exit successfully
|
|
7
|
+
without any indication that the stack was never applied, so a caller following
|
|
8
|
+
SKILL.md's "never assume a stack" guidance could believe stack guidance was
|
|
9
|
+
part of the generated design system. The combination must stay valid, but it
|
|
10
|
+
must say that --stack was ignored.
|
|
11
|
+
|
|
12
|
+
Stdlib-only (unittest, not pytest) to match test_core.py -- this project ships
|
|
13
|
+
with zero external dependencies.
|
|
14
|
+
|
|
15
|
+
Run with:
|
|
16
|
+
python -m unittest discover -s scripts/tests -v
|
|
17
|
+
or directly:
|
|
18
|
+
python scripts/tests/test_design_system_stack.py
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import unittest
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
27
|
+
SEARCH = SCRIPTS_DIR / "search.py"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestStackFlagWithDesignSystem(unittest.TestCase):
|
|
31
|
+
def run_search(self, *args):
|
|
32
|
+
# The child forces UTF-8 on its streams (search.py), so decode as UTF-8
|
|
33
|
+
# regardless of the parent's locale on Windows.
|
|
34
|
+
return subprocess.run(
|
|
35
|
+
[sys.executable, str(SEARCH), *map(str, args)],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
errors="replace",
|
|
40
|
+
check=False,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def test_design_system_with_stack_succeeds_and_says_stack_is_ignored(self):
|
|
44
|
+
proc = self.run_search("platform engineer dashboard", "--design-system", "--stack", "nextjs")
|
|
45
|
+
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
46
|
+
self.assertIn("--stack", proc.stderr)
|
|
47
|
+
self.assertIn("ignored", proc.stderr.lower())
|
|
48
|
+
|
|
49
|
+
def test_design_system_without_stack_stays_quiet(self):
|
|
50
|
+
proc = self.run_search("platform engineer dashboard", "--design-system")
|
|
51
|
+
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
52
|
+
self.assertNotIn("ignored", proc.stderr.lower())
|
|
53
|
+
|
|
54
|
+
def test_stack_search_alone_never_warns(self):
|
|
55
|
+
proc = self.run_search("dashboard table density", "--stack", "nextjs")
|
|
56
|
+
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
57
|
+
self.assertNotIn("ignored", proc.stderr.lower())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Freshness and migration contracts for native, desktop, and 3D stacks."""
|
|
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, STACK_CURRENT_APPLICABILITY,
|
|
15
|
+
search_stack) # noqa: E402
|
|
16
|
+
from validate_data import STACK_OFFICIAL_HOSTS # noqa: E402
|
|
17
|
+
|
|
18
|
+
STACKS = {
|
|
19
|
+
"react-native", "flutter", "swiftui", "jetpack-compose", "avalonia",
|
|
20
|
+
"uwp", "winui", "wpf", "uno", "javafx", "threejs", "laravel",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _rows(stack):
|
|
25
|
+
path = DATA_DIR / STACK_CONFIG[stack]["file"]
|
|
26
|
+
with path.open(encoding="utf-8", newline="") as handle:
|
|
27
|
+
return list(csv.DictReader(handle))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestNativeDesktopStackFreshness(unittest.TestCase):
|
|
31
|
+
def test_rows_have_final_freshness_metadata(self):
|
|
32
|
+
for stack in STACKS:
|
|
33
|
+
for row in _rows(stack):
|
|
34
|
+
with self.subTest(stack=stack, row=row["No"]):
|
|
35
|
+
expected = "deprecated" if stack == "uwp" else "active"
|
|
36
|
+
self.assertEqual(row["Status"], expected)
|
|
37
|
+
self.assertTrue(row["Applies To"].startswith(
|
|
38
|
+
STACK_CURRENT_APPLICABILITY[stack]))
|
|
39
|
+
self.assertRegex(row["Verified At"], r"^\d{4}-\d{2}-\d{2}$")
|
|
40
|
+
self.assertEqual("legacy" in row["Applies To"], stack == "uwp")
|
|
41
|
+
|
|
42
|
+
def test_high_impact_rows_use_official_sources(self):
|
|
43
|
+
for stack in STACKS:
|
|
44
|
+
for row in _rows(stack):
|
|
45
|
+
if row["Severity"] not in {"Critical", "High"}:
|
|
46
|
+
continue
|
|
47
|
+
with self.subTest(stack=stack, row=row["No"]):
|
|
48
|
+
parsed = urlsplit(row["Docs URL"])
|
|
49
|
+
self.assertEqual(parsed.scheme, "https")
|
|
50
|
+
self.assertIn(parsed.hostname, STACK_OFFICIAL_HOSTS[stack])
|
|
51
|
+
|
|
52
|
+
def test_current_mobile_contracts(self):
|
|
53
|
+
cases = {
|
|
54
|
+
("react-native", "Hermes bundled default engine"): "hermes",
|
|
55
|
+
("flutter", "predictive back result callback"): "onpopinvokedwithresult",
|
|
56
|
+
("flutter", "accessible nonlinear text scaling"): "textscaler",
|
|
57
|
+
("swiftui", "multicolumn navigation sidebar detail"): "navigationsplitview",
|
|
58
|
+
}
|
|
59
|
+
for (stack, query), expected in cases.items():
|
|
60
|
+
with self.subTest(stack=stack):
|
|
61
|
+
result = search_stack(query, stack, max_results=1)
|
|
62
|
+
self.assertEqual(result["count"], 1)
|
|
63
|
+
recommended = " ".join(
|
|
64
|
+
result["results"][0][field]
|
|
65
|
+
for field in ("Guideline", "Do", "Code Good")
|
|
66
|
+
).casefold()
|
|
67
|
+
self.assertIn(expected, recommended)
|
|
68
|
+
|
|
69
|
+
def test_windows_current_and_maintenance_lanes_are_visible(self):
|
|
70
|
+
current = search_stack("new Windows desktop app Windows App SDK", "winui")
|
|
71
|
+
legacy = search_stack("UWP maintenance x:Bind migration", "uwp")
|
|
72
|
+
self.assertGreater(current["count"], 0)
|
|
73
|
+
self.assertGreater(legacy["count"], 0)
|
|
74
|
+
self.assertEqual({row["Status"] for row in current["results"]}, {"active"})
|
|
75
|
+
self.assertEqual({row["Status"] for row in legacy["results"]}, {"deprecated"})
|
|
76
|
+
self.assertTrue(any("winui" in " ".join(row.values()).casefold()
|
|
77
|
+
for row in legacy["results"]))
|
|
78
|
+
successor = search_stack(
|
|
79
|
+
"which Windows UI framework should a brand new app choose instead of legacy UWP",
|
|
80
|
+
"uwp", max_results=1,
|
|
81
|
+
)
|
|
82
|
+
self.assertEqual("Prefer WinUI 3 for new projects", successor["results"][0]["Guideline"])
|
|
83
|
+
|
|
84
|
+
def test_old_version_without_curated_rows_abstains(self):
|
|
85
|
+
cases = {
|
|
86
|
+
"react-native": "React Native 0.75 Hermes",
|
|
87
|
+
"flutter": "Flutter SDK 3.22 back navigation",
|
|
88
|
+
"swiftui": "iOS 15 SwiftUI navigation",
|
|
89
|
+
"avalonia": "Avalonia UI v11 storage picker",
|
|
90
|
+
"winui": "WinUI SDK 2 desktop app",
|
|
91
|
+
"javafx": "JavaFX SDK 21 table view",
|
|
92
|
+
"threejs": "Three.js r128 OrbitControls",
|
|
93
|
+
"laravel": "Laravel 12 validation",
|
|
94
|
+
}
|
|
95
|
+
for stack, query in cases.items():
|
|
96
|
+
with self.subTest(stack=stack):
|
|
97
|
+
self.assertEqual(search_stack(query, stack)["count"], 0)
|
|
98
|
+
|
|
99
|
+
def test_migration_intent_returns_current_replacements(self):
|
|
100
|
+
cases = {
|
|
101
|
+
"flutter": "replace deprecated WillPopScope with current Flutter API",
|
|
102
|
+
"react-native": "upgrade legacy Hermes configuration",
|
|
103
|
+
"threejs": "replace deprecated outputEncoding with current color space",
|
|
104
|
+
}
|
|
105
|
+
for stack, query in cases.items():
|
|
106
|
+
with self.subTest(stack=stack):
|
|
107
|
+
result = search_stack(query, stack)
|
|
108
|
+
self.assertGreater(result["count"], 0)
|
|
109
|
+
self.assertEqual({row["Status"] for row in result["results"]}, {"active"})
|
|
110
|
+
|
|
111
|
+
versioned_cases = {
|
|
112
|
+
"react-native": "upgrade React Native 0.75 to React Native 0.86 Hermes",
|
|
113
|
+
"flutter": "upgrade Flutter 3.22 to Flutter 3.44 predictive back",
|
|
114
|
+
"threejs": "upgrade Three.js r128 to r185 outputColorSpace",
|
|
115
|
+
}
|
|
116
|
+
for stack, query in versioned_cases.items():
|
|
117
|
+
with self.subTest(stack=stack, query=query):
|
|
118
|
+
result = search_stack(query, stack)
|
|
119
|
+
self.assertGreater(result["count"], 0)
|
|
120
|
+
self.assertEqual({row["Status"] for row in result["results"]}, {"active"})
|
|
121
|
+
|
|
122
|
+
def test_standalone_current_and_deprecated_identifiers_resolve_replacement(self):
|
|
123
|
+
cases = {
|
|
124
|
+
("flutter", "onPopInvokedWithResult"): "PopScope",
|
|
125
|
+
("flutter", "TextScaler"): "large fonts",
|
|
126
|
+
("threejs", "outputColorSpace"): "Color Space",
|
|
127
|
+
("threejs", "outputEncoding"): "Color Space",
|
|
128
|
+
}
|
|
129
|
+
for (stack, query), guideline in cases.items():
|
|
130
|
+
with self.subTest(stack=stack, query=query):
|
|
131
|
+
result = search_stack(query, stack, max_results=1)
|
|
132
|
+
self.assertEqual(result["count"], 1)
|
|
133
|
+
self.assertIn(guideline.casefold(), result["results"][0]["Guideline"].casefold())
|
|
134
|
+
|
|
135
|
+
def test_current_threejs_uses_supported_module_and_color_apis(self):
|
|
136
|
+
cases = {
|
|
137
|
+
"Three.js current OrbitControls addon import": "three/addons/",
|
|
138
|
+
"Three.js current renderer color space": "outputcolorspace",
|
|
139
|
+
"Three.js frame-rate independent delta time": "three.timer",
|
|
140
|
+
}
|
|
141
|
+
for query, expected in cases.items():
|
|
142
|
+
with self.subTest(query=query):
|
|
143
|
+
result = search_stack(query, "threejs", max_results=1)
|
|
144
|
+
self.assertEqual(result["count"], 1)
|
|
145
|
+
recommended = " ".join(
|
|
146
|
+
result["results"][0][field]
|
|
147
|
+
for field in ("Guideline", "Do", "Code Good")
|
|
148
|
+
).casefold()
|
|
149
|
+
self.assertIn(expected, recommended)
|
|
150
|
+
|
|
151
|
+
def test_deprecated_symbols_are_not_recommended_by_current_rows(self):
|
|
152
|
+
forbidden = {
|
|
153
|
+
"react-native": ("hermes_enabled",),
|
|
154
|
+
"flutter": ("willpopscope", "onpopinvoked:", "textscalefactor"),
|
|
155
|
+
"swiftui": ("navigationview", "presentationmode"),
|
|
156
|
+
"winui": ("dispatcher.runasync", "system.windows"),
|
|
157
|
+
"uno": ("system.windows", 'requestedtheme="default"'),
|
|
158
|
+
"javafx": ("fxpermission", "javadoc/21"),
|
|
159
|
+
"threejs": (
|
|
160
|
+
"r128", "three.orbitcontrols", "outputencoding",
|
|
161
|
+
"three.srgbencoding", "examples/js/controls",
|
|
162
|
+
"three.clock", "clock.getdelta", "clock.getelapsedtime",
|
|
163
|
+
"pcfsoftshadowmap", "three/examples/jsm/",
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
for stack, tokens in forbidden.items():
|
|
167
|
+
for row in _rows(stack):
|
|
168
|
+
if row["Status"] != "active":
|
|
169
|
+
continue
|
|
170
|
+
recommended = " ".join(
|
|
171
|
+
row[field] for field in ("Guideline", "Do", "Code Good")
|
|
172
|
+
).casefold()
|
|
173
|
+
for token in tokens:
|
|
174
|
+
with self.subTest(stack=stack, row=row["No"], token=token):
|
|
175
|
+
self.assertNotIn(token, recommended)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
unittest.main()
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for metric math and relevance fixture validation."""
|
|
3
|
+
|
|
4
|
+
import importlib.util
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ROOT = next(parent for parent in Path(__file__).resolve().parents
|
|
10
|
+
if (parent / "scripts/evaluate-relevance.py").exists())
|
|
11
|
+
MODULE_PATH = ROOT / "scripts/evaluate-relevance.py"
|
|
12
|
+
SPEC = importlib.util.spec_from_file_location("evaluate_relevance", MODULE_PATH)
|
|
13
|
+
evaluator = importlib.util.module_from_spec(SPEC)
|
|
14
|
+
SPEC.loader.exec_module(evaluator)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestMetricMath(unittest.TestCase):
|
|
18
|
+
def test_precision_counts_missing_ranks_as_non_relevant(self):
|
|
19
|
+
self.assertEqual(evaluator.precision_at_k([2], 1), 1.0)
|
|
20
|
+
self.assertAlmostEqual(evaluator.precision_at_k([2], 3), 1 / 3)
|
|
21
|
+
self.assertEqual(evaluator.precision_at_k([], 3), 0.0)
|
|
22
|
+
self.assertEqual(evaluator.precision_at_k([2], 0), 0.0)
|
|
23
|
+
|
|
24
|
+
def test_reciprocal_rank_stops_at_k(self):
|
|
25
|
+
self.assertEqual(evaluator.reciprocal_rank([0, 2, 0]), 0.5)
|
|
26
|
+
self.assertEqual(evaluator.reciprocal_rank([0, 0, 0, 2]), 0.0)
|
|
27
|
+
self.assertEqual(evaluator.reciprocal_rank([]), 0.0)
|
|
28
|
+
|
|
29
|
+
def test_ndcg_uses_graded_gain_and_handles_empty_ideal(self):
|
|
30
|
+
self.assertEqual(evaluator.ndcg_at_k([2, 1], [2, 1]), 1.0)
|
|
31
|
+
self.assertLess(evaluator.ndcg_at_k([1, 2], [2, 1]), 1.0)
|
|
32
|
+
self.assertEqual(evaluator.ndcg_at_k([], [], 3), 0.0)
|
|
33
|
+
|
|
34
|
+
def test_result_grades_match_identity_subsets(self):
|
|
35
|
+
results = [
|
|
36
|
+
{"Category": "State", "Guideline": "Use useState", "Severity": "Medium"},
|
|
37
|
+
{"Category": "State", "Guideline": "Use useReducer", "Severity": "Medium"},
|
|
38
|
+
]
|
|
39
|
+
judgments = [
|
|
40
|
+
{"identity": {"Guideline": "Use useReducer"}, "grade": 2},
|
|
41
|
+
{"identity": {"Category": "State"}, "grade": 1},
|
|
42
|
+
]
|
|
43
|
+
self.assertEqual(evaluator.grades_for_results(results, judgments), [1, 2])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestFixtureValidation(unittest.TestCase):
|
|
47
|
+
@staticmethod
|
|
48
|
+
def valid_fixture():
|
|
49
|
+
case = {
|
|
50
|
+
"id": "domain-style-minimal",
|
|
51
|
+
"split": "calibration",
|
|
52
|
+
"mode": "domain",
|
|
53
|
+
"domain": "style",
|
|
54
|
+
"query": "minimal grid",
|
|
55
|
+
"judgments": [{"identity": {"Style Category": "Minimalism"}, "grade": 2}],
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
"schemaVersion": 1,
|
|
59
|
+
"globalNegativeApplicability": {"domains": ["style"], "stacks": []},
|
|
60
|
+
"cases": [dict(case, id=f"case-{index}") for index in range(60)],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def test_valid_schema(self):
|
|
64
|
+
self.assertEqual(evaluator.validate_fixture(self.valid_fixture(), {"style": {}}, []), [])
|
|
65
|
+
|
|
66
|
+
def test_rejects_bad_count_duplicate_id_and_grade(self):
|
|
67
|
+
fixture = self.valid_fixture()
|
|
68
|
+
fixture["cases"] = fixture["cases"][:2]
|
|
69
|
+
fixture["cases"][1]["id"] = fixture["cases"][0]["id"]
|
|
70
|
+
fixture["cases"][0]["judgments"][0]["grade"] = 3
|
|
71
|
+
errors = "\n".join(evaluator.validate_fixture(fixture, {"style": {}}, []))
|
|
72
|
+
self.assertIn("60-100", errors)
|
|
73
|
+
self.assertIn("duplicate case id", errors)
|
|
74
|
+
self.assertIn("grade 1 or 2", errors)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestThresholdGate(unittest.TestCase):
|
|
78
|
+
def test_runtime_fingerprint_binds_reasoning_contract(self):
|
|
79
|
+
original = evaluator.ROOT, evaluator.RUNTIME_DIR, evaluator.DATA_DIR
|
|
80
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
81
|
+
root = Path(tmp)
|
|
82
|
+
runtime = root / "src/ui-ux-pro-max/scripts"
|
|
83
|
+
data = root / "src/ui-ux-pro-max/data"
|
|
84
|
+
runtime.mkdir(parents=True)
|
|
85
|
+
data.mkdir(parents=True)
|
|
86
|
+
for name in ("core.py", "design_system.py", "reasoning_contract.py"):
|
|
87
|
+
(runtime / name).write_text(name, encoding="utf-8")
|
|
88
|
+
(data / "styles.csv").write_text("No,Style\n1,Test\n", encoding="utf-8")
|
|
89
|
+
evaluator.ROOT, evaluator.RUNTIME_DIR, evaluator.DATA_DIR = root, runtime, data
|
|
90
|
+
try:
|
|
91
|
+
before = evaluator.runtime_fingerprint()
|
|
92
|
+
(runtime / "reasoning_contract.py").write_text("changed", encoding="utf-8")
|
|
93
|
+
self.assertNotEqual(before, evaluator.runtime_fingerprint())
|
|
94
|
+
finally:
|
|
95
|
+
evaluator.ROOT, evaluator.RUNTIME_DIR, evaluator.DATA_DIR = original
|
|
96
|
+
|
|
97
|
+
def test_oracle_fingerprint_hashes_the_selected_cases_file(self):
|
|
98
|
+
canonical = evaluator.FIXTURE_DIR / "relevance-cases.json"
|
|
99
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
100
|
+
selected = Path(tmp) / "cases.json"
|
|
101
|
+
selected.write_bytes(canonical.read_bytes())
|
|
102
|
+
self.assertEqual(
|
|
103
|
+
evaluator.oracle_fingerprint(selected), evaluator.oracle_fingerprint(canonical))
|
|
104
|
+
selected.write_bytes(canonical.read_bytes() + b" ")
|
|
105
|
+
self.assertNotEqual(
|
|
106
|
+
evaluator.oracle_fingerprint(selected), evaluator.oracle_fingerprint(canonical))
|
|
107
|
+
|
|
108
|
+
def test_metric_sample_and_locked_case_failures_are_actionable(self):
|
|
109
|
+
report = {
|
|
110
|
+
"metrics": {"precisionAt1": 0.5},
|
|
111
|
+
"samples": {"retrieval": 1},
|
|
112
|
+
"cases": [{"id": "locked", "grades": [0], "actual": [{"Style Category": "Wrong"}]}],
|
|
113
|
+
}
|
|
114
|
+
manifest = {
|
|
115
|
+
"metrics": {"precisionAt1": {"floor": 0.8, "tolerance": 0.01}},
|
|
116
|
+
"sampleMinimums": {"retrieval": 2},
|
|
117
|
+
"lockedCases": {"locked": {"withinTop": 1, "minimumGrade": 2}},
|
|
118
|
+
}
|
|
119
|
+
manifest["splits"] = {"calibration": {"metrics": {}, "sampleMinimums": {}},
|
|
120
|
+
"held_out": {"metrics": {}, "sampleMinimums": {}}}
|
|
121
|
+
report["splits"] = {"calibration": {"metrics": {}, "samples": {}},
|
|
122
|
+
"held_out": {"metrics": {}, "samples": {}}}
|
|
123
|
+
failures = evaluator.check_thresholds(report, manifest)
|
|
124
|
+
self.assertEqual(len(failures), 3)
|
|
125
|
+
self.assertTrue(any("Wrong" in failure for failure in failures))
|
|
126
|
+
|
|
127
|
+
def test_manifest_rejects_missing_contract_sections(self):
|
|
128
|
+
errors = evaluator.validate_manifest({}, "fingerprint")
|
|
129
|
+
self.assertTrue(any("missing sections" in error for error in errors))
|
|
130
|
+
self.assertTrue(any("missing metrics" in error for error in errors))
|
|
131
|
+
|
|
132
|
+
def test_manifest_rejects_non_finite_and_invalid_sample_values(self):
|
|
133
|
+
manifest = {
|
|
134
|
+
"schemaVersion": 1,
|
|
135
|
+
"status": "approved",
|
|
136
|
+
"approvingMaintainer": "maintainer",
|
|
137
|
+
"units": "ratios",
|
|
138
|
+
"splitPolicy": {},
|
|
139
|
+
"runtimeFingerprint": "fingerprint",
|
|
140
|
+
"oracleFingerprint": "oracle",
|
|
141
|
+
"baselineRevision": "97eb2a2",
|
|
142
|
+
"metrics": {name: {"floor": float("nan")} for name in evaluator.REQUIRED_METRICS},
|
|
143
|
+
"sampleMinimums": {"cases": True},
|
|
144
|
+
"lockedCases": {"case": {}},
|
|
145
|
+
"splits": {
|
|
146
|
+
split: {
|
|
147
|
+
"metrics": {name: {"floor": 0.0} for name in evaluator.REQUIRED_METRICS},
|
|
148
|
+
"sampleMinimums": {"cases": 1},
|
|
149
|
+
} for split in ("calibration", "held_out")
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
errors = evaluator.validate_manifest(manifest, "fingerprint", "oracle")
|
|
153
|
+
self.assertTrue(any("finite" in error for error in errors))
|
|
154
|
+
self.assertTrue(any("non-negative integer" in error for error in errors))
|
|
155
|
+
|
|
156
|
+
def test_manifest_binds_oracle_and_validates_baseline_revision(self):
|
|
157
|
+
manifest = {
|
|
158
|
+
"schemaVersion": 1,
|
|
159
|
+
"status": "approved",
|
|
160
|
+
"approvingMaintainer": "maintainer",
|
|
161
|
+
"units": "ratios",
|
|
162
|
+
"splitPolicy": {},
|
|
163
|
+
"runtimeFingerprint": "runtime",
|
|
164
|
+
"oracleFingerprint": "wrong",
|
|
165
|
+
"baselineRevision": "not-a-revision",
|
|
166
|
+
"metrics": {name: {"floor": 0.0} for name in evaluator.REQUIRED_METRICS},
|
|
167
|
+
"sampleMinimums": {"cases": 1},
|
|
168
|
+
"lockedCases": {"case": {}},
|
|
169
|
+
"splits": {
|
|
170
|
+
split: {
|
|
171
|
+
"metrics": {name: {"floor": 0.0} for name in evaluator.REQUIRED_METRICS},
|
|
172
|
+
"sampleMinimums": {"cases": 1},
|
|
173
|
+
} for split in ("calibration", "held_out")
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
errors = evaluator.validate_manifest(manifest, "runtime", "expected")
|
|
177
|
+
self.assertTrue(any("oracleFingerprint" in error for error in errors))
|
|
178
|
+
self.assertTrue(any("baselineRevision" in error for error in errors))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Every script invocation in the shipped skill markdown resolves from the skill directory.
|
|
2
|
+
|
|
3
|
+
Regression test for #474. The sub-skills ship in two copies (.claude/skills/<skill>/
|
|
4
|
+
for the plugin, cli/assets/skills/<skill>/ for CLI installs) and land in layouts where
|
|
5
|
+
neither the project root nor ~/.claude/skills/ is a valid anchor: the plugin cache, a
|
|
6
|
+
project's .claude/skills/, ~/.claude/skills/ (--global), or a manual copy. The one anchor
|
|
7
|
+
that exists in all of them is the skill's own directory, so documented commands use
|
|
8
|
+
`scripts/<file>` for the skill's own scripts and `../<skill>/scripts/<file>` for a
|
|
9
|
+
sibling sub-skill (the sub-skills are always installed side by side).
|
|
10
|
+
|
|
11
|
+
This test extracts every `python|python3|node|bash <path>` invocation from every
|
|
12
|
+
markdown file under both trees and asserts that the path is skill-relative and names a
|
|
13
|
+
file that ships. The core skill's `${CLAUDE_PLUGIN_ROOT}/.claude/skills/...` form is
|
|
14
|
+
resolved against the repository root, which is what that variable denotes under a
|
|
15
|
+
plugin install - and accepted only in that file, because the sub-skills also ship
|
|
16
|
+
through the CLI, where the variable does not exist. The grep-based path contract in check-asset-sync.yml is the negative
|
|
17
|
+
side (no home-, project- or variable-rooted paths anywhere, code included); this is
|
|
18
|
+
the positive side (every documented invocation points at a real file).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
REPO = next(
|
|
26
|
+
parent for parent in Path(__file__).resolve().parents
|
|
27
|
+
if (parent / "scripts" / "generate-catalog-summary.py").is_file()
|
|
28
|
+
)
|
|
29
|
+
SKILL_TREES = ("cli/assets/skills", ".claude/skills")
|
|
30
|
+
# The only file that may use the plugin-root form: hand-authored for the plugin install
|
|
31
|
+
# and not shipped by the CLI (sync-assets.mjs mirrors data/ and scripts/, never SKILL.md).
|
|
32
|
+
# (Built from segments: the path contract in check-asset-sync.yml scans this file too.)
|
|
33
|
+
PLUGIN_ONLY_FILE = Path(".claude") / "skills" / "ui-ux-pro-max" / "SKILL.md"
|
|
34
|
+
INVOCATION = re.compile(r'(?<![\w/.-])(?:python3?|node|bash)\s+"?([^\s"`\']+\.(?:py|cjs|js|mjs|sh))')
|
|
35
|
+
PLUGIN_ROOT = "${CLAUDE_PLUGIN_ROOT}/"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def shipped_invocations():
|
|
39
|
+
for tree in SKILL_TREES:
|
|
40
|
+
for skill_dir in sorted((REPO / tree).iterdir()):
|
|
41
|
+
if not skill_dir.is_dir():
|
|
42
|
+
continue
|
|
43
|
+
for md in sorted(skill_dir.rglob("*.md")):
|
|
44
|
+
for lineno, line in enumerate(md.read_text(encoding="utf-8").splitlines(), 1):
|
|
45
|
+
for match in INVOCATION.finditer(line):
|
|
46
|
+
yield skill_dir, md, lineno, match.group(1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve(skill_dir, md, path):
|
|
50
|
+
"""Return (target, None) for a skill-relative path, or (None, reason)."""
|
|
51
|
+
if path.startswith(PLUGIN_ROOT):
|
|
52
|
+
if md.relative_to(REPO) != PLUGIN_ONLY_FILE:
|
|
53
|
+
return None, "the ${CLAUDE_PLUGIN_ROOT} form is only valid in the plugin-only core SKILL.md"
|
|
54
|
+
return REPO / path[len(PLUGIN_ROOT):], None
|
|
55
|
+
if path.startswith("scripts/"):
|
|
56
|
+
return skill_dir / path, None
|
|
57
|
+
if path.startswith("../"):
|
|
58
|
+
parts = path.split("/")
|
|
59
|
+
if len(parts) > 3 and parts[2] == "scripts" and (skill_dir.parent / parts[1]).is_dir():
|
|
60
|
+
return skill_dir.parent / parts[1] / "/".join(parts[2:]), None
|
|
61
|
+
return None, "a sibling invocation must be ../<skill>/scripts/<file> and the sibling must ship"
|
|
62
|
+
return None, "not skill-relative (expected scripts/<file> or ../<skill>/scripts/<file>)"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SkillScriptPathsTest(unittest.TestCase):
|
|
66
|
+
def test_every_shipped_markdown_invocation_resolves_from_the_skill_directory(self):
|
|
67
|
+
problems, seen = [], 0
|
|
68
|
+
for skill_dir, md, lineno, path in shipped_invocations():
|
|
69
|
+
seen += 1
|
|
70
|
+
target, reason = resolve(skill_dir, md, path)
|
|
71
|
+
if reason is None and not target.is_file():
|
|
72
|
+
reason = f"no such file: {target}"
|
|
73
|
+
if reason:
|
|
74
|
+
problems.append(f"{md.relative_to(REPO)}:{lineno}: {path} -- {reason}")
|
|
75
|
+
# Guard against a silently broken extractor: the two trees carry well over
|
|
76
|
+
# a hundred documented invocations between them.
|
|
77
|
+
self.assertGreater(seen, 100, f"extractor found only {seen} invocations")
|
|
78
|
+
self.assertEqual(problems, [], "\n" + "\n".join(problems))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
unittest.main()
|