···11+---
22+name: lexicons
33+description: Reference for the AT Protocol lexicon system. Use when working with lexicons, adding new record types, or modifying AT Protocol schemas.
44+user-invocable: false
55+---
66+77+# Lexicon System
88+99+## Overview
1010+1111+Lexicons define the schema for AT Protocol records. This project has two namespaces:
1212+- **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.)
1313+- **`site.standard.*`** - Standard site lexicons for interoperability
1414+1515+The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`.
1616+1717+## Key Files
1818+1919+- **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons
2020+- **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained)
2121+- **`lexicons/build.ts`** - Builds TypeScript sources to JSON
2222+- **`lexicons/api/`** - Generated TypeScript types and client
2323+- **`package.json`** - Contains `lexgen` script
2424+2525+## Running Lexicon Generation
2626+2727+```bash
2828+npm run lexgen
2929+```
3030+3131+This runs:
3232+1. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript
3333+2. `lex gen-api` - Generates TypeScript types from all JSON lexicons
3434+3. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions
3535+3636+## Adding a New pub.leaflet Lexicon
3737+3838+### 1. Create the Source Definition
3939+4040+Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`):
4141+4242+```typescript
4343+import { LexiconDoc } from "@atproto/lexicon";
4444+4545+export const PubLeafletMyLexicon: LexiconDoc = {
4646+ lexicon: 1,
4747+ id: "pub.leaflet.myLexicon",
4848+ defs: {
4949+ main: {
5050+ type: "record", // or "object" for non-record types
5151+ key: "tid",
5252+ record: {
5353+ type: "object",
5454+ required: ["field1"],
5555+ properties: {
5656+ field1: { type: "string", maxLength: 1000 },
5757+ field2: { type: "integer", minimum: 0 },
5858+ optionalRef: { type: "ref", ref: "other.lexicon#def" },
5959+ },
6060+ },
6161+ },
6262+ // Additional defs for sub-objects
6363+ subType: {
6464+ type: "object",
6565+ properties: {
6666+ nested: { type: "string" },
6767+ },
6868+ },
6969+ },
7070+};
7171+```
7272+7373+### 2. Add to Build
7474+7575+Update `lexicons/build.ts`:
7676+7777+```typescript
7878+import { PubLeafletMyLexicon } from "./src/myLexicon";
7979+8080+const lexicons = [
8181+ // ... existing lexicons
8282+ PubLeafletMyLexicon,
8383+];
8484+```
8585+8686+### 3. Update lexgen Command (if needed)
8787+8888+If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`:
8989+9090+```json
9191+"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..."
9292+```
9393+9494+Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included.
9595+9696+### 4. Add to authFullPermissions (for record types)
9797+9898+If your lexicon is a record type that users should be able to create/update/delete, add it to the `authFullPermissions` permission set in `lexicons/src/authFullPermissions.ts`:
9999+100100+```typescript
101101+import { PubLeafletMyLexicon } from "./myLexicon";
102102+103103+// In the permissions collection array:
104104+collection: [
105105+ // ... existing lexicons
106106+ PubLeafletMyLexicon.id,
107107+],
108108+```
109109+110110+### 5. Regenerate Types
111111+112112+```bash
113113+npm run lexgen
114114+```
115115+116116+### 6. Use the Generated Types
117117+118118+```typescript
119119+import { PubLeafletMyLexicon } from "lexicons/api";
120120+121121+// Type for the record
122122+type MyRecord = PubLeafletMyLexicon.Record;
123123+124124+// Validation
125125+const result = PubLeafletMyLexicon.validateRecord(data);
126126+if (result.success) {
127127+ // result.value is typed
128128+}
129129+130130+// Type guard
131131+if (PubLeafletMyLexicon.isRecord(data)) {
132132+ // data is typed as Record
133133+}
134134+```
135135+136136+## Adding a New site.standard Lexicon
137137+138138+### 1. Create the JSON Definition
139139+140140+Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`):
141141+142142+```json
143143+{
144144+ "lexicon": 1,
145145+ "id": "site.standard.myType",
146146+ "defs": {
147147+ "main": {
148148+ "type": "record",
149149+ "key": "tid",
150150+ "record": {
151151+ "type": "object",
152152+ "required": ["field1"],
153153+ "properties": {
154154+ "field1": {
155155+ "type": "string",
156156+ "maxLength": 1000
157157+ }
158158+ }
159159+ }
160160+ }
161161+ }
162162+}
163163+```
164164+165165+### 2. Regenerate Types
166166+167167+```bash
168168+npm run lexgen
169169+```
170170+171171+The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files.
172172+173173+## Common Lexicon Patterns
174174+175175+### Referencing Other Lexicons
176176+177177+```typescript
178178+// Reference another lexicon's main def
179179+{ type: "ref", ref: "pub.leaflet.publication" }
180180+181181+// Reference a specific def within a lexicon
182182+{ type: "ref", ref: "pub.leaflet.publication#theme" }
183183+184184+// Reference within the same lexicon
185185+{ type: "ref", ref: "#myDef" }
186186+```
187187+188188+### Union Types
189189+190190+```typescript
191191+{
192192+ type: "union",
193193+ refs: [
194194+ "pub.leaflet.pages.linearDocument",
195195+ "pub.leaflet.pages.canvas",
196196+ ],
197197+}
198198+199199+// Open union (allows unknown types)
200200+{
201201+ type: "union",
202202+ closed: false, // default is true
203203+ refs: ["pub.leaflet.content"],
204204+}
205205+```
206206+207207+### Blob Types (for images/files)
208208+209209+```typescript
210210+{
211211+ type: "blob",
212212+ accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"]
213213+ maxSize: 1000000, // bytes
214214+}
215215+```
216216+217217+### Color Types
218218+219219+The project has color types defined:
220220+- `pub.leaflet.theme.color#rgb` / `#rgba`
221221+- `site.standard.theme.color#rgb` / `#rgba`
222222+223223+```typescript
224224+// In lexicons/src/theme.ts
225225+export const ColorUnion = {
226226+ type: "union",
227227+ refs: [
228228+ "pub.leaflet.theme.color#rgba",
229229+ "pub.leaflet.theme.color#rgb",
230230+ ],
231231+};
232232+```
233233+234234+## Normalization Between Formats
235235+236236+Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats:
237237+238238+```typescript
239239+import {
240240+ normalizeDocument,
241241+ normalizePublication,
242242+ isLeafletDocument,
243243+ isStandardDocument,
244244+ getDocumentPages,
245245+} from "lexicons/src/normalize";
246246+247247+// Normalize a document from either format
248248+const normalized = normalizeDocument(record);
249249+if (normalized) {
250250+ // normalized is always in site.standard.document format
251251+ console.log(normalized.title, normalized.site);
252252+253253+ // Get pages if content is pub.leaflet.content
254254+ const pages = getDocumentPages(normalized);
255255+}
256256+257257+// Normalize a publication
258258+const pub = normalizePublication(record);
259259+if (pub) {
260260+ console.log(pub.name, pub.url);
261261+}
262262+```
263263+264264+## Handling in Appview (Firehose Consumer)
265265+266266+When processing records from the firehose in `appview/index.ts`:
267267+268268+```typescript
269269+import { ids } from "lexicons/api/lexicons";
270270+import { PubLeafletMyLexicon } from "lexicons/api";
271271+272272+// In filterCollections:
273273+filterCollections: [
274274+ ids.PubLeafletMyLexicon,
275275+ // ...
276276+],
277277+278278+// In handleEvent:
279279+if (evt.collection === ids.PubLeafletMyLexicon) {
280280+ if (evt.event === "create" || evt.event === "update") {
281281+ let record = PubLeafletMyLexicon.validateRecord(evt.record);
282282+ if (!record.success) return;
283283+284284+ // Store in database
285285+ await supabase.from("my_table").upsert({
286286+ uri: evt.uri.toString(),
287287+ data: record.value as Json,
288288+ });
289289+ }
290290+ if (evt.event === "delete") {
291291+ await supabase.from("my_table").delete().eq("uri", evt.uri.toString());
292292+ }
293293+}
294294+```
295295+296296+## Publishing Lexicons
297297+298298+To publish lexicons to an AT Protocol PDS:
299299+300300+```bash
301301+npm run publish-lexicons
302302+```
303303+304304+This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS.
+150
.claude/skills/notifications/SKILL.md
···11+---
22+name: notifications
33+description: Reference for the notification system. Use when adding new notification types or modifying notification handling.
44+user-invocable: false
55+---
66+77+# Notification System
88+99+## Overview
1010+1111+Notifications are stored in the database and hydrated with related data before being rendered. The system supports multiple notification types (comments, subscriptions, etc.) that are processed in parallel.
1212+1313+## Key Files
1414+1515+- **`src/notifications.ts`** - Core notification types and hydration logic
1616+- **`app/(home-pages)/notifications/NotificationList.tsx`** - Renders all notification types
1717+- **`app/(home-pages)/notifications/Notification.tsx`** - Base notification component
1818+- Individual notification components (e.g., `CommentNotification.tsx`, `FollowNotification.tsx`)
1919+2020+## Adding a New Notification Type
2121+2222+### 1. Update Notification Data Types (`src/notifications.ts`)
2323+2424+Add your type to the `NotificationData` union:
2525+2626+```typescript
2727+export type NotificationData =
2828+ | { type: "comment"; comment_uri: string; parent_uri?: string }
2929+ | { type: "subscribe"; subscription_uri: string }
3030+ | { type: "your_type"; your_field: string }; // Add here
3131+```
3232+3333+Add to the `HydratedNotification` union:
3434+3535+```typescript
3636+export type HydratedNotification =
3737+ | HydratedCommentNotification
3838+ | HydratedSubscribeNotification
3939+ | HydratedYourNotification; // Add here
4040+```
4141+4242+### 2. Create Hydration Function (`src/notifications.ts`)
4343+4444+```typescript
4545+export type HydratedYourNotification = Awaited<
4646+ ReturnType<typeof hydrateYourNotifications>
4747+>[0];
4848+4949+async function hydrateYourNotifications(notifications: NotificationRow[]) {
5050+ const yourNotifications = notifications.filter(
5151+ (n): n is NotificationRow & { data: ExtractNotificationType<"your_type"> } =>
5252+ (n.data as NotificationData)?.type === "your_type",
5353+ );
5454+5555+ if (yourNotifications.length === 0) return [];
5656+5757+ // Fetch related data with joins
5858+ const { data } = await supabaseServerClient
5959+ .from("your_table")
6060+ .select("*, related_table(*)")
6161+ .in("uri", yourNotifications.map((n) => n.data.your_field));
6262+6363+ return yourNotifications.map((notification) => ({
6464+ id: notification.id,
6565+ recipient: notification.recipient,
6666+ created_at: notification.created_at,
6767+ type: "your_type" as const,
6868+ your_field: notification.data.your_field,
6969+ yourData: data?.find((d) => d.uri === notification.data.your_field)!,
7070+ }));
7171+}
7272+```
7373+7474+Add to `hydrateNotifications` parallel array:
7575+7676+```typescript
7777+const [commentNotifications, subscribeNotifications, yourNotifications] = await Promise.all([
7878+ hydrateCommentNotifications(notifications),
7979+ hydrateSubscribeNotifications(notifications),
8080+ hydrateYourNotifications(notifications), // Add here
8181+]);
8282+8383+const allHydrated = [...commentNotifications, ...subscribeNotifications, ...yourNotifications];
8484+```
8585+8686+### 3. Trigger the Notification (in your action file)
8787+8888+```typescript
8989+import { Notification, pingIdentityToUpdateNotification } from "src/notifications";
9090+import { v7 } from "uuid";
9191+9292+// When the event occurs:
9393+const recipient = /* determine who should receive it */;
9494+if (recipient !== currentUser) {
9595+ const notification: Notification = {
9696+ id: v7(),
9797+ recipient,
9898+ data: {
9999+ type: "your_type",
100100+ your_field: "value",
101101+ },
102102+ };
103103+ await supabaseServerClient.from("notifications").insert(notification);
104104+ await pingIdentityToUpdateNotification(recipient);
105105+}
106106+```
107107+108108+### 4. Create Notification Component
109109+110110+Create a new component (e.g., `YourNotification.tsx`):
111111+112112+```typescript
113113+import { HydratedYourNotification } from "src/notifications";
114114+import { Notification } from "./Notification";
115115+116116+export const YourNotification = (props: HydratedYourNotification) => {
117117+ // Extract data from props.yourData
118118+119119+ return (
120120+ <Notification
121121+ timestamp={props.created_at}
122122+ href={/* link to relevant page */}
123123+ icon={/* icon or avatar */}
124124+ actionText={<>Message to display</>}
125125+ content={/* optional additional content */}
126126+ />
127127+ );
128128+};
129129+```
130130+131131+### 5. Update NotificationList (`NotificationList.tsx`)
132132+133133+Import and render your notification type:
134134+135135+```typescript
136136+import { YourNotification } from "./YourNotification";
137137+138138+// In the map function:
139139+if (n.type === "your_type") {
140140+ return <YourNotification key={n.id} {...n} />;
141141+}
142142+```
143143+144144+## Example: Subscribe Notifications
145145+146146+See the implementation in:
147147+- `src/notifications.ts:88-125` - Hydration logic
148148+- `app/lish/subscribeToPublication.ts:55-68` - Trigger
149149+- `app/(home-pages)/notifications/FollowNotification.tsx` - Component
150150+- `app/(home-pages)/notifications/NotificationList.tsx:40-42` - Rendering
+85
.claude/skills/spec/SKILL.md
···11+---
22+name: spec
33+description: Draft design documents and specs with research-informed questioning
44+disable-model-invocation: true
55+---
66+77+# Spec Writing
88+99+## When to Use
1010+1111+Use this skill when the user explicitly requests a spec, design document, or similar planning artifact. Do not auto-trigger for routine tasks.
1212+1313+## Process
1414+1515+### 1. Initial Scoping
1616+1717+When starting a spec:
1818+- Ask the user for a brief description of what they want to build/change
1919+- Reason about which question categories matter for this particular work—derive these from the problem, not a checklist
2020+2121+### 2. Interrogation Loop
2222+2323+Conduct open-ended questioning until the designer signals completion:
2424+2525+**Research-informed questions**: Before asking about an area, do just-in-time codebase research. Find relevant files, understand current patterns, identify constraints. Reference specific files and functions in questions when it adds clarity.
2626+2727+**Adaptive scope**: When the designer marks something as out of scope, update your mental model and don't revisit. However, flag when you believe there are gaps that could cause problems:
2828+- "This approach assumes X—is that intentional?"
2929+- "The current implementation of Y in `src/foo.ts:42` handles Z differently. Should we align or diverge?"
3030+3131+**Question style**:
3232+- Ask one focused question at a time, or a small related cluster
3333+- Ground questions in what you've learned from the codebase
3434+- Avoid hypotheticals that don't apply to this codebase
3535+- When presenting options, describe tradeoffs without recommending unless asked
3636+3737+### 3. Spec Writing
3838+3939+When the designer indicates readiness, produce the spec document.
4040+4141+## Output
4242+4343+Save to: `/specs/YYYY-MM-DD-short-name.md`
4444+4545+### Required Elements
4646+4747+**Title and status** (draft | approved | implemented)
4848+4949+**Goal**: What this achieves and why. 1-3 sentences.
5050+5151+**Design**: The substance of what will be built/changed. For each significant component or concern:
5252+- Describe the approach
5353+- State key decisions with their rationale inline
5454+- Reference specific files, functions, and types where relevant
5555+- Structure subsections to fit the problem—no fixed format
5656+5757+**Implementation**: Ordered steps that an agent or developer can execute. Each step should:
5858+- Be concrete and actionable
5959+- Reference specific files/functions to modify or create
6060+- Be sequenced correctly (dependencies before dependents)
6161+6262+### Optional
6363+6464+- Background context (only if necessary for understanding)
6565+- Open questions (only if unresolved items remain)
6666+6767+Add other sections if the problem demands it.
6868+6969+## Writing Style
7070+7171+- No filler, hedging, or preamble
7272+- No "This document describes..." or "In this spec we will..."
7373+- Start sections with substance, not meta-commentary
7474+- Use precise technical language
7575+- Keep decisions and rationale tight—one sentence each when possible
7676+- Code references use `file/path.ts:lineNumber` or `functionName` in backticks
7777+- Prefer concrete over abstract; specific over general
7878+7979+## Status Lifecycle
8080+8181+- **draft**: Work in progress. Should not be merged to main.
8282+- **approved**: Designer has signed off. Ready for implementation.
8383+- **implemented**: Work is complete. Spec is now historical record.
8484+8585+Update status in the document as it progresses. Specs are point-in-time snapshots—do not update content after implementation begins except to change status.