@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.
Files changed (100) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa/rules/reference/integration-access-layer.md +1 -1
  5. package/plugins/lisa/skills/analyze-claude-remote/SKILL.md +50 -6
  6. package/plugins/lisa/skills/generate-claude-remote-build-script/SKILL.md +21 -0
  7. package/plugins/lisa/skills/jam-access/SKILL.md +12 -11
  8. package/plugins/lisa-agy/plugin.json +1 -1
  9. package/plugins/lisa-agy/skills/analyze-claude-remote/SKILL.md +50 -6
  10. package/plugins/lisa-agy/skills/generate-claude-remote-build-script/SKILL.md +21 -0
  11. package/plugins/lisa-agy/skills/jam-access/SKILL.md +12 -11
  12. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  15. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +1 -1
  19. package/plugins/lisa-copilot/skills/analyze-claude-remote/SKILL.md +50 -6
  20. package/plugins/lisa-copilot/skills/generate-claude-remote-build-script/SKILL.md +21 -0
  21. package/plugins/lisa-copilot/skills/jam-access/SKILL.md +12 -11
  22. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +1 -1
  24. package/plugins/lisa-cursor/skills/analyze-claude-remote/SKILL.md +50 -6
  25. package/plugins/lisa-cursor/skills/generate-claude-remote-build-script/SKILL.md +21 -0
  26. package/plugins/lisa-cursor/skills/jam-access/SKILL.md +12 -11
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo/skills/atomic-design-gluestack/SKILL.md +8 -0
  30. package/plugins/lisa-expo/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  31. package/plugins/lisa-expo/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  32. package/plugins/lisa-expo/skills/gluestack-nativewind/SKILL.md +8 -0
  33. package/plugins/lisa-expo/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  34. package/plugins/lisa-expo/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  35. package/plugins/lisa-expo-agy/plugin.json +1 -1
  36. package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/SKILL.md +8 -0
  37. package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  38. package/plugins/lisa-expo-agy/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  39. package/plugins/lisa-expo-agy/skills/gluestack-nativewind/SKILL.md +8 -0
  40. package/plugins/lisa-expo-agy/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  41. package/plugins/lisa-expo-agy/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  42. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/SKILL.md +8 -0
  44. package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  45. package/plugins/lisa-expo-copilot/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  46. package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/SKILL.md +8 -0
  47. package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  48. package/plugins/lisa-expo-copilot/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  49. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/SKILL.md +8 -0
  51. package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  52. package/plugins/lisa-expo-cursor/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  53. package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/SKILL.md +8 -0
  54. package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  55. package/plugins/lisa-expo-cursor/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
  56. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  74. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails-agy/plugin.json +1 -1
  79. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  84. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  89. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/src/base/rules/reference/integration-access-layer.md +1 -1
  92. package/plugins/src/base/skills/analyze-claude-remote/SKILL.md +50 -6
  93. package/plugins/src/base/skills/generate-claude-remote-build-script/SKILL.md +21 -0
  94. package/plugins/src/base/skills/jam-access/SKILL.md +12 -11
  95. package/plugins/src/expo/skills/atomic-design-gluestack/SKILL.md +8 -0
  96. package/plugins/src/expo/skills/atomic-design-gluestack/scripts/test_validate_atomic_structure.py +240 -0
  97. package/plugins/src/expo/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +109 -0
  98. package/plugins/src/expo/skills/gluestack-nativewind/SKILL.md +8 -0
  99. package/plugins/src/expo/skills/gluestack-nativewind/scripts/test_validate_styling.py +240 -0
  100. package/plugins/src/expo/skills/gluestack-nativewind/scripts/validate_styling.py +109 -0
@@ -19,6 +19,7 @@ Arguments:
19
19
  Can be a file or directory.
20
20
  """
21
21
 
22
+ import json
22
23
  import os
23
24
  import re
24
25
  import sys
@@ -99,6 +100,109 @@ SKIP_PATTERNS = [
99
100
  ]
100
101
 
101
102
 
103
+ SEALED_DESIGN_SYSTEM_MARKERS = [
104
+ ".claude/rules/use-the-design-library.md",
105
+ ".claude/rules/figma-design-system.md",
106
+ "docs/design-system/tokens.md",
107
+ "docs/design-system-rfc.md",
108
+ ]
109
+
110
+ ATOM_BARREL_MARKERS = [
111
+ "components/atoms/index.ts",
112
+ "components/atoms/index.tsx",
113
+ "src/components/atoms/index.ts",
114
+ "src/components/atoms/index.tsx",
115
+ ]
116
+
117
+
118
+ def find_project_roots(path: str) -> list[Path]:
119
+ """Find candidate roots for marker detection, from nearest to farthest.
120
+
121
+ Returns all package boundaries (package.json or .git directories) found
122
+ when walking up from ``path``. In a monorepo this means both the inner
123
+ package root and the workspace root are returned, so seal markers stored at
124
+ the workspace level are not missed.
125
+ """
126
+ current = Path(path).resolve()
127
+ if current.is_file():
128
+ current = current.parent
129
+
130
+ return [
131
+ candidate
132
+ for candidate in [current, *current.parents]
133
+ if (candidate / "package.json").exists() or (candidate / ".git").exists()
134
+ ] or [current]
135
+
136
+
137
+ def has_design_system_eslint_rule(project_root: Path) -> bool:
138
+ """Detect project-owned design-system lint rules.
139
+
140
+ Only matches actual rule/plugin identifier syntax (``design-system/``) so
141
+ that comments, package names, or arbitrary strings containing
142
+ ``design-system`` as a substring do not trigger a false positive.
143
+ """
144
+ eslint_files = [
145
+ ".eslintrc",
146
+ ".eslintrc.js",
147
+ ".eslintrc.cjs",
148
+ ".eslintrc.yaml",
149
+ ".eslintrc.yml",
150
+ ".eslintrc.json",
151
+ "eslint.config.js",
152
+ "eslint.config.mjs",
153
+ "eslint.config.cjs",
154
+ "eslint.config.ts",
155
+ "eslint.config.mts",
156
+ "eslint.config.cts",
157
+ ]
158
+
159
+ for eslint_file in eslint_files:
160
+ path = project_root / eslint_file
161
+ if not path.exists() or not path.is_file():
162
+ continue
163
+ try:
164
+ content = path.read_text(encoding="utf-8")
165
+ except UnicodeDecodeError:
166
+ continue
167
+ if re.search(r"\bdesign-system/", content):
168
+ return True
169
+
170
+ # Check package.json eslintConfig field explicitly so that dependency
171
+ # names (e.g. "design-system" in dependencies) don't cause false positives.
172
+ package_json_path = project_root / "package.json"
173
+ if package_json_path.exists() and package_json_path.is_file():
174
+ try:
175
+ pkg = json.loads(package_json_path.read_text(encoding="utf-8"))
176
+ eslint_config = pkg.get("eslintConfig")
177
+ if eslint_config is not None:
178
+ eslint_config_str = json.dumps(eslint_config)
179
+ if re.search(r"\bdesign-system/", eslint_config_str):
180
+ return True
181
+ except (json.JSONDecodeError, UnicodeDecodeError):
182
+ pass
183
+
184
+ return False
185
+
186
+
187
+ def has_sealed_design_system(path: str) -> bool:
188
+ """Check whether this project has a closed local design-system seal."""
189
+ project_roots = find_project_roots(path)
190
+ has_seal_marker = any(
191
+ (root / marker).exists()
192
+ for root in project_roots
193
+ for marker in SEALED_DESIGN_SYSTEM_MARKERS
194
+ )
195
+ has_atom_barrel = any(
196
+ (root / marker).exists()
197
+ for root in project_roots
198
+ for marker in ATOM_BARREL_MARKERS
199
+ )
200
+
201
+ if has_seal_marker or has_atom_barrel:
202
+ return True
203
+ return any(has_design_system_eslint_rule(root) for root in project_roots)
204
+
205
+
102
206
  def should_skip_file(file_path: str) -> bool:
103
207
  """Check if file should be skipped."""
104
208
  for pattern in SKIP_PATTERNS:
@@ -270,6 +374,11 @@ def main():
270
374
  print(f"❌ Path not found: {path}")
271
375
  sys.exit(1)
272
376
 
377
+ if has_sealed_design_system(path):
378
+ print("Sealed design system detected; deferring to the project's design-library rules.")
379
+ print("Skipping generic Gluestack NativeWind validation.")
380
+ sys.exit(0)
381
+
273
382
  print(f"🔍 Validating styling patterns in: {path}\n")
274
383
 
275
384
  # Validate
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "2.178.1",
3
+ "version": "2.178.3",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -10,6 +10,14 @@ description: |
10
10
 
11
11
  This skill enforces Brad Frost's atomic design methodology adapted for React Native/Expo projects using Gluestack UI v3 + NativeWind v4. It provides clear guidelines for component categorization, directory structure, composition patterns, and testing strategies for each atomic level.
12
12
 
13
+ ## Sealed Design System Guard
14
+
15
+ 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.
16
+
17
+ If a sealed design system is present, defer to the project's design-library rules and skills instead, such as `use-the-design-library` and `add-design-library-item`. Keep the atomic hierarchy concepts only when they match the local seal. Do not redefine the atom library as `@/components/ui`, use Gluestack compound trees as canonical atoms, or recommend `className` and raw Gluestack tokens where the project seal forbids them.
18
+
19
+ For open Gluestack UI v3 + NativeWind v4 projects without those seal markers, continue with the guidance below.
20
+
13
21
  ## Core Principles
14
22
 
15
23
  ### The Atomic Hierarchy
@@ -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