···1+{
2+ "lexicon": 1,
3+ "id": "social.drydown.house",
4+ "defs": {
5+ "main": {
6+ "type": "record",
7+ "description": "A fragrance house or brand",
8+ "key": "tid",
9+ "record": {
10+ "type": "object",
11+ "required": ["name", "createdAt"],
12+ "properties": {
13+ "name": {
14+ "type": "string",
15+ "minLength": 1,
16+ "maxLength": 100,
17+ "description": "House/brand name (must be unique per user)"
18+ },
19+ "createdAt": {
20+ "type": "string",
21+ "format": "datetime",
22+ "description": "Timestamp when house was created"
23+ },
24+ "updatedAt": {
25+ "type": "string",
26+ "format": "datetime",
27+ "description": "Timestamp of last update (for name corrections)"
28+ }
29+ }
30+ }
31+ }
32+ }
33+}
+26
src/types/lexicon-types.ts
···00000000000000000000000000
···1+/**
2+ * AT Protocol URI format: at://did/collection/rkey
3+ */
4+export type AtUri = string
5+6+/**
7+ * House/brand record (social.drydown.house)
8+ */
9+export interface House {
10+ uri?: AtUri // AT Protocol record URI (assigned after creation)
11+ name: string // 1-100 chars, required, unique per user
12+ createdAt: string // ISO 8601
13+ updatedAt?: string // ISO 8601
14+}
15+16+/**
17+ * Fragrance record (social.drydown.fragrance)
18+ */
19+export interface Fragrance {
20+ uri?: AtUri // AT Protocol record URI
21+ name: string // 1-200 chars, required
22+ house: AtUri // Reference to house record (required)
23+ year?: number // 1000-2100, optional
24+ createdAt: string // ISO 8601
25+ updatedAt?: string // ISO 8601
26+}
+21
src/validation/cascade.ts
···000000000000000000000
···1+import type { Fragrance, AtUri } from '@/types/lexicon-types'
2+3+/**
4+ * Check if house can be deleted (no fragrances reference it)
5+ */
6+export function canDeleteHouse(
7+ houseUri: AtUri,
8+ fragrances: Fragrance[]
9+): { canDelete: boolean; reason?: string; count?: number } {
10+ const referencingFragrances = fragrances.filter(f => f.house === houseUri)
11+12+ if (referencingFragrances.length > 0) {
13+ return {
14+ canDelete: false,
15+ reason: `${referencingFragrances.length} fragrance(s) reference this house`,
16+ count: referencingFragrances.length
17+ }
18+ }
19+20+ return { canDelete: true }
21+}
+20
src/validation/uniqueness.ts
···00000000000000000000
···1+import type { House, AtUri } from '@/types/lexicon-types'
2+3+/**
4+ * Check if house name is unique (case-insensitive)
5+ */
6+export function isHouseNameUnique(
7+ name: string,
8+ existingHouses: House[],
9+ excludeUri?: AtUri
10+): boolean {
11+ const normalized = name.trim().toLowerCase()
12+ return !existingHouses.some(h =>
13+ h.uri !== excludeUri &&
14+ h.name.toLowerCase() === normalized
15+ )
16+}
17+18+// NOTE: Fragrances do NOT need to be unique
19+// The same house can have multiple fragrances with the same name but different years
20+// Example: "Creed Aventus 2004" and "Creed Aventus 2024" are both valid