Revision 17612 does Phase 1 and half of Phase 3.
1) When we parse items.txt, "candy", "candy1", and "candy2" items are registered with CandyDatabase, which has data structures and methods that are useful elsewhere.
2) Items have a string candy_type proxy record field:
3) Effects have an int candy_tier proxy record field:
> ash $item[ cool whip ].candy_type
> ash $item[ hard rock candy ].candy_type
> ash $item[ Angry Farmer candy ].candy_type
> ash $item[ sugar sheet ].candy_type
4) ASH now has a function to return all the candy in a tier:
> ash $effect[ Synthesis: Greed ].candy_tier
> ash $effect[ Synthesis: Pungent ].candy_tier
> ash $effect[ Synthesis: Smart ].candy_tier
> ash $effect[ Got Milk ].candy_tier
item [int] candy_for_tier( int tier )
Note that this is not convenient to check if a given candy is in a tier; you can't say "candy_for_tier( 1 ) contains $item[ Angry Farmer candy ]", for example. Instead, you'd say "$item[ Angry Farmer candy ].candy_type == "simple"". This is an array that you can use with "sort by" to order by mall price, quantity on hand, or what have you.
> ash candy_for_tier( 0 )
Returned: aggregate string 
0 => hard rock candy
> ash candy_for_tier( 1 ).count()
> ash candy_for_tier( 2 ).count()
> ash candy_for_tier( 3 ).count()
5) The "checkcandy" command will list all unspaded candy (with no argument) or will tell you the candy type of the specified candies. (That last was primarily to be able to test the CANDY_MATCH filter):
6) The "item string" generated by ItemDatabase now correctly includes "candy", "candy1", or "candy2", as appropriate.
***Unspaded candy: hard rock candy
> checkcandy angry farmer candy, sugar sheet
Angry Farmer candy: simple
sugar sheet: complex
> checkcandy cool whip
[cool whip] is not candy.