@3fn/core 13.0.0 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/.kiro/agents/ada-prompt.md +80 -132
  2. package/.kiro/agents/ada-prompt.md.attribution.json +45 -0
  3. package/.kiro/agents/ada.json +44 -59
  4. package/.kiro/agents/ada.json.attribution.json +13 -0
  5. package/.kiro/agents/data-prompt.md +83 -74
  6. package/.kiro/agents/data-prompt.md.attribution.json +53 -0
  7. package/.kiro/agents/data.json +31 -38
  8. package/.kiro/agents/data.json.attribution.json +13 -0
  9. package/.kiro/agents/kenya-prompt.md +83 -72
  10. package/.kiro/agents/kenya-prompt.md.attribution.json +53 -0
  11. package/.kiro/agents/kenya.json +27 -35
  12. package/.kiro/agents/kenya.json.attribution.json +13 -0
  13. package/.kiro/agents/leonardo-prompt.md +176 -234
  14. package/.kiro/agents/leonardo-prompt.md.attribution.json +45 -0
  15. package/.kiro/agents/leonardo.json +28 -36
  16. package/.kiro/agents/leonardo.json.attribution.json +13 -0
  17. package/.kiro/agents/lina-prompt.md +110 -151
  18. package/.kiro/agents/lina-prompt.md.attribution.json +53 -0
  19. package/.kiro/agents/lina.json +46 -59
  20. package/.kiro/agents/lina.json.attribution.json +13 -0
  21. package/.kiro/agents/sparky-prompt.md +89 -72
  22. package/.kiro/agents/sparky-prompt.md.attribution.json +53 -0
  23. package/.kiro/agents/sparky.json +37 -37
  24. package/.kiro/agents/sparky.json.attribution.json +13 -0
  25. package/.kiro/agents/stacy-prompt.md +74 -48
  26. package/.kiro/agents/stacy-prompt.md.attribution.json +45 -0
  27. package/.kiro/agents/stacy.json +28 -30
  28. package/.kiro/agents/stacy.json.attribution.json +13 -0
  29. package/.kiro/agents/thurgood-prompt.md +94 -138
  30. package/.kiro/agents/thurgood-prompt.md.attribution.json +45 -0
  31. package/.kiro/agents/thurgood.json +31 -35
  32. package/.kiro/agents/thurgood.json.attribution.json +13 -0
  33. package/.kiro/steering/AI-Collaboration-Principles.md +3 -3
  34. package/.kiro/steering/Civitas-System-Overview.md +7 -16
  35. package/.kiro/steering/DesignerPunk-Systems-Overview.md +6 -6
  36. package/.kiro/steering/Spec-Feedback-Protocol.md +2 -11
  37. package/.kiro/steering/Task-Completion-Protocol.md +98 -17
  38. package/.kiro/steering/core-goals.md +3 -3
  39. package/.kiro/steering/personal-note.md +1 -1
  40. package/.kiro/steering/start-up-tasks.md +17 -6
  41. package/application-mcp-server/src/index.ts +26 -0
  42. package/dist/ComponentTokens.android.kt +12 -12
  43. package/dist/ComponentTokens.ios.swift +12 -12
  44. package/dist/ComponentTokens.web.css +3 -3
  45. package/dist/DesignTokens.android.kt +1 -1
  46. package/dist/DesignTokens.dtcg.json +8 -5
  47. package/dist/DesignTokens.figma.json +2 -2
  48. package/dist/DesignTokens.ios.swift +1 -1
  49. package/dist/DesignTokens.web.css +1 -1
  50. package/dist/android/DesignTokens.android.kt +1 -1
  51. package/dist/blend/OklchBlendCalculator.js +1 -0
  52. package/dist/blend/ThemeAwareBlendUtilities.web.d.ts +13 -2
  53. package/dist/blend/ThemeAwareBlendUtilities.web.js +6 -1
  54. package/dist/browser/designerpunk.esm.js +36 -90
  55. package/dist/browser/designerpunk.esm.min.js +33 -36
  56. package/dist/browser/designerpunk.umd.js +36 -90
  57. package/dist/browser/designerpunk.umd.min.js +47 -50
  58. package/dist/browser/tokens.css +3 -3
  59. package/dist/build/tokens/defineComponentTokens.d.ts +10 -0
  60. package/dist/build/tokens/defineComponentTokens.js +26 -0
  61. package/dist/components/core/Avatar-Base/avatar.tokens.d.ts +21 -26
  62. package/dist/components/core/Avatar-Base/avatar.tokens.js +31 -34
  63. package/dist/components/core/Avatar-Base/index.d.ts +1 -1
  64. package/dist/components/core/Avatar-Base/index.js +2 -2
  65. package/dist/components/core/Avatar-Base/platforms/web/Avatar.web.js +24 -5
  66. package/dist/components/core/Button-CTA/examples/BasicUsage.d.ts +16 -28
  67. package/dist/components/core/Button-CTA/examples/BasicUsage.js +18 -43
  68. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.d.ts +3 -15
  69. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.js +9 -58
  70. package/dist/components/core/Button-CTA/types.d.ts +0 -24
  71. package/dist/components/core/Button-CTA/types.js +6 -0
  72. package/dist/components/core/Button-Icon/buttonIcon.tokens.d.ts +28 -14
  73. package/dist/components/core/Button-Icon/buttonIcon.tokens.js +35 -20
  74. package/dist/components/core/Input-Text-Base/types.d.ts +13 -1
  75. package/dist/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.js +11 -2
  76. package/dist/generators/DTCGFormatGenerator.js +8 -0
  77. package/dist/generators/TokenFileGenerator.js +7 -2
  78. package/dist/integration/BuildErrorHandler.js +2 -2
  79. package/dist/ios/DesignTokens.ios.swift +1 -1
  80. package/dist/mcp/application-mcp.js +24 -0
  81. package/dist/mcp/docs-mcp.js +130 -15
  82. package/dist/mcp/product-mcp.js +25 -0
  83. package/dist/tokens/OpacityTokens.js +1 -1
  84. package/dist/tokens/component/progress.d.ts +65 -5
  85. package/dist/tokens/component/progress.js +79 -18
  86. package/dist/tokens/semantic/BlendTokens.d.ts +10 -3
  87. package/dist/tokens/semantic/BlendTokens.js +17 -5
  88. package/dist/tokens/semantic/OpacityTokens.d.ts +4 -4
  89. package/dist/tokens/semantic/OpacityTokens.js +4 -4
  90. package/dist/types/ComponentTypes.d.ts +1 -1
  91. package/dist/types/generated/TokenTypes.d.ts +1 -1
  92. package/dist/types/generated/TokenTypes.js +1 -1
  93. package/dist/validators/StemmaTokenUsageValidator.js +3 -2
  94. package/dist/web/DesignTokens.web.css +1 -1
  95. package/governance/BUILD-SYSTEM-SETUP.md +1 -2
  96. package/governance/Component-Development-Guide.md +23 -13
  97. package/governance/Component-Development-Standards.md +21 -20
  98. package/governance/Component-Family-Avatar.md +6 -7
  99. package/governance/Component-Family-Badge.md +19 -20
  100. package/governance/Component-Family-Button.md +30 -43
  101. package/governance/Component-Family-Chip.md +14 -15
  102. package/governance/Component-Family-Container.md +12 -13
  103. package/governance/Component-Family-Data-Display.md +1 -2
  104. package/governance/Component-Family-Divider.md +1 -2
  105. package/governance/Component-Family-Form-Inputs.md +65 -65
  106. package/governance/Component-Family-Icon.md +9 -10
  107. package/governance/Component-Family-Loading.md +1 -2
  108. package/governance/Component-Family-Modal.md +1 -2
  109. package/governance/Component-Family-Navigation.md +1 -2
  110. package/governance/Component-Family-Progress.md +0 -1
  111. package/governance/Component-Inheritance-Structures.md +219 -99
  112. package/governance/Component-MCP-Document-Template.md +6 -5
  113. package/governance/Component-Primitive-vs-Semantic-Philosophy.md +1 -1
  114. package/governance/Component-Quick-Reference.md +33 -33
  115. package/governance/Component-Readiness-Status.md +59 -44
  116. package/governance/Component-Templates.md +57 -61
  117. package/governance/Contract-System-Reference.md +7 -7
  118. package/governance/MCP-Integration-Guide.md +1 -1
  119. package/governance/Process-Cross-Reference-Standards.md +31 -13
  120. package/governance/Process-Development-Workflow.md +49 -59
  121. package/governance/Process-File-Organization.md +25 -28
  122. package/governance/Process-Hook-Operations.md +22 -11
  123. package/governance/Process-Orchestration-Model-Selection.md +92 -0
  124. package/governance/Process-Spec-Planning.md +98 -52
  125. package/governance/Process-Task-Type-Definitions.md +80 -4
  126. package/governance/Product-Handoff-Protocol.md +2 -0
  127. package/governance/Rosetta-System-Architecture.md +13 -11
  128. package/governance/Test-Behavioral-Contract-Validation.md +38 -31
  129. package/governance/Test-Failure-Audit-Methodology.md +1 -1
  130. package/governance/Token-Family-Accessibility.md +1 -2
  131. package/governance/Token-Family-Blend.md +18 -16
  132. package/governance/Token-Family-Blur.md +0 -1
  133. package/governance/Token-Family-Border.md +1 -2
  134. package/governance/Token-Family-Color.md +0 -1
  135. package/governance/Token-Family-Glow.md +1 -2
  136. package/governance/Token-Family-Layering.md +0 -1
  137. package/governance/Token-Family-Motion.md +1 -2
  138. package/governance/Token-Family-Opacity.md +0 -1
  139. package/governance/Token-Family-Radius.md +1 -2
  140. package/governance/Token-Family-Responsive.md +1 -2
  141. package/governance/Token-Family-Shadow.md +1 -2
  142. package/governance/Token-Family-Sizing.md +0 -1
  143. package/governance/Token-Family-Spacing.md +1 -2
  144. package/governance/Token-Family-Typography.md +1 -2
  145. package/governance/Token-Governance.md +8 -8
  146. package/governance/Token-Quick-Reference.md +47 -34
  147. package/governance/Token-Resolution-Patterns.md +1 -1
  148. package/governance/Token-Semantic-Structure.md +1 -1
  149. package/governance/Web-Authoring-Standards.md +5 -5
  150. package/governance/browser-distribution-guide.md +1 -4
  151. package/governance/classification-map.md +474 -0
  152. package/governance/completion-documentation-guide.md +23 -37
  153. package/governance/component-meta-authoring-guide.md +1 -1
  154. package/governance/cross-platform-vs-platform-specific-decision-framework.md +1 -1
  155. package/governance/platform-implementation-guidelines.md +2 -3
  156. package/governance/release-management-system.md +28 -63
  157. package/governance/rosetta-system-principles.md +8 -6
  158. package/governance/stemma-system-principles.md +18 -17
  159. package/mcp-server/src/index.ts +24 -6
  160. package/mcp-server/src/indexer/DocumentIndexer.ts +119 -9
  161. package/mcp-server/src/indexer/__tests__/bare-id-crossrefs.test.ts +250 -0
  162. package/mcp-server/src/indexer/cross-ref-parser.ts +29 -1
  163. package/mcp-server/src/indexer/index-health.ts +27 -2
  164. package/mcp-server/src/query/__tests__/find-docs-calibration.test.ts +11 -26
  165. package/mcp-server/src/relocation-integrity-gate/__tests__/relocation-integrity-gate.test.ts +72 -5
  166. package/mcp-server/src/relocation-integrity-gate/relocation-integrity-gate.ts +81 -24
  167. package/mcp-server/src/tools/list-cross-references.ts +2 -2
  168. package/package.json +24 -26
  169. package/src/__tests__/browser-distribution/css-bundling.test.ts +6 -4
  170. package/src/__tests__/console-allowlist.json +14 -0
  171. package/src/__tests__/console-fail-setup.ts +169 -0
  172. package/src/__tests__/integration/Spec107-DesignLanguageContext.test.ts +16 -0
  173. package/src/__tests__/stemma-system/behavioral-contract-validation.test.ts +70 -17
  174. package/src/__tests__/stemma-system/contract-catalog-name-validation.test.ts +28 -0
  175. package/src/__tests__/stemma-system/form-inputs-contracts.test.ts +223 -16
  176. package/src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts +298 -0
  177. package/src/blend/OklchBlendCalculator.ts +3 -0
  178. package/src/blend/ThemeAwareBlendUtilities.android.kt +3 -0
  179. package/src/blend/ThemeAwareBlendUtilities.ios.swift +3 -0
  180. package/src/blend/ThemeAwareBlendUtilities.web.ts +9 -1
  181. package/src/blend/__tests__/InteractionStateAudit.test.ts +12 -9
  182. package/src/build/errors/__tests__/ErrorHandler.integration.test.ts +8 -0
  183. package/src/build/errors/__tests__/ErrorHandler.test.ts +5 -0
  184. package/src/build/tokens/__tests__/defineComponentTokens.test.ts +113 -0
  185. package/src/build/tokens/defineComponentTokens.ts +43 -1
  186. package/src/build/workflow/__tests__/CICDIntegration.test.ts +12 -1
  187. package/src/cli/__tests__/init.test.ts +45 -11
  188. package/src/components/core/Avatar-Base/Avatar-Base.schema.yaml +1 -1
  189. package/src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts +121 -7
  190. package/src/components/core/Avatar-Base/__tests__/Avatar.image.test.ts +3 -0
  191. package/src/components/core/Avatar-Base/__tests__/Avatar.test.ts +15 -6
  192. package/src/components/core/Avatar-Base/avatar.tokens.ts +31 -34
  193. package/src/components/core/Avatar-Base/contracts.yaml +11 -1
  194. package/src/components/core/Avatar-Base/index.ts +1 -1
  195. package/src/components/core/Avatar-Base/platforms/web/Avatar.web.ts +24 -5
  196. package/src/components/core/Badge-Count-Base/contracts.yaml +1 -1
  197. package/src/components/core/Badge-Label-Base/contracts.yaml +1 -1
  198. package/src/components/core/Button-CTA/Button-CTA.schema.yaml +2 -12
  199. package/src/components/core/Button-CTA/README.md +3 -6
  200. package/src/components/core/Button-CTA/__tests__/ButtonCTA.test.ts +35 -89
  201. package/src/components/core/Button-CTA/__tests__/setup.test.ts +0 -2
  202. package/src/components/core/Button-CTA/__tests__/test-utils.ts +0 -2
  203. package/src/components/core/Button-CTA/contracts.yaml +6 -29
  204. package/src/components/core/Button-CTA/examples/BasicUsage.html +2 -14
  205. package/src/components/core/Button-CTA/examples/BasicUsage.tsx +17 -44
  206. package/src/components/core/Button-CTA/platforms/android/ButtonCTA.android.kt +12 -20
  207. package/src/components/core/Button-CTA/platforms/ios/ButtonCTA.ios.swift +12 -51
  208. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.css +2 -26
  209. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.ts +18 -71
  210. package/src/components/core/Button-CTA/types.ts +10 -28
  211. package/src/components/core/Button-Icon/buttonIcon.tokens.ts +43 -27
  212. package/src/components/core/Chip-Base/__tests__/ChipBase.test.ts +13 -0
  213. package/src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts +13 -0
  214. package/src/components/core/Chip-Input/__tests__/ChipInput.test.ts +13 -0
  215. package/src/components/core/Input-Text-Base/Input-Text-Base.schema.yaml +30 -2
  216. package/src/components/core/Input-Text-Base/README.md +25 -2
  217. package/src/components/core/Input-Text-Base/__tests__/focusIndicators.test.ts +16 -15
  218. package/src/components/core/Input-Text-Base/contracts.yaml +90 -0
  219. package/src/components/core/Input-Text-Base/platforms/android/InputTextBase.android.kt +26 -12
  220. package/src/components/core/Input-Text-Base/platforms/ios/InputTextBase.ios.swift +195 -59
  221. package/src/components/core/Input-Text-Base/types.ts +13 -1
  222. package/src/components/core/Input-Text-Email/Input-Text-Email.schema.yaml +5 -1
  223. package/src/components/core/Input-Text-Email/README.md +8 -7
  224. package/src/components/core/Input-Text-Email/platforms/android/InputTextEmail.android.kt +1 -4
  225. package/src/components/core/Input-Text-Email/platforms/ios/InputTextEmail.ios.swift +2 -16
  226. package/src/components/core/Input-Text-Password/Input-Text-Password.schema.yaml +10 -3
  227. package/src/components/core/Input-Text-Password/README.md +9 -8
  228. package/src/components/core/Input-Text-Password/contracts.yaml +5 -0
  229. package/src/components/core/Input-Text-Password/platforms/android/InputTextPassword.android.kt +17 -7
  230. package/src/components/core/Input-Text-Password/platforms/ios/InputTextPassword.ios.swift +22 -20
  231. package/src/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.ts +11 -2
  232. package/src/components/core/Input-Text-PhoneNumber/Input-Text-PhoneNumber.schema.yaml +5 -1
  233. package/src/components/core/Input-Text-PhoneNumber/README.md +9 -8
  234. package/src/components/core/Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt +2 -5
  235. package/src/components/core/Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift +3 -17
  236. package/src/components/core/Nav-Header-App/contracts.yaml +1 -1
  237. package/src/components/core/Nav-SegmentedChoice-Base/contracts.yaml +1 -1
  238. package/src/components/core/Progress-Indicator-Connector-Base/contracts.yaml +1 -1
  239. package/src/components/core/Progress-Indicator-Label-Base/contracts.yaml +1 -1
  240. package/src/components/core/Progress-Indicator-Node-Base/contracts.yaml +1 -1
  241. package/src/components/core/Progress-Stepper-Base/__tests__/StepperBase.test.ts +5 -2
  242. package/src/components/core/Progress-Stepper-Detailed/__tests__/StepperDetailed.test.ts +5 -2
  243. package/src/generators/DTCGFormatGenerator.ts +6 -0
  244. package/src/generators/TokenFileGenerator.ts +7 -2
  245. package/src/generators/__tests__/DTCGConfigOptions.test.ts +14 -5
  246. package/src/integration/BuildErrorHandler.ts +2 -2
  247. package/src/tokens/OpacityTokens.ts +1 -1
  248. package/src/tokens/__tests__/OpacityTokens.test.ts +3 -1
  249. package/src/tokens/__tests__/ProgressTokenCompliance.test.ts +5 -3
  250. package/src/tokens/__tests__/ProgressTokenFormulas.test.ts +11 -11
  251. package/src/tokens/__tests__/ProgressTokenTranslation.test.ts +22 -20
  252. package/src/tokens/component/progress.ts +83 -21
  253. package/src/tokens/semantic/BlendTokens.ts +26 -5
  254. package/src/tokens/semantic/OpacityTokens.ts +4 -4
  255. package/src/types/ComponentTypes.ts +1 -1
  256. package/src/types/generated/TokenTypes.ts +1 -1
  257. package/src/validators/StemmaTokenUsageValidator.ts +3 -2
  258. package/token-index/components.yaml +8 -8
  259. package/token-index/semantics.yaml +1 -2
  260. package/src/tools/release/__tests__/ChangeClassifier.test.ts +0 -133
  261. package/src/tools/release/__tests__/ChangeExtractor.test.ts +0 -222
  262. package/src/tools/release/__tests__/GitHubPublisher.test.ts +0 -240
  263. package/src/tools/release/__tests__/NotesRenderer.test.ts +0 -142
  264. package/src/tools/release/__tests__/NpmPublisher.test.ts +0 -289
  265. package/src/tools/release/__tests__/PipelineIntegration.test.ts +0 -188
  266. package/src/tools/release/__tests__/ReleasePipeline.test.ts +0 -192
  267. package/src/tools/release/__tests__/SemanticVersionValidator.test.ts +0 -49
  268. package/src/tools/release/__tests__/SummaryScanner.test.ts +0 -141
  269. package/src/tools/release/__tests__/TagResolver.test.ts +0 -91
  270. package/src/tools/release/__tests__/VersionCalculator.test.ts +0 -270
  271. package/src/tools/release/__tests__/helpers/NpmMockHelper.ts +0 -80
  272. package/src/tools/release/cli/ReleasePipeline.ts +0 -165
  273. package/src/tools/release/cli/release-tool.ts +0 -107
  274. package/src/tools/release/pipeline/ChangeClassifier.ts +0 -61
  275. package/src/tools/release/pipeline/ChangeExtractor.ts +0 -87
  276. package/src/tools/release/pipeline/NotesRenderer.ts +0 -66
  277. package/src/tools/release/pipeline/SummaryScanner.ts +0 -70
  278. package/src/tools/release/pipeline/TagResolver.ts +0 -40
  279. package/src/tools/release/pipeline/VersionCalculator.ts +0 -375
  280. package/src/tools/release/publishers/GitHubPublisher.ts +0 -228
  281. package/src/tools/release/publishers/NpmPublisher.ts +0 -196
  282. package/src/tools/release/release-config.json +0 -5
  283. package/src/tools/release/types/index.ts +0 -282
  284. package/src/tools/release/validators/SemanticVersionValidator.ts +0 -67
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Input-Text Native Base-Call Alignment Validation
3
+ *
4
+ * Static analysis tests that validate the native (iOS/Android) Input-Text
5
+ * semantic variants call InputTextBase with parameters the base actually
6
+ * declares. The native platform files are NOT compiled in CI — Swift/Kotlin
7
+ * sources are only validated by static-analysis Jest tests — so a call-site
8
+ * passing an undeclared parameter (or referencing a nonexistent enum member)
9
+ * would not compile on a real platform build but previously went undetected.
10
+ *
11
+ * Catches:
12
+ * - Callers passing named arguments the base does not declare
13
+ * (e.g., `trailingContent` before the base grew that slot)
14
+ * - iOS callers passing memberwise-init arguments out of declaration order
15
+ * - References to InputType enum members that do not exist
16
+ * (e.g., `InputType.PHONE` when the enum defines TEL)
17
+ *
18
+ * Stemma System: Form Inputs Family
19
+ */
20
+
21
+ import * as fs from 'fs';
22
+ import * as path from 'path';
23
+
24
+ const CORE = path.join(__dirname, '../../components/core');
25
+
26
+ const IOS_BASE = path.join(CORE, 'Input-Text-Base/platforms/ios/InputTextBase.ios.swift');
27
+ const ANDROID_BASE = path.join(CORE, 'Input-Text-Base/platforms/android/InputTextBase.android.kt');
28
+
29
+ const IOS_CALLERS = [
30
+ 'Input-Text-Password/platforms/ios/InputTextPassword.ios.swift',
31
+ 'Input-Text-Email/platforms/ios/InputTextEmail.ios.swift',
32
+ 'Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift',
33
+ ].map((p) => path.join(CORE, p));
34
+
35
+ const ANDROID_CALLERS = [
36
+ 'Input-Text-Password/platforms/android/InputTextPassword.android.kt',
37
+ 'Input-Text-Email/platforms/android/InputTextEmail.android.kt',
38
+ 'Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt',
39
+ ].map((p) => path.join(CORE, p));
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Source scanning helpers (string- and comment-aware, brace/paren-depth aware)
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /**
46
+ * Extract the balanced-parentheses body starting at `openParenIndex`
47
+ * (which must point at a '('). Skips string literals and line comments.
48
+ */
49
+ function extractParenBody(source: string, openParenIndex: number): string {
50
+ let depth = 0;
51
+ for (let i = openParenIndex; i < source.length; i++) {
52
+ const ch = source[i];
53
+ if (ch === '"') {
54
+ // Skip string literal (handles escapes)
55
+ i++;
56
+ while (i < source.length && source[i] !== '"') {
57
+ if (source[i] === '\\') i++;
58
+ i++;
59
+ }
60
+ } else if (ch === '/' && source[i + 1] === '/') {
61
+ while (i < source.length && source[i] !== '\n') i++;
62
+ } else if (ch === '(') {
63
+ depth++;
64
+ } else if (ch === ')') {
65
+ depth--;
66
+ if (depth === 0) {
67
+ return source.slice(openParenIndex + 1, i);
68
+ }
69
+ }
70
+ }
71
+ throw new Error('Unbalanced parentheses in source');
72
+ }
73
+
74
+ /**
75
+ * Split an argument/parameter list into top-level entries — commas nested
76
+ * inside (), {}, [], strings, or line comments do not split.
77
+ */
78
+ function splitTopLevel(body: string): string[] {
79
+ const parts: string[] = [];
80
+ let depth = 0;
81
+ let current = '';
82
+ for (let i = 0; i < body.length; i++) {
83
+ const ch = body[i];
84
+ if (ch === '"') {
85
+ current += ch;
86
+ i++;
87
+ while (i < body.length && body[i] !== '"') {
88
+ if (body[i] === '\\') {
89
+ current += body[i];
90
+ i++;
91
+ }
92
+ current += body[i];
93
+ i++;
94
+ }
95
+ current += body[i] ?? '';
96
+ } else if (ch === '/' && body[i + 1] === '/') {
97
+ while (i < body.length && body[i] !== '\n') i++;
98
+ current += '\n';
99
+ } else if ('({['.includes(ch)) {
100
+ depth++;
101
+ current += ch;
102
+ } else if (')}]'.includes(ch)) {
103
+ depth--;
104
+ current += ch;
105
+ } else if (ch === ',' && depth === 0) {
106
+ parts.push(current);
107
+ current = '';
108
+ } else {
109
+ current += ch;
110
+ }
111
+ }
112
+ if (current.trim()) parts.push(current);
113
+ return parts;
114
+ }
115
+
116
+ /** Find every `InputTextBase(...)` call in a source file and return its args. */
117
+ function extractBaseCalls(source: string): string[][] {
118
+ const calls: string[][] = [];
119
+ const re = /\bInputTextBase\(/g;
120
+ let match: RegExpExecArray | null;
121
+ while ((match = re.exec(source)) !== null) {
122
+ const body = extractParenBody(source, match.index + match[0].length - 1);
123
+ calls.push(splitTopLevel(body));
124
+ re.lastIndex = match.index + match[0].length;
125
+ }
126
+ return calls;
127
+ }
128
+
129
+ /** Labels of named arguments: Swift `label: value` / Kotlin `name = value`. */
130
+ function argLabel(arg: string, platform: 'ios' | 'android'): string | null {
131
+ const m =
132
+ platform === 'ios'
133
+ ? arg.match(/^\s*(\w+)\s*:/)
134
+ : arg.match(/^\s*(\w+)\s*=(?!=)/);
135
+ return m ? m[1] : null;
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Base declaration parsers
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * iOS base: settable stored properties of the InputTextBase struct, in
144
+ * declaration order. These define the memberwise initializer, so caller
145
+ * argument labels must be an in-order subsequence of this list.
146
+ * Excludes private state (@State/@FocusState/@Environment are all private)
147
+ * and the `body` computed property.
148
+ */
149
+ function parseIOSBaseProperties(source: string): string[] {
150
+ const structStart = source.indexOf('struct InputTextBase: View {');
151
+ const bodyStart = source.indexOf('var body: some View', structStart);
152
+ const section = source.slice(structStart, bodyStart);
153
+ const props: string[] = [];
154
+ for (const line of section.split('\n')) {
155
+ if (line.includes('private')) continue;
156
+ const m = line.match(/^\s*(?:@Binding\s+)?(?:let|var)\s+(\w+)\s*[:=]/);
157
+ if (m) props.push(m[1]);
158
+ }
159
+ return props;
160
+ }
161
+
162
+ /** Android base: parameter names of the InputTextBase composable function. */
163
+ function parseAndroidBaseParams(source: string): string[] {
164
+ const funIndex = source.indexOf('fun InputTextBase(');
165
+ const body = extractParenBody(source, funIndex + 'fun InputTextBase'.length);
166
+ return splitTopLevel(body)
167
+ .map((p) => p.match(/^\s*(\w+)\s*:/)?.[1])
168
+ .filter((name): name is string => Boolean(name));
169
+ }
170
+
171
+ /** Enum members: Swift `case xyz` / Kotlin `enum class InputType { A, B }`. */
172
+ function parseInputTypeMembers(source: string, platform: 'ios' | 'android'): string[] {
173
+ if (platform === 'ios') {
174
+ const enumStart = source.indexOf('enum InputType {');
175
+ const enumBody = source.slice(enumStart, source.indexOf('}', enumStart));
176
+ return [...enumBody.matchAll(/case\s+(\w+)/g)].map((m) => m[1]);
177
+ }
178
+ const enumMatch = source.match(/enum class InputType \{([^}]*)\}/);
179
+ if (!enumMatch) return [];
180
+ return enumMatch[1]
181
+ .split(',')
182
+ .map((m) => m.trim())
183
+ .filter(Boolean);
184
+ }
185
+
186
+ /** Assert `subset` appears within `sequence` in order (subsequence check). */
187
+ function isInOrderSubsequence(subset: string[], sequence: string[]): boolean {
188
+ let cursor = 0;
189
+ for (const item of subset) {
190
+ const found = sequence.indexOf(item, cursor);
191
+ if (found === -1) return false;
192
+ cursor = found + 1;
193
+ }
194
+ return true;
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Tests
199
+ // ---------------------------------------------------------------------------
200
+
201
+ describe('Input-Text native base-call alignment', () => {
202
+ const iosBaseSource = fs.readFileSync(IOS_BASE, 'utf-8');
203
+ const androidBaseSource = fs.readFileSync(ANDROID_BASE, 'utf-8');
204
+
205
+ const iosBaseProps = parseIOSBaseProperties(iosBaseSource);
206
+ const androidBaseParams = parseAndroidBaseParams(androidBaseSource);
207
+ const iosEnumMembers = parseInputTypeMembers(iosBaseSource, 'ios');
208
+ const androidEnumMembers = parseInputTypeMembers(androidBaseSource, 'android');
209
+
210
+ describe('base declaration parsing (sanity)', () => {
211
+ it('parses the iOS base memberwise properties', () => {
212
+ expect(iosBaseProps).toEqual(
213
+ expect.arrayContaining(['id', 'label', 'value', 'type', 'readOnly', 'trailingContent'])
214
+ );
215
+ expect(iosBaseProps.length).toBeGreaterThanOrEqual(15);
216
+ });
217
+
218
+ it('parses the Android base composable parameters', () => {
219
+ expect(androidBaseParams).toEqual(
220
+ expect.arrayContaining(['id', 'label', 'value', 'type', 'visualTransformation', 'trailingContent'])
221
+ );
222
+ expect(androidBaseParams.length).toBeGreaterThanOrEqual(15);
223
+ });
224
+
225
+ it('parses the InputType enums on both platforms', () => {
226
+ expect(iosEnumMembers).toEqual(['text', 'email', 'password', 'tel', 'url']);
227
+ expect(androidEnumMembers).toEqual(['TEXT', 'EMAIL', 'PASSWORD', 'TEL', 'URL']);
228
+ });
229
+ });
230
+
231
+ describe('iOS callers', () => {
232
+ // The iOS base preview also exercises the memberwise initializer
233
+ const files = [...IOS_CALLERS, IOS_BASE];
234
+
235
+ files.forEach((file) => {
236
+ const name = path.basename(file);
237
+
238
+ it(`${name}: every InputTextBase argument is a declared property, in declaration order`, () => {
239
+ const source = fs.readFileSync(file, 'utf-8');
240
+ const calls = extractBaseCalls(source);
241
+ expect(calls.length).toBeGreaterThan(0);
242
+
243
+ calls.forEach((args) => {
244
+ const labels = args
245
+ .map((a) => argLabel(a, 'ios'))
246
+ .filter((l): l is string => Boolean(l));
247
+
248
+ const undeclared = labels.filter((l) => !iosBaseProps.includes(l));
249
+ expect(undeclared).toEqual([]);
250
+
251
+ // Memberwise init requires arguments in property declaration order
252
+ expect(isInOrderSubsequence(labels, iosBaseProps)).toBe(true);
253
+ });
254
+ });
255
+
256
+ it(`${name}: type argument references a declared InputType case`, () => {
257
+ const source = fs.readFileSync(file, 'utf-8');
258
+ extractBaseCalls(source).forEach((args) => {
259
+ const typeArg = args.find((a) => argLabel(a, 'ios') === 'type');
260
+ if (!typeArg) return;
261
+ const caseMatch = typeArg.match(/:\s*\.(\w+)/);
262
+ if (caseMatch) {
263
+ expect(iosEnumMembers).toContain(caseMatch[1]);
264
+ }
265
+ });
266
+ });
267
+ });
268
+ });
269
+
270
+ describe('Android callers', () => {
271
+ ANDROID_CALLERS.forEach((file) => {
272
+ const name = path.basename(file);
273
+
274
+ it(`${name}: every InputTextBase named argument is a declared parameter`, () => {
275
+ const source = fs.readFileSync(file, 'utf-8');
276
+ const calls = extractBaseCalls(source);
277
+ expect(calls.length).toBeGreaterThan(0);
278
+
279
+ calls.forEach((args) => {
280
+ const labels = args
281
+ .map((a) => argLabel(a, 'android'))
282
+ .filter((l): l is string => Boolean(l));
283
+ expect(labels.length).toBeGreaterThan(0);
284
+
285
+ const undeclared = labels.filter((l) => !androidBaseParams.includes(l));
286
+ expect(undeclared).toEqual([]);
287
+ });
288
+ });
289
+
290
+ it(`${name}: every InputType reference is a declared enum member`, () => {
291
+ const source = fs.readFileSync(file, 'utf-8');
292
+ const refs = [...source.matchAll(/\bInputType\.(\w+)/g)].map((m) => m[1]);
293
+ const unknown = refs.filter((r) => !androidEnumMembers.includes(r));
294
+ expect(unknown).toEqual([]);
295
+ });
296
+ });
297
+ });
298
+ });
@@ -17,6 +17,8 @@
17
17
 
18
18
  import type { Oklch } from '../color/OklchConverter';
19
19
 
20
+ // 'disabled' is deprecated calculator capability: DesignerPunk supports no disabled
21
+ // states (adjudicated 2026-07-15) — no component may use it; removal at next major.
20
22
  export type InteractionState = 'hover' | 'pressed' | 'focused' | 'disabled';
21
23
 
22
24
  /** Thresholds for interaction state blends. */
@@ -24,6 +26,7 @@ export const INTERACTION_THRESHOLDS = {
24
26
  hover: { deltaL: { min: 0.02, max: 0.05 }, deltaC: 0 },
25
27
  pressed: { deltaL: { min: 0.05, max: 0.10 }, deltaC: 0 },
26
28
  focused: { deltaL: 0, deltaC: { min: 0.02 } },
29
+ // deprecated 2026-07-15 (no-disabled-states philosophy) — removal at next major
27
30
  disabled: { deltaL: 0, deltaC: { min: 0.03 } },
28
31
  } as const;
29
32
 
@@ -37,6 +37,7 @@ object BlendTokenValues {
37
37
  const val focusSaturate: Float = 0.08f
38
38
 
39
39
  /** Disabled state desaturation - blend300 (12%) */
40
+ @Deprecated("DesignerPunk does not support disabled states (adjudicated 2026-07-15). Removal at next major.")
40
41
  const val disabledDesaturate: Float = 0.12f
41
42
 
42
43
  /** Icon optical balance lightening - blend200 (8%) */
@@ -163,6 +164,7 @@ fun Color.focusBlend(): Color {
163
164
  *
164
165
  * @see Requirements: 11.4 - Theme-aware wrapper functions
165
166
  */
167
+ @Deprecated("DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Use desaturate() for non-disabled desaturation. Removal at next major.")
166
168
  fun Color.disabledBlend(): Color {
167
169
  return this.desaturate(BlendTokenValues.disabledDesaturate)
168
170
  }
@@ -280,6 +282,7 @@ object BlendUtilitiesProvider {
280
282
  * @param color Base color
281
283
  * @return Desaturated color for disabled state
282
284
  */
285
+ @Deprecated("DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Removal at next major.")
283
286
  fun disabledColor(color: Color): Color {
284
287
  return color.disabledBlend()
285
288
  }
@@ -28,6 +28,7 @@ public struct BlendTokenValues {
28
28
  public static let focusSaturate: Double = 0.08
29
29
 
30
30
  /// Disabled state desaturation - blend300 (12%)
31
+ @available(*, deprecated, message: "DesignerPunk does not support disabled states (adjudicated 2026-07-15). Removal at next major.")
31
32
  public static let disabledDesaturate: Double = 0.12
32
33
 
33
34
  /// Icon optical balance lightening - blend200 (8%)
@@ -172,6 +173,7 @@ extension Color {
172
173
  /// ```
173
174
  ///
174
175
  /// @see Requirements: 11.4 - Theme-aware wrapper functions
176
+ @available(*, deprecated, message: "DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Use desaturate(_:) for non-disabled desaturation. Removal at next major.")
175
177
  public func disabledBlend() -> Color {
176
178
  return self.desaturate(BlendTokenValues.disabledDesaturate)
177
179
  }
@@ -276,6 +278,7 @@ public class BlendUtilitiesProvider {
276
278
  /// Calculate disabled color for a given base color
277
279
  /// - Parameter color: Base color
278
280
  /// - Returns: Desaturated color for disabled state
281
+ @available(*, deprecated, message: "DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Removal at next major.")
279
282
  public func disabledColor(_ color: Color) -> Color {
280
283
  return color.disabledBlend()
281
284
  }
@@ -54,7 +54,11 @@ export const BlendTokenValues = {
54
54
  pressedDarker: 0.12,
55
55
  /** Focus state saturation increase - blend200 (8%) */
56
56
  focusSaturate: 0.08,
57
- /** Disabled state desaturation - blend300 (12%) */
57
+ /**
58
+ * Disabled state desaturation - blend300 (12%)
59
+ * @deprecated 2026-07-15 — DesignerPunk does not support disabled states; if an action
60
+ * is unavailable, do not render the component. Scheduled for removal at the next major.
61
+ */
58
62
  disabledDesaturate: 0.12,
59
63
  /** Icon optical balance lightening - blend200 (8%) */
60
64
  iconLighter: 0.08
@@ -89,6 +93,9 @@ export interface BlendUtilitiesResult {
89
93
  * Calculate disabled color by desaturating the base color
90
94
  * @param baseColor - Base color as hex string (e.g., "#A855F7")
91
95
  * @returns Desaturated hex color string for disabled state
96
+ * @deprecated 2026-07-15 — DesignerPunk does not support disabled states; if an action
97
+ * is unavailable, do not render the component. For non-disabled desaturation, use the
98
+ * generic desaturate() utility. Scheduled for removal at the next major.
92
99
  */
93
100
  disabledColor: (baseColor: string) => string;
94
101
 
@@ -242,6 +249,7 @@ export function createBlendUtilities(): BlendUtilitiesResult {
242
249
  hoverColor: (baseColor: string) => darkerBlend(baseColor, BlendTokenValues.hoverDarker),
243
250
  pressedColor: (baseColor: string) => darkerBlend(baseColor, BlendTokenValues.pressedDarker),
244
251
  focusColor: (baseColor: string) => saturate(baseColor, BlendTokenValues.focusSaturate),
252
+ // deprecated 2026-07-15 (no-disabled-states philosophy) — removal at next major
245
253
  disabledColor: (baseColor: string) => desaturate(baseColor, BlendTokenValues.disabledDesaturate),
246
254
  iconColor: (baseColor: string) => lighterBlend(baseColor, BlendTokenValues.iconLighter),
247
255
 
@@ -13,7 +13,10 @@
13
13
  * hover: ΔL ∈ [0.02, 0.05], preserve chroma
14
14
  * pressed: ΔL ∈ [0.05, 0.10], preserve chroma
15
15
  * focused: ΔC ≥ 0.02 (chroma boost)
16
- * disabled: ΔC ≥ 0.03 (chroma reduction)
16
+ * disabled: ΔC ≥ 0.03 (chroma reduction) — calculator capability only;
17
+ * no component declares a disabled state (state_disabled removed
18
+ * from Button-CTA 2026-07-15, completing the no-disabled-states
19
+ * philosophy across the corpus)
17
20
  *
18
21
  * Components audited:
19
22
  * Button-CTA, Button-Icon, Button-VerticalList-Item, Chip-Base,
@@ -125,14 +128,14 @@ describe('Spec 112 Task 5.1: Interaction State Visual Audit', () => {
125
128
  });
126
129
 
127
130
  describe('Disabled state (ΔC ≥ 0.03 reduction)', () => {
128
- // Only Button-CTA has a disabled state in DesignerPunk
129
- const cases: [string, Oklch][] = [
130
- ['Button-CTA (primary)', primaryButton],
131
- ];
132
-
133
- it.each(cases)('%s — ΔC reduction meets minimum', (name, base) => {
134
- const result = calc.interactionBlend(base, 'disabled', lightSurface);
135
- const dc = deltaC(base, result);
131
+ // No component declares a disabled state (no-disabled-states philosophy).
132
+ // blend.disabledDesaturate was deprecated 2026-07-15 (PR #83, Button-CTA
133
+ // disabled-state adjudication). This test validates the calculator
134
+ // capability only, retained for backward compatibility; remove at the
135
+ // next major version alongside the token itself.
136
+ it('calculator disabled blend — ΔC reduction meets minimum', () => {
137
+ const result = calc.interactionBlend(primaryButton, 'disabled', lightSurface);
138
+ const dc = deltaC(primaryButton, result);
136
139
  // dc should be negative (chroma reduced), absolute value ≥ 0.03
137
140
  expect(dc).toBeLessThan(0);
138
141
  expect(Math.abs(dc)).toBeGreaterThanOrEqual(INTERACTION_THRESHOLDS.disabled.deltaC.min);
@@ -13,6 +13,14 @@ import { createBuildError } from '../BuildError';
13
13
  import { BuildResultSummary } from '../ErrorReporter';
14
14
 
15
15
  describe('ErrorHandler - ErrorReporter Integration', () => {
16
+ beforeEach(() => {
17
+ jest.spyOn(console, 'error').mockImplementation(() => {});
18
+ });
19
+
20
+ afterEach(() => {
21
+ jest.restoreAllMocks();
22
+ });
23
+
16
24
  describe('generateReport', () => {
17
25
  it('should generate report from logged errors', () => {
18
26
  const handler = new ErrorHandler();
@@ -13,9 +13,14 @@ describe('ErrorHandler', () => {
13
13
  let errorHandler: ErrorHandler;
14
14
 
15
15
  beforeEach(() => {
16
+ jest.spyOn(console, 'error').mockImplementation(() => {});
16
17
  errorHandler = new ErrorHandler();
17
18
  });
18
19
 
20
+ afterEach(() => {
21
+ jest.restoreAllMocks();
22
+ });
23
+
19
24
  describe('constructor', () => {
20
25
  it('should create ErrorHandler with default options', () => {
21
26
  const handler = new ErrorHandler();
@@ -392,6 +392,119 @@ describe('defineComponentTokens', () => {
392
392
  });
393
393
  });
394
394
 
395
+ describe('Family-mismatch guard (reference path)', () => {
396
+ // A reference literal that carries `category`, as every real primitive in
397
+ // src/tokens/** does. The guard keys off this field.
398
+ const createMockPrimitiveWithCategory = (
399
+ name: string,
400
+ baseValue: number,
401
+ category: string
402
+ ): PrimitiveTokenReference => ({ name, baseValue, category });
403
+
404
+ test('throws when a reference primitive belongs to a different family than declared', () => {
405
+ const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
406
+
407
+ expect(() =>
408
+ defineComponentTokens({
409
+ component: 'ButtonIcon',
410
+ family: 'spacing',
411
+ tokens: {
412
+ 'size.large': { reference: size600, reasoning: 'Large button size' },
413
+ },
414
+ })
415
+ ).toThrow(/Token family mismatch/);
416
+ });
417
+
418
+ test('error names the component, token, declared family, primitive and its real family', () => {
419
+ const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
420
+
421
+ let message = '';
422
+ try {
423
+ defineComponentTokens({
424
+ component: 'ButtonIcon',
425
+ family: 'spacing',
426
+ tokens: {
427
+ 'size.large': { reference: size600, reasoning: 'Large button size' },
428
+ },
429
+ });
430
+ } catch (error) {
431
+ message = (error as Error).message;
432
+ }
433
+
434
+ expect(message).toContain("component 'ButtonIcon'");
435
+ expect(message).toContain("token 'size.large'");
436
+ expect(message).toContain("'spacing' family call");
437
+ expect(message).toContain("'size600'");
438
+ expect(message).toContain("'sizing' family");
439
+ });
440
+
441
+ test('regression: the real Button-Icon defect (PR #126) now fails at authoring time', () => {
442
+ // Before PR #126, buttonIcon.tokens.ts declared family 'spacing' while referencing
443
+ // sizing primitives. That produced `SpacingTokens.size600` — a member no generated
444
+ // platform file defines — and was caught only by reading generated Swift/Kotlin.
445
+ expect(() =>
446
+ defineComponentTokens({
447
+ component: 'ButtonIcon',
448
+ family: 'spacing',
449
+ tokens: {
450
+ 'inset.large': {
451
+ reference: createMockPrimitiveWithCategory('space150', 12, 'spacing'),
452
+ reasoning: 'Correctly family-matched spacing token',
453
+ },
454
+ 'size.large': {
455
+ reference: createMockPrimitiveWithCategory('size600', 48, 'sizing'),
456
+ reasoning: 'Mis-stamped sizing token — the defect',
457
+ },
458
+ },
459
+ })
460
+ ).toThrow(/references primitive 'size600' from the 'sizing' family/);
461
+ });
462
+
463
+ test('accepts a reference whose category matches the declared family', () => {
464
+ const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
465
+
466
+ const result = defineComponentTokens({
467
+ component: 'ButtonIcon',
468
+ family: 'sizing',
469
+ tokens: {
470
+ 'size.large': { reference: size600, reasoning: 'Large button size' },
471
+ },
472
+ });
473
+
474
+ expect(result['size.large']).toBe(48);
475
+ expect((getTokenContract(result) ?? [])[0].family).toBe('sizing');
476
+ });
477
+
478
+ test('does not fire for reference literals without a category (back-compat)', () => {
479
+ const bareRef = createMockPrimitiveToken('space100', 8);
480
+
481
+ expect(() =>
482
+ defineComponentTokens({
483
+ component: 'ButtonIcon',
484
+ family: 'sizing',
485
+ tokens: {
486
+ 'inset.small': { reference: bareRef, reasoning: 'Bare reference literal' },
487
+ },
488
+ })
489
+ ).not.toThrow();
490
+ });
491
+
492
+ test('does not fire for value-path tokens (the Avatar icon-size case is NOT covered)', () => {
493
+ // Documents a real limitation: the Avatar `icon.size.*` gap fillers were mis-stamped
494
+ // `family: 'spacing'` on the VALUE path. No reference exists to cross-check, so this
495
+ // guard cannot catch that class of mislabel.
496
+ expect(() =>
497
+ defineComponentTokens({
498
+ component: 'Avatar',
499
+ family: 'spacing',
500
+ tokens: {
501
+ 'icon.size.xs': { value: 12, reasoning: 'Dimensional value in a spacing call' },
502
+ },
503
+ })
504
+ ).not.toThrow();
505
+ });
506
+ });
507
+
395
508
  describe('Idempotent re-branding (Spec 124, caveat c)', () => {
396
509
  test('re-applying the brand to the same return does not throw', () => {
397
510
  const result = defineComponentTokens({
@@ -65,6 +65,16 @@ export interface PrimitiveTokenReference {
65
65
  name: string;
66
66
  /** Unitless base value from the primitive token */
67
67
  baseValue: number;
68
+ /**
69
+ * Primitive token family (the `category` field of a `PrimitiveToken`, e.g. 'spacing',
70
+ * 'sizing', 'radius'). OPTIONAL for backward compatibility: minimal `{ name, baseValue }`
71
+ * reference literals remain valid.
72
+ *
73
+ * When present, it is cross-checked against the call's declared `family` by the
74
+ * family-mismatch guard in {@link defineComponentTokens}. Real primitives from
75
+ * src/tokens/** always carry it, so the guard is active for all production authoring.
76
+ */
77
+ category?: string;
68
78
  }
69
79
 
70
80
  /**
@@ -216,8 +226,40 @@ export function defineComponentTokens<T extends Record<string, TokenDefinition>>
216
226
  if (isTokenWithReference(definition)) {
217
227
  // Token with primitive reference
218
228
  const primitiveToken = definition.reference;
229
+
230
+ // FAMILY-MISMATCH GUARD.
231
+ // The call's `family` is stamped onto every token it registers, and the generator
232
+ // derives platform output from it — notably the primitive class name used for
233
+ // reference-path tokens (getFamilyClassName) and the Android `.dp` suffix. A call
234
+ // that declares one family but references another family's primitive therefore emits
235
+ // a member that does not exist on the target platform. That is exactly the Button-Icon
236
+ // defect: a `family: 'spacing'` call referencing `sizingTokens.size600` generated
237
+ // `SpacingTokens.size600`, a non-existent member (PR #126).
238
+ //
239
+ // Only enforced when the reference carries a `category` — minimal `{ name, baseValue }`
240
+ // literals (used widely in tests) are intentionally exempt rather than rejected.
241
+ //
242
+ // Escape hatch: a token that legitimately needs another family's VALUE should use the
243
+ // value path (`value:`), which asserts no cross-family token-chain claim.
244
+ if (
245
+ typeof primitiveToken.category === 'string' &&
246
+ primitiveToken.category !== family
247
+ ) {
248
+ throw new Error(
249
+ `Token family mismatch in defineComponentTokens() for component '${component}': ` +
250
+ `token '${key}' is declared in a '${family}' family call but references primitive ` +
251
+ `'${primitiveToken.name}' from the '${primitiveToken.category}' family. ` +
252
+ `Every token in a call is stamped with that call's family, which drives platform ` +
253
+ `output (e.g. the generated '${family.charAt(0).toUpperCase() + family.slice(1)}Tokens' ` +
254
+ `class reference), so this would emit a non-existent platform member. ` +
255
+ `Fix: move '${key}' into a separate defineComponentTokens() call with ` +
256
+ `family: '${primitiveToken.category}', or use the value path if no token-chain ` +
257
+ `relationship is intended.`
258
+ );
259
+ }
260
+
219
261
  const value = primitiveToken.baseValue;
220
-
262
+
221
263
  values[key] = value;
222
264
  registeredTokens.push({
223
265
  name: tokenName,
@@ -490,9 +490,20 @@ describe('CICDIntegration', () => {
490
490
 
491
491
  describe('Environment Detection', () => {
492
492
  const originalEnv = process.env;
493
-
493
+
494
494
  beforeEach(() => {
495
495
  process.env = { ...originalEnv };
496
+ // Neutralize the HOST's own CI environment before each scenario. On a real CI
497
+ // runner (e.g. GitHub Actions) the ambient GITHUB_ACTIONS/CI vars leak into the
498
+ // copied env and win detectCICDEnvironment()'s precedence order, so the GitLab
499
+ // and generic-CI scenarios detected 'github' instead — these tests only ever
500
+ // passed on laptops because laptops aren't CI (125-A Task 6, lane-timing run #2
501
+ // finding). Each scenario sets exactly the vars it means to test.
502
+ delete process.env.GITHUB_ACTIONS;
503
+ delete process.env.GITHUB_WORKFLOW;
504
+ delete process.env.GITLAB_CI;
505
+ delete process.env.CI_PIPELINE_ID;
506
+ delete process.env.CI;
496
507
  });
497
508
 
498
509
  afterEach(() => {