Skip to content

Commit b336c19

Browse files
authored
Merge pull request #331 from yn1323/feat/0417
feat: LPに道具集約セクションと画面プレビューセクションを追加
2 parents 84479f1 + 761befe commit b336c19

File tree

33 files changed

+1325
-204
lines changed

33 files changed

+1325
-204
lines changed

.agents/skills/convex-create-component/SKILL.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ Create reusable Convex components with clear boundaries and a small app-facing A
4242

4343
Ask the user, then pick one path:
4444

45-
| Goal | Shape | Reference |
46-
|------|-------|-----------|
47-
| Component for this app only | Local | `references/local-components.md` |
48-
| Publish or share across apps | Packaged | `references/packaged-components.md` |
49-
| User explicitly needs local + shared library code | Hybrid | `references/hybrid-components.md` |
50-
| Not sure | Default to local | `references/local-components.md` |
45+
| Goal | Shape | Reference |
46+
| ------------------------------------------------- | ---------------- | ----------------------------------- |
47+
| Component for this app only | Local | `references/local-components.md` |
48+
| Publish or share across apps | Packaged | `references/packaged-components.md` |
49+
| User explicitly needs local + shared library code | Hybrid | `references/hybrid-components.md` |
50+
| Not sure | Default to local | `references/local-components.md` |
5151

5252
Read exactly one reference file before proceeding.
5353

@@ -111,7 +111,7 @@ export const listUnread = query({
111111
userId: v.string(),
112112
message: v.string(),
113113
read: v.boolean(),
114-
})
114+
}),
115115
),
116116
handler: async (ctx, args) => {
117117
return await ctx.db
@@ -234,12 +234,16 @@ export const sendNotification = mutation({
234234

235235
```ts
236236
// Bad: parent app table IDs are not valid component validators
237-
args: { userId: v.id("users") }
237+
args: {
238+
userId: v.id("users");
239+
}
238240
```
239241

240242
```ts
241243
// Good: treat parent-owned IDs as strings at the boundary
242-
args: { userId: v.string() }
244+
args: {
245+
userId: v.string();
246+
}
243247
```
244248

245249
### Advanced Patterns

.agents/skills/convex-migration-helper/SKILL.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ Unless you are certain, prefer deprecating fields over deleting them. Mark the f
5555
// Before
5656
users: defineTable({
5757
name: v.string(),
58-
})
58+
});
5959

6060
// After - safe, new field is optional
6161
users: defineTable({
6262
name: v.string(),
6363
bio: v.optional(v.string()),
64-
})
64+
});
6565
```
6666

6767
### Adding New Table
@@ -70,7 +70,7 @@ users: defineTable({
7070
posts: defineTable({
7171
userId: v.id("users"),
7272
title: v.string(),
73-
}).index("by_user", ["userId"])
73+
}).index("by_user", ["userId"]);
7474
```
7575

7676
### Adding Index
@@ -79,8 +79,7 @@ posts: defineTable({
7979
users: defineTable({
8080
name: v.string(),
8181
email: v.string(),
82-
})
83-
.index("by_email", ["email"])
82+
}).index("by_email", ["email"]);
8483
```
8584

8685
## Breaking Changes: The Deployment Workflow

.agents/skills/convex-migration-helper/references/migration-patterns.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Common migration patterns, zero-downtime strategies, and verification techniques
99
users: defineTable({
1010
name: v.string(),
1111
role: v.optional(v.union(v.literal("user"), v.literal("admin"))),
12-
})
12+
});
1313

1414
// Migration: backfill the field
1515
export const addDefaultRole = migrations.define({
@@ -25,7 +25,7 @@ export const addDefaultRole = migrations.define({
2525
users: defineTable({
2626
name: v.string(),
2727
role: v.union(v.literal("user"), v.literal("admin")),
28-
})
28+
});
2929
```
3030

3131
## Deleting a Field

.agents/skills/convex-migration-helper/references/migrations-component.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,7 @@ Process only matching documents instead of the full table:
151151
```typescript
152152
export const fixEmptyNames = migrations.define({
153153
table: "users",
154-
customRange: (query) =>
155-
query.withIndex("by_name", (q) => q.eq("name", "")),
154+
customRange: (query) => query.withIndex("by_name", (q) => q.eq("name", "")),
156155
migrateOne: () => ({ name: "<unknown>" }),
157156
});
158157
```

.agents/skills/convex-performance-audit/SKILL.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ Start with the strongest signal available:
4343

4444
After gathering signals, identify the problem class and read the matching reference file.
4545

46-
| Signal | Reference |
47-
|---|---|
48-
| High bytes or documents read, JS filtering, unnecessary joins | `references/hot-path-rules.md` |
49-
| OCC conflict errors, write contention, mutation retries | `references/occ-conflicts.md` |
50-
| High subscription count, slow UI updates, excessive re-renders | `references/subscription-cost.md` |
51-
| Function timeouts, transaction size errors, large payloads | `references/function-budget.md` |
52-
| General "it's slow" with no specific signal | Start with `references/hot-path-rules.md` |
46+
| Signal | Reference |
47+
| -------------------------------------------------------------- | ----------------------------------------- |
48+
| High bytes or documents read, JS filtering, unnecessary joins | `references/hot-path-rules.md` |
49+
| OCC conflict errors, write contention, mutation retries | `references/occ-conflicts.md` |
50+
| High subscription count, slow UI updates, excessive re-renders | `references/subscription-cost.md` |
51+
| Function timeouts, transaction size errors, large payloads | `references/function-budget.md` |
52+
| General "it's slow" with no specific signal | Start with `references/hot-path-rules.md` |
5353

5454
Multiple problem classes can overlap. Read the most relevant reference first, then check the others if symptoms remain.
5555

@@ -107,7 +107,7 @@ After finding one problem, inspect both sibling readers and sibling writers for
107107
Examples:
108108

109109
- If one list query switches from full docs to a digest table, inspect the other list queries for that table
110-
- If one mutation needs no-op write protection, inspect the other writers to the same table
110+
- If one mutation isolates a frequently-updated field or splits a hot document, inspect the other writers to the same table
111111
- If one read path needs a migration-safe rollout for an unbackfilled field, inspect sibling reads for the same rollout risk
112112

113113
Do not leave one path fixed and another path on the old pattern unless there is a clear product reason.
@@ -119,7 +119,7 @@ Confirm all of these:
119119
1. Results are the same as before, no dropped records
120120
2. Eliminated reads or writes are no longer in the path where expected
121121
3. Fallback behavior works when denormalized or indexed fields are missing
122-
4. New writes avoid unnecessary invalidation when data is unchanged
122+
4. Frequently-updated fields are isolated from widely-read documents where needed
123123
5. Every relevant sibling reader and writer was inspected, not just the original function
124124

125125
## Reference Files

.agents/skills/convex-performance-audit/references/function-budget.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ Convex functions run inside transactions with budgets for time, reads, and write
1010

1111
These are the current values from the [Convex limits docs](https://docs.convex.dev/production/state/limits). Check that page for the latest numbers.
1212

13-
| Resource | Limit |
14-
|---|---|
15-
| Query/mutation execution time | 1 second (user code only, excludes DB operations) |
16-
| Action execution time | 10 minutes |
17-
| Data read per transaction | 16 MiB |
18-
| Data written per transaction | 16 MiB |
13+
| Resource | Limit |
14+
| --------------------------------- | ----------------------------------------------------- |
15+
| Query/mutation execution time | 1 second (user code only, excludes DB operations) |
16+
| Action execution time | 10 minutes |
17+
| Data read per transaction | 16 MiB |
18+
| Data written per transaction | 16 MiB |
1919
| Documents scanned per transaction | 32,000 (includes documents filtered out by `.filter`) |
20-
| Index ranges read per transaction | 4,096 (each `db.get` and `db.query` call) |
21-
| Documents written per transaction | 16,000 |
22-
| Individual document size | 1 MiB |
23-
| Function return value size | 16 MiB |
20+
| Index ranges read per transaction | 4,096 (each `db.get` and `db.query` call) |
21+
| Documents written per transaction | 16,000 |
22+
| Individual document size | 1 MiB |
23+
| Function return value size | 16 MiB |
2424

2525
## Symptoms
2626

.agents/skills/convex-performance-audit/references/hot-path-rules.md

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,15 @@ Indexes like `by_foo` and `by_foo_and_bar` are usually redundant. You only need
121121
// Bad: two indexes where one would do
122122
defineTable({ team: v.id("teams"), user: v.id("users") })
123123
.index("by_team", ["team"])
124-
.index("by_team_and_user", ["team", "user"])
124+
.index("by_team_and_user", ["team", "user"]);
125125
```
126126

127127
```ts
128128
// Good: single compound index serves both query patterns
129-
defineTable({ team: v.id("teams"), user: v.id("users") })
130-
.index("by_team_and_user", ["team", "user"])
129+
defineTable({ team: v.id("teams"), user: v.id("users") }).index(
130+
"by_team_and_user",
131+
["team", "user"],
132+
);
131133
```
132134

133135
Exception: `.index("by_foo", ["foo"])` is really an index on `foo` + `_creationTime`, while `.index("by_foo_and_bar", ["foo", "bar"])` is on `foo` + `bar` + `_creationTime`. If you need results sorted by `foo` then `_creationTime`, you need the single-field index because the compound one would sort by `bar` first.
@@ -170,9 +172,7 @@ const ownerName = project.ownerName ?? "Unknown owner";
170172
```ts
171173
// Good: denormalized data is an optimization, not the only source of truth
172174
const ownerName =
173-
project.ownerName ??
174-
(await ctx.db.get(project.ownerId))?.name ??
175-
null;
175+
project.ownerName ?? (await ctx.db.get(project.ownerId))?.name ?? null;
176176
```
177177

178178
Bad lookup map pattern:
@@ -241,35 +241,33 @@ const projects = await ctx.db
241241
.take(20);
242242
```
243243

244-
## 4. Skip No-Op Writes
245-
246-
No-op writes still cost work in Convex:
244+
## 4. Isolate Frequently-Updated Fields
247245

248-
- invalidation
249-
- replication
250-
- trigger execution
251-
- downstream sync
246+
Convex already no-ops unchanged writes. The invalidation problem here is real writes hitting documents that many queries subscribe to.
252247

253-
Before `patch` or `replace`, compare against the existing document and skip the write if nothing changed.
248+
Move high-churn fields like `lastSeen`, counters, presence, or ephemeral status off widely-read documents when most readers do not need them.
254249

255-
Apply this across sibling writers too. One careful writer does not help much if three other mutations still patch unconditionally.
250+
Apply this across sibling writers too. Splitting one write path does not help much if three other mutations still update the same widely-read document.
256251

257252
```ts
258-
// Bad: patching unchanged values still triggers invalidation and downstream work
259-
await ctx.db.patch(settings._id, {
260-
theme: args.theme,
261-
locale: args.locale,
253+
// Bad: every presence heartbeat invalidates subscribers to the whole profile
254+
await ctx.db.patch(user._id, {
255+
name: args.name,
256+
avatarUrl: args.avatarUrl,
257+
lastSeen: Date.now(),
262258
});
263259
```
264260

265261
```ts
266-
// Good: only write when something actually changed
267-
if (settings.theme !== args.theme || settings.locale !== args.locale) {
268-
await ctx.db.patch(settings._id, {
269-
theme: args.theme,
270-
locale: args.locale,
271-
});
272-
}
262+
// Good: keep profile reads stable, move heartbeat updates to a separate document
263+
await ctx.db.patch(user._id, {
264+
name: args.name,
265+
avatarUrl: args.avatarUrl,
266+
});
267+
268+
await ctx.db.patch(presence._id, {
269+
lastSeen: Date.now(),
270+
});
273271
```
274272

275273
## 5. Match Consistency To Read Patterns

.agents/skills/convex-performance-audit/references/occ-conflicts.md

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -73,42 +73,30 @@ await ctx.db.patch(shardId, { count: shard!.count + 1 });
7373

7474
Aggregate the shards in a query or scheduled job when you need the total.
7575

76-
### 3. Skip no-op writes
76+
### 3. Move non-critical work to scheduled functions
7777

78-
Writes that do not change data still participate in conflict detection and trigger invalidation.
78+
If a mutation does primary work plus secondary bookkeeping (analytics, non-critical notifications, cache warming), the bookkeeping extends the transaction's lifetime and read/write set.
7979

8080
```ts
81-
// Bad: patches even when nothing changed
82-
await ctx.db.patch(doc._id, { status: args.status });
83-
```
84-
85-
```ts
86-
// Good: only write when the value actually differs
87-
if (doc.status !== args.status) {
88-
await ctx.db.patch(doc._id, { status: args.status });
89-
}
90-
```
91-
92-
### 4. Move non-critical work to scheduled functions
93-
94-
If a mutation does primary work plus secondary bookkeeping (analytics, notifications, cache warming), the bookkeeping extends the transaction's lifetime and read/write set.
95-
96-
```ts
97-
// Bad: analytics update in the same transaction as the user action
98-
await ctx.db.patch(userId, { lastActiveAt: Date.now() });
99-
await ctx.db.insert("analytics", { event: "action", userId, ts: Date.now() });
81+
// Bad: canonical write and derived work happen in the same transaction
82+
await ctx.db.patch(userId, { name: args.name });
83+
await ctx.db.insert("userUpdateAnalytics", {
84+
userId,
85+
kind: "name_changed",
86+
name: args.name,
87+
});
10088
```
10189

10290
```ts
103-
// Good: schedule the bookkeeping so the primary transaction is smaller
104-
await ctx.db.patch(userId, { lastActiveAt: Date.now() });
105-
await ctx.scheduler.runAfter(0, internal.analytics.recordEvent, {
106-
event: "action",
91+
// Good: keep the primary write small, defer the analytics work
92+
await ctx.db.patch(userId, { name: args.name });
93+
await ctx.scheduler.runAfter(0, internal.users.recordNameChangeAnalytics, {
10794
userId,
95+
name: args.name,
10896
});
10997
```
11098

111-
### 5. Combine competing writes
99+
### 4. Combine competing writes
112100

113101
If two mutations must update the same document atomically, consider whether they can be combined into a single mutation call from the client, reducing round trips and conflict windows.
114102

0 commit comments

Comments
 (0)