@codyswann/lisa 2.178.1 → 2.178.3
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/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/rules/reference/integration-access-layer.md +1 -1
- package/plugins/lisa/skills/analyze-claude-remote/SKILL.md +50 -6
- package/plugins/lisa/skills/generate-claude-remote-build-script/SKILL.md +21 -0
- package/plugins/lisa/skills/jam-access/SKILL.md +12 -11
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/analyze-claude-remote/SKILL.md +50 -6
- package/plugins/lisa-agy/skills/generate-claude-remote-build-script/SKILL.md +21 -0
- package/plugins/lisa-agy/skills/jam-access/SKILL.md +12 -11
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +1 -1
- package/plugins/lisa-copilot/skills/analyze-claude-remote/SKILL.md +50 -6
- package/plugins/lisa-copilot/skills/generate-claude-remote-build-script/SKILL.md +21 -0
- package/plugins/lisa-copilot/skills/jam-access/SKILL.md +12 -11
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +1 -1
- package/plugins/lisa-cursor/skills/analyze-claude-remote/SKILL.md +50 -6
- package/plugins/lisa-cursor/skills/generate-claude-remote-build-script/SKILL.md +21 -0
- package/plugins/lisa-cursor/skills/jam-access/SKILL.md +12 -11
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/skills/atomic-design-gluestack/SKILL.md +8 -0
- package/plugins/lisa-expo/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
- package/plugins/lisa-expo/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
- package/plugins/lisa-expo/skills/gluestack-nativewind/SKILL.md +8 -0
- package/plugins/lisa-expo/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
- package/plugins/lisa-expo/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/SKILL.md +8 -0
- package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
- package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
- package/plugins/lisa-expo-agy/skills/gluestack-nativewind/SKILL.md +8 -0
- package/plugins/lisa-expo-agy/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
- package/plugins/lisa-expo-agy/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/SKILL.md +8 -0
- package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
- package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
- package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/SKILL.md +8 -0
- package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
- package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/SKILL.md +8 -0
- package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
- package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
- package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/SKILL.md +8 -0
- package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
- package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/reference/integration-access-layer.md +1 -1
- package/plugins/src/base/skills/analyze-claude-remote/SKILL.md +50 -6
- package/plugins/src/base/skills/generate-claude-remote-build-script/SKILL.md +21 -0
- package/plugins/src/base/skills/jam-access/SKILL.md +12 -11
- package/plugins/src/expo/skills/atomic-design-gluestack/SKILL.md +8 -0
- package/plugins/src/expo/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
- package/plugins/src/expo/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
- package/plugins/src/expo/skills/gluestack-nativewind/SKILL.md +8 -0
- package/plugins/src/expo/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
- package/plugins/src/expo/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for validate_atomic_structure.py seal detection helpers.
|
|
3
|
+
|
|
4
|
+
Covers:
|
|
5
|
+
- find_project_roots: should return ALL ancestor roots (not just first)
|
|
6
|
+
- has_design_system_eslint_rule: should only match actual rule/plugin syntax
|
|
7
|
+
- has_design_system_eslint_rule: should check .eslintrc.yaml/.yml, eslint.config.mts/.cts
|
|
8
|
+
- has_design_system_eslint_rule: should parse package.json eslintConfig field
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Allow importing the module under test
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
19
|
+
from validate_atomic_structure import (
|
|
20
|
+
find_project_roots,
|
|
21
|
+
has_design_system_eslint_rule,
|
|
22
|
+
has_sealed_design_system,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestFindProjectRoots(unittest.TestCase):
|
|
27
|
+
"""find_project_roots should return ALL package boundaries, not just the nearest."""
|
|
28
|
+
|
|
29
|
+
def test_returns_all_ancestor_roots_in_monorepo(self) -> None:
|
|
30
|
+
"""In a monorepo, both the inner package root and the workspace root are returned."""
|
|
31
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
32
|
+
workspace = Path(tmp) / "workspace"
|
|
33
|
+
inner = workspace / "apps" / "mobile"
|
|
34
|
+
inner.mkdir(parents=True)
|
|
35
|
+
|
|
36
|
+
# workspace root: has .git
|
|
37
|
+
(workspace / ".git").mkdir()
|
|
38
|
+
# inner package: has package.json
|
|
39
|
+
(inner / "package.json").write_text("{}", encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
source_file = inner / "src" / "components"
|
|
42
|
+
source_file.mkdir(parents=True)
|
|
43
|
+
|
|
44
|
+
roots = find_project_roots(str(source_file))
|
|
45
|
+
# Both the inner package root and the workspace root should be returned.
|
|
46
|
+
# Use .resolve() to handle symlinked temp dirs (e.g. /var → /private/var on macOS).
|
|
47
|
+
self.assertIn(inner.resolve(), roots)
|
|
48
|
+
self.assertIn(workspace.resolve(), roots)
|
|
49
|
+
|
|
50
|
+
def test_returns_single_root_for_simple_project(self) -> None:
|
|
51
|
+
"""A flat project returns its single root."""
|
|
52
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
53
|
+
project = Path(tmp) / "project"
|
|
54
|
+
project.mkdir()
|
|
55
|
+
(project / "package.json").write_text("{}", encoding="utf-8")
|
|
56
|
+
(project / ".git").mkdir()
|
|
57
|
+
|
|
58
|
+
roots = find_project_roots(str(project / "src"))
|
|
59
|
+
self.assertEqual(len(roots), 1)
|
|
60
|
+
# Use .resolve() to handle symlinked temp dirs (e.g. /var → /private/var on macOS).
|
|
61
|
+
self.assertEqual(roots[0], project.resolve())
|
|
62
|
+
|
|
63
|
+
def test_falls_back_to_current_directory_when_no_root_found(self) -> None:
|
|
64
|
+
"""When no package.json or .git exists, returns the starting path."""
|
|
65
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
66
|
+
orphan = Path(tmp) / "orphan" / "dir"
|
|
67
|
+
orphan.mkdir(parents=True)
|
|
68
|
+
roots = find_project_roots(str(orphan))
|
|
69
|
+
self.assertEqual(len(roots), 1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestHasDesignSystemEslintRule(unittest.TestCase):
|
|
73
|
+
"""has_design_system_eslint_rule should match actual rule/plugin identifiers only."""
|
|
74
|
+
|
|
75
|
+
def test_ignores_comment_containing_design_system_substring(self) -> None:
|
|
76
|
+
"""A comment that mentions 'design-system' without a slash must not trigger the seal."""
|
|
77
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
78
|
+
root = Path(tmp)
|
|
79
|
+
eslint = root / ".eslintrc.json"
|
|
80
|
+
eslint.write_text(
|
|
81
|
+
'{"rules": {}, "// note": "do not use design-system here"}',
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
self.assertFalse(
|
|
85
|
+
has_design_system_eslint_rule(root),
|
|
86
|
+
msg="Plain 'design-system' substring in a comment must not match",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_matches_design_system_slash_rule_syntax(self) -> None:
|
|
90
|
+
"""An ESLint rule like 'design-system/no-raw-colors' should trigger the seal."""
|
|
91
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
92
|
+
root = Path(tmp)
|
|
93
|
+
eslint = root / ".eslintrc.json"
|
|
94
|
+
eslint.write_text(
|
|
95
|
+
'{"rules": {"design-system/no-raw-colors": "error"}}',
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
)
|
|
98
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
99
|
+
|
|
100
|
+
def test_checks_eslintrc_yaml(self) -> None:
|
|
101
|
+
""".eslintrc.yaml is a valid ESLint config and must be checked."""
|
|
102
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
103
|
+
root = Path(tmp)
|
|
104
|
+
yaml_config = root / ".eslintrc.yaml"
|
|
105
|
+
yaml_config.write_text(
|
|
106
|
+
'rules:\n design-system/no-raw-colors: error\n',
|
|
107
|
+
encoding="utf-8",
|
|
108
|
+
)
|
|
109
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
110
|
+
|
|
111
|
+
def test_checks_eslintrc_yml(self) -> None:
|
|
112
|
+
""".eslintrc.yml is a valid ESLint config and must be checked."""
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
114
|
+
root = Path(tmp)
|
|
115
|
+
yml_config = root / ".eslintrc.yml"
|
|
116
|
+
yml_config.write_text(
|
|
117
|
+
'rules:\n design-system/no-raw-colors: error\n',
|
|
118
|
+
encoding="utf-8",
|
|
119
|
+
)
|
|
120
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
121
|
+
|
|
122
|
+
def test_checks_eslint_config_mts(self) -> None:
|
|
123
|
+
"""eslint.config.mts is a valid ESLint flat-config entry point and must be checked."""
|
|
124
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
125
|
+
root = Path(tmp)
|
|
126
|
+
mts_config = root / "eslint.config.mts"
|
|
127
|
+
mts_config.write_text(
|
|
128
|
+
'export default [{"rules": {"design-system/no-raw-colors": "error"}}];',
|
|
129
|
+
encoding="utf-8",
|
|
130
|
+
)
|
|
131
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
132
|
+
|
|
133
|
+
def test_checks_eslint_config_cts(self) -> None:
|
|
134
|
+
"""eslint.config.cts is a valid ESLint flat-config entry point and must be checked."""
|
|
135
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
136
|
+
root = Path(tmp)
|
|
137
|
+
cts_config = root / "eslint.config.cts"
|
|
138
|
+
cts_config.write_text(
|
|
139
|
+
'module.exports = [{"rules": {"design-system/no-raw-colors": "error"}}];',
|
|
140
|
+
encoding="utf-8",
|
|
141
|
+
)
|
|
142
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
143
|
+
|
|
144
|
+
def test_checks_package_json_eslintconfig_field(self) -> None:
|
|
145
|
+
"""The eslintConfig key in package.json must be checked for design-system rules."""
|
|
146
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
147
|
+
root = Path(tmp)
|
|
148
|
+
pkg = {
|
|
149
|
+
"name": "my-app",
|
|
150
|
+
"eslintConfig": {
|
|
151
|
+
"rules": {"design-system/no-raw-colors": "error"}
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
(root / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
|
|
155
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
156
|
+
|
|
157
|
+
def test_package_json_non_eslintconfig_does_not_trigger(self) -> None:
|
|
158
|
+
"""A 'design-system' substring in package.json outside eslintConfig must not match."""
|
|
159
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
160
|
+
root = Path(tmp)
|
|
161
|
+
pkg = {
|
|
162
|
+
"name": "my-app",
|
|
163
|
+
"dependencies": {"design-system": "^1.0.0"},
|
|
164
|
+
}
|
|
165
|
+
(root / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
|
|
166
|
+
self.assertFalse(
|
|
167
|
+
has_design_system_eslint_rule(root),
|
|
168
|
+
msg="'design-system' in dependencies must not trigger the ESLint seal check",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestHasSealedDesignSystem(unittest.TestCase):
|
|
173
|
+
"""has_sealed_design_system seal-detection logic."""
|
|
174
|
+
|
|
175
|
+
def test_seal_marker_alone_is_sufficient(self) -> None:
|
|
176
|
+
"""A seal marker without an atom barrel or .claude/rules directory seals the project."""
|
|
177
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
178
|
+
project = Path(tmp) / "project"
|
|
179
|
+
project.mkdir()
|
|
180
|
+
(project / ".git").mkdir()
|
|
181
|
+
docs = project / "docs"
|
|
182
|
+
docs.mkdir()
|
|
183
|
+
(docs / "design-system-rfc.md").write_text("", encoding="utf-8")
|
|
184
|
+
# Deliberately NO atom barrel and NO .claude/rules directory.
|
|
185
|
+
|
|
186
|
+
self.assertTrue(
|
|
187
|
+
has_sealed_design_system(str(project)),
|
|
188
|
+
msg="A single seal marker must be sufficient to detect a sealed design system",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def test_atom_barrel_alone_is_sufficient(self) -> None:
|
|
192
|
+
"""A project exposing only an atoms barrel is treated as having a sealed design system."""
|
|
193
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
194
|
+
project = Path(tmp) / "project"
|
|
195
|
+
project.mkdir()
|
|
196
|
+
(project / ".git").mkdir()
|
|
197
|
+
atoms = project / "src" / "components" / "atoms"
|
|
198
|
+
atoms.mkdir(parents=True)
|
|
199
|
+
(atoms / "index.ts").write_text("", encoding="utf-8")
|
|
200
|
+
# Deliberately NO seal marker and NO .claude/rules directory.
|
|
201
|
+
|
|
202
|
+
self.assertTrue(
|
|
203
|
+
has_sealed_design_system(str(project)),
|
|
204
|
+
msg="An atoms barrel alone must be sufficient to detect a sealed design system",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestHasSealedDesignSystemMonorepo(unittest.TestCase):
|
|
209
|
+
"""has_sealed_design_system should find seal markers in ancestor (workspace) roots."""
|
|
210
|
+
|
|
211
|
+
def test_finds_seal_marker_at_workspace_root_not_inner_package(self) -> None:
|
|
212
|
+
"""Seal markers placed at the workspace root must be respected for inner packages."""
|
|
213
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
214
|
+
workspace = Path(tmp) / "workspace"
|
|
215
|
+
inner = workspace / "apps" / "mobile"
|
|
216
|
+
inner.mkdir(parents=True)
|
|
217
|
+
|
|
218
|
+
# workspace root: .git + seal marker
|
|
219
|
+
(workspace / ".git").mkdir()
|
|
220
|
+
rules_dir = workspace / ".claude" / "rules"
|
|
221
|
+
rules_dir.mkdir(parents=True)
|
|
222
|
+
(rules_dir / "use-the-design-library.md").write_text("", encoding="utf-8")
|
|
223
|
+
|
|
224
|
+
# inner package: package.json + atom barrel
|
|
225
|
+
(inner / "package.json").write_text("{}", encoding="utf-8")
|
|
226
|
+
atoms = inner / "components" / "atoms"
|
|
227
|
+
atoms.mkdir(parents=True)
|
|
228
|
+
(atoms / "index.ts").write_text("", encoding="utf-8")
|
|
229
|
+
|
|
230
|
+
target_dir = inner / "src" / "components" / "atoms"
|
|
231
|
+
target_dir.mkdir(parents=True)
|
|
232
|
+
|
|
233
|
+
self.assertTrue(
|
|
234
|
+
has_sealed_design_system(str(target_dir)),
|
|
235
|
+
msg="Seal marker at workspace root must be detected even for nested packages",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
unittest.main()
|
|
@@ -13,6 +13,7 @@ Usage:
|
|
|
13
13
|
If no path provided, validates the entire project from current directory.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import json
|
|
16
17
|
import os
|
|
17
18
|
import re
|
|
18
19
|
import sys
|
|
@@ -29,6 +30,109 @@ ATOMIC_LEVELS = {
|
|
|
29
30
|
"app": 4, # Expo Router pages
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
SEALED_DESIGN_SYSTEM_MARKERS = [
|
|
34
|
+
".claude/rules/use-the-design-library.md",
|
|
35
|
+
".claude/rules/figma-design-system.md",
|
|
36
|
+
"docs/design-system/tokens.md",
|
|
37
|
+
"docs/design-system-rfc.md",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
ATOM_BARREL_MARKERS = [
|
|
41
|
+
"components/atoms/index.ts",
|
|
42
|
+
"components/atoms/index.tsx",
|
|
43
|
+
"src/components/atoms/index.ts",
|
|
44
|
+
"src/components/atoms/index.tsx",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def find_project_roots(path: str) -> list[Path]:
|
|
49
|
+
"""Find candidate roots for marker detection, from nearest to farthest.
|
|
50
|
+
|
|
51
|
+
Returns all package boundaries (package.json or .git directories) found
|
|
52
|
+
when walking up from ``path``. In a monorepo this means both the inner
|
|
53
|
+
package root and the workspace root are returned, so seal markers stored at
|
|
54
|
+
the workspace level are not missed.
|
|
55
|
+
"""
|
|
56
|
+
current = Path(path).resolve()
|
|
57
|
+
if current.is_file():
|
|
58
|
+
current = current.parent
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
candidate
|
|
62
|
+
for candidate in [current, *current.parents]
|
|
63
|
+
if (candidate / "package.json").exists() or (candidate / ".git").exists()
|
|
64
|
+
] or [current]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def has_design_system_eslint_rule(project_root: Path) -> bool:
|
|
68
|
+
"""Detect project-owned design-system lint rules.
|
|
69
|
+
|
|
70
|
+
Only matches actual rule/plugin identifier syntax (``design-system/``) so
|
|
71
|
+
that comments, package names, or arbitrary strings containing
|
|
72
|
+
``design-system`` as a substring do not trigger a false positive.
|
|
73
|
+
"""
|
|
74
|
+
eslint_files = [
|
|
75
|
+
".eslintrc",
|
|
76
|
+
".eslintrc.js",
|
|
77
|
+
".eslintrc.cjs",
|
|
78
|
+
".eslintrc.yaml",
|
|
79
|
+
".eslintrc.yml",
|
|
80
|
+
".eslintrc.json",
|
|
81
|
+
"eslint.config.js",
|
|
82
|
+
"eslint.config.mjs",
|
|
83
|
+
"eslint.config.cjs",
|
|
84
|
+
"eslint.config.ts",
|
|
85
|
+
"eslint.config.mts",
|
|
86
|
+
"eslint.config.cts",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for eslint_file in eslint_files:
|
|
90
|
+
path = project_root / eslint_file
|
|
91
|
+
if not path.exists() or not path.is_file():
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
content = path.read_text(encoding="utf-8")
|
|
95
|
+
except UnicodeDecodeError:
|
|
96
|
+
continue
|
|
97
|
+
if re.search(r"\bdesign-system/", content):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
# Check package.json eslintConfig field explicitly so that dependency
|
|
101
|
+
# names (e.g. "design-system" in dependencies) don't cause false positives.
|
|
102
|
+
package_json_path = project_root / "package.json"
|
|
103
|
+
if package_json_path.exists() and package_json_path.is_file():
|
|
104
|
+
try:
|
|
105
|
+
pkg = json.loads(package_json_path.read_text(encoding="utf-8"))
|
|
106
|
+
eslint_config = pkg.get("eslintConfig")
|
|
107
|
+
if eslint_config is not None:
|
|
108
|
+
eslint_config_str = json.dumps(eslint_config)
|
|
109
|
+
if re.search(r"\bdesign-system/", eslint_config_str):
|
|
110
|
+
return True
|
|
111
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def has_sealed_design_system(path: str) -> bool:
|
|
118
|
+
"""Check whether this project has a closed local design-system seal."""
|
|
119
|
+
project_roots = find_project_roots(path)
|
|
120
|
+
has_seal_marker = any(
|
|
121
|
+
(root / marker).exists()
|
|
122
|
+
for root in project_roots
|
|
123
|
+
for marker in SEALED_DESIGN_SYSTEM_MARKERS
|
|
124
|
+
)
|
|
125
|
+
has_atom_barrel = any(
|
|
126
|
+
(root / marker).exists()
|
|
127
|
+
for root in project_roots
|
|
128
|
+
for marker in ATOM_BARREL_MARKERS
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if has_seal_marker or has_atom_barrel:
|
|
132
|
+
return True
|
|
133
|
+
return any(has_design_system_eslint_rule(root) for root in project_roots)
|
|
134
|
+
|
|
135
|
+
|
|
32
136
|
# Patterns to identify atomic level from file path
|
|
33
137
|
LEVEL_PATTERNS = [
|
|
34
138
|
(r"/components/atoms/", "atoms"),
|
|
@@ -303,6 +407,11 @@ def main():
|
|
|
303
407
|
print(f"Error: Path '{path}' does not exist")
|
|
304
408
|
sys.exit(1)
|
|
305
409
|
|
|
410
|
+
if has_sealed_design_system(path):
|
|
411
|
+
print("Sealed design system detected; deferring to the project's design-library rules.")
|
|
412
|
+
print("Skipping generic Gluestack atomic-design validation.")
|
|
413
|
+
sys.exit(0)
|
|
414
|
+
|
|
306
415
|
print(f"Validating atomic design structure in: {path}")
|
|
307
416
|
print("-" * 60)
|
|
308
417
|
|
|
@@ -7,6 +7,14 @@ description: This skill enforces Gluestack UI v3 and NativeWind v4 design patter
|
|
|
7
7
|
|
|
8
8
|
This skill enforces constrained, opinionated styling patterns that reduce decision fatigue, improve performance, enable consistent theming, and limit the solution space to canonical patterns.
|
|
9
9
|
|
|
10
|
+
## Sealed Design System Guard
|
|
11
|
+
|
|
12
|
+
Before applying this skill, check whether the target project has a sealed or closed design system. Common markers include `.claude/rules/use-the-design-library.md`, `.claude/rules/figma-design-system.md`, design-system ESLint rules, or a project-owned `@/components/atoms` barrel paired with design-system docs/configuration.
|
|
13
|
+
|
|
14
|
+
If a sealed design system is present, this skill does not apply. Defer to the project's design-library rules and skills instead, such as `use-the-design-library` and `add-design-library-item`. Do not instruct `@/components/ui` imports, Gluestack compound trees, raw shade tokens, margins, or `className` usage above the atom layer when the project seal forbids them.
|
|
15
|
+
|
|
16
|
+
For open Gluestack UI v3 + NativeWind v4 projects without those seal markers, continue with the guidance below.
|
|
17
|
+
|
|
10
18
|
## Core Principles
|
|
11
19
|
|
|
12
20
|
1. **Gluestack components over React Native primitives** - Gluestack wraps RN with theming, accessibility, and cross-platform consistency
|
package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/scripts/test_validate_styling.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for validate_styling.py seal detection helpers.
|
|
3
|
+
|
|
4
|
+
Covers:
|
|
5
|
+
- find_project_roots: should return ALL ancestor roots (not just first)
|
|
6
|
+
- has_design_system_eslint_rule: should only match actual rule/plugin syntax
|
|
7
|
+
- has_design_system_eslint_rule: should check .eslintrc.yaml/.yml, eslint.config.mts/.cts
|
|
8
|
+
- has_design_system_eslint_rule: should parse package.json eslintConfig field
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Allow importing the module under test
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
19
|
+
from validate_styling import (
|
|
20
|
+
find_project_roots,
|
|
21
|
+
has_design_system_eslint_rule,
|
|
22
|
+
has_sealed_design_system,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestFindProjectRoots(unittest.TestCase):
|
|
27
|
+
"""find_project_roots should return ALL package boundaries, not just the nearest."""
|
|
28
|
+
|
|
29
|
+
def test_returns_all_ancestor_roots_in_monorepo(self) -> None:
|
|
30
|
+
"""In a monorepo, both the inner package root and the workspace root are returned."""
|
|
31
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
32
|
+
workspace = Path(tmp) / "workspace"
|
|
33
|
+
inner = workspace / "apps" / "mobile"
|
|
34
|
+
inner.mkdir(parents=True)
|
|
35
|
+
|
|
36
|
+
# workspace root: has .git
|
|
37
|
+
(workspace / ".git").mkdir()
|
|
38
|
+
# inner package: has package.json
|
|
39
|
+
(inner / "package.json").write_text("{}", encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
source_file = inner / "src" / "components"
|
|
42
|
+
source_file.mkdir(parents=True)
|
|
43
|
+
|
|
44
|
+
roots = find_project_roots(str(source_file))
|
|
45
|
+
# Both the inner package root and the workspace root should be returned.
|
|
46
|
+
# Use .resolve() to handle symlinked temp dirs (e.g. /var → /private/var on macOS).
|
|
47
|
+
self.assertIn(inner.resolve(), roots)
|
|
48
|
+
self.assertIn(workspace.resolve(), roots)
|
|
49
|
+
|
|
50
|
+
def test_returns_single_root_for_simple_project(self) -> None:
|
|
51
|
+
"""A flat project returns its single root."""
|
|
52
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
53
|
+
project = Path(tmp) / "project"
|
|
54
|
+
project.mkdir()
|
|
55
|
+
(project / "package.json").write_text("{}", encoding="utf-8")
|
|
56
|
+
(project / ".git").mkdir()
|
|
57
|
+
|
|
58
|
+
roots = find_project_roots(str(project / "src"))
|
|
59
|
+
self.assertEqual(len(roots), 1)
|
|
60
|
+
# Use .resolve() to handle symlinked temp dirs (e.g. /var → /private/var on macOS).
|
|
61
|
+
self.assertEqual(roots[0], project.resolve())
|
|
62
|
+
|
|
63
|
+
def test_falls_back_to_current_directory_when_no_root_found(self) -> None:
|
|
64
|
+
"""When no package.json or .git exists, returns the starting path."""
|
|
65
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
66
|
+
orphan = Path(tmp) / "orphan" / "dir"
|
|
67
|
+
orphan.mkdir(parents=True)
|
|
68
|
+
roots = find_project_roots(str(orphan))
|
|
69
|
+
self.assertEqual(len(roots), 1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestHasDesignSystemEslintRule(unittest.TestCase):
|
|
73
|
+
"""has_design_system_eslint_rule should match actual rule/plugin identifiers only."""
|
|
74
|
+
|
|
75
|
+
def test_ignores_comment_containing_design_system_substring(self) -> None:
|
|
76
|
+
"""A comment that mentions 'design-system' without a slash must not trigger the seal."""
|
|
77
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
78
|
+
root = Path(tmp)
|
|
79
|
+
eslint = root / ".eslintrc.json"
|
|
80
|
+
eslint.write_text(
|
|
81
|
+
'{"rules": {}, "// note": "do not use design-system here"}',
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
self.assertFalse(
|
|
85
|
+
has_design_system_eslint_rule(root),
|
|
86
|
+
msg="Plain 'design-system' substring in a comment must not match",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def test_matches_design_system_slash_rule_syntax(self) -> None:
|
|
90
|
+
"""An ESLint rule like 'design-system/no-raw-colors' should trigger the seal."""
|
|
91
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
92
|
+
root = Path(tmp)
|
|
93
|
+
eslint = root / ".eslintrc.json"
|
|
94
|
+
eslint.write_text(
|
|
95
|
+
'{"rules": {"design-system/no-raw-colors": "error"}}',
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
)
|
|
98
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
99
|
+
|
|
100
|
+
def test_checks_eslintrc_yaml(self) -> None:
|
|
101
|
+
""".eslintrc.yaml is a valid ESLint config and must be checked."""
|
|
102
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
103
|
+
root = Path(tmp)
|
|
104
|
+
yaml_config = root / ".eslintrc.yaml"
|
|
105
|
+
yaml_config.write_text(
|
|
106
|
+
'rules:\n design-system/no-raw-colors: error\n',
|
|
107
|
+
encoding="utf-8",
|
|
108
|
+
)
|
|
109
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
110
|
+
|
|
111
|
+
def test_checks_eslintrc_yml(self) -> None:
|
|
112
|
+
""".eslintrc.yml is a valid ESLint config and must be checked."""
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
114
|
+
root = Path(tmp)
|
|
115
|
+
yml_config = root / ".eslintrc.yml"
|
|
116
|
+
yml_config.write_text(
|
|
117
|
+
'rules:\n design-system/no-raw-colors: error\n',
|
|
118
|
+
encoding="utf-8",
|
|
119
|
+
)
|
|
120
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
121
|
+
|
|
122
|
+
def test_checks_eslint_config_mts(self) -> None:
|
|
123
|
+
"""eslint.config.mts is a valid ESLint flat-config entry point and must be checked."""
|
|
124
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
125
|
+
root = Path(tmp)
|
|
126
|
+
mts_config = root / "eslint.config.mts"
|
|
127
|
+
mts_config.write_text(
|
|
128
|
+
'export default [{"rules": {"design-system/no-raw-colors": "error"}}];',
|
|
129
|
+
encoding="utf-8",
|
|
130
|
+
)
|
|
131
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
132
|
+
|
|
133
|
+
def test_checks_eslint_config_cts(self) -> None:
|
|
134
|
+
"""eslint.config.cts is a valid ESLint flat-config entry point and must be checked."""
|
|
135
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
136
|
+
root = Path(tmp)
|
|
137
|
+
cts_config = root / "eslint.config.cts"
|
|
138
|
+
cts_config.write_text(
|
|
139
|
+
'module.exports = [{"rules": {"design-system/no-raw-colors": "error"}}];',
|
|
140
|
+
encoding="utf-8",
|
|
141
|
+
)
|
|
142
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
143
|
+
|
|
144
|
+
def test_checks_package_json_eslintconfig_field(self) -> None:
|
|
145
|
+
"""The eslintConfig key in package.json must be checked for design-system rules."""
|
|
146
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
147
|
+
root = Path(tmp)
|
|
148
|
+
pkg = {
|
|
149
|
+
"name": "my-app",
|
|
150
|
+
"eslintConfig": {
|
|
151
|
+
"rules": {"design-system/no-raw-colors": "error"}
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
(root / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
|
|
155
|
+
self.assertTrue(has_design_system_eslint_rule(root))
|
|
156
|
+
|
|
157
|
+
def test_package_json_non_eslintconfig_does_not_trigger(self) -> None:
|
|
158
|
+
"""A 'design-system' substring in package.json outside eslintConfig must not match."""
|
|
159
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
160
|
+
root = Path(tmp)
|
|
161
|
+
pkg = {
|
|
162
|
+
"name": "my-app",
|
|
163
|
+
"dependencies": {"design-system": "^1.0.0"},
|
|
164
|
+
}
|
|
165
|
+
(root / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
|
|
166
|
+
self.assertFalse(
|
|
167
|
+
has_design_system_eslint_rule(root),
|
|
168
|
+
msg="'design-system' in dependencies must not trigger the ESLint seal check",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestHasSealedDesignSystem(unittest.TestCase):
|
|
173
|
+
"""has_sealed_design_system seal-detection logic."""
|
|
174
|
+
|
|
175
|
+
def test_seal_marker_alone_is_sufficient(self) -> None:
|
|
176
|
+
"""A seal marker without an atom barrel or .claude/rules directory seals the project."""
|
|
177
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
178
|
+
project = Path(tmp) / "project"
|
|
179
|
+
project.mkdir()
|
|
180
|
+
(project / ".git").mkdir()
|
|
181
|
+
docs = project / "docs"
|
|
182
|
+
docs.mkdir()
|
|
183
|
+
(docs / "design-system-rfc.md").write_text("", encoding="utf-8")
|
|
184
|
+
# Deliberately NO atom barrel and NO .claude/rules directory.
|
|
185
|
+
|
|
186
|
+
self.assertTrue(
|
|
187
|
+
has_sealed_design_system(str(project)),
|
|
188
|
+
msg="A single seal marker must be sufficient to detect a sealed design system",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def test_atom_barrel_alone_is_sufficient(self) -> None:
|
|
192
|
+
"""A project exposing only an atoms barrel is treated as having a sealed design system."""
|
|
193
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
194
|
+
project = Path(tmp) / "project"
|
|
195
|
+
project.mkdir()
|
|
196
|
+
(project / ".git").mkdir()
|
|
197
|
+
atoms = project / "src" / "components" / "atoms"
|
|
198
|
+
atoms.mkdir(parents=True)
|
|
199
|
+
(atoms / "index.ts").write_text("", encoding="utf-8")
|
|
200
|
+
# Deliberately NO seal marker and NO .claude/rules directory.
|
|
201
|
+
|
|
202
|
+
self.assertTrue(
|
|
203
|
+
has_sealed_design_system(str(project)),
|
|
204
|
+
msg="An atoms barrel alone must be sufficient to detect a sealed design system",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestHasSealedDesignSystemMonorepo(unittest.TestCase):
|
|
209
|
+
"""has_sealed_design_system should find seal markers in ancestor (workspace) roots."""
|
|
210
|
+
|
|
211
|
+
def test_finds_seal_marker_at_workspace_root_not_inner_package(self) -> None:
|
|
212
|
+
"""Seal markers placed at the workspace root must be respected for inner packages."""
|
|
213
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
214
|
+
workspace = Path(tmp) / "workspace"
|
|
215
|
+
inner = workspace / "apps" / "mobile"
|
|
216
|
+
inner.mkdir(parents=True)
|
|
217
|
+
|
|
218
|
+
# workspace root: .git + seal marker
|
|
219
|
+
(workspace / ".git").mkdir()
|
|
220
|
+
rules_dir = workspace / ".claude" / "rules"
|
|
221
|
+
rules_dir.mkdir(parents=True)
|
|
222
|
+
(rules_dir / "use-the-design-library.md").write_text("", encoding="utf-8")
|
|
223
|
+
|
|
224
|
+
# inner package: package.json + atom barrel
|
|
225
|
+
(inner / "package.json").write_text("{}", encoding="utf-8")
|
|
226
|
+
atoms = inner / "components" / "atoms"
|
|
227
|
+
atoms.mkdir(parents=True)
|
|
228
|
+
(atoms / "index.ts").write_text("", encoding="utf-8")
|
|
229
|
+
|
|
230
|
+
target_dir = inner / "src" / "components" / "atoms"
|
|
231
|
+
target_dir.mkdir(parents=True)
|
|
232
|
+
|
|
233
|
+
self.assertTrue(
|
|
234
|
+
has_sealed_design_system(str(target_dir)),
|
|
235
|
+
msg="Seal marker at workspace root must be detected even for nested packages",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
unittest.main()
|