Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccc5d36a0d | ||
|
|
bfdb58fc81 | ||
|
|
b32dd24765 | ||
|
|
2c81d129f9 | ||
|
|
f4b713c3f4 | ||
|
|
5b130aca66 | ||
|
|
4653fa2b41 | ||
|
|
a3eabc2aaa | ||
|
|
099671a4e5 | ||
|
|
0f9d08018b | ||
|
|
83489d5084 | ||
|
|
0454aecf70 | ||
|
|
27da62afc9 | ||
|
|
faabf7ba7a | ||
|
|
433dccbaa9 | ||
|
|
e524a340cb | ||
|
|
6577f1c51f | ||
|
|
3e1e10b2a5 | ||
|
|
7093c982ba | ||
|
|
440a934280 | ||
|
|
87887dc4b9 | ||
|
|
00ff50055f | ||
|
|
d0c282dfff | ||
|
|
bc35b96b5d | ||
|
|
0223e8d807 | ||
|
|
1fd0fde700 | ||
|
|
2c1755e578 | ||
|
|
3100db93ed | ||
|
|
e9c4f72883 | ||
|
|
87c17a1ed0 | ||
|
|
4fa0a3c63d | ||
|
|
388fbcb2a9 | ||
|
|
bcdf2a6c24 | ||
|
|
385036d577 | ||
|
|
f970ccb2ed | ||
|
|
fce38ec6f0 | ||
|
|
61716bb512 | ||
|
|
ed0dd811a8 | ||
|
|
50a325be11 | ||
|
|
20fffff29c | ||
|
|
c95f494569 | ||
|
|
5401e40943 | ||
|
|
0a60c0eafa | ||
|
|
30e38dcb8b | ||
|
|
cad8657148 | ||
|
|
8ed4593f73 | ||
|
|
b9497648c6 | ||
|
|
1e59a1c901 | ||
|
|
118fad567e | ||
|
|
41a3a10ab6 | ||
|
|
caaaaf5718 | ||
|
|
4dc4e4e5a7 | ||
|
|
6f8e8085ef | ||
|
|
caf9d5849a |
No files matched your search
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Implement tasks from an OpenSpec change.
|
Implement tasks from an OpenSpec change.
|
||||||
@@ -25,9 +25,11 @@ Implement tasks from an OpenSpec change.
|
|||||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||||
|
|
||||||
2. **Check status to understand the schema**
|
2. **Check status to understand the schema**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand:
|
Parse the JSON to understand:
|
||||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||||
@@ -139,6 +141,7 @@ What would you like to do?
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Keep going through tasks until done or blocked
|
- Keep going through tasks until done or blocked
|
||||||
- Always read context files before starting (from the apply instructions output)
|
- Always read context files before starting (from the apply instructions output)
|
||||||
- If task is ambiguous, pause and ask before implementing
|
- If task is ambiguous, pause and ask before implementing
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Archive a completed change in the experimental workflow.
|
Archive a completed change in the experimental workflow.
|
||||||
@@ -68,6 +68,7 @@ Archive a completed change in the experimental workflow.
|
|||||||
5. **Perform the archive**
|
5. **Perform the archive**
|
||||||
|
|
||||||
Create the archive directory if it doesn't exist:
|
Create the archive directory if it doesn't exist:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p openspec/changes/archive
|
mkdir -p openspec/changes/archive
|
||||||
```
|
```
|
||||||
@@ -105,6 +106,7 @@ All artifacts complete. All tasks complete.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Always prompt for change selection if not provided
|
- Always prompt for change selection if not provided
|
||||||
- Use artifact graph (openspec status --json) for completion checking
|
- Use artifact graph (openspec status --json) for completion checking
|
||||||
- Don't block archive on warnings - just inform and confirm
|
- Don't block archive on warnings - just inform and confirm
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Archive multiple completed changes in a single operation.
|
Archive multiple completed changes in a single operation.
|
||||||
@@ -37,16 +37,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
For each selected change, collect:
|
For each selected change, collect:
|
||||||
|
|
||||||
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
||||||
- Parse `schemaName` and `artifacts` list
|
- Parse `schemaName` and `artifacts` list
|
||||||
- Note which artifacts are `done` vs other states
|
- Note which artifacts are `done` vs other states
|
||||||
|
|
||||||
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
||||||
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||||
- If no tasks file exists, note as "No tasks"
|
- If no tasks file exists, note as "No tasks"
|
||||||
|
|
||||||
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
||||||
- List which capability specs exist
|
- List which capability specs exist
|
||||||
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
||||||
|
|
||||||
4. **Detect spec conflicts**
|
4. **Detect spec conflicts**
|
||||||
|
|
||||||
@@ -66,18 +66,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||||
|
|
||||||
b. **Search the codebase** for implementation evidence:
|
b. **Search the codebase** for implementation evidence:
|
||||||
- Look for code implementing requirements from each delta spec
|
- Look for code implementing requirements from each delta spec
|
||||||
- Check for related files, functions, or tests
|
- Check for related files, functions, or tests
|
||||||
|
|
||||||
c. **Determine resolution**:
|
c. **Determine resolution**:
|
||||||
- If only one change is actually implemented -> sync that one's specs
|
- If only one change is actually implemented -> sync that one's specs
|
||||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||||
- If neither implemented -> skip spec sync, warn user
|
- If neither implemented -> skip spec sync, warn user
|
||||||
|
|
||||||
d. **Record resolution** for each conflict:
|
d. **Record resolution** for each conflict:
|
||||||
- Which change's specs to apply
|
- Which change's specs to apply
|
||||||
- In what order (if both)
|
- In what order (if both)
|
||||||
- Rationale (what was found in codebase)
|
- Rationale (what was found in codebase)
|
||||||
|
|
||||||
6. **Show consolidated status table**
|
6. **Show consolidated status table**
|
||||||
|
|
||||||
@@ -93,12 +93,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
```
|
```
|
||||||
|
|
||||||
For conflicts, show the resolution:
|
For conflicts, show the resolution:
|
||||||
|
|
||||||
```
|
```
|
||||||
* Conflict resolution:
|
* Conflict resolution:
|
||||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||||
```
|
```
|
||||||
|
|
||||||
For incomplete changes, show warnings:
|
For incomplete changes, show warnings:
|
||||||
|
|
||||||
```
|
```
|
||||||
Warnings:
|
Warnings:
|
||||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||||
@@ -107,7 +109,6 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
7. **Confirm batch operation**
|
7. **Confirm batch operation**
|
||||||
|
|
||||||
Use **AskUserQuestion tool** with a single confirmation:
|
Use **AskUserQuestion tool** with a single confirmation:
|
||||||
|
|
||||||
- "Archive N changes?" with options based on status
|
- "Archive N changes?" with options based on status
|
||||||
- Options might include:
|
- Options might include:
|
||||||
- "Archive all N changes"
|
- "Archive all N changes"
|
||||||
@@ -121,20 +122,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
Process changes in the determined order (respecting conflict resolution):
|
Process changes in the determined order (respecting conflict resolution):
|
||||||
|
|
||||||
a. **Sync specs** if delta specs exist:
|
a. **Sync specs** if delta specs exist:
|
||||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||||
- For conflicts, apply in resolved order
|
- For conflicts, apply in resolved order
|
||||||
- Track if sync was done
|
- Track if sync was done
|
||||||
|
|
||||||
b. **Perform the archive**:
|
b. **Perform the archive**:
|
||||||
```bash
|
|
||||||
mkdir -p openspec/changes/archive
|
```bash
|
||||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
mkdir -p openspec/changes/archive
|
||||||
```
|
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||||
|
```
|
||||||
|
|
||||||
c. **Track outcome** for each change:
|
c. **Track outcome** for each change:
|
||||||
- Success: archived successfully
|
- Success: archived successfully
|
||||||
- Failed: error during archive (record error)
|
- Failed: error during archive (record error)
|
||||||
- Skipped: user chose not to archive (if applicable)
|
- Skipped: user chose not to archive (if applicable)
|
||||||
|
|
||||||
9. **Display summary**
|
9. **Display summary**
|
||||||
|
|
||||||
@@ -157,6 +159,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
```
|
```
|
||||||
|
|
||||||
If any failures:
|
If any failures:
|
||||||
|
|
||||||
```
|
```
|
||||||
Failed 1 change:
|
Failed 1 change:
|
||||||
- some-change: Archive directory already exists
|
- some-change: Archive directory already exists
|
||||||
@@ -165,6 +168,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
**Conflict Resolution Examples**
|
**Conflict Resolution Examples**
|
||||||
|
|
||||||
Example 1: Only one implemented
|
Example 1: Only one implemented
|
||||||
|
|
||||||
```
|
```
|
||||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||||
|
|
||||||
@@ -180,6 +184,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Example 2: Both implemented
|
Example 2: Both implemented
|
||||||
|
|
||||||
```
|
```
|
||||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||||
|
|
||||||
@@ -233,6 +238,7 @@ No active changes found. Use `/opsx:new` to create a new change.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||||
- Always prompt for selection, never auto-select
|
- Always prompt for selection, never auto-select
|
||||||
- Detect spec conflicts early and resolve by checking codebase
|
- Detect spec conflicts early and resolve by checking codebase
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Continue working on a change by creating the next artifact.
|
Continue working on a change by creating the next artifact.
|
||||||
@@ -30,9 +30,11 @@ Continue working on a change by creating the next artifact.
|
|||||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
2. **Check current status**
|
2. **Check current status**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand current state. The response includes:
|
Parse the JSON to understand current state. The response includes:
|
||||||
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
||||||
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||||
@@ -40,7 +42,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
|
|
||||||
3. **Act based on status**:
|
3. **Act based on status**:
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If all artifacts are complete (`isComplete: true`)**:
|
**If all artifacts are complete (`isComplete: true`)**:
|
||||||
- Congratulate the user
|
- Congratulate the user
|
||||||
@@ -48,7 +50,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
- Suggest: "All artifacts created! You can now implement this change or archive it."
|
||||||
- STOP
|
- STOP
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||||
- Pick the FIRST artifact with `status: "ready"` from the status output
|
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||||
@@ -71,7 +73,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
- Show what was created and what's now unlocked
|
- Show what was created and what's now unlocked
|
||||||
- STOP after creating ONE artifact
|
- STOP after creating ONE artifact
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If no artifacts are ready (all blocked)**:
|
**If no artifacts are ready (all blocked)**:
|
||||||
- This shouldn't happen with a valid schema
|
- This shouldn't happen with a valid schema
|
||||||
@@ -85,6 +87,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After each invocation, show:
|
After each invocation, show:
|
||||||
|
|
||||||
- Which artifact was created
|
- Which artifact was created
|
||||||
- Schema workflow being used
|
- Schema workflow being used
|
||||||
- Current progress (N/M complete)
|
- Current progress (N/M complete)
|
||||||
@@ -98,6 +101,7 @@ The artifact types and their purpose depend on the schema. Use the `instruction`
|
|||||||
Common artifact patterns:
|
Common artifact patterns:
|
||||||
|
|
||||||
**spec-driven schema** (proposal → specs → design → tasks):
|
**spec-driven schema** (proposal → specs → design → tasks):
|
||||||
|
|
||||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||||
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
||||||
@@ -107,6 +111,7 @@ Common artifact patterns:
|
|||||||
For other schemas, follow the `instruction` field from the CLI output.
|
For other schemas, follow the `instruction` field from the CLI output.
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Create ONE artifact per invocation
|
- Create ONE artifact per invocation
|
||||||
- Always read dependency artifacts before creating a new one
|
- Always read dependency artifacts before creating a new one
|
||||||
- Never skip artifacts or create out of order
|
- Never skip artifacts or create out of order
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||||
@@ -33,24 +33,28 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
|
|||||||
Depending on what the user brings, you might:
|
Depending on what the user brings, you might:
|
||||||
|
|
||||||
**Explore the problem space**
|
**Explore the problem space**
|
||||||
|
|
||||||
- Ask clarifying questions that emerge from what they said
|
- Ask clarifying questions that emerge from what they said
|
||||||
- Challenge assumptions
|
- Challenge assumptions
|
||||||
- Reframe the problem
|
- Reframe the problem
|
||||||
- Find analogies
|
- Find analogies
|
||||||
|
|
||||||
**Investigate the codebase**
|
**Investigate the codebase**
|
||||||
|
|
||||||
- Map existing architecture relevant to the discussion
|
- Map existing architecture relevant to the discussion
|
||||||
- Find integration points
|
- Find integration points
|
||||||
- Identify patterns already in use
|
- Identify patterns already in use
|
||||||
- Surface hidden complexity
|
- Surface hidden complexity
|
||||||
|
|
||||||
**Compare options**
|
**Compare options**
|
||||||
|
|
||||||
- Brainstorm multiple approaches
|
- Brainstorm multiple approaches
|
||||||
- Build comparison tables
|
- Build comparison tables
|
||||||
- Sketch tradeoffs
|
- Sketch tradeoffs
|
||||||
- Recommend a path (if asked)
|
- Recommend a path (if asked)
|
||||||
|
|
||||||
**Visualize**
|
**Visualize**
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ Use ASCII diagrams liberally │
|
│ Use ASCII diagrams liberally │
|
||||||
@@ -69,6 +73,7 @@ Depending on what the user brings, you might:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Surface risks and unknowns**
|
**Surface risks and unknowns**
|
||||||
|
|
||||||
- Identify what could go wrong
|
- Identify what could go wrong
|
||||||
- Find gaps in understanding
|
- Find gaps in understanding
|
||||||
- Suggest spikes or investigations
|
- Suggest spikes or investigations
|
||||||
@@ -82,11 +87,13 @@ You have full context of the OpenSpec system. Use it naturally, don't force it.
|
|||||||
### Check for context
|
### Check for context
|
||||||
|
|
||||||
At the start, quickly check what exists:
|
At the start, quickly check what exists:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec list --json
|
openspec list --json
|
||||||
```
|
```
|
||||||
|
|
||||||
This tells you:
|
This tells you:
|
||||||
|
|
||||||
- If there are active changes
|
- If there are active changes
|
||||||
- Their names, schemas, and status
|
- Their names, schemas, and status
|
||||||
- What the user might be working on
|
- What the user might be working on
|
||||||
@@ -115,14 +122,14 @@ If the user mentions a change or you detect one is relevant:
|
|||||||
|
|
||||||
3. **Offer to capture when decisions are made**
|
3. **Offer to capture when decisions are made**
|
||||||
|
|
||||||
| Insight Type | Where to Capture |
|
| Insight Type | Where to Capture |
|
||||||
|--------------|------------------|
|
| -------------------------- | ---------------------------- |
|
||||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||||
| Requirement changed | `specs/<capability>/spec.md` |
|
| Requirement changed | `specs/<capability>/spec.md` |
|
||||||
| Design decision made | `design.md` |
|
| Design decision made | `design.md` |
|
||||||
| Scope changed | `proposal.md` |
|
| Scope changed | `proposal.md` |
|
||||||
| New work identified | `tasks.md` |
|
| New work identified | `tasks.md` |
|
||||||
| Assumption invalidated | Relevant artifact |
|
| Assumption invalidated | Relevant artifact |
|
||||||
|
|
||||||
Example offers:
|
Example offers:
|
||||||
- "That's a design decision. Capture it in design.md?"
|
- "That's a design decision. Capture it in design.md?"
|
||||||
@@ -147,6 +154,7 @@ If the user mentions a change or you detect one is relevant:
|
|||||||
## Handling Different Entry Points
|
## Handling Different Entry Points
|
||||||
|
|
||||||
**User brings a vague idea:**
|
**User brings a vague idea:**
|
||||||
|
|
||||||
```
|
```
|
||||||
User: I'm thinking about adding real-time collaboration
|
User: I'm thinking about adding real-time collaboration
|
||||||
|
|
||||||
@@ -170,6 +178,7 @@ You: Real-time collab is a big space. Let me think about this...
|
|||||||
```
|
```
|
||||||
|
|
||||||
**User brings a specific problem:**
|
**User brings a specific problem:**
|
||||||
|
|
||||||
```
|
```
|
||||||
User: The auth system is a mess
|
User: The auth system is a mess
|
||||||
|
|
||||||
@@ -201,6 +210,7 @@ You: [reads codebase]
|
|||||||
```
|
```
|
||||||
|
|
||||||
**User is stuck mid-implementation:**
|
**User is stuck mid-implementation:**
|
||||||
|
|
||||||
```
|
```
|
||||||
User: /opsx:explore add-auth-system
|
User: /opsx:explore add-auth-system
|
||||||
The OAuth integration is more complex than expected
|
The OAuth integration is more complex than expected
|
||||||
@@ -218,6 +228,7 @@ You: [reads change artifacts]
|
|||||||
```
|
```
|
||||||
|
|
||||||
**User wants to compare options:**
|
**User wants to compare options:**
|
||||||
|
|
||||||
```
|
```
|
||||||
User: Should we use Postgres or SQLite?
|
User: Should we use Postgres or SQLite?
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
|
Fast-forward through artifact creation - generate everything needed to start implementation in one go.
|
||||||
@@ -18,6 +18,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
1. **If no clear input provided, ask what they want to build**
|
1. **If no clear input provided, ask what they want to build**
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
@@ -25,15 +26,19 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||||
|
|
||||||
2. **Create the change directory**
|
2. **Create the change directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<name>"
|
openspec new change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/`.
|
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||||
|
|
||||||
3. **Get the artifact build order**
|
3. **Get the artifact build order**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to get:
|
Parse the JSON to get:
|
||||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||||
- `artifacts`: list of all artifacts with their status and dependencies
|
- `artifacts`: list of all artifacts with their status and dependencies
|
||||||
@@ -45,30 +50,30 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||||
|
|
||||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||||
- Get instructions:
|
- Get instructions:
|
||||||
```bash
|
```bash
|
||||||
openspec instructions <artifact-id> --change "<name>" --json
|
openspec instructions <artifact-id> --change "<name>" --json
|
||||||
```
|
```
|
||||||
- The instructions JSON includes:
|
- The instructions JSON includes:
|
||||||
- `context`: Project background (constraints for you - do NOT include in output)
|
- `context`: Project background (constraints for you - do NOT include in output)
|
||||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||||
- `template`: The structure to use for your output file
|
- `template`: The structure to use for your output file
|
||||||
- `instruction`: Schema-specific guidance for this artifact type
|
- `instruction`: Schema-specific guidance for this artifact type
|
||||||
- `outputPath`: Where to write the artifact
|
- `outputPath`: Where to write the artifact
|
||||||
- `dependencies`: Completed artifacts to read for context
|
- `dependencies`: Completed artifacts to read for context
|
||||||
- Read any completed dependency files for context
|
- Read any completed dependency files for context
|
||||||
- Create the artifact file using `template` as the structure
|
- Create the artifact file using `template` as the structure
|
||||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||||
- Show brief progress: "✓ Created <artifact-id>"
|
- Show brief progress: "✓ Created <artifact-id>"
|
||||||
|
|
||||||
b. **Continue until all `applyRequires` artifacts are complete**
|
b. **Continue until all `applyRequires` artifacts are complete**
|
||||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||||
- Stop when all `applyRequires` artifacts are done
|
- Stop when all `applyRequires` artifacts are done
|
||||||
|
|
||||||
c. **If an artifact requires user input** (unclear context):
|
c. **If an artifact requires user input** (unclear context):
|
||||||
- Use **AskUserQuestion tool** to clarify
|
- Use **AskUserQuestion tool** to clarify
|
||||||
- Then continue with creation
|
- Then continue with creation
|
||||||
|
|
||||||
5. **Show final status**
|
5. **Show final status**
|
||||||
```bash
|
```bash
|
||||||
@@ -78,6 +83,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After completing all artifacts, summarize:
|
After completing all artifacts, summarize:
|
||||||
|
|
||||||
- Change name and location
|
- Change name and location
|
||||||
- List of artifacts created with brief descriptions
|
- List of artifacts created with brief descriptions
|
||||||
- What's ready: "All artifacts created! Ready for implementation."
|
- What's ready: "All artifacts created! Ready for implementation."
|
||||||
@@ -94,6 +100,7 @@ After completing all artifacts, summarize:
|
|||||||
- These guide what you write, but should never appear in the output
|
- These guide what you write, but should never appear in the output
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||||
- Always read dependency artifacts before creating a new one
|
- Always read dependency artifacts before creating a new one
|
||||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Start a new change using the experimental artifact-driven approach.
|
Start a new change using the experimental artifact-driven approach.
|
||||||
@@ -18,6 +18,7 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
1. **If no clear input provided, ask what they want to build**
|
1. **If no clear input provided, ask what they want to build**
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
@@ -35,24 +36,30 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
**Otherwise**: Omit `--schema` to use the default.
|
**Otherwise**: Omit `--schema` to use the default.
|
||||||
|
|
||||||
3. **Create the change directory**
|
3. **Create the change directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<name>"
|
openspec new change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `--schema <name>` only if the user requested a specific workflow.
|
Add `--schema <name>` only if the user requested a specific workflow.
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
||||||
|
|
||||||
4. **Show the artifact status**
|
4. **Show the artifact status**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>"
|
openspec status --change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||||
|
|
||||||
5. **Get instructions for the first artifact**
|
5. **Get instructions for the first artifact**
|
||||||
The first artifact depends on the schema (e.g., `proposal` for spec-driven).
|
The first artifact depends on the schema (e.g., `proposal` for spec-driven).
|
||||||
Check the status output to find the first artifact with status "ready".
|
Check the status output to find the first artifact with status "ready".
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec instructions <first-artifact-id> --change "<name>"
|
openspec instructions <first-artifact-id> --change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This outputs the template and context for creating the first artifact.
|
This outputs the template and context for creating the first artifact.
|
||||||
|
|
||||||
6. **STOP and wait for user direction**
|
6. **STOP and wait for user direction**
|
||||||
@@ -60,6 +67,7 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After completing the steps, summarize:
|
After completing the steps, summarize:
|
||||||
|
|
||||||
- Change name and location
|
- Change name and location
|
||||||
- Schema/workflow being used and its artifact sequence
|
- Schema/workflow being used and its artifact sequence
|
||||||
- Current status (0/N artifacts complete)
|
- Current status (0/N artifacts complete)
|
||||||
@@ -67,6 +75,7 @@ After completing the steps, summarize:
|
|||||||
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
|
- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Do NOT create any artifacts yet - just show the instructions
|
- Do NOT create any artifacts yet - just show the instructions
|
||||||
- Do NOT advance beyond showing the first artifact template
|
- Do NOT advance beyond showing the first artifact template
|
||||||
- If the name is invalid (not kebab-case), ask for a valid name
|
- If the name is invalid (not kebab-case), ask for a valid name
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
|
Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
|
||||||
@@ -22,6 +22,7 @@ openspec status --json 2>&1 || echo "NOT_INITIALIZED"
|
|||||||
```
|
```
|
||||||
|
|
||||||
**If not initialized:**
|
**If not initialized:**
|
||||||
|
|
||||||
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
|
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
|
||||||
|
|
||||||
Stop here if not initialized.
|
Stop here if not initialized.
|
||||||
@@ -66,6 +67,7 @@ Scan the codebase for small improvement opportunities. Look for:
|
|||||||
6. **Missing validation** - User input handlers without validation
|
6. **Missing validation** - User input handlers without validation
|
||||||
|
|
||||||
Also check recent git activity:
|
Also check recent git activity:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git log --oneline -10 2>/dev/null || echo "No git history"
|
git log --oneline -10 2>/dev/null || echo "No git history"
|
||||||
```
|
```
|
||||||
@@ -101,6 +103,7 @@ Which task interests you? (Pick a number or describe your own)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**If nothing found:** Fall back to asking what the user wants to build:
|
**If nothing found:** Fall back to asking what the user wants to build:
|
||||||
|
|
||||||
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
||||||
|
|
||||||
### Scope Guardrail
|
### Scope Guardrail
|
||||||
@@ -133,6 +136,7 @@ Before we create a change, let me quickly show you **explore mode**—it's how y
|
|||||||
```
|
```
|
||||||
|
|
||||||
Spend 1-2 minutes investigating the relevant code:
|
Spend 1-2 minutes investigating the relevant code:
|
||||||
|
|
||||||
- Read the file(s) involved
|
- Read the file(s) involved
|
||||||
- Draw a quick ASCII diagram if it helps
|
- Draw a quick ASCII diagram if it helps
|
||||||
- Note any considerations
|
- Note any considerations
|
||||||
@@ -158,6 +162,7 @@ Now let's create a change to hold our work.
|
|||||||
## Phase 4: Create the Change
|
## Phase 4: Create the Change
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Creating a Change
|
## Creating a Change
|
||||||
|
|
||||||
@@ -167,21 +172,25 @@ Let me create one for our task.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:** Create the change with a derived kebab-case name:
|
**DO:** Create the change with a derived kebab-case name:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<derived-name>"
|
openspec new change "<derived-name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
**SHOW:**
|
**SHOW:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Created: `openspec/changes/<name>/`
|
Created: `openspec/changes/<name>/`
|
||||||
|
|
||||||
The folder structure:
|
The folder structure:
|
||||||
```
|
```
|
||||||
|
|
||||||
openspec/changes/<name>/
|
openspec/changes/<name>/
|
||||||
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
||||||
├── design.md ← How we'll build it (empty)
|
├── design.md ← How we'll build it (empty)
|
||||||
├── specs/ ← Detailed requirements (empty)
|
├── specs/ ← Detailed requirements (empty)
|
||||||
└── tasks.md ← Implementation checklist (empty)
|
└── tasks.md ← Implementation checklist (empty)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's fill in the first artifact—the proposal.
|
Now let's fill in the first artifact—the proposal.
|
||||||
@@ -192,6 +201,7 @@ Now let's fill in the first artifact—the proposal.
|
|||||||
## Phase 5: Proposal
|
## Phase 5: Proposal
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## The Proposal
|
## The Proposal
|
||||||
|
|
||||||
@@ -236,9 +246,11 @@ Does this capture the intent? I can adjust before we save it.
|
|||||||
**PAUSE** - Wait for user approval/feedback.
|
**PAUSE** - Wait for user approval/feedback.
|
||||||
|
|
||||||
After approval, save the proposal:
|
After approval, save the proposal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec instructions proposal --change "<name>" --json
|
openspec instructions proposal --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Then write the content to `openspec/changes/<name>/proposal.md`.
|
Then write the content to `openspec/changes/<name>/proposal.md`.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -252,6 +264,7 @@ Next up: specs.
|
|||||||
## Phase 6: Specs
|
## Phase 6: Specs
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Specs
|
## Specs
|
||||||
|
|
||||||
@@ -261,6 +274,7 @@ For a small task like this, we might only need one spec file.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:** Create the spec file:
|
**DO:** Create the spec file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
||||||
```
|
```
|
||||||
@@ -296,6 +310,7 @@ Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
|
|||||||
## Phase 7: Design
|
## Phase 7: Design
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
@@ -341,6 +356,7 @@ Save to `openspec/changes/<name>/design.md`.
|
|||||||
## Phase 8: Tasks
|
## Phase 8: Tasks
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@@ -379,6 +395,7 @@ Save to `openspec/changes/<name>/tasks.md`.
|
|||||||
## Phase 9: Apply (Implementation)
|
## Phase 9: Apply (Implementation)
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
@@ -413,6 +430,7 @@ The change is implemented! One more step—let's archive it.
|
|||||||
## Phase 10: Archive
|
## Phase 10: Archive
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Archiving
|
## Archiving
|
||||||
|
|
||||||
@@ -422,11 +440,13 @@ Archived changes become your project's decision history—you can always find th
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:**
|
**DO:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec archive "<name>"
|
openspec archive "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
**SHOW:**
|
**SHOW:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Sync delta specs from a change to main specs.
|
Sync delta specs from a change to main specs.
|
||||||
@@ -47,28 +47,28 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|||||||
|
|
||||||
c. **Apply changes intelligently**:
|
c. **Apply changes intelligently**:
|
||||||
|
|
||||||
**ADDED Requirements:**
|
**ADDED Requirements:**
|
||||||
- If requirement doesn't exist in main spec → add it
|
- If requirement doesn't exist in main spec → add it
|
||||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||||
|
|
||||||
**MODIFIED Requirements:**
|
**MODIFIED Requirements:**
|
||||||
- Find the requirement in main spec
|
- Find the requirement in main spec
|
||||||
- Apply the changes - this can be:
|
- Apply the changes - this can be:
|
||||||
- Adding new scenarios (don't need to copy existing ones)
|
- Adding new scenarios (don't need to copy existing ones)
|
||||||
- Modifying existing scenarios
|
- Modifying existing scenarios
|
||||||
- Changing the requirement description
|
- Changing the requirement description
|
||||||
- Preserve scenarios/content not mentioned in the delta
|
- Preserve scenarios/content not mentioned in the delta
|
||||||
|
|
||||||
**REMOVED Requirements:**
|
**REMOVED Requirements:**
|
||||||
- Remove the entire requirement block from main spec
|
- Remove the entire requirement block from main spec
|
||||||
|
|
||||||
**RENAMED Requirements:**
|
**RENAMED Requirements:**
|
||||||
- Find the FROM requirement, rename to TO
|
- Find the FROM requirement, rename to TO
|
||||||
|
|
||||||
d. **Create new main spec** if capability doesn't exist yet:
|
d. **Create new main spec** if capability doesn't exist yet:
|
||||||
- Create `openspec/specs/<capability>/spec.md`
|
- Create `openspec/specs/<capability>/spec.md`
|
||||||
- Add Purpose section (can be brief, mark as TBD)
|
- Add Purpose section (can be brief, mark as TBD)
|
||||||
- Add Requirements section with the ADDED requirements
|
- Add Requirements section with the ADDED requirements
|
||||||
|
|
||||||
4. **Show summary**
|
4. **Show summary**
|
||||||
|
|
||||||
@@ -82,16 +82,20 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|||||||
## ADDED Requirements
|
## ADDED Requirements
|
||||||
|
|
||||||
### Requirement: New Feature
|
### Requirement: New Feature
|
||||||
|
|
||||||
The system SHALL do something new.
|
The system SHALL do something new.
|
||||||
|
|
||||||
#### Scenario: Basic case
|
#### Scenario: Basic case
|
||||||
|
|
||||||
- **WHEN** user does X
|
- **WHEN** user does X
|
||||||
- **THEN** system does Y
|
- **THEN** system does Y
|
||||||
|
|
||||||
## MODIFIED Requirements
|
## MODIFIED Requirements
|
||||||
|
|
||||||
### Requirement: Existing Feature
|
### Requirement: Existing Feature
|
||||||
|
|
||||||
#### Scenario: New scenario to add
|
#### Scenario: New scenario to add
|
||||||
|
|
||||||
- **WHEN** user does A
|
- **WHEN** user does A
|
||||||
- **THEN** system does B
|
- **THEN** system does B
|
||||||
|
|
||||||
@@ -108,8 +112,9 @@ The system SHALL do something new.
|
|||||||
**Key Principle: Intelligent Merging**
|
**Key Principle: Intelligent Merging**
|
||||||
|
|
||||||
Unlike programmatic merging, you can apply **partial updates**:
|
Unlike programmatic merging, you can apply **partial updates**:
|
||||||
|
|
||||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||||
- The delta represents *intent*, not a wholesale replacement
|
- The delta represents _intent_, not a wholesale replacement
|
||||||
- Use your judgment to merge changes sensibly
|
- Use your judgment to merge changes sensibly
|
||||||
|
|
||||||
**Output On Success**
|
**Output On Success**
|
||||||
@@ -131,6 +136,7 @@ Main specs are now updated. The change remains active - archive when implementat
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Read both delta and main specs before making changes
|
- Read both delta and main specs before making changes
|
||||||
- Preserve existing content not mentioned in delta
|
- Preserve existing content not mentioned in delta
|
||||||
- If something is unclear, ask for clarification
|
- If something is unclear, ask for clarification
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ license: MIT
|
|||||||
compatibility: Requires openspec CLI.
|
compatibility: Requires openspec CLI.
|
||||||
metadata:
|
metadata:
|
||||||
author: openspec
|
author: openspec
|
||||||
version: "1.0"
|
version: '1.0'
|
||||||
generatedBy: "1.1.1"
|
generatedBy: '1.1.1'
|
||||||
---
|
---
|
||||||
|
|
||||||
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
Verify that an implementation matches the change artifacts (specs, tasks, design).
|
||||||
@@ -26,9 +26,11 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
2. **Check status to understand the schema**
|
2. **Check status to understand the schema**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand:
|
Parse the JSON to understand:
|
||||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||||
- Which artifacts exist for this change
|
- Which artifacts exist for this change
|
||||||
@@ -110,6 +112,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
8. **Generate Verification Report**
|
8. **Generate Verification Report**
|
||||||
|
|
||||||
**Summary Scorecard**:
|
**Summary Scorecard**:
|
||||||
|
|
||||||
```
|
```
|
||||||
## Verification Report: <change-name>
|
## Verification Report: <change-name>
|
||||||
|
|
||||||
@@ -122,7 +125,6 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Issues by Priority**:
|
**Issues by Priority**:
|
||||||
|
|
||||||
1. **CRITICAL** (Must fix before archive):
|
1. **CRITICAL** (Must fix before archive):
|
||||||
- Incomplete tasks
|
- Incomplete tasks
|
||||||
- Missing requirement implementations
|
- Missing requirement implementations
|
||||||
@@ -161,6 +163,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
**Output Format**
|
**Output Format**
|
||||||
|
|
||||||
Use clear markdown with:
|
Use clear markdown with:
|
||||||
|
|
||||||
- Table for summary scorecard
|
- Table for summary scorecard
|
||||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||||
- Code references in format: `file.ts:123`
|
- Code references in format: `file.ts:123`
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: antigravity-manager
|
name: geminiNexus-manager
|
||||||
description: Comprehensive guide to Antigravity Manager architecture, workflows, and development. Use this to understand how to work on the project.
|
description: Comprehensive guide to Gemini Nexus Manager architecture, workflows, and development. Use this to understand how to work on the project.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Antigravity Manager Developer Guide
|
# Gemini Nexus Manager Developer Guide
|
||||||
|
|
||||||
## 🏗️ Architecture Overview
|
## 🏗️ Architecture Overview
|
||||||
|
|
||||||
Antigravity Manager is a hybrid Desktop Application built with Electron, React, and NestJS. It follows a modular architecture where the frontend (Renderer) communicates with the backend (Main) via type-safe IPC (ORPC).
|
Gemini Nexus Manager is a hybrid Desktop Application built with Electron, React, and NestJS. It follows a modular architecture where the frontend (Renderer) communicates with the backend (Main) via type-safe IPC (ORPC).
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
@@ -69,7 +69,7 @@ The project uses `orpc` for type-safe communication.
|
|||||||
|
|
||||||
### Database Access
|
### Database Access
|
||||||
|
|
||||||
Data is stored in a local SQLite file (`test.db` in dev, user data in prod).
|
Data is stored in a local SQLite file.
|
||||||
|
|
||||||
- Use `Better-SQLite3` for direct access.
|
- Use `Better-SQLite3` for direct access.
|
||||||
- Logic should be encapsulated in `src/services` or `src/ipc`.
|
- Logic should be encapsulated in `src/services` or `src/ipc`.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
name: ui-ux-pro-max
|
name: ui-ux-pro-max
|
||||||
description: UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 9 stacks.
|
description: UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 9 stacks.
|
||||||
---
|
---
|
||||||
|
|
||||||
# ui-ux-pro-max
|
# ui-ux-pro-max
|
||||||
|
|
||||||
Comprehensive design guide for web and mobile applications. Contains 67 styles, 96 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 13 technology stacks. Searchable database with priority-based recommendations.
|
Comprehensive design guide for web and mobile applications. Contains 67 styles, 96 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 13 technology stacks. Searchable database with priority-based recommendations.
|
||||||
@@ -17,16 +18,19 @@ python3 --version || python --version
|
|||||||
If Python is not installed, install it based on user's OS:
|
If Python is not installed, install it based on user's OS:
|
||||||
|
|
||||||
**macOS:**
|
**macOS:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install python3
|
brew install python3
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ubuntu/Debian:**
|
**Ubuntu/Debian:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && sudo apt install python3
|
sudo apt update && sudo apt install python3
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows:**
|
**Windows:**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
winget install Python.Python.3.12
|
winget install Python.Python.3.12
|
||||||
```
|
```
|
||||||
@@ -40,6 +44,7 @@ When user requests UI/UX work (design, build, create, implement, review, fix, im
|
|||||||
### Step 1: Analyze User Requirements
|
### Step 1: Analyze User Requirements
|
||||||
|
|
||||||
Extract key information from user request:
|
Extract key information from user request:
|
||||||
|
|
||||||
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
|
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
|
||||||
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
|
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
|
||||||
- **Industry**: healthcare, fintech, gaming, education, etc.
|
- **Industry**: healthcare, fintech, gaming, education, etc.
|
||||||
@@ -54,12 +59,14 @@ python3 skills/ui-ux-pro-max/scripts/search.py "<product_type> <industry> <keywo
|
|||||||
```
|
```
|
||||||
|
|
||||||
This command:
|
This command:
|
||||||
|
|
||||||
1. Searches 5 domains in parallel (product, style, color, landing, typography)
|
1. Searches 5 domains in parallel (product, style, color, landing, typography)
|
||||||
2. Applies reasoning rules from `ui-reasoning.csv` to select best matches
|
2. Applies reasoning rules from `ui-reasoning.csv` to select best matches
|
||||||
3. Returns complete design system: pattern, style, colors, typography, effects
|
3. Returns complete design system: pattern, style, colors, typography, effects
|
||||||
4. Includes anti-patterns to avoid
|
4. Includes anti-patterns to avoid
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa"
|
python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa"
|
||||||
```
|
```
|
||||||
@@ -73,18 +80,22 @@ python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persi
|
|||||||
```
|
```
|
||||||
|
|
||||||
This creates:
|
This creates:
|
||||||
|
|
||||||
- `design-system/MASTER.md` — Global Source of Truth with all design rules
|
- `design-system/MASTER.md` — Global Source of Truth with all design rules
|
||||||
- `design-system/pages/` — Folder for page-specific overrides
|
- `design-system/pages/` — Folder for page-specific overrides
|
||||||
|
|
||||||
**With page-specific override:**
|
**With page-specific override:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "Project Name" --page "dashboard"
|
python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "Project Name" --page "dashboard"
|
||||||
```
|
```
|
||||||
|
|
||||||
This also creates:
|
This also creates:
|
||||||
|
|
||||||
- `design-system/pages/dashboard.md` — Page-specific deviations from Master
|
- `design-system/pages/dashboard.md` — Page-specific deviations from Master
|
||||||
|
|
||||||
**How hierarchical retrieval works:**
|
**How hierarchical retrieval works:**
|
||||||
|
|
||||||
1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md`
|
1. When building a specific page (e.g., "Checkout"), first check `design-system/pages/checkout.md`
|
||||||
2. If the page file exists, its rules **override** the Master file
|
2. If the page file exists, its rules **override** the Master file
|
||||||
3. If not, use `design-system/MASTER.md` exclusively
|
3. If not, use `design-system/MASTER.md` exclusively
|
||||||
@@ -99,13 +110,13 @@ python3 skills/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n
|
|||||||
|
|
||||||
**When to use detailed searches:**
|
**When to use detailed searches:**
|
||||||
|
|
||||||
| Need | Domain | Example |
|
| Need | Domain | Example |
|
||||||
|------|--------|---------|
|
| --------------------- | ------------ | --------------------------------------- |
|
||||||
| More style options | `style` | `--domain style "glassmorphism dark"` |
|
| More style options | `style` | `--domain style "glassmorphism dark"` |
|
||||||
| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` |
|
| Chart recommendations | `chart` | `--domain chart "real-time dashboard"` |
|
||||||
| UX best practices | `ux` | `--domain ux "animation accessibility"` |
|
| UX best practices | `ux` | `--domain ux "animation accessibility"` |
|
||||||
| Alternative fonts | `typography` | `--domain typography "elegant luxury"` |
|
| Alternative fonts | `typography` | `--domain typography "elegant luxury"` |
|
||||||
| Landing structure | `landing` | `--domain landing "hero social-proof"` |
|
| Landing structure | `landing` | `--domain landing "hero social-proof"` |
|
||||||
|
|
||||||
### Step 4: Stack Guidelines (Default: html-tailwind)
|
### Step 4: Stack Guidelines (Default: html-tailwind)
|
||||||
|
|
||||||
@@ -123,32 +134,32 @@ Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`
|
|||||||
|
|
||||||
### Available Domains
|
### Available Domains
|
||||||
|
|
||||||
| Domain | Use For | Example Keywords |
|
| Domain | Use For | Example Keywords |
|
||||||
|--------|---------|------------------|
|
| ------------ | ------------------------------------ | -------------------------------------------------------- |
|
||||||
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
|
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
|
||||||
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
|
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
|
||||||
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
|
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
|
||||||
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
|
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
|
||||||
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
|
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
|
||||||
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
|
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
|
||||||
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
|
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
|
||||||
| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache |
|
| `react` | React/Next.js performance | waterfall, bundle, suspense, memo, rerender, cache |
|
||||||
| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize |
|
| `web` | Web interface guidelines | aria, focus, keyboard, semantic, virtualize |
|
||||||
| `prompt` | AI prompts, CSS keywords | (style name) |
|
| `prompt` | AI prompts, CSS keywords | (style name) |
|
||||||
|
|
||||||
### Available Stacks
|
### Available Stacks
|
||||||
|
|
||||||
| Stack | Focus |
|
| Stack | Focus |
|
||||||
|-------|-------|
|
| ----------------- | ----------------------------------------------------- |
|
||||||
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
|
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
|
||||||
| `react` | State, hooks, performance, patterns |
|
| `react` | State, hooks, performance, patterns |
|
||||||
| `nextjs` | SSR, routing, images, API routes |
|
| `nextjs` | SSR, routing, images, API routes |
|
||||||
| `vue` | Composition API, Pinia, Vue Router |
|
| `vue` | Composition API, Pinia, Vue Router |
|
||||||
| `svelte` | Runes, stores, SvelteKit |
|
| `svelte` | Runes, stores, SvelteKit |
|
||||||
| `swiftui` | Views, State, Navigation, Animation |
|
| `swiftui` | Views, State, Navigation, Animation |
|
||||||
| `react-native` | Components, Navigation, Lists |
|
| `react-native` | Components, Navigation, Lists |
|
||||||
| `flutter` | Widgets, State, Layout, Theming |
|
| `flutter` | Widgets, State, Layout, Theming |
|
||||||
| `shadcn` | shadcn/ui components, theming, forms, patterns |
|
| `shadcn` | shadcn/ui components, theming, forms, patterns |
|
||||||
| `jetpack-compose` | Composables, Modifiers, State Hoisting, Recomposition |
|
| `jetpack-compose` | Composables, Modifiers, State Hoisting, Recomposition |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -158,6 +169,7 @@ Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`
|
|||||||
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
|
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
|
||||||
|
|
||||||
### Step 1: Analyze Requirements
|
### Step 1: Analyze Requirements
|
||||||
|
|
||||||
- Product type: Beauty/Spa service
|
- Product type: Beauty/Spa service
|
||||||
- Style keywords: elegant, professional, soft
|
- Style keywords: elegant, professional, soft
|
||||||
- Industry: Beauty/Wellness
|
- Industry: Beauty/Wellness
|
||||||
@@ -222,37 +234,37 @@ These are frequently overlooked issues that make UI look unprofessional:
|
|||||||
|
|
||||||
### Icons & Visual Elements
|
### Icons & Visual Elements
|
||||||
|
|
||||||
| Rule | Do | Don't |
|
| Rule | Do | Don't |
|
||||||
|------|----|----- |
|
| -------------------------- | ----------------------------------------------- | -------------------------------------- |
|
||||||
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
|
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
|
||||||
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
|
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
|
||||||
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
|
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
|
||||||
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
|
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
|
||||||
|
|
||||||
### Interaction & Cursor
|
### Interaction & Cursor
|
||||||
|
|
||||||
| Rule | Do | Don't |
|
| Rule | Do | Don't |
|
||||||
|------|----|----- |
|
| ---------------------- | ----------------------------------------------------- | -------------------------------------------- |
|
||||||
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
|
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
|
||||||
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
|
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
|
||||||
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
|
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
|
||||||
|
|
||||||
### Light/Dark Mode Contrast
|
### Light/Dark Mode Contrast
|
||||||
|
|
||||||
| Rule | Do | Don't |
|
| Rule | Do | Don't |
|
||||||
|------|----|----- |
|
| ------------------------- | ----------------------------------- | --------------------------------------- |
|
||||||
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
|
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
|
||||||
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
|
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
|
||||||
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
|
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
|
||||||
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
|
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
|
||||||
|
|
||||||
### Layout & Spacing
|
### Layout & Spacing
|
||||||
|
|
||||||
| Rule | Do | Don't |
|
| Rule | Do | Don't |
|
||||||
|------|----|----- |
|
| ------------------------ | ----------------------------------- | -------------------------------------- |
|
||||||
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
|
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
|
||||||
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
|
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
|
||||||
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
|
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -261,6 +273,7 @@ These are frequently overlooked issues that make UI look unprofessional:
|
|||||||
Before delivering UI code, verify these items:
|
Before delivering UI code, verify these items:
|
||||||
|
|
||||||
### Visual Quality
|
### Visual Quality
|
||||||
|
|
||||||
- [ ] No emojis used as icons (use SVG instead)
|
- [ ] No emojis used as icons (use SVG instead)
|
||||||
- [ ] All icons from consistent icon set (Heroicons/Lucide)
|
- [ ] All icons from consistent icon set (Heroicons/Lucide)
|
||||||
- [ ] Brand logos are correct (verified from Simple Icons)
|
- [ ] Brand logos are correct (verified from Simple Icons)
|
||||||
@@ -268,24 +281,28 @@ Before delivering UI code, verify these items:
|
|||||||
- [ ] Use theme colors directly (bg-primary) not var() wrapper
|
- [ ] Use theme colors directly (bg-primary) not var() wrapper
|
||||||
|
|
||||||
### Interaction
|
### Interaction
|
||||||
|
|
||||||
- [ ] All clickable elements have `cursor-pointer`
|
- [ ] All clickable elements have `cursor-pointer`
|
||||||
- [ ] Hover states provide clear visual feedback
|
- [ ] Hover states provide clear visual feedback
|
||||||
- [ ] Transitions are smooth (150-300ms)
|
- [ ] Transitions are smooth (150-300ms)
|
||||||
- [ ] Focus states visible for keyboard navigation
|
- [ ] Focus states visible for keyboard navigation
|
||||||
|
|
||||||
### Light/Dark Mode
|
### Light/Dark Mode
|
||||||
|
|
||||||
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
|
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
|
||||||
- [ ] Glass/transparent elements visible in light mode
|
- [ ] Glass/transparent elements visible in light mode
|
||||||
- [ ] Borders visible in both modes
|
- [ ] Borders visible in both modes
|
||||||
- [ ] Test both modes before delivery
|
- [ ] Test both modes before delivery
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
- [ ] Floating elements have proper spacing from edges
|
- [ ] Floating elements have proper spacing from edges
|
||||||
- [ ] No content hidden behind fixed navbars
|
- [ ] No content hidden behind fixed navbars
|
||||||
- [ ] Responsive at 375px, 768px, 1024px, 1440px
|
- [ ] Responsive at 375px, 768px, 1024px, 1440px
|
||||||
- [ ] No horizontal scroll on mobile
|
- [ ] No horizontal scroll on mobile
|
||||||
|
|
||||||
### Accessibility
|
### Accessibility
|
||||||
|
|
||||||
- [ ] All images have alt text
|
- [ ] All images have alt text
|
||||||
- [ ] Form inputs have labels
|
- [ ] Form inputs have labels
|
||||||
- [ ] Color is not the only indicator
|
- [ ] Color is not the only indicator
|
||||||
|
|||||||
@@ -73,16 +73,10 @@ function Composer({
|
|||||||
) : isThread ? (
|
) : isThread ? (
|
||||||
<AlsoSendToChannelField id={channelId} />
|
<AlsoSendToChannelField id={channelId} />
|
||||||
) : null}
|
) : null}
|
||||||
{isEditing ? (
|
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
|
||||||
<EditActions />
|
|
||||||
) : isForwarding ? (
|
|
||||||
<ForwardActions />
|
|
||||||
) : (
|
|
||||||
<DefaultActions />
|
|
||||||
)}
|
|
||||||
<Footer onSubmit={onSubmit} />
|
<Footer onSubmit={onSubmit} />
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -102,7 +96,7 @@ function ChannelComposer() {
|
|||||||
<Composer.Submit />
|
<Composer.Submit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread composer - adds "also send to channel" field
|
// Thread composer - adds "also send to channel" field
|
||||||
@@ -118,7 +112,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
|||||||
<Composer.Submit />
|
<Composer.Submit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit composer - different footer actions
|
// Edit composer - different footer actions
|
||||||
@@ -133,7 +127,7 @@ function EditComposer() {
|
|||||||
<Composer.SaveEdit />
|
<Composer.SaveEdit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -177,25 +171,21 @@ function Composer({
|
|||||||
</Footer>
|
</Footer>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct: compound components with shared context**
|
**Correct: compound components with shared context**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||||
|
|
||||||
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
||||||
return (
|
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
|
||||||
<ComposerContext value={{ state, actions, meta }}>
|
|
||||||
{children}
|
|
||||||
</ComposerContext>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||||
return <form>{children}</form>
|
return <form>{children}</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerInput() {
|
function ComposerInput() {
|
||||||
@@ -203,21 +193,21 @@ function ComposerInput() {
|
|||||||
state,
|
state,
|
||||||
actions: { update },
|
actions: { update },
|
||||||
meta: { inputRef },
|
meta: { inputRef },
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={state.input}
|
value={state.input}
|
||||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerSubmit() {
|
function ComposerSubmit() {
|
||||||
const {
|
const {
|
||||||
actions: { submit },
|
actions: { submit },
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
return <Button onPress={submit}>Send</Button>
|
return <Button onPress={submit}>Send</Button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export as compound component
|
// Export as compound component
|
||||||
@@ -231,7 +221,7 @@ const Composer = {
|
|||||||
Attachments: ComposerAttachments,
|
Attachments: ComposerAttachments,
|
||||||
Formatting: ComposerFormatting,
|
Formatting: ComposerFormatting,
|
||||||
Emojis: ComposerEmojis,
|
Emojis: ComposerEmojis,
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
@@ -275,18 +265,15 @@ useState, Zustand, or a server sync.
|
|||||||
```tsx
|
```tsx
|
||||||
function ChannelComposer({ channelId }: { channelId: string }) {
|
function ChannelComposer({ channelId }: { channelId: string }) {
|
||||||
// UI component knows about global state implementation
|
// UI component knows about global state implementation
|
||||||
const state = useGlobalChannelState(channelId)
|
const state = useGlobalChannelState(channelId);
|
||||||
const { submit, updateInput } = useChannelSync(channelId)
|
const { submit, updateInput } = useChannelSync(channelId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Frame>
|
<Composer.Frame>
|
||||||
<Composer.Input
|
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
|
||||||
value={state.input}
|
|
||||||
onChange={(text) => sync.updateInput(text)}
|
|
||||||
/>
|
|
||||||
<Composer.Submit onPress={() => sync.submit()} />
|
<Composer.Submit onPress={() => sync.submit()} />
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -298,21 +285,17 @@ function ChannelProvider({
|
|||||||
channelId,
|
channelId,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
channelId: string
|
channelId: string;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { state, update, submit } = useGlobalChannel(channelId)
|
const { state, update, submit } = useGlobalChannel(channelId);
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider
|
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
|
||||||
state={state}
|
|
||||||
actions={{ update, submit }}
|
|
||||||
meta={{ inputRef }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI component only knows about the context interface
|
// UI component only knows about the context interface
|
||||||
@@ -325,7 +308,7 @@ function ChannelComposer() {
|
|||||||
<Composer.Submit />
|
<Composer.Submit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
@@ -334,7 +317,7 @@ function Channel({ channelId }: { channelId: string }) {
|
|||||||
<ChannelProvider channelId={channelId}>
|
<ChannelProvider channelId={channelId}>
|
||||||
<ChannelComposer />
|
<ChannelComposer />
|
||||||
</ChannelProvider>
|
</ChannelProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -343,28 +326,25 @@ function Channel({ channelId }: { channelId: string }) {
|
|||||||
```tsx
|
```tsx
|
||||||
// Local state for ephemeral forms
|
// Local state for ephemeral forms
|
||||||
function ForwardMessageProvider({ children }) {
|
function ForwardMessageProvider({ children }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const forwardMessage = useForwardMessage()
|
const forwardMessage = useForwardMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider
|
<Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
|
||||||
state={state}
|
|
||||||
actions={{ update: setState, submit: forwardMessage }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global synced state for channels
|
// Global synced state for channels
|
||||||
function ChannelProvider({ channelId, children }) {
|
function ChannelProvider({ channelId, children }) {
|
||||||
const { state, update, submit } = useGlobalChannel(channelId)
|
const { state, update, submit } = useGlobalChannel(channelId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider state={state} actions={{ update, submit }}>
|
<Composer.Provider state={state} actions={{ update, submit }}>
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -393,8 +373,8 @@ dependency-injectable.
|
|||||||
```tsx
|
```tsx
|
||||||
function ComposerInput() {
|
function ComposerInput() {
|
||||||
// Tightly coupled to a specific hook
|
// Tightly coupled to a specific hook
|
||||||
const { input, setInput } = useChannelComposerState()
|
const { input, setInput } = useChannelComposerState();
|
||||||
return <TextInput value={input} onChangeText={setInput} />
|
return <TextInput value={input} onChangeText={setInput} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -403,27 +383,27 @@ function ComposerInput() {
|
|||||||
```tsx
|
```tsx
|
||||||
// Define a GENERIC interface that any provider can implement
|
// Define a GENERIC interface that any provider can implement
|
||||||
interface ComposerState {
|
interface ComposerState {
|
||||||
input: string
|
input: string;
|
||||||
attachments: Attachment[]
|
attachments: Attachment[];
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComposerActions {
|
interface ComposerActions {
|
||||||
update: (updater: (state: ComposerState) => ComposerState) => void
|
update: (updater: (state: ComposerState) => ComposerState) => void;
|
||||||
submit: () => void
|
submit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComposerMeta {
|
interface ComposerMeta {
|
||||||
inputRef: React.RefObject<TextInput>
|
inputRef: React.RefObject<TextInput>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComposerContextValue {
|
interface ComposerContextValue {
|
||||||
state: ComposerState
|
state: ComposerState;
|
||||||
actions: ComposerActions
|
actions: ComposerActions;
|
||||||
meta: ComposerMeta
|
meta: ComposerMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||||
```
|
```
|
||||||
|
|
||||||
**UI components consume the interface, not the implementation:**
|
**UI components consume the interface, not the implementation:**
|
||||||
@@ -434,7 +414,7 @@ function ComposerInput() {
|
|||||||
state,
|
state,
|
||||||
actions: { update },
|
actions: { update },
|
||||||
meta,
|
meta,
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
|
|
||||||
// This component works with ANY provider that implements the interface
|
// This component works with ANY provider that implements the interface
|
||||||
return (
|
return (
|
||||||
@@ -443,7 +423,7 @@ function ComposerInput() {
|
|||||||
value={state.input}
|
value={state.input}
|
||||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -452,9 +432,9 @@ function ComposerInput() {
|
|||||||
```tsx
|
```tsx
|
||||||
// Provider A: Local state for ephemeral forms
|
// Provider A: Local state for ephemeral forms
|
||||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
const submit = useForwardMessage()
|
const submit = useForwardMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerContext
|
<ComposerContext
|
||||||
@@ -466,13 +446,13 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ComposerContext>
|
</ComposerContext>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider B: Global synced state for channels
|
// Provider B: Global synced state for channels
|
||||||
function ChannelProvider({ channelId, children }: Props) {
|
function ChannelProvider({ channelId, children }: Props) {
|
||||||
const { state, update, submit } = useGlobalChannel(channelId)
|
const { state, update, submit } = useGlobalChannel(channelId);
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerContext
|
<ComposerContext
|
||||||
@@ -484,7 +464,7 @@ function ChannelProvider({ channelId, children }: Props) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ComposerContext>
|
</ComposerContext>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -534,21 +514,21 @@ function ForwardMessageDialog() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</ForwardMessageProvider>
|
</ForwardMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
|
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
|
||||||
function ForwardButton() {
|
function ForwardButton() {
|
||||||
const {
|
const {
|
||||||
actions: { submit },
|
actions: { submit },
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
return <Button onPress={submit}>Forward</Button>
|
return <Button onPress={submit}>Forward</Button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
|
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
|
||||||
function MessagePreview() {
|
function MessagePreview() {
|
||||||
const { state } = use(ComposerContext)
|
const { state } = use(ComposerContext);
|
||||||
return <Preview message={state.input} attachments={state.attachments} />
|
return <Preview message={state.input} attachments={state.attachments} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -582,15 +562,15 @@ or awkward refs.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageComposer() {
|
function ForwardMessageComposer() {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const forwardMessage = useForwardMessage()
|
const forwardMessage = useForwardMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Frame>
|
<Composer.Frame>
|
||||||
<Composer.Input />
|
<Composer.Input />
|
||||||
<Composer.Footer />
|
<Composer.Footer />
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Problem: How does this button access composer state?
|
// Problem: How does this button access composer state?
|
||||||
@@ -604,7 +584,7 @@ function ForwardMessageDialog() {
|
|||||||
<ForwardButton /> {/* Needs to call submit */}
|
<ForwardButton /> {/* Needs to call submit */}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -612,20 +592,20 @@ function ForwardMessageDialog() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageDialog() {
|
function ForwardMessageDialog() {
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('');
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<ForwardMessageComposer onInputChange={setInput} />
|
<ForwardMessageComposer onInputChange={setInput} />
|
||||||
<MessagePreview input={input} />
|
<MessagePreview input={input} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardMessageComposer({ onInputChange }) {
|
function ForwardMessageComposer({ onInputChange }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onInputChange(state.input) // Sync on every change 😬
|
onInputChange(state.input); // Sync on every change 😬
|
||||||
}, [state.input])
|
}, [state.input]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -633,13 +613,13 @@ function ForwardMessageComposer({ onInputChange }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageDialog() {
|
function ForwardMessageDialog() {
|
||||||
const stateRef = useRef(null)
|
const stateRef = useRef(null);
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<ForwardMessageComposer stateRef={stateRef} />
|
<ForwardMessageComposer stateRef={stateRef} />
|
||||||
<ForwardButton onPress={() => submit(stateRef.current)} />
|
<ForwardButton onPress={() => submit(stateRef.current)} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -647,9 +627,9 @@ function ForwardMessageDialog() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const forwardMessage = useForwardMessage()
|
const forwardMessage = useForwardMessage();
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider
|
<Composer.Provider
|
||||||
@@ -659,7 +639,7 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardMessageDialog() {
|
function ForwardMessageDialog() {
|
||||||
@@ -674,12 +654,12 @@ function ForwardMessageDialog() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</ForwardMessageProvider>
|
</ForwardMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardButton() {
|
function ForwardButton() {
|
||||||
const { actions } = use(Composer.Context)
|
const { actions } = use(Composer.Context);
|
||||||
return <Button onPress={actions.submit}>Forward</Button>
|
return <Button onPress={actions.submit}>Forward</Button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -718,13 +698,7 @@ itself.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// What does this component actually render?
|
// What does this component actually render?
|
||||||
<Composer
|
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
|
||||||
isThread
|
|
||||||
isEditing={false}
|
|
||||||
channelId='abc'
|
|
||||||
showAttachments
|
|
||||||
showFormatting={false}
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct: explicit variants**
|
**Correct: explicit variants**
|
||||||
@@ -760,7 +734,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
|||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
</ThreadProvider>
|
</ThreadProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditMessageComposer({ messageId }: { messageId: string }) {
|
function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||||
@@ -776,7 +750,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) {
|
|||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
</EditMessageProvider>
|
</EditMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||||
@@ -791,7 +765,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
|||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
</ForwardMessageProvider>
|
</ForwardMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -823,9 +797,9 @@ function Composer({
|
|||||||
renderFooter,
|
renderFooter,
|
||||||
renderActions,
|
renderActions,
|
||||||
}: {
|
}: {
|
||||||
renderHeader?: () => React.ReactNode
|
renderHeader?: () => React.ReactNode;
|
||||||
renderFooter?: () => React.ReactNode
|
renderFooter?: () => React.ReactNode;
|
||||||
renderActions?: () => React.ReactNode
|
renderActions?: () => React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<form>
|
<form>
|
||||||
@@ -834,7 +808,7 @@ function Composer({
|
|||||||
{renderFooter ? renderFooter() : <DefaultFooter />}
|
{renderFooter ? renderFooter() : <DefaultFooter />}
|
||||||
{renderActions?.()}
|
{renderActions?.()}
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage is awkward and inflexible
|
// Usage is awkward and inflexible
|
||||||
@@ -849,18 +823,18 @@ return (
|
|||||||
)}
|
)}
|
||||||
renderActions={() => <SubmitButton />}
|
renderActions={() => <SubmitButton />}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct: compound components with children**
|
**Correct: compound components with children**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||||
return <form>{children}</form>
|
return <form>{children}</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
||||||
return <footer className='flex'>{children}</footer>
|
return <footer className="flex">{children}</footer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage is flexible
|
// Usage is flexible
|
||||||
@@ -874,17 +848,14 @@ return (
|
|||||||
<SubmitButton />
|
<SubmitButton />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
**When render props are appropriate:**
|
**When render props are appropriate:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// Render props work well when you need to pass data back
|
// Render props work well when you need to pass data back
|
||||||
<List
|
<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
|
||||||
data={items}
|
|
||||||
renderItem={({ item, index }) => <Item item={item} index={index} />}
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use render props when the parent needs to provide data or state to the child.
|
Use render props when the parent needs to provide data or state to the child.
|
||||||
@@ -911,28 +882,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
||||||
return <TextInput ref={ref} {...props} />
|
return <TextInput ref={ref} {...props} />;
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct: ref as a regular prop**
|
**Correct: ref as a regular prop**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
|
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
|
||||||
return <TextInput ref={ref} {...props} />
|
return <TextInput ref={ref} {...props} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Incorrect: useContext in React 19**
|
**Incorrect: useContext in React 19**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const value = useContext(MyContext)
|
const value = useContext(MyContext);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct: use instead of useContext**
|
**Correct: use instead of useContext**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const value = use(MyContext)
|
const value = use(MyContext);
|
||||||
```
|
```
|
||||||
|
|
||||||
`use()` can also be called conditionally, unlike `useContext()`.
|
`use()` can also be called conditionally, unlike `useContext()`.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: vercel-composition-patterns
|
name: vercel-composition-patterns
|
||||||
description:
|
description: React composition patterns that scale. Use when refactoring components with
|
||||||
React composition patterns that scale. Use when refactoring components with
|
|
||||||
boolean prop proliferation, building flexible component libraries, or
|
boolean prop proliferation, building flexible component libraries, or
|
||||||
designing reusable APIs. Triggers on tasks involving compound components,
|
designing reusable APIs. Triggers on tasks involving compound components,
|
||||||
render props, context providers, or component architecture. Includes React 19
|
render props, context providers, or component architecture. Includes React 19
|
||||||
|
|||||||
+5
-11
@@ -32,16 +32,10 @@ function Composer({
|
|||||||
) : isThread ? (
|
) : isThread ? (
|
||||||
<AlsoSendToChannelField id={channelId} />
|
<AlsoSendToChannelField id={channelId} />
|
||||||
) : null}
|
) : null}
|
||||||
{isEditing ? (
|
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
|
||||||
<EditActions />
|
|
||||||
) : isForwarding ? (
|
|
||||||
<ForwardActions />
|
|
||||||
) : (
|
|
||||||
<DefaultActions />
|
|
||||||
)}
|
|
||||||
<Footer onSubmit={onSubmit} />
|
<Footer onSubmit={onSubmit} />
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,7 +55,7 @@ function ChannelComposer() {
|
|||||||
<Composer.Submit />
|
<Composer.Submit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread composer - adds "also send to channel" field
|
// Thread composer - adds "also send to channel" field
|
||||||
@@ -77,7 +71,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
|||||||
<Composer.Submit />
|
<Composer.Submit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit composer - different footer actions
|
// Edit composer - different footer actions
|
||||||
@@ -92,7 +86,7 @@ function EditComposer() {
|
|||||||
<Composer.SaveEdit />
|
<Composer.SaveEdit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+9
-13
@@ -37,25 +37,21 @@ function Composer({
|
|||||||
</Footer>
|
</Footer>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (compound components with shared context):**
|
**Correct (compound components with shared context):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||||
|
|
||||||
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
||||||
return (
|
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
|
||||||
<ComposerContext value={{ state, actions, meta }}>
|
|
||||||
{children}
|
|
||||||
</ComposerContext>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||||
return <form>{children}</form>
|
return <form>{children}</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerInput() {
|
function ComposerInput() {
|
||||||
@@ -63,21 +59,21 @@ function ComposerInput() {
|
|||||||
state,
|
state,
|
||||||
actions: { update },
|
actions: { update },
|
||||||
meta: { inputRef },
|
meta: { inputRef },
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={state.input}
|
value={state.input}
|
||||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerSubmit() {
|
function ComposerSubmit() {
|
||||||
const {
|
const {
|
||||||
actions: { submit },
|
actions: { submit },
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
return <Button onPress={submit}>Send</Button>
|
return <Button onPress={submit}>Send</Button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export as compound component
|
// Export as compound component
|
||||||
@@ -91,7 +87,7 @@ const Composer = {
|
|||||||
Attachments: ComposerAttachments,
|
Attachments: ComposerAttachments,
|
||||||
Formatting: ComposerFormatting,
|
Formatting: ComposerFormatting,
|
||||||
Emojis: ComposerEmojis,
|
Emojis: ComposerEmojis,
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|||||||
+9
-12
@@ -19,9 +19,9 @@ function Composer({
|
|||||||
renderFooter,
|
renderFooter,
|
||||||
renderActions,
|
renderActions,
|
||||||
}: {
|
}: {
|
||||||
renderHeader?: () => React.ReactNode
|
renderHeader?: () => React.ReactNode;
|
||||||
renderFooter?: () => React.ReactNode
|
renderFooter?: () => React.ReactNode;
|
||||||
renderActions?: () => React.ReactNode
|
renderActions?: () => React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<form>
|
<form>
|
||||||
@@ -30,7 +30,7 @@ function Composer({
|
|||||||
{renderFooter ? renderFooter() : <DefaultFooter />}
|
{renderFooter ? renderFooter() : <DefaultFooter />}
|
||||||
{renderActions?.()}
|
{renderActions?.()}
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage is awkward and inflexible
|
// Usage is awkward and inflexible
|
||||||
@@ -45,18 +45,18 @@ return (
|
|||||||
)}
|
)}
|
||||||
renderActions={() => <SubmitButton />}
|
renderActions={() => <SubmitButton />}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (compound components with children):**
|
**Correct (compound components with children):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||||
return <form>{children}</form>
|
return <form>{children}</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
||||||
return <footer className='flex'>{children}</footer>
|
return <footer className="flex">{children}</footer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage is flexible
|
// Usage is flexible
|
||||||
@@ -70,17 +70,14 @@ return (
|
|||||||
<SubmitButton />
|
<SubmitButton />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
**When render props are appropriate:**
|
**When render props are appropriate:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// Render props work well when you need to pass data back
|
// Render props work well when you need to pass data back
|
||||||
<List
|
<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
|
||||||
data={items}
|
|
||||||
renderItem={({ item, index }) => <Item item={item} index={index} />}
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use render props when the parent needs to provide data or state to the child.
|
Use render props when the parent needs to provide data or state to the child.
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ itself.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// What does this component actually render?
|
// What does this component actually render?
|
||||||
<Composer
|
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
|
||||||
isThread
|
|
||||||
isEditing={false}
|
|
||||||
channelId='abc'
|
|
||||||
showAttachments
|
|
||||||
showFormatting={false}
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (explicit variants):**
|
**Correct (explicit variants):**
|
||||||
@@ -56,7 +50,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
|||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
</ThreadProvider>
|
</ThreadProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditMessageComposer({ messageId }: { messageId: string }) {
|
function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||||
@@ -72,7 +66,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) {
|
|||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
</EditMessageProvider>
|
</EditMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||||
@@ -87,7 +81,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
|||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
</ForwardMessageProvider>
|
</ForwardMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,28 +15,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
||||||
return <TextInput ref={ref} {...props} />
|
return <TextInput ref={ref} {...props} />;
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (ref as a regular prop):**
|
**Correct (ref as a regular prop):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
|
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
|
||||||
return <TextInput ref={ref} {...props} />
|
return <TextInput ref={ref} {...props} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Incorrect (useContext in React 19):**
|
**Incorrect (useContext in React 19):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const value = useContext(MyContext)
|
const value = useContext(MyContext);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (use instead of useContext):**
|
**Correct (use instead of useContext):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const value = use(MyContext)
|
const value = use(MyContext);
|
||||||
```
|
```
|
||||||
|
|
||||||
`use()` can also be called conditionally, unlike `useContext()`.
|
`use()` can also be called conditionally, unlike `useContext()`.
|
||||||
@@ -20,8 +20,8 @@ dependency-injectable.
|
|||||||
```tsx
|
```tsx
|
||||||
function ComposerInput() {
|
function ComposerInput() {
|
||||||
// Tightly coupled to a specific hook
|
// Tightly coupled to a specific hook
|
||||||
const { input, setInput } = useChannelComposerState()
|
const { input, setInput } = useChannelComposerState();
|
||||||
return <TextInput value={input} onChangeText={setInput} />
|
return <TextInput value={input} onChangeText={setInput} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,27 +30,27 @@ function ComposerInput() {
|
|||||||
```tsx
|
```tsx
|
||||||
// Define a GENERIC interface that any provider can implement
|
// Define a GENERIC interface that any provider can implement
|
||||||
interface ComposerState {
|
interface ComposerState {
|
||||||
input: string
|
input: string;
|
||||||
attachments: Attachment[]
|
attachments: Attachment[];
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComposerActions {
|
interface ComposerActions {
|
||||||
update: (updater: (state: ComposerState) => ComposerState) => void
|
update: (updater: (state: ComposerState) => ComposerState) => void;
|
||||||
submit: () => void
|
submit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComposerMeta {
|
interface ComposerMeta {
|
||||||
inputRef: React.RefObject<TextInput>
|
inputRef: React.RefObject<TextInput>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComposerContextValue {
|
interface ComposerContextValue {
|
||||||
state: ComposerState
|
state: ComposerState;
|
||||||
actions: ComposerActions
|
actions: ComposerActions;
|
||||||
meta: ComposerMeta
|
meta: ComposerMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||||
```
|
```
|
||||||
|
|
||||||
**UI components consume the interface, not the implementation:**
|
**UI components consume the interface, not the implementation:**
|
||||||
@@ -61,7 +61,7 @@ function ComposerInput() {
|
|||||||
state,
|
state,
|
||||||
actions: { update },
|
actions: { update },
|
||||||
meta,
|
meta,
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
|
|
||||||
// This component works with ANY provider that implements the interface
|
// This component works with ANY provider that implements the interface
|
||||||
return (
|
return (
|
||||||
@@ -70,7 +70,7 @@ function ComposerInput() {
|
|||||||
value={state.input}
|
value={state.input}
|
||||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -79,9 +79,9 @@ function ComposerInput() {
|
|||||||
```tsx
|
```tsx
|
||||||
// Provider A: Local state for ephemeral forms
|
// Provider A: Local state for ephemeral forms
|
||||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
const submit = useForwardMessage()
|
const submit = useForwardMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerContext
|
<ComposerContext
|
||||||
@@ -93,13 +93,13 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ComposerContext>
|
</ComposerContext>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider B: Global synced state for channels
|
// Provider B: Global synced state for channels
|
||||||
function ChannelProvider({ channelId, children }: Props) {
|
function ChannelProvider({ channelId, children }: Props) {
|
||||||
const { state, update, submit } = useGlobalChannel(channelId)
|
const { state, update, submit } = useGlobalChannel(channelId);
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComposerContext
|
<ComposerContext
|
||||||
@@ -111,7 +111,7 @@ function ChannelProvider({ channelId, children }: Props) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ComposerContext>
|
</ComposerContext>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -165,21 +165,21 @@ function ForwardMessageDialog() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</ForwardMessageProvider>
|
</ForwardMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
|
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
|
||||||
function ForwardButton() {
|
function ForwardButton() {
|
||||||
const {
|
const {
|
||||||
actions: { submit },
|
actions: { submit },
|
||||||
} = use(ComposerContext)
|
} = use(ComposerContext);
|
||||||
return <Button onPress={submit}>Forward</Button>
|
return <Button onPress={submit}>Forward</Button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
|
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
|
||||||
function MessagePreview() {
|
function MessagePreview() {
|
||||||
const { state } = use(ComposerContext)
|
const { state } = use(ComposerContext);
|
||||||
return <Preview message={state.input} attachments={state.attachments} />
|
return <Preview message={state.input} attachments={state.attachments} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,15 @@ useState, Zustand, or a server sync.
|
|||||||
```tsx
|
```tsx
|
||||||
function ChannelComposer({ channelId }: { channelId: string }) {
|
function ChannelComposer({ channelId }: { channelId: string }) {
|
||||||
// UI component knows about global state implementation
|
// UI component knows about global state implementation
|
||||||
const state = useGlobalChannelState(channelId)
|
const state = useGlobalChannelState(channelId);
|
||||||
const { submit, updateInput } = useChannelSync(channelId)
|
const { submit, updateInput } = useChannelSync(channelId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Frame>
|
<Composer.Frame>
|
||||||
<Composer.Input
|
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
|
||||||
value={state.input}
|
|
||||||
onChange={(text) => sync.updateInput(text)}
|
|
||||||
/>
|
|
||||||
<Composer.Submit onPress={() => sync.submit()} />
|
<Composer.Submit onPress={() => sync.submit()} />
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -39,21 +36,17 @@ function ChannelProvider({
|
|||||||
channelId,
|
channelId,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
channelId: string
|
channelId: string;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { state, update, submit } = useGlobalChannel(channelId)
|
const { state, update, submit } = useGlobalChannel(channelId);
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider
|
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
|
||||||
state={state}
|
|
||||||
actions={{ update, submit }}
|
|
||||||
meta={{ inputRef }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI component only knows about the context interface
|
// UI component only knows about the context interface
|
||||||
@@ -66,7 +59,7 @@ function ChannelComposer() {
|
|||||||
<Composer.Submit />
|
<Composer.Submit />
|
||||||
</Composer.Footer>
|
</Composer.Footer>
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
@@ -75,7 +68,7 @@ function Channel({ channelId }: { channelId: string }) {
|
|||||||
<ChannelProvider channelId={channelId}>
|
<ChannelProvider channelId={channelId}>
|
||||||
<ChannelComposer />
|
<ChannelComposer />
|
||||||
</ChannelProvider>
|
</ChannelProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -84,28 +77,25 @@ function Channel({ channelId }: { channelId: string }) {
|
|||||||
```tsx
|
```tsx
|
||||||
// Local state for ephemeral forms
|
// Local state for ephemeral forms
|
||||||
function ForwardMessageProvider({ children }) {
|
function ForwardMessageProvider({ children }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const forwardMessage = useForwardMessage()
|
const forwardMessage = useForwardMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider
|
<Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
|
||||||
state={state}
|
|
||||||
actions={{ update: setState, submit: forwardMessage }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global synced state for channels
|
// Global synced state for channels
|
||||||
function ChannelProvider({ channelId, children }) {
|
function ChannelProvider({ channelId, children }) {
|
||||||
const { state, update, submit } = useGlobalChannel(channelId)
|
const { state, update, submit } = useGlobalChannel(channelId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider state={state} actions={{ update, submit }}>
|
<Composer.Provider state={state} actions={{ update, submit }}>
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ or awkward refs.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageComposer() {
|
function ForwardMessageComposer() {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const forwardMessage = useForwardMessage()
|
const forwardMessage = useForwardMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Frame>
|
<Composer.Frame>
|
||||||
<Composer.Input />
|
<Composer.Input />
|
||||||
<Composer.Footer />
|
<Composer.Footer />
|
||||||
</Composer.Frame>
|
</Composer.Frame>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Problem: How does this button access composer state?
|
// Problem: How does this button access composer state?
|
||||||
@@ -37,7 +37,7 @@ function ForwardMessageDialog() {
|
|||||||
<ForwardButton /> {/* Needs to call submit */}
|
<ForwardButton /> {/* Needs to call submit */}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,20 +45,20 @@ function ForwardMessageDialog() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageDialog() {
|
function ForwardMessageDialog() {
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('');
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<ForwardMessageComposer onInputChange={setInput} />
|
<ForwardMessageComposer onInputChange={setInput} />
|
||||||
<MessagePreview input={input} />
|
<MessagePreview input={input} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardMessageComposer({ onInputChange }) {
|
function ForwardMessageComposer({ onInputChange }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onInputChange(state.input) // Sync on every change 😬
|
onInputChange(state.input); // Sync on every change 😬
|
||||||
}, [state.input])
|
}, [state.input]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -66,13 +66,13 @@ function ForwardMessageComposer({ onInputChange }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageDialog() {
|
function ForwardMessageDialog() {
|
||||||
const stateRef = useRef(null)
|
const stateRef = useRef(null);
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<ForwardMessageComposer stateRef={stateRef} />
|
<ForwardMessageComposer stateRef={stateRef} />
|
||||||
<ForwardButton onPress={() => submit(stateRef.current)} />
|
<ForwardButton onPress={() => submit(stateRef.current)} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,9 +80,9 @@ function ForwardMessageDialog() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState(initialState);
|
||||||
const forwardMessage = useForwardMessage()
|
const forwardMessage = useForwardMessage();
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Composer.Provider
|
<Composer.Provider
|
||||||
@@ -92,7 +92,7 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Composer.Provider>
|
</Composer.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardMessageDialog() {
|
function ForwardMessageDialog() {
|
||||||
@@ -107,12 +107,12 @@ function ForwardMessageDialog() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</ForwardMessageProvider>
|
</ForwardMessageProvider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardButton() {
|
function ForwardButton() {
|
||||||
const { actions } = use(Composer.Context)
|
const { actions } = use(Composer.Context);
|
||||||
return <Button onPress={actions.submit}>Forward</Button>
|
return <Button onPress={actions.submit}>Forward</Button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@ description: React and Next.js performance optimization guidelines from Vercel E
|
|||||||
license: MIT
|
license: MIT
|
||||||
metadata:
|
metadata:
|
||||||
author: vercel
|
author: vercel
|
||||||
version: "1.0.0"
|
version: '1.0.0'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Vercel React Best Practices
|
# Vercel React Best Practices
|
||||||
@@ -14,6 +14,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
|||||||
## When to Apply
|
## When to Apply
|
||||||
|
|
||||||
Reference these guidelines when:
|
Reference these guidelines when:
|
||||||
|
|
||||||
- Writing new React components or Next.js pages
|
- Writing new React components or Next.js pages
|
||||||
- Implementing data fetching (client or server-side)
|
- Implementing data fetching (client or server-side)
|
||||||
- Reviewing code for performance issues
|
- Reviewing code for performance issues
|
||||||
@@ -22,16 +23,16 @@ Reference these guidelines when:
|
|||||||
|
|
||||||
## Rule Categories by Priority
|
## Rule Categories by Priority
|
||||||
|
|
||||||
| Priority | Category | Impact | Prefix |
|
| Priority | Category | Impact | Prefix |
|
||||||
|----------|----------|--------|--------|
|
| -------- | ------------------------- | ----------- | ------------ |
|
||||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
@@ -126,6 +127,7 @@ rules/bundle-barrel-imports.md
|
|||||||
```
|
```
|
||||||
|
|
||||||
Each rule file contains:
|
Each rule file contains:
|
||||||
|
|
||||||
- Brief explanation of why it matters
|
- Brief explanation of why it matters
|
||||||
- Incorrect code example with explanation
|
- Incorrect code example with explanation
|
||||||
- Correct code example with explanation
|
- Correct code example with explanation
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
|
|||||||
```tsx
|
```tsx
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(event, handler)
|
window.addEventListener(event, handler);
|
||||||
return () => window.removeEventListener(event, handler)
|
return () => window.removeEventListener(event, handler);
|
||||||
}, [event, handler])
|
}, [event, handler]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,31 +24,31 @@ function useWindowEvent(event: string, handler: (e) => void) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
const handlerRef = useRef(handler)
|
const handlerRef = useRef(handler);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handlerRef.current = handler
|
handlerRef.current = handler;
|
||||||
}, [handler])
|
}, [handler]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (e) => handlerRef.current(e)
|
const listener = (e) => handlerRef.current(e);
|
||||||
window.addEventListener(event, listener)
|
window.addEventListener(event, listener);
|
||||||
return () => window.removeEventListener(event, listener)
|
return () => window.removeEventListener(event, listener);
|
||||||
}, [event])
|
}, [event]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useEffectEvent } from 'react'
|
import { useEffectEvent } from 'react';
|
||||||
|
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
const onEvent = useEffectEvent(handler)
|
const onEvent = useEffectEvent(handler);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(event, onEvent)
|
window.addEventListener(event, onEvent);
|
||||||
return () => window.removeEventListener(event, onEvent)
|
return () => window.removeEventListener(event, onEvent);
|
||||||
}, [event])
|
}, [event]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ Do not put app-wide initialization that must run once per app load inside `useEf
|
|||||||
```tsx
|
```tsx
|
||||||
function Comp() {
|
function Comp() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFromStorage()
|
loadFromStorage();
|
||||||
checkAuthToken()
|
checkAuthToken();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
@@ -25,15 +25,15 @@ function Comp() {
|
|||||||
**Correct (once per app load):**
|
**Correct (once per app load):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
let didInit = false
|
let didInit = false;
|
||||||
|
|
||||||
function Comp() {
|
function Comp() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (didInit) return
|
if (didInit) return;
|
||||||
didInit = true
|
didInit = true;
|
||||||
loadFromStorage()
|
loadFromStorage();
|
||||||
checkAuthToken()
|
checkAuthToken();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearch(query), 300)
|
const timeout = setTimeout(() => onSearch(query), 300);
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout);
|
||||||
}, [query, onSearch])
|
}, [query, onSearch]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -28,12 +28,12 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|||||||
import { useEffectEvent } from 'react';
|
import { useEffectEvent } from 'react';
|
||||||
|
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('');
|
||||||
const onSearchEvent = useEffectEvent(onSearch)
|
const onSearchEvent = useEffectEvent(onSearch);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearchEvent(query), 300)
|
const timeout = setTimeout(() => onSearchEvent(query), 300);
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout);
|
||||||
}, [query])
|
}, [query]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,10 +13,10 @@ In API routes and Server Actions, start independent operations immediately, even
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
const config = await fetchConfig()
|
const config = await fetchConfig();
|
||||||
const data = await fetchData(session.user.id)
|
const data = await fetchData(session.user.id);
|
||||||
return Response.json({ data, config })
|
return Response.json({ data, config });
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,14 +24,11 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const sessionPromise = auth()
|
const sessionPromise = auth();
|
||||||
const configPromise = fetchConfig()
|
const configPromise = fetchConfig();
|
||||||
const session = await sessionPromise
|
const session = await sessionPromise;
|
||||||
const [config, data] = await Promise.all([
|
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
|
||||||
configPromise,
|
return Response.json({ data, config });
|
||||||
fetchData(session.user.id)
|
|
||||||
])
|
|
||||||
return Response.json({ data, config })
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ Move `await` operations into the branches where they're actually used to avoid b
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||||
const userData = await fetchUserData(userId)
|
const userData = await fetchUserData(userId);
|
||||||
|
|
||||||
if (skipProcessing) {
|
if (skipProcessing) {
|
||||||
// Returns immediately but still waited for userData
|
// Returns immediately but still waited for userData
|
||||||
return { skipped: true }
|
return { skipped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only this branch uses userData
|
// Only this branch uses userData
|
||||||
return processUserData(userData)
|
return processUserData(userData);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -31,12 +31,12 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
|
|||||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||||
if (skipProcessing) {
|
if (skipProcessing) {
|
||||||
// Returns immediately without waiting
|
// Returns immediately without waiting
|
||||||
return { skipped: true }
|
return { skipped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch only when needed
|
// Fetch only when needed
|
||||||
const userData = await fetchUserData(userId)
|
const userData = await fetchUserData(userId);
|
||||||
return processUserData(userData)
|
return processUserData(userData);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,35 +45,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
|
|||||||
```typescript
|
```typescript
|
||||||
// Incorrect: always fetches permissions
|
// Incorrect: always fetches permissions
|
||||||
async function updateResource(resourceId: string, userId: string) {
|
async function updateResource(resourceId: string, userId: string) {
|
||||||
const permissions = await fetchPermissions(userId)
|
const permissions = await fetchPermissions(userId);
|
||||||
const resource = await getResource(resourceId)
|
const resource = await getResource(resourceId);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return { error: 'Not found' }
|
return { error: 'Not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permissions.canEdit) {
|
if (!permissions.canEdit) {
|
||||||
return { error: 'Forbidden' }
|
return { error: 'Forbidden' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateResourceData(resource, permissions)
|
return await updateResourceData(resource, permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correct: fetches only when needed
|
// Correct: fetches only when needed
|
||||||
async function updateResource(resourceId: string, userId: string) {
|
async function updateResource(resourceId: string, userId: string) {
|
||||||
const resource = await getResource(resourceId)
|
const resource = await getResource(resourceId);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return { error: 'Not found' }
|
return { error: 'Not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = await fetchPermissions(userId)
|
const permissions = await fetchPermissions(userId);
|
||||||
|
|
||||||
if (!permissions.canEdit) {
|
if (!permissions.canEdit) {
|
||||||
return { error: 'Forbidden' }
|
return { error: 'Forbidden' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateResourceData(resource, permissions)
|
return await updateResourceData(resource, permissions);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,25 +12,26 @@ For operations with partial dependencies, use `better-all` to maximize paralleli
|
|||||||
**Incorrect (profile waits for config unnecessarily):**
|
**Incorrect (profile waits for config unnecessarily):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [user, config] = await Promise.all([
|
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
|
||||||
fetchUser(),
|
const profile = await fetchProfile(user.id);
|
||||||
fetchConfig()
|
|
||||||
])
|
|
||||||
const profile = await fetchProfile(user.id)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (config and profile run in parallel):**
|
**Correct (config and profile run in parallel):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { all } from 'better-all'
|
import { all } from 'better-all';
|
||||||
|
|
||||||
const { user, config, profile } = await all({
|
const { user, config, profile } = await all({
|
||||||
async user() { return fetchUser() },
|
async user() {
|
||||||
async config() { return fetchConfig() },
|
return fetchUser();
|
||||||
|
},
|
||||||
|
async config() {
|
||||||
|
return fetchConfig();
|
||||||
|
},
|
||||||
async profile() {
|
async profile() {
|
||||||
return fetchProfile((await this.$.user).id)
|
return fetchProfile((await this.$.user).id);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Alternative without extra dependencies:**
|
**Alternative without extra dependencies:**
|
||||||
@@ -38,14 +39,10 @@ const { user, config, profile } = await all({
|
|||||||
We can also create all the promises first, and do `Promise.all()` at the end.
|
We can also create all the promises first, and do `Promise.all()` at the end.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const userPromise = fetchUser()
|
const userPromise = fetchUser();
|
||||||
const profilePromise = userPromise.then(user => fetchProfile(user.id))
|
const profilePromise = userPromise.then((user) => fetchProfile(user.id));
|
||||||
|
|
||||||
const [user, config, profile] = await Promise.all([
|
const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise]);
|
||||||
userPromise,
|
|
||||||
fetchConfig(),
|
|
||||||
profilePromise
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||||
@@ -12,17 +12,13 @@ When async operations have no interdependencies, execute them concurrently using
|
|||||||
**Incorrect (sequential execution, 3 round trips):**
|
**Incorrect (sequential execution, 3 round trips):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const user = await fetchUser()
|
const user = await fetchUser();
|
||||||
const posts = await fetchPosts()
|
const posts = await fetchPosts();
|
||||||
const comments = await fetchComments()
|
const comments = await fetchComments();
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (parallel execution, 1 round trip):**
|
**Correct (parallel execution, 1 round trip):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [user, posts, comments] = await Promise.all([
|
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
|
||||||
fetchUser(),
|
|
||||||
fetchPosts(),
|
|
||||||
fetchComments()
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
@@ -13,8 +13,8 @@ Instead of awaiting data in async components before returning JSX, use Suspense
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const data = await fetchData() // Blocks entire page
|
const data = await fetchData(); // Blocks entire page
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Sidebar</div>
|
<div>Sidebar</div>
|
||||||
@@ -24,7 +24,7 @@ async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,12 +45,12 @@ function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function DataDisplay() {
|
async function DataDisplay() {
|
||||||
const data = await fetchData() // Only blocks this component
|
const data = await fetchData(); // Only blocks this component
|
||||||
return <div>{data.content}</div>
|
return <div>{data.content}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
|||||||
```tsx
|
```tsx
|
||||||
function Page() {
|
function Page() {
|
||||||
// Start fetch immediately, but don't await
|
// Start fetch immediately, but don't await
|
||||||
const dataPromise = fetchData()
|
const dataPromise = fetchData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Sidebar</div>
|
<div>Sidebar</div>
|
||||||
@@ -73,17 +73,17 @@ function Page() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||||
const data = use(dataPromise) // Unwraps the promise
|
const data = use(dataPromise); // Unwraps the promise
|
||||||
return <div>{data.content}</div>
|
return <div>{data.content}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||||
const data = use(dataPromise) // Reuses the same promise
|
const data = use(dataPromise); // Reuses the same promise
|
||||||
return <div>{data.summary}</div>
|
return <div>{data.summary}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,24 +16,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the
|
|||||||
**Incorrect (imports entire library):**
|
**Incorrect (imports entire library):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Check, X, Menu } from 'lucide-react'
|
import { Check, X, Menu } from 'lucide-react';
|
||||||
// Loads 1,583 modules, takes ~2.8s extra in dev
|
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||||
// Runtime cost: 200-800ms on every cold start
|
// Runtime cost: 200-800ms on every cold start
|
||||||
|
|
||||||
import { Button, TextField } from '@mui/material'
|
import { Button, TextField } from '@mui/material';
|
||||||
// Loads 2,225 modules, takes ~4.2s extra in dev
|
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (imports only what you need):**
|
**Correct (imports only what you need):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import Check from 'lucide-react/dist/esm/icons/check'
|
import Check from 'lucide-react/dist/esm/icons/check';
|
||||||
import X from 'lucide-react/dist/esm/icons/x'
|
import X from 'lucide-react/dist/esm/icons/x';
|
||||||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
import Menu from 'lucide-react/dist/esm/icons/menu';
|
||||||
// Loads only 3 modules (~2KB vs ~1MB)
|
// Loads only 3 modules (~2KB vs ~1MB)
|
||||||
|
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button';
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField';
|
||||||
// Loads only what you use
|
// Loads only what you use
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -43,12 +43,12 @@ import TextField from '@mui/material/TextField'
|
|||||||
// next.config.js - use optimizePackageImports
|
// next.config.js - use optimizePackageImports
|
||||||
module.exports = {
|
module.exports = {
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ['lucide-react', '@mui/material']
|
optimizePackageImports: ['lucide-react', '@mui/material'],
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// Then you can keep the ergonomic barrel imports:
|
// Then you can keep the ergonomic barrel imports:
|
||||||
import { Check, X, Menu } from 'lucide-react'
|
import { Check, X, Menu } from 'lucide-react';
|
||||||
// Automatically transformed to direct imports at build time
|
// Automatically transformed to direct imports at build time
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,25 @@ Load large data or modules only when a feature is activated.
|
|||||||
**Example (lazy-load animation frames):**
|
**Example (lazy-load animation frames):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
|
function AnimationPlayer({
|
||||||
const [frames, setFrames] = useState<Frame[] | null>(null)
|
enabled,
|
||||||
|
setEnabled,
|
||||||
|
}: {
|
||||||
|
enabled: boolean;
|
||||||
|
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) {
|
||||||
|
const [frames, setFrames] = useState<Frame[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled && !frames && typeof window !== 'undefined') {
|
if (enabled && !frames && typeof window !== 'undefined') {
|
||||||
import('./animation-frames.js')
|
import('./animation-frames.js')
|
||||||
.then(mod => setFrames(mod.frames))
|
.then((mod) => setFrames(mod.frames))
|
||||||
.catch(() => setEnabled(false))
|
.catch(() => setEnabled(false));
|
||||||
}
|
}
|
||||||
}, [enabled, frames, setEnabled])
|
}, [enabled, frames, setEnabled]);
|
||||||
|
|
||||||
if (!frames) return <Skeleton />
|
if (!frames) return <Skeleton />;
|
||||||
return <Canvas frames={frames} />
|
return <Canvas frames={frames} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a
|
|||||||
**Incorrect (blocks initial bundle):**
|
**Incorrect (blocks initial bundle):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Analytics } from '@vercel/analytics/react'
|
import { Analytics } from '@vercel/analytics/react';
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
@@ -22,19 +22,18 @@ export default function RootLayout({ children }) {
|
|||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (loads after hydration):**
|
**Correct (loads after hydration):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
const Analytics = dynamic(
|
const Analytics = dynamic(() => import('@vercel/analytics/react').then((m) => m.Analytics), {
|
||||||
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
ssr: false,
|
||||||
{ ssr: false }
|
});
|
||||||
)
|
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +43,6 @@ export default function RootLayout({ children }) {
|
|||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -12,24 +12,23 @@ Use `next/dynamic` to lazy-load large components not needed on initial render.
|
|||||||
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { MonacoEditor } from './monaco-editor'
|
import { MonacoEditor } from './monaco-editor';
|
||||||
|
|
||||||
function CodePanel({ code }: { code: string }) {
|
function CodePanel({ code }: { code: string }) {
|
||||||
return <MonacoEditor value={code} />
|
return <MonacoEditor value={code} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (Monaco loads on demand):**
|
**Correct (Monaco loads on demand):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
const MonacoEditor = dynamic(
|
const MonacoEditor = dynamic(() => import('./monaco-editor').then((m) => m.MonacoEditor), {
|
||||||
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
ssr: false,
|
||||||
{ ssr: false }
|
});
|
||||||
)
|
|
||||||
|
|
||||||
function CodePanel({ code }: { code: string }) {
|
function CodePanel({ code }: { code: string }) {
|
||||||
return <MonacoEditor value={code} />
|
return <MonacoEditor value={code} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -15,19 +15,15 @@ Preload heavy bundles before they're needed to reduce perceived latency.
|
|||||||
function EditorButton({ onClick }: { onClick: () => void }) {
|
function EditorButton({ onClick }: { onClick: () => void }) {
|
||||||
const preload = () => {
|
const preload = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
void import('./monaco-editor')
|
void import('./monaco-editor');
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
||||||
onMouseEnter={preload}
|
|
||||||
onFocus={preload}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
Open Editor
|
Open Editor
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -37,13 +33,11 @@ function EditorButton({ onClick }: { onClick: () => void }) {
|
|||||||
function FlagsProvider({ children, flags }: Props) {
|
function FlagsProvider({ children, flags }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.editorEnabled && typeof window !== 'undefined') {
|
if (flags.editorEnabled && typeof window !== 'undefined') {
|
||||||
void import('./monaco-editor').then(mod => mod.init())
|
void import('./monaco-editor').then((mod) => mod.init());
|
||||||
}
|
}
|
||||||
}, [flags.editorEnabled])
|
}, [flags.editorEnabled]);
|
||||||
|
|
||||||
return <FlagsContext.Provider value={flags}>
|
return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
|
||||||
{children}
|
|
||||||
</FlagsContext.Provider>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ function useKeyboardShortcut(key: string, callback: () => void) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
if (e.metaKey && e.key === key) {
|
if (e.metaKey && e.key === key) {
|
||||||
callback()
|
callback();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
window.addEventListener('keydown', handler)
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener('keydown', handler);
|
||||||
}, [key, callback])
|
}, [key, callback]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,45 +30,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg
|
|||||||
**Correct (N instances = 1 listener):**
|
**Correct (N instances = 1 listener):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import useSWRSubscription from 'swr/subscription'
|
import useSWRSubscription from 'swr/subscription';
|
||||||
|
|
||||||
// Module-level Map to track callbacks per key
|
// Module-level Map to track callbacks per key
|
||||||
const keyCallbacks = new Map<string, Set<() => void>>()
|
const keyCallbacks = new Map<string, Set<() => void>>();
|
||||||
|
|
||||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||||
// Register this callback in the Map
|
// Register this callback in the Map
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!keyCallbacks.has(key)) {
|
if (!keyCallbacks.has(key)) {
|
||||||
keyCallbacks.set(key, new Set())
|
keyCallbacks.set(key, new Set());
|
||||||
}
|
}
|
||||||
keyCallbacks.get(key)!.add(callback)
|
keyCallbacks.get(key)!.add(callback);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const set = keyCallbacks.get(key)
|
const set = keyCallbacks.get(key);
|
||||||
if (set) {
|
if (set) {
|
||||||
set.delete(callback)
|
set.delete(callback);
|
||||||
if (set.size === 0) {
|
if (set.size === 0) {
|
||||||
keyCallbacks.delete(key)
|
keyCallbacks.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [key, callback])
|
}, [key, callback]);
|
||||||
|
|
||||||
useSWRSubscription('global-keydown', () => {
|
useSWRSubscription('global-keydown', () => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
if (e.metaKey && keyCallbacks.has(e.key)) {
|
if (e.metaKey && keyCallbacks.has(e.key)) {
|
||||||
keyCallbacks.get(e.key)!.forEach(cb => cb())
|
keyCallbacks.get(e.key)!.forEach((cb) => cb());
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
window.addEventListener('keydown', handler)
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener('keydown', handler);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function Profile() {
|
function Profile() {
|
||||||
// Multiple shortcuts will share the same listener
|
// Multiple shortcuts will share the same listener
|
||||||
useKeyboardShortcut('p', () => { /* ... */ })
|
useKeyboardShortcut('p', () => {
|
||||||
useKeyboardShortcut('k', () => { /* ... */ })
|
/* ... */
|
||||||
|
});
|
||||||
|
useKeyboardShortcut('k', () => {
|
||||||
|
/* ... */
|
||||||
|
});
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,18 +13,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// No version, stores everything, no error handling
|
// No version, stores everything, no error handling
|
||||||
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
|
localStorage.setItem('userConfig', JSON.stringify(fullUserObject));
|
||||||
const data = localStorage.getItem('userConfig')
|
const data = localStorage.getItem('userConfig');
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct:**
|
**Correct:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const VERSION = 'v2'
|
const VERSION = 'v2';
|
||||||
|
|
||||||
function saveConfig(config: { theme: string; language: string }) {
|
function saveConfig(config: { theme: string; language: string }) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
|
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
|
||||||
} catch {
|
} catch {
|
||||||
// Throws in incognito/private browsing, quota exceeded, or disabled
|
// Throws in incognito/private browsing, quota exceeded, or disabled
|
||||||
}
|
}
|
||||||
@@ -32,21 +32,21 @@ function saveConfig(config: { theme: string; language: string }) {
|
|||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(`userConfig:${VERSION}`)
|
const data = localStorage.getItem(`userConfig:${VERSION}`);
|
||||||
return data ? JSON.parse(data) : null
|
return data ? JSON.parse(data) : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration from v1 to v2
|
// Migration from v1 to v2
|
||||||
function migrate() {
|
function migrate() {
|
||||||
try {
|
try {
|
||||||
const v1 = localStorage.getItem('userConfig:v1')
|
const v1 = localStorage.getItem('userConfig:v1');
|
||||||
if (v1) {
|
if (v1) {
|
||||||
const old = JSON.parse(v1)
|
const old = JSON.parse(v1);
|
||||||
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
|
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang });
|
||||||
localStorage.removeItem('userConfig:v1')
|
localStorage.removeItem('userConfig:v1');
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,13 @@ function migrate() {
|
|||||||
// User object has 20+ fields, only store what UI needs
|
// User object has 20+ fields, only store what UI needs
|
||||||
function cachePrefs(user: FullUser) {
|
function cachePrefs(user: FullUser) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('prefs:v1', JSON.stringify({
|
localStorage.setItem(
|
||||||
theme: user.preferences.theme,
|
'prefs:v1',
|
||||||
notifications: user.preferences.notifications
|
JSON.stringify({
|
||||||
}))
|
theme: user.preferences.theme,
|
||||||
|
notifications: user.preferences.notifications,
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,34 +13,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||||
|
|
||||||
document.addEventListener('touchstart', handleTouch)
|
document.addEventListener('touchstart', handleTouch);
|
||||||
document.addEventListener('wheel', handleWheel)
|
document.addEventListener('wheel', handleWheel);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('touchstart', handleTouch)
|
document.removeEventListener('touchstart', handleTouch);
|
||||||
document.removeEventListener('wheel', handleWheel)
|
document.removeEventListener('wheel', handleWheel);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct:**
|
**Correct:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||||
|
|
||||||
document.addEventListener('touchstart', handleTouch, { passive: true })
|
document.addEventListener('touchstart', handleTouch, { passive: true });
|
||||||
document.addEventListener('wheel', handleWheel, { passive: true })
|
document.addEventListener('wheel', handleWheel, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('touchstart', handleTouch)
|
document.removeEventListener('touchstart', handleTouch);
|
||||||
document.removeEventListener('wheel', handleWheel)
|
document.removeEventListener('wheel', handleWheel);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
||||||
|
|||||||
@@ -13,43 +13,43 @@ SWR enables request deduplication, caching, and revalidation across component in
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function UserList() {
|
function UserList() {
|
||||||
const [users, setUsers] = useState([])
|
const [users, setUsers] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/users')
|
fetch('/api/users')
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(setUsers)
|
.then(setUsers);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (multiple instances share one request):**
|
**Correct (multiple instances share one request):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr';
|
||||||
|
|
||||||
function UserList() {
|
function UserList() {
|
||||||
const { data: users } = useSWR('/api/users', fetcher)
|
const { data: users } = useSWR('/api/users', fetcher);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**For immutable data:**
|
**For immutable data:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useImmutableSWR } from '@/lib/swr'
|
import { useImmutableSWR } from '@/lib/swr';
|
||||||
|
|
||||||
function StaticContent() {
|
function StaticContent() {
|
||||||
const { data } = useImmutableSWR('/api/config', fetcher)
|
const { data } = useImmutableSWR('/api/config', fetcher);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**For mutations:**
|
**For mutations:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useSWRMutation } from 'swr/mutation'
|
import { useSWRMutation } from 'swr/mutation';
|
||||||
|
|
||||||
function UpdateButton() {
|
function UpdateButton() {
|
||||||
const { trigger } = useSWRMutation('/api/user', updateUser)
|
const { trigger } = useSWRMutation('/api/user', updateUser);
|
||||||
return <button onClick={() => trigger()}>Update</button>
|
return <button onClick={() => trigger()}>Update</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -10,55 +10,60 @@ tags: javascript, dom, css, performance, reflow, layout-thrashing
|
|||||||
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
|
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
|
||||||
|
|
||||||
**This is OK (browser batches style changes):**
|
**This is OK (browser batches style changes):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
// Each line invalidates style, but browser batches the recalculation
|
// Each line invalidates style, but browser batches the recalculation
|
||||||
element.style.width = '100px'
|
element.style.width = '100px';
|
||||||
element.style.height = '200px'
|
element.style.height = '200px';
|
||||||
element.style.backgroundColor = 'blue'
|
element.style.backgroundColor = 'blue';
|
||||||
element.style.border = '1px solid black'
|
element.style.border = '1px solid black';
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Incorrect (interleaved reads and writes force reflows):**
|
**Incorrect (interleaved reads and writes force reflows):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function layoutThrashing(element: HTMLElement) {
|
function layoutThrashing(element: HTMLElement) {
|
||||||
element.style.width = '100px'
|
element.style.width = '100px';
|
||||||
const width = element.offsetWidth // Forces reflow
|
const width = element.offsetWidth; // Forces reflow
|
||||||
element.style.height = '200px'
|
element.style.height = '200px';
|
||||||
const height = element.offsetHeight // Forces another reflow
|
const height = element.offsetHeight; // Forces another reflow
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (batch writes, then read once):**
|
**Correct (batch writes, then read once):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
// Batch all writes together
|
// Batch all writes together
|
||||||
element.style.width = '100px'
|
element.style.width = '100px';
|
||||||
element.style.height = '200px'
|
element.style.height = '200px';
|
||||||
element.style.backgroundColor = 'blue'
|
element.style.backgroundColor = 'blue';
|
||||||
element.style.border = '1px solid black'
|
element.style.border = '1px solid black';
|
||||||
|
|
||||||
// Read after all writes are done (single reflow)
|
// Read after all writes are done (single reflow)
|
||||||
const { width, height } = element.getBoundingClientRect()
|
const { width, height } = element.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (batch reads, then writes):**
|
**Correct (batch reads, then writes):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function avoidThrashing(element: HTMLElement) {
|
function avoidThrashing(element: HTMLElement) {
|
||||||
// Read phase - all layout queries first
|
// Read phase - all layout queries first
|
||||||
const rect1 = element.getBoundingClientRect()
|
const rect1 = element.getBoundingClientRect();
|
||||||
const offsetWidth = element.offsetWidth
|
const offsetWidth = element.offsetWidth;
|
||||||
const offsetHeight = element.offsetHeight
|
const offsetHeight = element.offsetHeight;
|
||||||
|
|
||||||
// Write phase - all style changes after
|
// Write phase - all style changes after
|
||||||
element.style.width = '100px'
|
element.style.width = '100px';
|
||||||
element.style.height = '200px'
|
element.style.height = '200px';
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Better: use CSS classes**
|
**Better: use CSS classes**
|
||||||
|
|
||||||
```css
|
```css
|
||||||
.highlighted-box {
|
.highlighted-box {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
@@ -67,38 +72,36 @@ function avoidThrashing(element: HTMLElement) {
|
|||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
element.classList.add('highlighted-box')
|
element.classList.add('highlighted-box');
|
||||||
|
|
||||||
const { width, height } = element.getBoundingClientRect()
|
const { width, height } = element.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**React example:**
|
**React example:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// Incorrect: interleaving style changes with layout queries
|
// Incorrect: interleaving style changes with layout queries
|
||||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current && isHighlighted) {
|
if (ref.current && isHighlighted) {
|
||||||
ref.current.style.width = '100px'
|
ref.current.style.width = '100px';
|
||||||
const width = ref.current.offsetWidth // Forces layout
|
const width = ref.current.offsetWidth; // Forces layout
|
||||||
ref.current.style.height = '200px'
|
ref.current.style.height = '200px';
|
||||||
}
|
}
|
||||||
}, [isHighlighted])
|
}, [isHighlighted]);
|
||||||
|
|
||||||
return <div ref={ref}>Content</div>
|
return <div ref={ref}>Content</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correct: toggle class
|
// Correct: toggle class
|
||||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||||
return (
|
return <div className={isHighlighted ? 'highlighted-box' : ''}>Content</div>;
|
||||||
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
|
||||||
Content
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function ProjectList({ projects }: { projects: Project[] }) {
|
|||||||
{projects.map(project => {
|
{projects.map(project => {
|
||||||
// slugify() called 100+ times for same project names
|
// slugify() called 100+ times for same project names
|
||||||
const slug = slugify(project.name)
|
const slug = slugify(project.name)
|
||||||
|
|
||||||
return <ProjectCard key={project.id} slug={slug} />
|
return <ProjectCard key={project.id} slug={slug} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@ function ProjectList({ projects }: { projects: Project[] }) {
|
|||||||
{projects.map(project => {
|
{projects.map(project => {
|
||||||
// Computed only once per unique project name
|
// Computed only once per unique project name
|
||||||
const slug = cachedSlugify(project.name)
|
const slug = cachedSlugify(project.name)
|
||||||
|
|
||||||
return <ProjectCard key={project.id} slug={slug} />
|
return <ProjectCard key={project.id} slug={slug} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -58,20 +58,20 @@ function ProjectList({ projects }: { projects: Project[] }) {
|
|||||||
**Simpler pattern for single-value functions:**
|
**Simpler pattern for single-value functions:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
let isLoggedInCache: boolean | null = null
|
let isLoggedInCache: boolean | null = null;
|
||||||
|
|
||||||
function isLoggedIn(): boolean {
|
function isLoggedIn(): boolean {
|
||||||
if (isLoggedInCache !== null) {
|
if (isLoggedInCache !== null) {
|
||||||
return isLoggedInCache
|
return isLoggedInCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedInCache = document.cookie.includes('auth=')
|
isLoggedInCache = document.cookie.includes('auth=');
|
||||||
return isLoggedInCache
|
return isLoggedInCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache when auth changes
|
// Clear cache when auth changes
|
||||||
function onAuthChange() {
|
function onAuthChange() {
|
||||||
isLoggedInCache = null
|
isLoggedInCache = null;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ Cache object property lookups in hot paths.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
for (let i = 0; i < arr.length; i++) {
|
for (let i = 0; i < arr.length; i++) {
|
||||||
process(obj.config.settings.value)
|
process(obj.config.settings.value);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (1 lookup total):**
|
**Correct (1 lookup total):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const value = obj.config.settings.value
|
const value = obj.config.settings.value;
|
||||||
const len = arr.length
|
const len = arr.length;
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
process(value)
|
process(value);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,7 +13,7 @@ tags: javascript, localStorage, storage, caching, performance
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getTheme() {
|
function getTheme() {
|
||||||
return localStorage.getItem('theme') ?? 'light'
|
return localStorage.getItem('theme') ?? 'light';
|
||||||
}
|
}
|
||||||
// Called 10 times = 10 storage reads
|
// Called 10 times = 10 storage reads
|
||||||
```
|
```
|
||||||
@@ -21,18 +21,18 @@ function getTheme() {
|
|||||||
**Correct (Map cache):**
|
**Correct (Map cache):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const storageCache = new Map<string, string | null>()
|
const storageCache = new Map<string, string | null>();
|
||||||
|
|
||||||
function getLocalStorage(key: string) {
|
function getLocalStorage(key: string) {
|
||||||
if (!storageCache.has(key)) {
|
if (!storageCache.has(key)) {
|
||||||
storageCache.set(key, localStorage.getItem(key))
|
storageCache.set(key, localStorage.getItem(key));
|
||||||
}
|
}
|
||||||
return storageCache.get(key)
|
return storageCache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLocalStorage(key: string, value: string) {
|
function setLocalStorage(key: string, value: string) {
|
||||||
localStorage.setItem(key, value)
|
localStorage.setItem(key, value);
|
||||||
storageCache.set(key, value) // keep cache in sync
|
storageCache.set(key, value); // keep cache in sync
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -41,15 +41,13 @@ Use a Map (not a hook) so it works everywhere: utilities, event handlers, not ju
|
|||||||
**Cookie caching:**
|
**Cookie caching:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
let cookieCache: Record<string, string> | null = null
|
let cookieCache: Record<string, string> | null = null;
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
if (!cookieCache) {
|
if (!cookieCache) {
|
||||||
cookieCache = Object.fromEntries(
|
cookieCache = Object.fromEntries(document.cookie.split('; ').map((c) => c.split('=')));
|
||||||
document.cookie.split('; ').map(c => c.split('='))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return cookieCache[name]
|
return cookieCache[name];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -59,12 +57,12 @@ If storage can change externally (another tab, server-set cookies), invalidate c
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
window.addEventListener('storage', (e) => {
|
window.addEventListener('storage', (e) => {
|
||||||
if (e.key) storageCache.delete(e.key)
|
if (e.key) storageCache.delete(e.key);
|
||||||
})
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
storageCache.clear()
|
storageCache.clear();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
@@ -12,21 +12,21 @@ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine
|
|||||||
**Incorrect (3 iterations):**
|
**Incorrect (3 iterations):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const admins = users.filter(u => u.isAdmin)
|
const admins = users.filter((u) => u.isAdmin);
|
||||||
const testers = users.filter(u => u.isTester)
|
const testers = users.filter((u) => u.isTester);
|
||||||
const inactive = users.filter(u => !u.isActive)
|
const inactive = users.filter((u) => !u.isActive);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (1 iteration):**
|
**Correct (1 iteration):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const admins: User[] = []
|
const admins: User[] = [];
|
||||||
const testers: User[] = []
|
const testers: User[] = [];
|
||||||
const inactive: User[] = []
|
const inactive: User[] = [];
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (user.isAdmin) admins.push(user)
|
if (user.isAdmin) admins.push(user);
|
||||||
if (user.isTester) testers.push(user)
|
if (user.isTester) testers.push(user);
|
||||||
if (!user.isActive) inactive.push(user)
|
if (!user.isActive) inactive.push(user);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,22 +13,22 @@ Return early when result is determined to skip unnecessary processing.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function validateUsers(users: User[]) {
|
function validateUsers(users: User[]) {
|
||||||
let hasError = false
|
let hasError = false;
|
||||||
let errorMessage = ''
|
let errorMessage = '';
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
hasError = true
|
hasError = true;
|
||||||
errorMessage = 'Email required'
|
errorMessage = 'Email required';
|
||||||
}
|
}
|
||||||
if (!user.name) {
|
if (!user.name) {
|
||||||
hasError = true
|
hasError = true;
|
||||||
errorMessage = 'Name required'
|
errorMessage = 'Name required';
|
||||||
}
|
}
|
||||||
// Continues checking all users even after error found
|
// Continues checking all users even after error found
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasError ? { valid: false, error: errorMessage } : { valid: true }
|
return hasError ? { valid: false, error: errorMessage } : { valid: true };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,13 +38,13 @@ function validateUsers(users: User[]) {
|
|||||||
function validateUsers(users: User[]) {
|
function validateUsers(users: User[]) {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
return { valid: false, error: 'Email required' }
|
return { valid: false, error: 'Email required' };
|
||||||
}
|
}
|
||||||
if (!user.name) {
|
if (!user.name) {
|
||||||
return { valid: false, error: 'Name required' }
|
return { valid: false, error: 'Name required' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true }
|
return { valid: true };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -39,7 +39,7 @@ function Highlighter({ text, query }: Props) {
|
|||||||
Global regex (`/g`) has mutable `lastIndex` state:
|
Global regex (`/g`) has mutable `lastIndex` state:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const regex = /foo/g
|
const regex = /foo/g;
|
||||||
regex.test('foo') // true, lastIndex = 3
|
regex.test('foo'); // true, lastIndex = 3
|
||||||
regex.test('foo') // false, lastIndex = 0
|
regex.test('foo'); // false, lastIndex = 0
|
||||||
```
|
```
|
||||||
@@ -13,10 +13,10 @@ Multiple `.find()` calls by the same key should use a Map.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function processOrders(orders: Order[], users: User[]) {
|
function processOrders(orders: Order[], users: User[]) {
|
||||||
return orders.map(order => ({
|
return orders.map((order) => ({
|
||||||
...order,
|
...order,
|
||||||
user: users.find(u => u.id === order.userId)
|
user: users.find((u) => u.id === order.userId),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ function processOrders(orders: Order[], users: User[]) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function processOrders(orders: Order[], users: User[]) {
|
function processOrders(orders: Order[], users: User[]) {
|
||||||
const userById = new Map(users.map(u => [u.id, u]))
|
const userById = new Map(users.map((u) => [u.id, u]));
|
||||||
|
|
||||||
return orders.map(order => ({
|
return orders.map((order) => ({
|
||||||
...order,
|
...order,
|
||||||
user: userById.get(order.userId)
|
user: userById.get(order.userId),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ In real-world applications, this optimization is especially valuable when the co
|
|||||||
```typescript
|
```typescript
|
||||||
function hasChanges(current: string[], original: string[]) {
|
function hasChanges(current: string[], original: string[]) {
|
||||||
// Always sorts and joins, even when lengths differ
|
// Always sorts and joins, even when lengths differ
|
||||||
return current.sort().join() !== original.sort().join()
|
return current.sort().join() !== original.sort().join();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -28,21 +28,22 @@ Two O(n log n) sorts run even when `current.length` is 5 and `original.length` i
|
|||||||
function hasChanges(current: string[], original: string[]) {
|
function hasChanges(current: string[], original: string[]) {
|
||||||
// Early return if lengths differ
|
// Early return if lengths differ
|
||||||
if (current.length !== original.length) {
|
if (current.length !== original.length) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
// Only sort when lengths match
|
// Only sort when lengths match
|
||||||
const currentSorted = current.toSorted()
|
const currentSorted = current.toSorted();
|
||||||
const originalSorted = original.toSorted()
|
const originalSorted = original.toSorted();
|
||||||
for (let i = 0; i < currentSorted.length; i++) {
|
for (let i = 0; i < currentSorted.length; i++) {
|
||||||
if (currentSorted[i] !== originalSorted[i]) {
|
if (currentSorted[i] !== originalSorted[i]) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This new approach is more efficient because:
|
This new approach is more efficient because:
|
||||||
|
|
||||||
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
||||||
- It avoids consuming memory for the joined strings (especially important for large arrays)
|
- It avoids consuming memory for the joined strings (especially important for large arrays)
|
||||||
- It avoids mutating the original arrays
|
- It avoids mutating the original arrays
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ Finding the smallest or largest element only requires a single pass through the
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
updatedAt: number
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestProject(projects: Project[]) {
|
function getLatestProject(projects: Project[]) {
|
||||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
|
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||||
return sorted[0]
|
return sorted[0];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Sorts the entire array just to find the maximum value.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getOldestAndNewest(projects: Project[]) {
|
function getOldestAndNewest(projects: Project[]) {
|
||||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
|
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
|
||||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
|
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -41,31 +41,31 @@ Still sorts unnecessarily when only min/max are needed.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getLatestProject(projects: Project[]) {
|
function getLatestProject(projects: Project[]) {
|
||||||
if (projects.length === 0) return null
|
if (projects.length === 0) return null;
|
||||||
|
|
||||||
let latest = projects[0]
|
let latest = projects[0];
|
||||||
|
|
||||||
for (let i = 1; i < projects.length; i++) {
|
for (let i = 1; i < projects.length; i++) {
|
||||||
if (projects[i].updatedAt > latest.updatedAt) {
|
if (projects[i].updatedAt > latest.updatedAt) {
|
||||||
latest = projects[i]
|
latest = projects[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return latest
|
return latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOldestAndNewest(projects: Project[]) {
|
function getOldestAndNewest(projects: Project[]) {
|
||||||
if (projects.length === 0) return { oldest: null, newest: null }
|
if (projects.length === 0) return { oldest: null, newest: null };
|
||||||
|
|
||||||
let oldest = projects[0]
|
let oldest = projects[0];
|
||||||
let newest = projects[0]
|
let newest = projects[0];
|
||||||
|
|
||||||
for (let i = 1; i < projects.length; i++) {
|
for (let i = 1; i < projects.length; i++) {
|
||||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
|
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
|
||||||
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
|
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
return { oldest, newest }
|
return { oldest, newest };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -74,9 +74,9 @@ Single pass through the array, no copying, no sorting.
|
|||||||
**Alternative (Math.min/Math.max for small arrays):**
|
**Alternative (Math.min/Math.max for small arrays):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const numbers = [5, 2, 8, 1, 9]
|
const numbers = [5, 2, 8, 1, 9];
|
||||||
const min = Math.min(...numbers)
|
const min = Math.min(...numbers);
|
||||||
const max = Math.max(...numbers)
|
const max = Math.max(...numbers);
|
||||||
```
|
```
|
||||||
|
|
||||||
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
|
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
|
||||||
@@ -46,7 +46,7 @@ function UserList({ users }: { users: User[] }) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Fallback for older browsers
|
// Fallback for older browsers
|
||||||
const sorted = [...items].sort((a, b) => a.value - b.value)
|
const sorted = [...items].sort((a, b) => a.value - b.value);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Other immutable array methods:**
|
**Other immutable array methods:**
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ Use React's `<Activity>` to preserve state/DOM for expensive components that fre
|
|||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Activity } from 'react'
|
import { Activity } from 'react';
|
||||||
|
|
||||||
function Dropdown({ isOpen }: Props) {
|
function Dropdown({ isOpen }: Props) {
|
||||||
return (
|
return (
|
||||||
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
||||||
<ExpensiveMenu />
|
<ExpensiveMenu />
|
||||||
</Activity>
|
</Activity>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ Many browsers don't have hardware acceleration for CSS3 animations on SVG elemen
|
|||||||
```tsx
|
```tsx
|
||||||
function LoadingSpinner() {
|
function LoadingSpinner() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
|
||||||
className="animate-spin"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,15 +27,11 @@ function LoadingSpinner() {
|
|||||||
function LoadingSpinner() {
|
function LoadingSpinner() {
|
||||||
return (
|
return (
|
||||||
<div className="animate-spin">
|
<div className="animate-spin">
|
||||||
<svg
|
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Badge({ count }: { count: number }) {
|
function Badge({ count }: { count: number }) {
|
||||||
return (
|
return <div>{count && <span className="badge">{count}</span>}</div>;
|
||||||
<div>
|
|
||||||
{count && <span className="badge">{count}</span>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When count = 0, renders: <div>0</div>
|
// When count = 0, renders: <div>0</div>
|
||||||
@@ -28,11 +24,7 @@ function Badge({ count }: { count: number }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Badge({ count }: { count: number }) {
|
function Badge({ count }: { count: number }) {
|
||||||
return (
|
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
|
||||||
<div>
|
|
||||||
{count > 0 ? <span className="badge">{count}</span> : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When count = 0, renders: <div></div>
|
// When count = 0, renders: <div></div>
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ Apply `content-visibility: auto` to defer off-screen rendering.
|
|||||||
```tsx
|
```tsx
|
||||||
function MessageList({ messages }: { messages: Message[] }) {
|
function MessageList({ messages }: { messages: Message[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-auto h-screen">
|
<div className="h-screen overflow-y-auto">
|
||||||
{messages.map(msg => (
|
{messages.map((msg) => (
|
||||||
<div key={msg.id} className="message-item">
|
<div key={msg.id} className="message-item">
|
||||||
<Avatar user={msg.author} />
|
<Avatar user={msg.author} />
|
||||||
<div>{msg.content}</div>
|
<div>{msg.content}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,31 +13,21 @@ Extract static JSX outside components to avoid re-creation.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function LoadingSkeleton() {
|
function LoadingSkeleton() {
|
||||||
return <div className="animate-pulse h-20 bg-gray-200" />
|
return <div className="h-20 animate-pulse bg-gray-200" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Container() {
|
function Container() {
|
||||||
return (
|
return <div>{loading && <LoadingSkeleton />}</div>;
|
||||||
<div>
|
|
||||||
{loading && <LoadingSkeleton />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (reuses same element):**
|
**Correct (reuses same element):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const loadingSkeleton = (
|
const loadingSkeleton = <div className="h-20 animate-pulse bg-gray-200" />;
|
||||||
<div className="animate-pulse h-20 bg-gray-200" />
|
|
||||||
)
|
|
||||||
|
|
||||||
function Container() {
|
function Container() {
|
||||||
return (
|
return <div>{loading && loadingSkeleton}</div>;
|
||||||
<div>
|
|
||||||
{loading && loadingSkeleton}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,9 @@ When rendering content that depends on client-side storage (localStorage, cookie
|
|||||||
```tsx
|
```tsx
|
||||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
// localStorage is not available on server - throws error
|
// localStorage is not available on server - throws error
|
||||||
const theme = localStorage.getItem('theme') || 'light'
|
const theme = localStorage.getItem('theme') || 'light';
|
||||||
|
|
||||||
return (
|
return <div className={theme}>{children}</div>;
|
||||||
<div className={theme}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,21 +26,17 @@ Server-side rendering will fail because `localStorage` is undefined.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
const [theme, setTheme] = useState('light')
|
const [theme, setTheme] = useState('light');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Runs after hydration - causes visible flash
|
// Runs after hydration - causes visible flash
|
||||||
const stored = localStorage.getItem('theme')
|
const stored = localStorage.getItem('theme');
|
||||||
if (stored) {
|
if (stored) {
|
||||||
setTheme(stored)
|
setTheme(stored);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return <div className={theme}>{children}</div>;
|
||||||
<div className={theme}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,9 +48,7 @@ Component first renders with default value (`light`), then updates after hydrati
|
|||||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="theme-wrapper">
|
<div id="theme-wrapper">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
@@ -73,7 +63,7 @@ function ThemeWrapper({ children }: { children: ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+3
-7
@@ -7,13 +7,13 @@ tags: rendering, hydration, ssr, nextjs
|
|||||||
|
|
||||||
## Suppress Expected Hydration Mismatches
|
## Suppress Expected Hydration Mismatches
|
||||||
|
|
||||||
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
|
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these _expected_ mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
|
||||||
|
|
||||||
**Incorrect (known mismatch warnings):**
|
**Incorrect (known mismatch warnings):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Timestamp() {
|
function Timestamp() {
|
||||||
return <span>{new Date().toLocaleString()}</span>
|
return <span>{new Date().toLocaleString()}</span>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -21,10 +21,6 @@ function Timestamp() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Timestamp() {
|
function Timestamp() {
|
||||||
return (
|
return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;
|
||||||
<span suppressHydrationWarning>
|
|
||||||
{new Date().toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
+21
-21
@@ -13,17 +13,17 @@ Use `useTransition` instead of manual `useState` for loading states. This provid
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function SearchResults() {
|
function SearchResults() {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('');
|
||||||
const [results, setResults] = useState([])
|
const [results, setResults] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleSearch = async (value: string) => {
|
const handleSearch = async (value: string) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true);
|
||||||
setQuery(value)
|
setQuery(value);
|
||||||
const data = await fetchResults(value)
|
const data = await fetchResults(value);
|
||||||
setResults(data)
|
setResults(data);
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -31,29 +31,29 @@ function SearchResults() {
|
|||||||
{isLoading && <Spinner />}
|
{isLoading && <Spinner />}
|
||||||
<ResultsList results={results} />
|
<ResultsList results={results} />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (useTransition with built-in pending state):**
|
**Correct (useTransition with built-in pending state):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useTransition, useState } from 'react'
|
import { useTransition, useState } from 'react';
|
||||||
|
|
||||||
function SearchResults() {
|
function SearchResults() {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('');
|
||||||
const [results, setResults] = useState([])
|
const [results, setResults] = useState([]);
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
setQuery(value) // Update input immediately
|
setQuery(value); // Update input immediately
|
||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
// Fetch and update results
|
// Fetch and update results
|
||||||
const data = await fetchResults(value)
|
const data = await fetchResults(value);
|
||||||
setResults(data)
|
setResults(data);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -61,7 +61,7 @@ function SearchResults() {
|
|||||||
{isPending && <Spinner />}
|
{isPending && <Spinner />}
|
||||||
<ResultsList results={results} />
|
<ResultsList results={results} />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ShareButton({ chatId }: { chatId: string }) {
|
function ShareButton({ chatId }: { chatId: string }) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
const ref = searchParams.get('ref')
|
const ref = searchParams.get('ref');
|
||||||
shareChat(chatId, { ref })
|
shareChat(chatId, { ref });
|
||||||
}
|
};
|
||||||
|
|
||||||
return <button onClick={handleShare}>Share</button>
|
return <button onClick={handleShare}>Share</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@ function ShareButton({ chatId }: { chatId: string }) {
|
|||||||
```tsx
|
```tsx
|
||||||
function ShareButton({ chatId }: { chatId: string }) {
|
function ShareButton({ chatId }: { chatId: string }) {
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search);
|
||||||
const ref = params.get('ref')
|
const ref = params.get('ref');
|
||||||
shareChat(chatId, { ref })
|
shareChat(chatId, { ref });
|
||||||
}
|
};
|
||||||
|
|
||||||
return <button onClick={handleShare}>Share</button>
|
return <button onClick={handleShare}>Share</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,16 +13,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(user.id)
|
console.log(user.id);
|
||||||
}, [user])
|
}, [user]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (re-runs only when id changes):**
|
**Correct (re-runs only when id changes):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(user.id)
|
console.log(user.id);
|
||||||
}, [user.id])
|
}, [user.id]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**For derived state, compute outside effect:**
|
**For derived state, compute outside effect:**
|
||||||
@@ -31,15 +31,15 @@ useEffect(() => {
|
|||||||
// Incorrect: runs on width=767, 766, 765...
|
// Incorrect: runs on width=767, 766, 765...
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (width < 768) {
|
if (width < 768) {
|
||||||
enableMobileMode()
|
enableMobileMode();
|
||||||
}
|
}
|
||||||
}, [width])
|
}, [width]);
|
||||||
|
|
||||||
// Correct: runs only on boolean transition
|
// Correct: runs only on boolean transition
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
enableMobileMode()
|
enableMobileMode();
|
||||||
}
|
}
|
||||||
}, [isMobile])
|
}, [isMobile]);
|
||||||
```
|
```
|
||||||
+10
-10
@@ -13,15 +13,15 @@ If a value can be computed from current props/state, do not store it in state or
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Form() {
|
function Form() {
|
||||||
const [firstName, setFirstName] = useState('First')
|
const [firstName, setFirstName] = useState('First');
|
||||||
const [lastName, setLastName] = useState('Last')
|
const [lastName, setLastName] = useState('Last');
|
||||||
const [fullName, setFullName] = useState('')
|
const [fullName, setFullName] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFullName(firstName + ' ' + lastName)
|
setFullName(firstName + ' ' + lastName);
|
||||||
}, [firstName, lastName])
|
}, [firstName, lastName]);
|
||||||
|
|
||||||
return <p>{fullName}</p>
|
return <p>{fullName}</p>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@ function Form() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Form() {
|
function Form() {
|
||||||
const [firstName, setFirstName] = useState('First')
|
const [firstName, setFirstName] = useState('First');
|
||||||
const [lastName, setLastName] = useState('Last')
|
const [lastName, setLastName] = useState('Last');
|
||||||
const fullName = firstName + ' ' + lastName
|
const fullName = firstName + ' ' + lastName;
|
||||||
|
|
||||||
return <p>{fullName}</p>
|
return <p>{fullName}</p>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const width = useWindowWidth() // updates continuously
|
const width = useWindowWidth(); // updates continuously
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768;
|
||||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
return <nav className={isMobile ? 'mobile' : 'desktop'} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ function Sidebar() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
const isMobile = useMediaQuery('(max-width: 767px)');
|
||||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
return <nav className={isMobile ? 'mobile' : 'desktop'} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,19 +13,22 @@ When updating state based on the current state value, use the functional update
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function TodoList() {
|
function TodoList() {
|
||||||
const [items, setItems] = useState(initialItems)
|
const [items, setItems] = useState(initialItems);
|
||||||
|
|
||||||
// Callback must depend on items, recreated on every items change
|
// Callback must depend on items, recreated on every items change
|
||||||
const addItems = useCallback((newItems: Item[]) => {
|
const addItems = useCallback(
|
||||||
setItems([...items, ...newItems])
|
(newItems: Item[]) => {
|
||||||
}, [items]) // ❌ items dependency causes recreations
|
setItems([...items, ...newItems]);
|
||||||
|
},
|
||||||
|
[items],
|
||||||
|
); // ❌ items dependency causes recreations
|
||||||
|
|
||||||
// Risk of stale closure if dependency is forgotten
|
// Risk of stale closure if dependency is forgotten
|
||||||
const removeItem = useCallback((id: string) => {
|
const removeItem = useCallback((id: string) => {
|
||||||
setItems(items.filter(item => item.id !== id))
|
setItems(items.filter((item) => item.id !== id));
|
||||||
}, []) // ❌ Missing items dependency - will use stale items!
|
}, []); // ❌ Missing items dependency - will use stale items!
|
||||||
|
|
||||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -35,19 +38,19 @@ The first callback is recreated every time `items` changes, which can cause chil
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function TodoList() {
|
function TodoList() {
|
||||||
const [items, setItems] = useState(initialItems)
|
const [items, setItems] = useState(initialItems);
|
||||||
|
|
||||||
// Stable callback, never recreated
|
// Stable callback, never recreated
|
||||||
const addItems = useCallback((newItems: Item[]) => {
|
const addItems = useCallback((newItems: Item[]) => {
|
||||||
setItems(curr => [...curr, ...newItems])
|
setItems((curr) => [...curr, ...newItems]);
|
||||||
}, []) // ✅ No dependencies needed
|
}, []); // ✅ No dependencies needed
|
||||||
|
|
||||||
// Always uses latest state, no stale closure risk
|
// Always uses latest state, no stale closure risk
|
||||||
const removeItem = useCallback((id: string) => {
|
const removeItem = useCallback((id: string) => {
|
||||||
setItems(curr => curr.filter(item => item.id !== id))
|
setItems((curr) => curr.filter((item) => item.id !== id));
|
||||||
}, []) // ✅ Safe and stable
|
}, []); // ✅ Safe and stable
|
||||||
|
|
||||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,20 +14,18 @@ Pass a function to `useState` for expensive initial values. Without the function
|
|||||||
```tsx
|
```tsx
|
||||||
function FilteredList({ items }: { items: Item[] }) {
|
function FilteredList({ items }: { items: Item[] }) {
|
||||||
// buildSearchIndex() runs on EVERY render, even after initialization
|
// buildSearchIndex() runs on EVERY render, even after initialization
|
||||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
|
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
// When query changes, buildSearchIndex runs again unnecessarily
|
// When query changes, buildSearchIndex runs again unnecessarily
|
||||||
return <SearchResults index={searchIndex} query={query} />
|
return <SearchResults index={searchIndex} query={query} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserProfile() {
|
function UserProfile() {
|
||||||
// JSON.parse runs on every render
|
// JSON.parse runs on every render
|
||||||
const [settings, setSettings] = useState(
|
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'));
|
||||||
JSON.parse(localStorage.getItem('settings') || '{}')
|
|
||||||
)
|
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||||
|
|
||||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,20 +34,20 @@ function UserProfile() {
|
|||||||
```tsx
|
```tsx
|
||||||
function FilteredList({ items }: { items: Item[] }) {
|
function FilteredList({ items }: { items: Item[] }) {
|
||||||
// buildSearchIndex() runs ONLY on initial render
|
// buildSearchIndex() runs ONLY on initial render
|
||||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
|
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
return <SearchResults index={searchIndex} query={query} />
|
return <SearchResults index={searchIndex} query={query} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserProfile() {
|
function UserProfile() {
|
||||||
// JSON.parse runs only on initial render
|
// JSON.parse runs only on initial render
|
||||||
const [settings, setSettings] = useState(() => {
|
const [settings, setSettings] = useState(() => {
|
||||||
const stored = localStorage.getItem('settings')
|
const stored = localStorage.getItem('settings');
|
||||||
return stored ? JSON.parse(stored) : {}
|
return stored ? JSON.parse(stored) : {};
|
||||||
})
|
});
|
||||||
|
|
||||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
||||||
impact: MEDIUM
|
impact: MEDIUM
|
||||||
impactDescription: restores memoization by using a constant for default value
|
impactDescription: restores memoization by using a constant for default value
|
||||||
tags: rerender, memo, optimization
|
tags: rerender, memo, optimization
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ Extract expensive work into memoized components to enable early returns before c
|
|||||||
```tsx
|
```tsx
|
||||||
function Profile({ user, loading }: Props) {
|
function Profile({ user, loading }: Props) {
|
||||||
const avatar = useMemo(() => {
|
const avatar = useMemo(() => {
|
||||||
const id = computeAvatarId(user)
|
const id = computeAvatarId(user);
|
||||||
return <Avatar id={id} />
|
return <Avatar id={id} />;
|
||||||
}, [user])
|
}, [user]);
|
||||||
|
|
||||||
if (loading) return <Skeleton />
|
if (loading) return <Skeleton />;
|
||||||
return <div>{avatar}</div>
|
return <div>{avatar}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,17 +27,17 @@ function Profile({ user, loading }: Props) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
||||||
const id = useMemo(() => computeAvatarId(user), [user])
|
const id = useMemo(() => computeAvatarId(user), [user]);
|
||||||
return <Avatar id={id} />
|
return <Avatar id={id} />;
|
||||||
})
|
});
|
||||||
|
|
||||||
function Profile({ user, loading }: Props) {
|
function Profile({ user, loading }: Props) {
|
||||||
if (loading) return <Skeleton />
|
if (loading) return <Skeleton />;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar user={user} />
|
<UserAvatar user={user} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ If a side effect is triggered by a specific user action (submit, click, drag), r
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Form() {
|
function Form() {
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false);
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
post('/api/register')
|
post('/api/register');
|
||||||
showToast('Registered', theme)
|
showToast('Registered', theme);
|
||||||
}
|
}
|
||||||
}, [submitted, theme])
|
}, [submitted, theme]);
|
||||||
|
|
||||||
return <button onClick={() => setSubmitted(true)}>Submit</button>
|
return <button onClick={() => setSubmitted(true)}>Submit</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -31,14 +31,14 @@ function Form() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Form() {
|
function Form() {
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext);
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
post('/api/register')
|
post('/api/register');
|
||||||
showToast('Registered', theme)
|
showToast('Registered', theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <button onClick={handleSubmit}>Submit</button>
|
return <button onClick={handleSubmit}>Submit</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -15,10 +15,10 @@ Calling `useMemo` and comparing hook dependencies may consume more resources tha
|
|||||||
```tsx
|
```tsx
|
||||||
function Header({ user, notifications }: Props) {
|
function Header({ user, notifications }: Props) {
|
||||||
const isLoading = useMemo(() => {
|
const isLoading = useMemo(() => {
|
||||||
return user.isLoading || notifications.isLoading
|
return user.isLoading || notifications.isLoading;
|
||||||
}, [user.isLoading, notifications.isLoading])
|
}, [user.isLoading, notifications.isLoading]);
|
||||||
|
|
||||||
if (isLoading) return <Skeleton />
|
if (isLoading) return <Skeleton />;
|
||||||
// return some markup
|
// return some markup
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -27,9 +27,9 @@ function Header({ user, notifications }: Props) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Header({ user, notifications }: Props) {
|
function Header({ user, notifications }: Props) {
|
||||||
const isLoading = user.isLoading || notifications.isLoading
|
const isLoading = user.isLoading || notifications.isLoading;
|
||||||
|
|
||||||
if (isLoading) return <Skeleton />
|
if (isLoading) return <Skeleton />;
|
||||||
// return some markup
|
// return some markup
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,28 +13,28 @@ Mark frequent, non-urgent state updates as transitions to maintain UI responsive
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ScrollTracker() {
|
function ScrollTracker() {
|
||||||
const [scrollY, setScrollY] = useState(0)
|
const [scrollY, setScrollY] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => setScrollY(window.scrollY)
|
const handler = () => setScrollY(window.scrollY);
|
||||||
window.addEventListener('scroll', handler, { passive: true })
|
window.addEventListener('scroll', handler, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handler)
|
return () => window.removeEventListener('scroll', handler);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (non-blocking updates):**
|
**Correct (non-blocking updates):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { startTransition } from 'react'
|
import { startTransition } from 'react';
|
||||||
|
|
||||||
function ScrollTracker() {
|
function ScrollTracker() {
|
||||||
const [scrollY, setScrollY] = useState(0)
|
const [scrollY, setScrollY] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
startTransition(() => setScrollY(window.scrollY))
|
startTransition(() => setScrollY(window.scrollY));
|
||||||
}
|
};
|
||||||
window.addEventListener('scroll', handler, { passive: true })
|
window.addEventListener('scroll', handler, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handler)
|
return () => window.removeEventListener('scroll', handler);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
+16
-16
@@ -13,13 +13,13 @@ When a value changes frequently and you don't want a re-render on every update (
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Tracker() {
|
function Tracker() {
|
||||||
const [lastX, setLastX] = useState(0)
|
const [lastX, setLastX] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onMove = (e: MouseEvent) => setLastX(e.clientX)
|
const onMove = (e: MouseEvent) => setLastX(e.clientX);
|
||||||
window.addEventListener('mousemove', onMove)
|
window.addEventListener('mousemove', onMove);
|
||||||
return () => window.removeEventListener('mousemove', onMove)
|
return () => window.removeEventListener('mousemove', onMove);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -32,7 +32,7 @@ function Tracker() {
|
|||||||
background: 'black',
|
background: 'black',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,20 +40,20 @@ function Tracker() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Tracker() {
|
function Tracker() {
|
||||||
const lastXRef = useRef(0)
|
const lastXRef = useRef(0);
|
||||||
const dotRef = useRef<HTMLDivElement>(null)
|
const dotRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onMove = (e: MouseEvent) => {
|
const onMove = (e: MouseEvent) => {
|
||||||
lastXRef.current = e.clientX
|
lastXRef.current = e.clientX;
|
||||||
const node = dotRef.current
|
const node = dotRef.current;
|
||||||
if (node) {
|
if (node) {
|
||||||
node.style.transform = `translateX(${e.clientX}px)`
|
node.style.transform = `translateX(${e.clientX}px)`;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
window.addEventListener('mousemove', onMove)
|
window.addEventListener('mousemove', onMove);
|
||||||
return () => window.removeEventListener('mousemove', onMove)
|
return () => window.removeEventListener('mousemove', onMove);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -68,6 +68,6 @@ function Tracker() {
|
|||||||
transform: 'translateX(0px)',
|
transform: 'translateX(0px)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -12,46 +12,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is
|
|||||||
**Incorrect (blocks response):**
|
**Incorrect (blocks response):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { logUserAction } from '@/app/utils'
|
import { logUserAction } from '@/app/utils';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// Perform mutation
|
// Perform mutation
|
||||||
await updateDatabase(request)
|
await updateDatabase(request);
|
||||||
|
|
||||||
// Logging blocks the response
|
// Logging blocks the response
|
||||||
const userAgent = request.headers.get('user-agent') || 'unknown'
|
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||||
await logUserAction({ userAgent })
|
await logUserAction({ userAgent });
|
||||||
|
|
||||||
return new Response(JSON.stringify({ status: 'success' }), {
|
return new Response(JSON.stringify({ status: 'success' }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (non-blocking):**
|
**Correct (non-blocking):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { after } from 'next/server'
|
import { after } from 'next/server';
|
||||||
import { headers, cookies } from 'next/headers'
|
import { headers, cookies } from 'next/headers';
|
||||||
import { logUserAction } from '@/app/utils'
|
import { logUserAction } from '@/app/utils';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// Perform mutation
|
// Perform mutation
|
||||||
await updateDatabase(request)
|
await updateDatabase(request);
|
||||||
|
|
||||||
// Log after response is sent
|
// Log after response is sent
|
||||||
after(async () => {
|
after(async () => {
|
||||||
const userAgent = (await headers()).get('user-agent') || 'unknown'
|
const userAgent = (await headers()).get('user-agent') || 'unknown';
|
||||||
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
|
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous';
|
||||||
|
|
||||||
logUserAction({ sessionCookie, userAgent })
|
logUserAction({ sessionCookie, userAgent });
|
||||||
})
|
});
|
||||||
|
|
||||||
return new Response(JSON.stringify({ status: 'success' }), {
|
return new Response(JSON.stringify({ status: 'success' }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,80 +16,80 @@ Next.js documentation explicitly states: "Treat Server Actions with the same sec
|
|||||||
**Incorrect (no authentication check):**
|
**Incorrect (no authentication check):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'use server'
|
'use server';
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
export async function deleteUser(userId: string) {
|
||||||
// Anyone can call this! No auth check
|
// Anyone can call this! No auth check
|
||||||
await db.user.delete({ where: { id: userId } })
|
await db.user.delete({ where: { id: userId } });
|
||||||
return { success: true }
|
return { success: true };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (authentication inside the action):**
|
**Correct (authentication inside the action):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'use server'
|
'use server';
|
||||||
|
|
||||||
import { verifySession } from '@/lib/auth'
|
import { verifySession } from '@/lib/auth';
|
||||||
import { unauthorized } from '@/lib/errors'
|
import { unauthorized } from '@/lib/errors';
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
export async function deleteUser(userId: string) {
|
||||||
// Always check auth inside the action
|
// Always check auth inside the action
|
||||||
const session = await verifySession()
|
const session = await verifySession();
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw unauthorized('Must be logged in')
|
throw unauthorized('Must be logged in');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authorization too
|
// Check authorization too
|
||||||
if (session.user.role !== 'admin' && session.user.id !== userId) {
|
if (session.user.role !== 'admin' && session.user.id !== userId) {
|
||||||
throw unauthorized('Cannot delete other users')
|
throw unauthorized('Cannot delete other users');
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.user.delete({ where: { id: userId } })
|
await db.user.delete({ where: { id: userId } });
|
||||||
return { success: true }
|
return { success: true };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**With input validation:**
|
**With input validation:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
'use server'
|
'use server';
|
||||||
|
|
||||||
import { verifySession } from '@/lib/auth'
|
import { verifySession } from '@/lib/auth';
|
||||||
import { z } from 'zod'
|
import { z } from 'zod';
|
||||||
|
|
||||||
const updateProfileSchema = z.object({
|
const updateProfileSchema = z.object({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
email: z.string().email()
|
email: z.string().email(),
|
||||||
})
|
});
|
||||||
|
|
||||||
export async function updateProfile(data: unknown) {
|
export async function updateProfile(data: unknown) {
|
||||||
// Validate input first
|
// Validate input first
|
||||||
const validated = updateProfileSchema.parse(data)
|
const validated = updateProfileSchema.parse(data);
|
||||||
|
|
||||||
// Then authenticate
|
// Then authenticate
|
||||||
const session = await verifySession()
|
const session = await verifySession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then authorize
|
// Then authorize
|
||||||
if (session.user.id !== validated.userId) {
|
if (session.user.id !== validated.userId) {
|
||||||
throw new Error('Can only update own profile')
|
throw new Error('Can only update own profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally perform the mutation
|
// Finally perform the mutation
|
||||||
await db.user.update({
|
await db.user.update({
|
||||||
where: { id: validated.userId },
|
where: { id: validated.userId },
|
||||||
data: {
|
data: {
|
||||||
name: validated.name,
|
name: validated.name,
|
||||||
email: validated.email
|
email: validated.email,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return { success: true }
|
return { success: true };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ tags: server, cache, lru, cross-request
|
|||||||
**Implementation:**
|
**Implementation:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache';
|
||||||
|
|
||||||
const cache = new LRUCache<string, any>({
|
const cache = new LRUCache<string, any>({
|
||||||
max: 1000,
|
max: 1000,
|
||||||
ttl: 5 * 60 * 1000 // 5 minutes
|
ttl: 5 * 60 * 1000, // 5 minutes
|
||||||
})
|
});
|
||||||
|
|
||||||
export async function getUser(id: string) {
|
export async function getUser(id: string) {
|
||||||
const cached = cache.get(id)
|
const cached = cache.get(id);
|
||||||
if (cached) return cached
|
if (cached) return cached;
|
||||||
|
|
||||||
const user = await db.user.findUnique({ where: { id } })
|
const user = await db.user.findUnique({ where: { id } });
|
||||||
cache.set(id, user)
|
cache.set(id, user);
|
||||||
return user
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request 1: DB query, result cached
|
// Request 1: DB query, result cached
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da
|
|||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { cache } from 'react'
|
import { cache } from 'react';
|
||||||
|
|
||||||
export const getCurrentUser = cache(async () => {
|
export const getCurrentUser = cache(async () => {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
if (!session?.user?.id) return null
|
if (!session?.user?.id) return null;
|
||||||
return await db.user.findUnique({
|
return await db.user.findUnique({
|
||||||
where: { id: session.user.id }
|
where: { id: session.user.id },
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
|
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
|
||||||
@@ -33,32 +33,32 @@ Within a single request, multiple calls to `getCurrentUser()` execute the query
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const getUser = cache(async (params: { uid: number }) => {
|
const getUser = cache(async (params: { uid: number }) => {
|
||||||
return await db.user.findUnique({ where: { id: params.uid } })
|
return await db.user.findUnique({ where: { id: params.uid } });
|
||||||
})
|
});
|
||||||
|
|
||||||
// Each call creates new object, never hits cache
|
// Each call creates new object, never hits cache
|
||||||
getUser({ uid: 1 })
|
getUser({ uid: 1 });
|
||||||
getUser({ uid: 1 }) // Cache miss, runs query again
|
getUser({ uid: 1 }); // Cache miss, runs query again
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (cache hit):**
|
**Correct (cache hit):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const getUser = cache(async (uid: number) => {
|
const getUser = cache(async (uid: number) => {
|
||||||
return await db.user.findUnique({ where: { id: uid } })
|
return await db.user.findUnique({ where: { id: uid } });
|
||||||
})
|
});
|
||||||
|
|
||||||
// Primitive args use value equality
|
// Primitive args use value equality
|
||||||
getUser(1)
|
getUser(1);
|
||||||
getUser(1) // Cache hit, returns cached result
|
getUser(1); // Cache hit, returns cached result
|
||||||
```
|
```
|
||||||
|
|
||||||
If you must pass objects, pass the same reference:
|
If you must pass objects, pass the same reference:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const params = { uid: 1 }
|
const params = { uid: 1 };
|
||||||
getUser(params) // Query runs
|
getUser(params); // Query runs
|
||||||
getUser(params) // Cache hit (same reference)
|
getUser(params); // Cache hit (same reference)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Next.js-Specific Note:**
|
**Next.js-Specific Note:**
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ RSC→client serialization deduplicates by object reference, not value. Same ref
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// RSC: send once
|
// RSC: send once
|
||||||
<ClientList usernames={usernames} />
|
<ClientList usernames={usernames} />;
|
||||||
|
|
||||||
// Client: transform there
|
// Client: transform there
|
||||||
'use client'
|
('use client');
|
||||||
const sorted = useMemo(() => [...usernames].sort(), [usernames])
|
const sorted = useMemo(() => [...usernames].sort(), [usernames]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Nested deduplication behavior:**
|
**Nested deduplication behavior:**
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ React Server Components execute sequentially within a tree. Restructure with com
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const header = await fetchHeader()
|
const header = await fetchHeader();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>{header}</div>
|
<div>{header}</div>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@ async function Sidebar() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Header() {
|
async function Header() {
|
||||||
const data = await fetchHeader()
|
const data = await fetchHeader();
|
||||||
return <div>{data}</div>
|
return <div>{data}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -47,7 +47,7 @@ export default function Page() {
|
|||||||
<Header />
|
<Header />
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,13 +55,13 @@ export default function Page() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Header() {
|
async function Header() {
|
||||||
const data = await fetchHeader()
|
const data = await fetchHeader();
|
||||||
return <div>{data}</div>
|
return <div>{data}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Layout({ children }: { children: ReactNode }) {
|
function Layout({ children }: { children: ReactNode }) {
|
||||||
@@ -70,7 +70,7 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -78,6 +78,6 @@ export default function Page() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -13,13 +13,13 @@ The React Server/Client boundary serializes all object properties into strings a
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const user = await fetchUser() // 50 fields
|
const user = await fetchUser(); // 50 fields
|
||||||
return <Profile user={user} />
|
return <Profile user={user} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
'use client'
|
('use client');
|
||||||
function Profile({ user }: { user: User }) {
|
function Profile({ user }: { user: User }) {
|
||||||
return <div>{user.name}</div> // uses 1 field
|
return <div>{user.name}</div>; // uses 1 field
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,12 +27,12 @@ function Profile({ user }: { user: User }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const user = await fetchUser()
|
const user = await fetchUser();
|
||||||
return <Profile name={user.name} />
|
return <Profile name={user.name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
'use client'
|
('use client');
|
||||||
function Profile({ name }: { name: string }) {
|
function Profile({ name }: { name: string }) {
|
||||||
return <div>{name}</div>
|
return <div>{name}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -3,7 +3,7 @@ name: web-design-guidelines
|
|||||||
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
|
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
|
||||||
metadata:
|
metadata:
|
||||||
author: vercel
|
author: vercel
|
||||||
version: "1.0.0"
|
version: '1.0.0'
|
||||||
argument-hint: <file-or-pattern>
|
argument-hint: <file-or-pattern>
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ Use WebFetch to retrieve the latest rules. The fetched content contains all the
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
When a user provides a file or pattern argument:
|
When a user provides a file or pattern argument:
|
||||||
|
|
||||||
1. Fetch guidelines from the source URL above
|
1. Fetch guidelines from the source URL above
|
||||||
2. Read the specified files
|
2. Read the specified files
|
||||||
3. Apply all rules from the fetched guidelines
|
3. Apply all rules from the fetched guidelines
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ Implement tasks from an OpenSpec change.
|
|||||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||||
|
|
||||||
2. **Check status to understand the schema**
|
2. **Check status to understand the schema**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand:
|
Parse the JSON to understand:
|
||||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||||
@@ -132,6 +134,7 @@ What would you like to do?
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Keep going through tasks until done or blocked
|
- Keep going through tasks until done or blocked
|
||||||
- Always read context files before starting (from the apply instructions output)
|
- Always read context files before starting (from the apply instructions output)
|
||||||
- If task is ambiguous, pause and ask before implementing
|
- If task is ambiguous, pause and ask before implementing
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ Archive a completed change in the experimental workflow.
|
|||||||
5. **Perform the archive**
|
5. **Perform the archive**
|
||||||
|
|
||||||
Create the archive directory if it doesn't exist:
|
Create the archive directory if it doesn't exist:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p openspec/changes/archive
|
mkdir -p openspec/changes/archive
|
||||||
```
|
```
|
||||||
@@ -145,6 +146,7 @@ Target archive directory already exists.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Always prompt for change selection if not provided
|
- Always prompt for change selection if not provided
|
||||||
- Use artifact graph (openspec status --json) for completion checking
|
- Use artifact graph (openspec status --json) for completion checking
|
||||||
- Don't block archive on warnings - just inform and confirm
|
- Don't block archive on warnings - just inform and confirm
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
For each selected change, collect:
|
For each selected change, collect:
|
||||||
|
|
||||||
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
||||||
- Parse `schemaName` and `artifacts` list
|
- Parse `schemaName` and `artifacts` list
|
||||||
- Note which artifacts are `done` vs other states
|
- Note which artifacts are `done` vs other states
|
||||||
|
|
||||||
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
||||||
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||||
- If no tasks file exists, note as "No tasks"
|
- If no tasks file exists, note as "No tasks"
|
||||||
|
|
||||||
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
||||||
- List which capability specs exist
|
- List which capability specs exist
|
||||||
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
||||||
|
|
||||||
4. **Detect spec conflicts**
|
4. **Detect spec conflicts**
|
||||||
|
|
||||||
@@ -59,18 +59,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||||
|
|
||||||
b. **Search the codebase** for implementation evidence:
|
b. **Search the codebase** for implementation evidence:
|
||||||
- Look for code implementing requirements from each delta spec
|
- Look for code implementing requirements from each delta spec
|
||||||
- Check for related files, functions, or tests
|
- Check for related files, functions, or tests
|
||||||
|
|
||||||
c. **Determine resolution**:
|
c. **Determine resolution**:
|
||||||
- If only one change is actually implemented -> sync that one's specs
|
- If only one change is actually implemented -> sync that one's specs
|
||||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||||
- If neither implemented -> skip spec sync, warn user
|
- If neither implemented -> skip spec sync, warn user
|
||||||
|
|
||||||
d. **Record resolution** for each conflict:
|
d. **Record resolution** for each conflict:
|
||||||
- Which change's specs to apply
|
- Which change's specs to apply
|
||||||
- In what order (if both)
|
- In what order (if both)
|
||||||
- Rationale (what was found in codebase)
|
- Rationale (what was found in codebase)
|
||||||
|
|
||||||
6. **Show consolidated status table**
|
6. **Show consolidated status table**
|
||||||
|
|
||||||
@@ -86,12 +86,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
```
|
```
|
||||||
|
|
||||||
For conflicts, show the resolution:
|
For conflicts, show the resolution:
|
||||||
|
|
||||||
```
|
```
|
||||||
* Conflict resolution:
|
* Conflict resolution:
|
||||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||||
```
|
```
|
||||||
|
|
||||||
For incomplete changes, show warnings:
|
For incomplete changes, show warnings:
|
||||||
|
|
||||||
```
|
```
|
||||||
Warnings:
|
Warnings:
|
||||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||||
@@ -100,7 +102,6 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
7. **Confirm batch operation**
|
7. **Confirm batch operation**
|
||||||
|
|
||||||
Use **AskUserQuestion tool** with a single confirmation:
|
Use **AskUserQuestion tool** with a single confirmation:
|
||||||
|
|
||||||
- "Archive N changes?" with options based on status
|
- "Archive N changes?" with options based on status
|
||||||
- Options might include:
|
- Options might include:
|
||||||
- "Archive all N changes"
|
- "Archive all N changes"
|
||||||
@@ -114,20 +115,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
Process changes in the determined order (respecting conflict resolution):
|
Process changes in the determined order (respecting conflict resolution):
|
||||||
|
|
||||||
a. **Sync specs** if delta specs exist:
|
a. **Sync specs** if delta specs exist:
|
||||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||||
- For conflicts, apply in resolved order
|
- For conflicts, apply in resolved order
|
||||||
- Track if sync was done
|
- Track if sync was done
|
||||||
|
|
||||||
b. **Perform the archive**:
|
b. **Perform the archive**:
|
||||||
```bash
|
|
||||||
mkdir -p openspec/changes/archive
|
```bash
|
||||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
mkdir -p openspec/changes/archive
|
||||||
```
|
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||||
|
```
|
||||||
|
|
||||||
c. **Track outcome** for each change:
|
c. **Track outcome** for each change:
|
||||||
- Success: archived successfully
|
- Success: archived successfully
|
||||||
- Failed: error during archive (record error)
|
- Failed: error during archive (record error)
|
||||||
- Skipped: user chose not to archive (if applicable)
|
- Skipped: user chose not to archive (if applicable)
|
||||||
|
|
||||||
9. **Display summary**
|
9. **Display summary**
|
||||||
|
|
||||||
@@ -150,6 +152,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
```
|
```
|
||||||
|
|
||||||
If any failures:
|
If any failures:
|
||||||
|
|
||||||
```
|
```
|
||||||
Failed 1 change:
|
Failed 1 change:
|
||||||
- some-change: Archive directory already exists
|
- some-change: Archive directory already exists
|
||||||
@@ -158,6 +161,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
**Conflict Resolution Examples**
|
**Conflict Resolution Examples**
|
||||||
|
|
||||||
Example 1: Only one implemented
|
Example 1: Only one implemented
|
||||||
|
|
||||||
```
|
```
|
||||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||||
|
|
||||||
@@ -173,6 +177,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Example 2: Both implemented
|
Example 2: Both implemented
|
||||||
|
|
||||||
```
|
```
|
||||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||||
|
|
||||||
@@ -226,6 +231,7 @@ No active changes found. Use `/opsx:new` to create a new change.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||||
- Always prompt for selection, never auto-select
|
- Always prompt for selection, never auto-select
|
||||||
- Detect spec conflicts early and resolve by checking codebase
|
- Detect spec conflicts early and resolve by checking codebase
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ Continue working on a change by creating the next artifact.
|
|||||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
2. **Check current status**
|
2. **Check current status**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand current state. The response includes:
|
Parse the JSON to understand current state. The response includes:
|
||||||
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
||||||
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||||
@@ -33,7 +35,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
|
|
||||||
3. **Act based on status**:
|
3. **Act based on status**:
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If all artifacts are complete (`isComplete: true`)**:
|
**If all artifacts are complete (`isComplete: true`)**:
|
||||||
- Congratulate the user
|
- Congratulate the user
|
||||||
@@ -41,7 +43,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
|
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
|
||||||
- STOP
|
- STOP
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||||
- Pick the FIRST artifact with `status: "ready"` from the status output
|
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||||
@@ -64,7 +66,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
- Show what was created and what's now unlocked
|
- Show what was created and what's now unlocked
|
||||||
- STOP after creating ONE artifact
|
- STOP after creating ONE artifact
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If no artifacts are ready (all blocked)**:
|
**If no artifacts are ready (all blocked)**:
|
||||||
- This shouldn't happen with a valid schema
|
- This shouldn't happen with a valid schema
|
||||||
@@ -78,6 +80,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After each invocation, show:
|
After each invocation, show:
|
||||||
|
|
||||||
- Which artifact was created
|
- Which artifact was created
|
||||||
- Schema workflow being used
|
- Schema workflow being used
|
||||||
- Current progress (N/M complete)
|
- Current progress (N/M complete)
|
||||||
@@ -91,6 +94,7 @@ The artifact types and their purpose depend on the schema. Use the `instruction`
|
|||||||
Common artifact patterns:
|
Common artifact patterns:
|
||||||
|
|
||||||
**spec-driven schema** (proposal → specs → design → tasks):
|
**spec-driven schema** (proposal → specs → design → tasks):
|
||||||
|
|
||||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||||
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
||||||
@@ -100,6 +104,7 @@ Common artifact patterns:
|
|||||||
For other schemas, follow the `instruction` field from the CLI output.
|
For other schemas, follow the `instruction` field from the CLI output.
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Create ONE artifact per invocation
|
- Create ONE artifact per invocation
|
||||||
- Always read dependency artifacts before creating a new one
|
- Always read dependency artifacts before creating a new one
|
||||||
- Never skip artifacts or create out of order
|
- Never skip artifacts or create out of order
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
|
|||||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||||
|
|
||||||
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
||||||
|
|
||||||
- A vague idea: "real-time collaboration"
|
- A vague idea: "real-time collaboration"
|
||||||
- A specific problem: "the auth system is getting unwieldy"
|
- A specific problem: "the auth system is getting unwieldy"
|
||||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
- A change name: "add-dark-mode" (to explore in context of that change)
|
||||||
@@ -33,24 +34,28 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
|
|||||||
Depending on what the user brings, you might:
|
Depending on what the user brings, you might:
|
||||||
|
|
||||||
**Explore the problem space**
|
**Explore the problem space**
|
||||||
|
|
||||||
- Ask clarifying questions that emerge from what they said
|
- Ask clarifying questions that emerge from what they said
|
||||||
- Challenge assumptions
|
- Challenge assumptions
|
||||||
- Reframe the problem
|
- Reframe the problem
|
||||||
- Find analogies
|
- Find analogies
|
||||||
|
|
||||||
**Investigate the codebase**
|
**Investigate the codebase**
|
||||||
|
|
||||||
- Map existing architecture relevant to the discussion
|
- Map existing architecture relevant to the discussion
|
||||||
- Find integration points
|
- Find integration points
|
||||||
- Identify patterns already in use
|
- Identify patterns already in use
|
||||||
- Surface hidden complexity
|
- Surface hidden complexity
|
||||||
|
|
||||||
**Compare options**
|
**Compare options**
|
||||||
|
|
||||||
- Brainstorm multiple approaches
|
- Brainstorm multiple approaches
|
||||||
- Build comparison tables
|
- Build comparison tables
|
||||||
- Sketch tradeoffs
|
- Sketch tradeoffs
|
||||||
- Recommend a path (if asked)
|
- Recommend a path (if asked)
|
||||||
|
|
||||||
**Visualize**
|
**Visualize**
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ Use ASCII diagrams liberally │
|
│ Use ASCII diagrams liberally │
|
||||||
@@ -69,6 +74,7 @@ Depending on what the user brings, you might:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Surface risks and unknowns**
|
**Surface risks and unknowns**
|
||||||
|
|
||||||
- Identify what could go wrong
|
- Identify what could go wrong
|
||||||
- Find gaps in understanding
|
- Find gaps in understanding
|
||||||
- Suggest spikes or investigations
|
- Suggest spikes or investigations
|
||||||
@@ -82,11 +88,13 @@ You have full context of the OpenSpec system. Use it naturally, don't force it.
|
|||||||
### Check for context
|
### Check for context
|
||||||
|
|
||||||
At the start, quickly check what exists:
|
At the start, quickly check what exists:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec list --json
|
openspec list --json
|
||||||
```
|
```
|
||||||
|
|
||||||
This tells you:
|
This tells you:
|
||||||
|
|
||||||
- If there are active changes
|
- If there are active changes
|
||||||
- Their names, schemas, and status
|
- Their names, schemas, and status
|
||||||
- What the user might be working on
|
- What the user might be working on
|
||||||
@@ -117,14 +125,14 @@ If the user mentions a change or you detect one is relevant:
|
|||||||
|
|
||||||
3. **Offer to capture when decisions are made**
|
3. **Offer to capture when decisions are made**
|
||||||
|
|
||||||
| Insight Type | Where to Capture |
|
| Insight Type | Where to Capture |
|
||||||
|--------------|------------------|
|
| -------------------------- | ---------------------------- |
|
||||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||||
| Requirement changed | `specs/<capability>/spec.md` |
|
| Requirement changed | `specs/<capability>/spec.md` |
|
||||||
| Design decision made | `design.md` |
|
| Design decision made | `design.md` |
|
||||||
| Scope changed | `proposal.md` |
|
| Scope changed | `proposal.md` |
|
||||||
| New work identified | `tasks.md` |
|
| New work identified | `tasks.md` |
|
||||||
| Assumption invalidated | Relevant artifact |
|
| Assumption invalidated | Relevant artifact |
|
||||||
|
|
||||||
Example offers:
|
Example offers:
|
||||||
- "That's a design decision. Capture it in design.md?"
|
- "That's a design decision. Capture it in design.md?"
|
||||||
|
|||||||
+27
-20
@@ -11,6 +11,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
1. **If no input provided, ask what they want to build**
|
1. **If no input provided, ask what they want to build**
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
@@ -18,15 +19,19 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||||
|
|
||||||
2. **Create the change directory**
|
2. **Create the change directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<name>"
|
openspec new change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/`.
|
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||||
|
|
||||||
3. **Get the artifact build order**
|
3. **Get the artifact build order**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to get:
|
Parse the JSON to get:
|
||||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||||
- `artifacts`: list of all artifacts with their status and dependencies
|
- `artifacts`: list of all artifacts with their status and dependencies
|
||||||
@@ -38,30 +43,30 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||||
|
|
||||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||||
- Get instructions:
|
- Get instructions:
|
||||||
```bash
|
```bash
|
||||||
openspec instructions <artifact-id> --change "<name>" --json
|
openspec instructions <artifact-id> --change "<name>" --json
|
||||||
```
|
```
|
||||||
- The instructions JSON includes:
|
- The instructions JSON includes:
|
||||||
- `context`: Project background (constraints for you - do NOT include in output)
|
- `context`: Project background (constraints for you - do NOT include in output)
|
||||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||||
- `template`: The structure to use for your output file
|
- `template`: The structure to use for your output file
|
||||||
- `instruction`: Schema-specific guidance for this artifact type
|
- `instruction`: Schema-specific guidance for this artifact type
|
||||||
- `outputPath`: Where to write the artifact
|
- `outputPath`: Where to write the artifact
|
||||||
- `dependencies`: Completed artifacts to read for context
|
- `dependencies`: Completed artifacts to read for context
|
||||||
- Read any completed dependency files for context
|
- Read any completed dependency files for context
|
||||||
- Create the artifact file using `template` as the structure
|
- Create the artifact file using `template` as the structure
|
||||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||||
- Show brief progress: "✓ Created <artifact-id>"
|
- Show brief progress: "✓ Created <artifact-id>"
|
||||||
|
|
||||||
b. **Continue until all `applyRequires` artifacts are complete**
|
b. **Continue until all `applyRequires` artifacts are complete**
|
||||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||||
- Stop when all `applyRequires` artifacts are done
|
- Stop when all `applyRequires` artifacts are done
|
||||||
|
|
||||||
c. **If an artifact requires user input** (unclear context):
|
c. **If an artifact requires user input** (unclear context):
|
||||||
- Use **AskUserQuestion tool** to clarify
|
- Use **AskUserQuestion tool** to clarify
|
||||||
- Then continue with creation
|
- Then continue with creation
|
||||||
|
|
||||||
5. **Show final status**
|
5. **Show final status**
|
||||||
```bash
|
```bash
|
||||||
@@ -71,6 +76,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After completing all artifacts, summarize:
|
After completing all artifacts, summarize:
|
||||||
|
|
||||||
- Change name and location
|
- Change name and location
|
||||||
- List of artifacts created with brief descriptions
|
- List of artifacts created with brief descriptions
|
||||||
- What's ready: "All artifacts created! Ready for implementation."
|
- What's ready: "All artifacts created! Ready for implementation."
|
||||||
@@ -84,6 +90,7 @@ After completing all artifacts, summarize:
|
|||||||
- Use the `template` as a starting point, filling in based on context
|
- Use the `template` as a starting point, filling in based on context
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||||
- Always read dependency artifacts before creating a new one
|
- Always read dependency artifacts before creating a new one
|
||||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
1. **If no input provided, ask what they want to build**
|
1. **If no input provided, ask what they want to build**
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
@@ -28,23 +29,29 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
**Otherwise**: Omit `--schema` to use the default.
|
**Otherwise**: Omit `--schema` to use the default.
|
||||||
|
|
||||||
3. **Create the change directory**
|
3. **Create the change directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<name>"
|
openspec new change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `--schema <name>` only if the user requested a specific workflow.
|
Add `--schema <name>` only if the user requested a specific workflow.
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
||||||
|
|
||||||
4. **Show the artifact status**
|
4. **Show the artifact status**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>"
|
openspec status --change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||||
|
|
||||||
5. **Get instructions for the first artifact**
|
5. **Get instructions for the first artifact**
|
||||||
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
|
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec instructions <first-artifact-id> --change "<name>"
|
openspec instructions <first-artifact-id> --change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This outputs the template and context for creating the first artifact.
|
This outputs the template and context for creating the first artifact.
|
||||||
|
|
||||||
6. **STOP and wait for user direction**
|
6. **STOP and wait for user direction**
|
||||||
@@ -52,6 +59,7 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After completing the steps, summarize:
|
After completing the steps, summarize:
|
||||||
|
|
||||||
- Change name and location
|
- Change name and location
|
||||||
- Schema/workflow being used and its artifact sequence
|
- Schema/workflow being used and its artifact sequence
|
||||||
- Current status (0/N artifacts complete)
|
- Current status (0/N artifacts complete)
|
||||||
@@ -59,6 +67,7 @@ After completing the steps, summarize:
|
|||||||
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
|
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Do NOT create any artifacts yet - just show the instructions
|
- Do NOT create any artifacts yet - just show the instructions
|
||||||
- Do NOT advance beyond showing the first artifact template
|
- Do NOT advance beyond showing the first artifact template
|
||||||
- If the name is invalid (not kebab-case), ask for a valid name
|
- If the name is invalid (not kebab-case), ask for a valid name
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ openspec status --json 2>&1 || echo "NOT_INITIALIZED"
|
|||||||
```
|
```
|
||||||
|
|
||||||
**If not initialized:**
|
**If not initialized:**
|
||||||
|
|
||||||
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
|
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
|
||||||
|
|
||||||
Stop here if not initialized.
|
Stop here if not initialized.
|
||||||
@@ -59,6 +60,7 @@ Scan the codebase for small improvement opportunities. Look for:
|
|||||||
6. **Missing validation** - User input handlers without validation
|
6. **Missing validation** - User input handlers without validation
|
||||||
|
|
||||||
Also check recent git activity:
|
Also check recent git activity:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git log --oneline -10 2>/dev/null || echo "No git history"
|
git log --oneline -10 2>/dev/null || echo "No git history"
|
||||||
```
|
```
|
||||||
@@ -94,6 +96,7 @@ Which task interests you? (Pick a number or describe your own)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**If nothing found:** Fall back to asking what the user wants to build:
|
**If nothing found:** Fall back to asking what the user wants to build:
|
||||||
|
|
||||||
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
||||||
|
|
||||||
### Scope Guardrail
|
### Scope Guardrail
|
||||||
@@ -126,6 +129,7 @@ Before we create a change, let me quickly show you **explore mode**—it's how y
|
|||||||
```
|
```
|
||||||
|
|
||||||
Spend 1-2 minutes investigating the relevant code:
|
Spend 1-2 minutes investigating the relevant code:
|
||||||
|
|
||||||
- Read the file(s) involved
|
- Read the file(s) involved
|
||||||
- Draw a quick ASCII diagram if it helps
|
- Draw a quick ASCII diagram if it helps
|
||||||
- Note any considerations
|
- Note any considerations
|
||||||
@@ -151,6 +155,7 @@ Now let's create a change to hold our work.
|
|||||||
## Phase 4: Create the Change
|
## Phase 4: Create the Change
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Creating a Change
|
## Creating a Change
|
||||||
|
|
||||||
@@ -160,21 +165,25 @@ Let me create one for our task.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:** Create the change with a derived kebab-case name:
|
**DO:** Create the change with a derived kebab-case name:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<derived-name>"
|
openspec new change "<derived-name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
**SHOW:**
|
**SHOW:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Created: `openspec/changes/<name>/`
|
Created: `openspec/changes/<name>/`
|
||||||
|
|
||||||
The folder structure:
|
The folder structure:
|
||||||
```
|
```
|
||||||
|
|
||||||
openspec/changes/<name>/
|
openspec/changes/<name>/
|
||||||
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
||||||
├── design.md ← How we'll build it (empty)
|
├── design.md ← How we'll build it (empty)
|
||||||
├── specs/ ← Detailed requirements (empty)
|
├── specs/ ← Detailed requirements (empty)
|
||||||
└── tasks.md ← Implementation checklist (empty)
|
└── tasks.md ← Implementation checklist (empty)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's fill in the first artifact—the proposal.
|
Now let's fill in the first artifact—the proposal.
|
||||||
@@ -185,6 +194,7 @@ Now let's fill in the first artifact—the proposal.
|
|||||||
## Phase 5: Proposal
|
## Phase 5: Proposal
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## The Proposal
|
## The Proposal
|
||||||
|
|
||||||
@@ -229,9 +239,11 @@ Does this capture the intent? I can adjust before we save it.
|
|||||||
**PAUSE** - Wait for user approval/feedback.
|
**PAUSE** - Wait for user approval/feedback.
|
||||||
|
|
||||||
After approval, save the proposal:
|
After approval, save the proposal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec instructions proposal --change "<name>" --json
|
openspec instructions proposal --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Then write the content to `openspec/changes/<name>/proposal.md`.
|
Then write the content to `openspec/changes/<name>/proposal.md`.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -245,6 +257,7 @@ Next up: specs.
|
|||||||
## Phase 6: Specs
|
## Phase 6: Specs
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Specs
|
## Specs
|
||||||
|
|
||||||
@@ -254,6 +267,7 @@ For a small task like this, we might only need one spec file.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:** Create the spec file:
|
**DO:** Create the spec file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
||||||
```
|
```
|
||||||
@@ -289,6 +303,7 @@ Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
|
|||||||
## Phase 7: Design
|
## Phase 7: Design
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
@@ -334,6 +349,7 @@ Save to `openspec/changes/<name>/design.md`.
|
|||||||
## Phase 8: Tasks
|
## Phase 8: Tasks
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@@ -372,6 +388,7 @@ Save to `openspec/changes/<name>/tasks.md`.
|
|||||||
## Phase 9: Apply (Implementation)
|
## Phase 9: Apply (Implementation)
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
@@ -406,6 +423,7 @@ The change is implemented! One more step—let's archive it.
|
|||||||
## Phase 10: Archive
|
## Phase 10: Archive
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Archiving
|
## Archiving
|
||||||
|
|
||||||
@@ -415,11 +433,13 @@ Archived changes become your project's decision history—you can always find th
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:**
|
**DO:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec archive "<name>"
|
openspec archive "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
**SHOW:**
|
**SHOW:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
||||||
|
|
||||||
|
|||||||
@@ -40,28 +40,28 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|||||||
|
|
||||||
c. **Apply changes intelligently**:
|
c. **Apply changes intelligently**:
|
||||||
|
|
||||||
**ADDED Requirements:**
|
**ADDED Requirements:**
|
||||||
- If requirement doesn't exist in main spec → add it
|
- If requirement doesn't exist in main spec → add it
|
||||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||||
|
|
||||||
**MODIFIED Requirements:**
|
**MODIFIED Requirements:**
|
||||||
- Find the requirement in main spec
|
- Find the requirement in main spec
|
||||||
- Apply the changes - this can be:
|
- Apply the changes - this can be:
|
||||||
- Adding new scenarios (don't need to copy existing ones)
|
- Adding new scenarios (don't need to copy existing ones)
|
||||||
- Modifying existing scenarios
|
- Modifying existing scenarios
|
||||||
- Changing the requirement description
|
- Changing the requirement description
|
||||||
- Preserve scenarios/content not mentioned in the delta
|
- Preserve scenarios/content not mentioned in the delta
|
||||||
|
|
||||||
**REMOVED Requirements:**
|
**REMOVED Requirements:**
|
||||||
- Remove the entire requirement block from main spec
|
- Remove the entire requirement block from main spec
|
||||||
|
|
||||||
**RENAMED Requirements:**
|
**RENAMED Requirements:**
|
||||||
- Find the FROM requirement, rename to TO
|
- Find the FROM requirement, rename to TO
|
||||||
|
|
||||||
d. **Create new main spec** if capability doesn't exist yet:
|
d. **Create new main spec** if capability doesn't exist yet:
|
||||||
- Create `openspec/specs/<capability>/spec.md`
|
- Create `openspec/specs/<capability>/spec.md`
|
||||||
- Add Purpose section (can be brief, mark as TBD)
|
- Add Purpose section (can be brief, mark as TBD)
|
||||||
- Add Requirements section with the ADDED requirements
|
- Add Requirements section with the ADDED requirements
|
||||||
|
|
||||||
4. **Show summary**
|
4. **Show summary**
|
||||||
|
|
||||||
@@ -75,16 +75,20 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|||||||
## ADDED Requirements
|
## ADDED Requirements
|
||||||
|
|
||||||
### Requirement: New Feature
|
### Requirement: New Feature
|
||||||
|
|
||||||
The system SHALL do something new.
|
The system SHALL do something new.
|
||||||
|
|
||||||
#### Scenario: Basic case
|
#### Scenario: Basic case
|
||||||
|
|
||||||
- **WHEN** user does X
|
- **WHEN** user does X
|
||||||
- **THEN** system does Y
|
- **THEN** system does Y
|
||||||
|
|
||||||
## MODIFIED Requirements
|
## MODIFIED Requirements
|
||||||
|
|
||||||
### Requirement: Existing Feature
|
### Requirement: Existing Feature
|
||||||
|
|
||||||
#### Scenario: New scenario to add
|
#### Scenario: New scenario to add
|
||||||
|
|
||||||
- **WHEN** user does A
|
- **WHEN** user does A
|
||||||
- **THEN** system does B
|
- **THEN** system does B
|
||||||
|
|
||||||
@@ -101,8 +105,9 @@ The system SHALL do something new.
|
|||||||
**Key Principle: Intelligent Merging**
|
**Key Principle: Intelligent Merging**
|
||||||
|
|
||||||
Unlike programmatic merging, you can apply **partial updates**:
|
Unlike programmatic merging, you can apply **partial updates**:
|
||||||
|
|
||||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||||
- The delta represents *intent*, not a wholesale replacement
|
- The delta represents _intent_, not a wholesale replacement
|
||||||
- Use your judgment to merge changes sensibly
|
- Use your judgment to merge changes sensibly
|
||||||
|
|
||||||
**Output On Success**
|
**Output On Success**
|
||||||
@@ -124,6 +129,7 @@ Main specs are now updated. The change remains active - archive when implementat
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Read both delta and main specs before making changes
|
- Read both delta and main specs before making changes
|
||||||
- Preserve existing content not mentioned in delta
|
- Preserve existing content not mentioned in delta
|
||||||
- If something is unclear, ask for clarification
|
- If something is unclear, ask for clarification
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
2. **Check status to understand the schema**
|
2. **Check status to understand the schema**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand:
|
Parse the JSON to understand:
|
||||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||||
- Which artifacts exist for this change
|
- Which artifacts exist for this change
|
||||||
@@ -103,6 +105,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
8. **Generate Verification Report**
|
8. **Generate Verification Report**
|
||||||
|
|
||||||
**Summary Scorecard**:
|
**Summary Scorecard**:
|
||||||
|
|
||||||
```
|
```
|
||||||
## Verification Report: <change-name>
|
## Verification Report: <change-name>
|
||||||
|
|
||||||
@@ -115,7 +118,6 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Issues by Priority**:
|
**Issues by Priority**:
|
||||||
|
|
||||||
1. **CRITICAL** (Must fix before archive):
|
1. **CRITICAL** (Must fix before archive):
|
||||||
- Incomplete tasks
|
- Incomplete tasks
|
||||||
- Missing requirement implementations
|
- Missing requirement implementations
|
||||||
@@ -154,6 +156,7 @@ Verify that an implementation matches the change artifacts (specs, tasks, design
|
|||||||
**Output Format**
|
**Output Format**
|
||||||
|
|
||||||
Use clear markdown with:
|
Use clear markdown with:
|
||||||
|
|
||||||
- Table for summary scorecard
|
- Table for summary scorecard
|
||||||
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
- Grouped lists for issues (CRITICAL/WARNING/SUGGESTION)
|
||||||
- Code references in format: `file.ts:123`
|
- Code references in format: `file.ts:123`
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# lodash-es-runtime-guards
|
||||||
|
|
||||||
|
Replace runtime `typeof` checks and ad-hoc empty-string checks with `lodash-es` predicates.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- You see runtime checks like `typeof value === 'string'`.
|
||||||
|
- You see object guards like `value && typeof value === 'object'`.
|
||||||
|
- You see emptiness checks like `value.trim() !== ''` or `value.trim() === ''`.
|
||||||
|
- You want consistent guard style across app, server, IPC, and tests.
|
||||||
|
|
||||||
|
## Do Not Replace
|
||||||
|
|
||||||
|
- Type-level `typeof` (TypeScript only), for example:
|
||||||
|
- `ReturnType<typeof fn>`
|
||||||
|
- `keyof typeof CONST_MAP`
|
||||||
|
- `z.infer<typeof Schema>`
|
||||||
|
|
||||||
|
These are compile-time types and must remain as-is.
|
||||||
|
|
||||||
|
## Guard Mapping
|
||||||
|
|
||||||
|
- `typeof x === 'string'` -> `isString(x)`
|
||||||
|
- `typeof x !== 'string'` -> `!isString(x)`
|
||||||
|
- `typeof x === 'number'` -> `isNumber(x)`
|
||||||
|
- `typeof x === 'boolean'` -> `isBoolean(x)`
|
||||||
|
- `typeof x === 'function'` -> `isFunction(x)`
|
||||||
|
- `typeof x === 'undefined'` -> `isUndefined(x)`
|
||||||
|
- `x && typeof x === 'object'` -> `isObjectLike(x)`
|
||||||
|
- `typeof x === 'object' && x !== null` -> `isObjectLike(x)`
|
||||||
|
- `typeof x === 'object' && !Array.isArray(x)` -> `isObjectLike(x) && !isArray(x)`
|
||||||
|
|
||||||
|
## String Emptiness Pattern
|
||||||
|
|
||||||
|
- Prefer:
|
||||||
|
- `isEmpty(value.trim())`
|
||||||
|
- Instead of:
|
||||||
|
- `value.trim() === ''`
|
||||||
|
- `value.trim() !== ''`
|
||||||
|
- `value.trim().length === 0`
|
||||||
|
|
||||||
|
## Import Style
|
||||||
|
|
||||||
|
- Use named imports from `lodash-es`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { isString, isNumber, isObjectLike, isEmpty } from 'lodash-es';
|
||||||
|
```
|
||||||
|
|
||||||
|
- Do not use full-package imports.
|
||||||
|
- Reuse existing imports in file; avoid duplicate imports.
|
||||||
|
|
||||||
|
## Refactor Checklist
|
||||||
|
|
||||||
|
1. Replace runtime checks first, preserving behavior.
|
||||||
|
2. Keep array/object branching semantics unchanged.
|
||||||
|
3. Re-scan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "typeof\s+[^\n]+(?:===|!==)\s*'" src
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Confirm only type-level `typeof` remains:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "\btypeof\b" src
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run type-check
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Apply"
|
name: 'OPSX: Apply'
|
||||||
description: Implement tasks from an OpenSpec change (Experimental)
|
description: Implement tasks from an OpenSpec change (Experimental)
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, artifacts, experimental]
|
tags: [workflow, artifacts, experimental]
|
||||||
@@ -21,9 +21,11 @@ Implement tasks from an OpenSpec change.
|
|||||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||||
|
|
||||||
2. **Check status to understand the schema**
|
2. **Check status to understand the schema**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand:
|
Parse the JSON to understand:
|
||||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||||
@@ -135,6 +137,7 @@ What would you like to do?
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Keep going through tasks until done or blocked
|
- Keep going through tasks until done or blocked
|
||||||
- Always read context files before starting (from the apply instructions output)
|
- Always read context files before starting (from the apply instructions output)
|
||||||
- If task is ambiguous, pause and ask before implementing
|
- If task is ambiguous, pause and ask before implementing
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Archive"
|
name: 'OPSX: Archive'
|
||||||
description: Archive a completed change in the experimental workflow
|
description: Archive a completed change in the experimental workflow
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, archive, experimental]
|
tags: [workflow, archive, experimental]
|
||||||
@@ -64,6 +64,7 @@ Archive a completed change in the experimental workflow.
|
|||||||
5. **Perform the archive**
|
5. **Perform the archive**
|
||||||
|
|
||||||
Create the archive directory if it doesn't exist:
|
Create the archive directory if it doesn't exist:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p openspec/changes/archive
|
mkdir -p openspec/changes/archive
|
||||||
```
|
```
|
||||||
@@ -148,6 +149,7 @@ Target archive directory already exists.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Always prompt for change selection if not provided
|
- Always prompt for change selection if not provided
|
||||||
- Use artifact graph (openspec status --json) for completion checking
|
- Use artifact graph (openspec status --json) for completion checking
|
||||||
- Don't block archive on warnings - just inform and confirm
|
- Don't block archive on warnings - just inform and confirm
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Bulk Archive"
|
name: 'OPSX: Bulk Archive'
|
||||||
description: Archive multiple completed changes at once
|
description: Archive multiple completed changes at once
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, archive, experimental, bulk]
|
tags: [workflow, archive, experimental, bulk]
|
||||||
@@ -33,16 +33,16 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
For each selected change, collect:
|
For each selected change, collect:
|
||||||
|
|
||||||
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
a. **Artifact status** - Run `openspec status --change "<name>" --json`
|
||||||
- Parse `schemaName` and `artifacts` list
|
- Parse `schemaName` and `artifacts` list
|
||||||
- Note which artifacts are `done` vs other states
|
- Note which artifacts are `done` vs other states
|
||||||
|
|
||||||
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
b. **Task completion** - Read `openspec/changes/<name>/tasks.md`
|
||||||
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
- Count `- [ ]` (incomplete) vs `- [x]` (complete)
|
||||||
- If no tasks file exists, note as "No tasks"
|
- If no tasks file exists, note as "No tasks"
|
||||||
|
|
||||||
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
c. **Delta specs** - Check `openspec/changes/<name>/specs/` directory
|
||||||
- List which capability specs exist
|
- List which capability specs exist
|
||||||
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
- For each, extract requirement names (lines matching `### Requirement: <name>`)
|
||||||
|
|
||||||
4. **Detect spec conflicts**
|
4. **Detect spec conflicts**
|
||||||
|
|
||||||
@@ -62,18 +62,18 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
|
||||||
|
|
||||||
b. **Search the codebase** for implementation evidence:
|
b. **Search the codebase** for implementation evidence:
|
||||||
- Look for code implementing requirements from each delta spec
|
- Look for code implementing requirements from each delta spec
|
||||||
- Check for related files, functions, or tests
|
- Check for related files, functions, or tests
|
||||||
|
|
||||||
c. **Determine resolution**:
|
c. **Determine resolution**:
|
||||||
- If only one change is actually implemented -> sync that one's specs
|
- If only one change is actually implemented -> sync that one's specs
|
||||||
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
- If both implemented -> apply in chronological order (older first, newer overwrites)
|
||||||
- If neither implemented -> skip spec sync, warn user
|
- If neither implemented -> skip spec sync, warn user
|
||||||
|
|
||||||
d. **Record resolution** for each conflict:
|
d. **Record resolution** for each conflict:
|
||||||
- Which change's specs to apply
|
- Which change's specs to apply
|
||||||
- In what order (if both)
|
- In what order (if both)
|
||||||
- Rationale (what was found in codebase)
|
- Rationale (what was found in codebase)
|
||||||
|
|
||||||
6. **Show consolidated status table**
|
6. **Show consolidated status table**
|
||||||
|
|
||||||
@@ -89,12 +89,14 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
```
|
```
|
||||||
|
|
||||||
For conflicts, show the resolution:
|
For conflicts, show the resolution:
|
||||||
|
|
||||||
```
|
```
|
||||||
* Conflict resolution:
|
* Conflict resolution:
|
||||||
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
- auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
|
||||||
```
|
```
|
||||||
|
|
||||||
For incomplete changes, show warnings:
|
For incomplete changes, show warnings:
|
||||||
|
|
||||||
```
|
```
|
||||||
Warnings:
|
Warnings:
|
||||||
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
- add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
|
||||||
@@ -103,7 +105,6 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
7. **Confirm batch operation**
|
7. **Confirm batch operation**
|
||||||
|
|
||||||
Use **AskUserQuestion tool** with a single confirmation:
|
Use **AskUserQuestion tool** with a single confirmation:
|
||||||
|
|
||||||
- "Archive N changes?" with options based on status
|
- "Archive N changes?" with options based on status
|
||||||
- Options might include:
|
- Options might include:
|
||||||
- "Archive all N changes"
|
- "Archive all N changes"
|
||||||
@@ -117,20 +118,21 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
Process changes in the determined order (respecting conflict resolution):
|
Process changes in the determined order (respecting conflict resolution):
|
||||||
|
|
||||||
a. **Sync specs** if delta specs exist:
|
a. **Sync specs** if delta specs exist:
|
||||||
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
- Use the openspec-sync-specs approach (agent-driven intelligent merge)
|
||||||
- For conflicts, apply in resolved order
|
- For conflicts, apply in resolved order
|
||||||
- Track if sync was done
|
- Track if sync was done
|
||||||
|
|
||||||
b. **Perform the archive**:
|
b. **Perform the archive**:
|
||||||
```bash
|
|
||||||
mkdir -p openspec/changes/archive
|
```bash
|
||||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
mkdir -p openspec/changes/archive
|
||||||
```
|
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||||
|
```
|
||||||
|
|
||||||
c. **Track outcome** for each change:
|
c. **Track outcome** for each change:
|
||||||
- Success: archived successfully
|
- Success: archived successfully
|
||||||
- Failed: error during archive (record error)
|
- Failed: error during archive (record error)
|
||||||
- Skipped: user chose not to archive (if applicable)
|
- Skipped: user chose not to archive (if applicable)
|
||||||
|
|
||||||
9. **Display summary**
|
9. **Display summary**
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
```
|
```
|
||||||
|
|
||||||
If any failures:
|
If any failures:
|
||||||
|
|
||||||
```
|
```
|
||||||
Failed 1 change:
|
Failed 1 change:
|
||||||
- some-change: Archive directory already exists
|
- some-change: Archive directory already exists
|
||||||
@@ -161,6 +164,7 @@ This skill allows you to batch-archive changes, handling spec conflicts intellig
|
|||||||
**Conflict Resolution Examples**
|
**Conflict Resolution Examples**
|
||||||
|
|
||||||
Example 1: Only one implemented
|
Example 1: Only one implemented
|
||||||
|
|
||||||
```
|
```
|
||||||
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
|
||||||
|
|
||||||
@@ -176,6 +180,7 @@ Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Example 2: Both implemented
|
Example 2: Both implemented
|
||||||
|
|
||||||
```
|
```
|
||||||
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
|
||||||
|
|
||||||
@@ -229,6 +234,7 @@ No active changes found. Use `/opsx:new` to create a new change.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
- Allow any number of changes (1+ is fine, 2+ is the typical use case)
|
||||||
- Always prompt for selection, never auto-select
|
- Always prompt for selection, never auto-select
|
||||||
- Detect spec conflicts early and resolve by checking codebase
|
- Detect spec conflicts early and resolve by checking codebase
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Continue"
|
name: 'OPSX: Continue'
|
||||||
description: Continue working on a change - create the next artifact (Experimental)
|
description: Continue working on a change - create the next artifact (Experimental)
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, artifacts, experimental]
|
tags: [workflow, artifacts, experimental]
|
||||||
@@ -26,9 +26,11 @@ Continue working on a change by creating the next artifact.
|
|||||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||||
|
|
||||||
2. **Check current status**
|
2. **Check current status**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to understand current state. The response includes:
|
Parse the JSON to understand current state. The response includes:
|
||||||
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
- `schemaName`: The workflow schema being used (e.g., "spec-driven")
|
||||||
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
- `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
|
||||||
@@ -36,7 +38,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
|
|
||||||
3. **Act based on status**:
|
3. **Act based on status**:
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If all artifacts are complete (`isComplete: true`)**:
|
**If all artifacts are complete (`isComplete: true`)**:
|
||||||
- Congratulate the user
|
- Congratulate the user
|
||||||
@@ -44,7 +46,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
|
- Suggest: "All artifacts created! You can now implement this change with `/opsx:apply` or archive it with `/opsx:archive`."
|
||||||
- STOP
|
- STOP
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
**If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
|
||||||
- Pick the FIRST artifact with `status: "ready"` from the status output
|
- Pick the FIRST artifact with `status: "ready"` from the status output
|
||||||
@@ -67,7 +69,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
- Show what was created and what's now unlocked
|
- Show what was created and what's now unlocked
|
||||||
- STOP after creating ONE artifact
|
- STOP after creating ONE artifact
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
**If no artifacts are ready (all blocked)**:
|
**If no artifacts are ready (all blocked)**:
|
||||||
- This shouldn't happen with a valid schema
|
- This shouldn't happen with a valid schema
|
||||||
@@ -81,6 +83,7 @@ Continue working on a change by creating the next artifact.
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After each invocation, show:
|
After each invocation, show:
|
||||||
|
|
||||||
- Which artifact was created
|
- Which artifact was created
|
||||||
- Schema workflow being used
|
- Schema workflow being used
|
||||||
- Current progress (N/M complete)
|
- Current progress (N/M complete)
|
||||||
@@ -94,6 +97,7 @@ The artifact types and their purpose depend on the schema. Use the `instruction`
|
|||||||
Common artifact patterns:
|
Common artifact patterns:
|
||||||
|
|
||||||
**spec-driven schema** (proposal → specs → design → tasks):
|
**spec-driven schema** (proposal → specs → design → tasks):
|
||||||
|
|
||||||
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
|
||||||
- The Capabilities section is critical - each capability listed will need a spec file.
|
- The Capabilities section is critical - each capability listed will need a spec file.
|
||||||
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
- **specs/<capability>/spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
|
||||||
@@ -103,6 +107,7 @@ Common artifact patterns:
|
|||||||
For other schemas, follow the `instruction` field from the CLI output.
|
For other schemas, follow the `instruction` field from the CLI output.
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Create ONE artifact per invocation
|
- Create ONE artifact per invocation
|
||||||
- Always read dependency artifacts before creating a new one
|
- Always read dependency artifacts before creating a new one
|
||||||
- Never skip artifacts or create out of order
|
- Never skip artifacts or create out of order
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Explore"
|
name: 'OPSX: Explore'
|
||||||
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
|
description: 'Enter explore mode - think through ideas, investigate problems, clarify requirements'
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, explore, experimental, thinking]
|
tags: [workflow, explore, experimental, thinking]
|
||||||
---
|
---
|
||||||
@@ -12,6 +12,7 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
|
|||||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||||
|
|
||||||
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
||||||
|
|
||||||
- A vague idea: "real-time collaboration"
|
- A vague idea: "real-time collaboration"
|
||||||
- A specific problem: "the auth system is getting unwieldy"
|
- A specific problem: "the auth system is getting unwieldy"
|
||||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
- A change name: "add-dark-mode" (to explore in context of that change)
|
||||||
@@ -36,24 +37,28 @@ Enter explore mode. Think deeply. Visualize freely. Follow the conversation wher
|
|||||||
Depending on what the user brings, you might:
|
Depending on what the user brings, you might:
|
||||||
|
|
||||||
**Explore the problem space**
|
**Explore the problem space**
|
||||||
|
|
||||||
- Ask clarifying questions that emerge from what they said
|
- Ask clarifying questions that emerge from what they said
|
||||||
- Challenge assumptions
|
- Challenge assumptions
|
||||||
- Reframe the problem
|
- Reframe the problem
|
||||||
- Find analogies
|
- Find analogies
|
||||||
|
|
||||||
**Investigate the codebase**
|
**Investigate the codebase**
|
||||||
|
|
||||||
- Map existing architecture relevant to the discussion
|
- Map existing architecture relevant to the discussion
|
||||||
- Find integration points
|
- Find integration points
|
||||||
- Identify patterns already in use
|
- Identify patterns already in use
|
||||||
- Surface hidden complexity
|
- Surface hidden complexity
|
||||||
|
|
||||||
**Compare options**
|
**Compare options**
|
||||||
|
|
||||||
- Brainstorm multiple approaches
|
- Brainstorm multiple approaches
|
||||||
- Build comparison tables
|
- Build comparison tables
|
||||||
- Sketch tradeoffs
|
- Sketch tradeoffs
|
||||||
- Recommend a path (if asked)
|
- Recommend a path (if asked)
|
||||||
|
|
||||||
**Visualize**
|
**Visualize**
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────┐
|
┌─────────────────────────────────────────┐
|
||||||
│ Use ASCII diagrams liberally │
|
│ Use ASCII diagrams liberally │
|
||||||
@@ -72,6 +77,7 @@ Depending on what the user brings, you might:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Surface risks and unknowns**
|
**Surface risks and unknowns**
|
||||||
|
|
||||||
- Identify what could go wrong
|
- Identify what could go wrong
|
||||||
- Find gaps in understanding
|
- Find gaps in understanding
|
||||||
- Suggest spikes or investigations
|
- Suggest spikes or investigations
|
||||||
@@ -85,11 +91,13 @@ You have full context of the OpenSpec system. Use it naturally, don't force it.
|
|||||||
### Check for context
|
### Check for context
|
||||||
|
|
||||||
At the start, quickly check what exists:
|
At the start, quickly check what exists:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec list --json
|
openspec list --json
|
||||||
```
|
```
|
||||||
|
|
||||||
This tells you:
|
This tells you:
|
||||||
|
|
||||||
- If there are active changes
|
- If there are active changes
|
||||||
- Their names, schemas, and status
|
- Their names, schemas, and status
|
||||||
- What the user might be working on
|
- What the user might be working on
|
||||||
@@ -120,14 +128,14 @@ If the user mentions a change or you detect one is relevant:
|
|||||||
|
|
||||||
3. **Offer to capture when decisions are made**
|
3. **Offer to capture when decisions are made**
|
||||||
|
|
||||||
| Insight Type | Where to Capture |
|
| Insight Type | Where to Capture |
|
||||||
|--------------|------------------|
|
| -------------------------- | ---------------------------- |
|
||||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||||
| Requirement changed | `specs/<capability>/spec.md` |
|
| Requirement changed | `specs/<capability>/spec.md` |
|
||||||
| Design decision made | `design.md` |
|
| Design decision made | `design.md` |
|
||||||
| Scope changed | `proposal.md` |
|
| Scope changed | `proposal.md` |
|
||||||
| New work identified | `tasks.md` |
|
| New work identified | `tasks.md` |
|
||||||
| Assumption invalidated | Relevant artifact |
|
| Assumption invalidated | Relevant artifact |
|
||||||
|
|
||||||
Example offers:
|
Example offers:
|
||||||
- "That's a design decision. Capture it in design.md?"
|
- "That's a design decision. Capture it in design.md?"
|
||||||
|
|||||||
+28
-21
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Fast Forward"
|
name: 'OPSX: Fast Forward'
|
||||||
description: Create a change and generate all artifacts needed for implementation in one go
|
description: Create a change and generate all artifacts needed for implementation in one go
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, artifacts, experimental]
|
tags: [workflow, artifacts, experimental]
|
||||||
@@ -14,6 +14,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
1. **If no input provided, ask what they want to build**
|
1. **If no input provided, ask what they want to build**
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
@@ -21,15 +22,19 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||||
|
|
||||||
2. **Create the change directory**
|
2. **Create the change directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<name>"
|
openspec new change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/`.
|
This creates a scaffolded change at `openspec/changes/<name>/`.
|
||||||
|
|
||||||
3. **Get the artifact build order**
|
3. **Get the artifact build order**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>" --json
|
openspec status --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Parse the JSON to get:
|
Parse the JSON to get:
|
||||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||||
- `artifacts`: list of all artifacts with their status and dependencies
|
- `artifacts`: list of all artifacts with their status and dependencies
|
||||||
@@ -41,30 +46,30 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||||
|
|
||||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||||
- Get instructions:
|
- Get instructions:
|
||||||
```bash
|
```bash
|
||||||
openspec instructions <artifact-id> --change "<name>" --json
|
openspec instructions <artifact-id> --change "<name>" --json
|
||||||
```
|
```
|
||||||
- The instructions JSON includes:
|
- The instructions JSON includes:
|
||||||
- `context`: Project background (constraints for you - do NOT include in output)
|
- `context`: Project background (constraints for you - do NOT include in output)
|
||||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||||
- `template`: The structure to use for your output file
|
- `template`: The structure to use for your output file
|
||||||
- `instruction`: Schema-specific guidance for this artifact type
|
- `instruction`: Schema-specific guidance for this artifact type
|
||||||
- `outputPath`: Where to write the artifact
|
- `outputPath`: Where to write the artifact
|
||||||
- `dependencies`: Completed artifacts to read for context
|
- `dependencies`: Completed artifacts to read for context
|
||||||
- Read any completed dependency files for context
|
- Read any completed dependency files for context
|
||||||
- Create the artifact file using `template` as the structure
|
- Create the artifact file using `template` as the structure
|
||||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||||
- Show brief progress: "✓ Created <artifact-id>"
|
- Show brief progress: "✓ Created <artifact-id>"
|
||||||
|
|
||||||
b. **Continue until all `applyRequires` artifacts are complete**
|
b. **Continue until all `applyRequires` artifacts are complete**
|
||||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||||
- Stop when all `applyRequires` artifacts are done
|
- Stop when all `applyRequires` artifacts are done
|
||||||
|
|
||||||
c. **If an artifact requires user input** (unclear context):
|
c. **If an artifact requires user input** (unclear context):
|
||||||
- Use **AskUserQuestion tool** to clarify
|
- Use **AskUserQuestion tool** to clarify
|
||||||
- Then continue with creation
|
- Then continue with creation
|
||||||
|
|
||||||
5. **Show final status**
|
5. **Show final status**
|
||||||
```bash
|
```bash
|
||||||
@@ -74,6 +79,7 @@ Fast-forward through artifact creation - generate everything needed to start imp
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After completing all artifacts, summarize:
|
After completing all artifacts, summarize:
|
||||||
|
|
||||||
- Change name and location
|
- Change name and location
|
||||||
- List of artifacts created with brief descriptions
|
- List of artifacts created with brief descriptions
|
||||||
- What's ready: "All artifacts created! Ready for implementation."
|
- What's ready: "All artifacts created! Ready for implementation."
|
||||||
@@ -87,6 +93,7 @@ After completing all artifacts, summarize:
|
|||||||
- Use the `template` as a starting point, filling in based on context
|
- Use the `template` as a starting point, filling in based on context
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||||
- Always read dependency artifacts before creating a new one
|
- Always read dependency artifacts before creating a new one
|
||||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: New"
|
name: 'OPSX: New'
|
||||||
description: Start a new change using the experimental artifact workflow (OPSX)
|
description: Start a new change using the experimental artifact workflow (OPSX)
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, artifacts, experimental]
|
tags: [workflow, artifacts, experimental]
|
||||||
@@ -14,6 +14,7 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
1. **If no input provided, ask what they want to build**
|
1. **If no input provided, ask what they want to build**
|
||||||
|
|
||||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||||
|
|
||||||
> "What change do you want to work on? Describe what you want to build or fix."
|
> "What change do you want to work on? Describe what you want to build or fix."
|
||||||
|
|
||||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||||
@@ -31,23 +32,29 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
**Otherwise**: Omit `--schema` to use the default.
|
**Otherwise**: Omit `--schema` to use the default.
|
||||||
|
|
||||||
3. **Create the change directory**
|
3. **Create the change directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<name>"
|
openspec new change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `--schema <name>` only if the user requested a specific workflow.
|
Add `--schema <name>` only if the user requested a specific workflow.
|
||||||
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
|
||||||
|
|
||||||
4. **Show the artifact status**
|
4. **Show the artifact status**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec status --change "<name>"
|
openspec status --change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
This shows which artifacts need to be created and which are ready (dependencies satisfied).
|
||||||
|
|
||||||
5. **Get instructions for the first artifact**
|
5. **Get instructions for the first artifact**
|
||||||
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
|
The first artifact depends on the schema. Check the status output to find the first artifact with status "ready".
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec instructions <first-artifact-id> --change "<name>"
|
openspec instructions <first-artifact-id> --change "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
This outputs the template and context for creating the first artifact.
|
This outputs the template and context for creating the first artifact.
|
||||||
|
|
||||||
6. **STOP and wait for user direction**
|
6. **STOP and wait for user direction**
|
||||||
@@ -55,6 +62,7 @@ Start a new change using the experimental artifact-driven approach.
|
|||||||
**Output**
|
**Output**
|
||||||
|
|
||||||
After completing the steps, summarize:
|
After completing the steps, summarize:
|
||||||
|
|
||||||
- Change name and location
|
- Change name and location
|
||||||
- Schema/workflow being used and its artifact sequence
|
- Schema/workflow being used and its artifact sequence
|
||||||
- Current status (0/N artifacts complete)
|
- Current status (0/N artifacts complete)
|
||||||
@@ -62,6 +70,7 @@ After completing the steps, summarize:
|
|||||||
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
|
- Prompt: "Ready to create the first artifact? Run `/opsx:continue` or just describe what this change is about and I'll draft it."
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Do NOT create any artifacts yet - just show the instructions
|
- Do NOT create any artifacts yet - just show the instructions
|
||||||
- Do NOT advance beyond showing the first artifact template
|
- Do NOT advance beyond showing the first artifact template
|
||||||
- If the name is invalid (not kebab-case), ask for a valid name
|
- If the name is invalid (not kebab-case), ask for a valid name
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Onboard"
|
name: 'OPSX: Onboard'
|
||||||
description: Guided onboarding - walk through a complete OpenSpec workflow cycle with narration
|
description: Guided onboarding - walk through a complete OpenSpec workflow cycle with narration
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, onboarding, tutorial, learning]
|
tags: [workflow, onboarding, tutorial, learning]
|
||||||
@@ -18,6 +18,7 @@ openspec status --json 2>&1 || echo "NOT_INITIALIZED"
|
|||||||
```
|
```
|
||||||
|
|
||||||
**If not initialized:**
|
**If not initialized:**
|
||||||
|
|
||||||
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
|
> OpenSpec isn't set up in this project yet. Run `openspec init` first, then come back to `/opsx:onboard`.
|
||||||
|
|
||||||
Stop here if not initialized.
|
Stop here if not initialized.
|
||||||
@@ -62,6 +63,7 @@ Scan the codebase for small improvement opportunities. Look for:
|
|||||||
6. **Missing validation** - User input handlers without validation
|
6. **Missing validation** - User input handlers without validation
|
||||||
|
|
||||||
Also check recent git activity:
|
Also check recent git activity:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git log --oneline -10 2>/dev/null || echo "No git history"
|
git log --oneline -10 2>/dev/null || echo "No git history"
|
||||||
```
|
```
|
||||||
@@ -97,6 +99,7 @@ Which task interests you? (Pick a number or describe your own)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**If nothing found:** Fall back to asking what the user wants to build:
|
**If nothing found:** Fall back to asking what the user wants to build:
|
||||||
|
|
||||||
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
|
||||||
|
|
||||||
### Scope Guardrail
|
### Scope Guardrail
|
||||||
@@ -129,6 +132,7 @@ Before we create a change, let me quickly show you **explore mode**—it's how y
|
|||||||
```
|
```
|
||||||
|
|
||||||
Spend 1-2 minutes investigating the relevant code:
|
Spend 1-2 minutes investigating the relevant code:
|
||||||
|
|
||||||
- Read the file(s) involved
|
- Read the file(s) involved
|
||||||
- Draw a quick ASCII diagram if it helps
|
- Draw a quick ASCII diagram if it helps
|
||||||
- Note any considerations
|
- Note any considerations
|
||||||
@@ -154,6 +158,7 @@ Now let's create a change to hold our work.
|
|||||||
## Phase 4: Create the Change
|
## Phase 4: Create the Change
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Creating a Change
|
## Creating a Change
|
||||||
|
|
||||||
@@ -163,21 +168,25 @@ Let me create one for our task.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:** Create the change with a derived kebab-case name:
|
**DO:** Create the change with a derived kebab-case name:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec new change "<derived-name>"
|
openspec new change "<derived-name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
**SHOW:**
|
**SHOW:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Created: `openspec/changes/<name>/`
|
Created: `openspec/changes/<name>/`
|
||||||
|
|
||||||
The folder structure:
|
The folder structure:
|
||||||
```
|
```
|
||||||
|
|
||||||
openspec/changes/<name>/
|
openspec/changes/<name>/
|
||||||
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
├── proposal.md ← Why we're doing this (empty, we'll fill it)
|
||||||
├── design.md ← How we'll build it (empty)
|
├── design.md ← How we'll build it (empty)
|
||||||
├── specs/ ← Detailed requirements (empty)
|
├── specs/ ← Detailed requirements (empty)
|
||||||
└── tasks.md ← Implementation checklist (empty)
|
└── tasks.md ← Implementation checklist (empty)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's fill in the first artifact—the proposal.
|
Now let's fill in the first artifact—the proposal.
|
||||||
@@ -188,6 +197,7 @@ Now let's fill in the first artifact—the proposal.
|
|||||||
## Phase 5: Proposal
|
## Phase 5: Proposal
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## The Proposal
|
## The Proposal
|
||||||
|
|
||||||
@@ -232,9 +242,11 @@ Does this capture the intent? I can adjust before we save it.
|
|||||||
**PAUSE** - Wait for user approval/feedback.
|
**PAUSE** - Wait for user approval/feedback.
|
||||||
|
|
||||||
After approval, save the proposal:
|
After approval, save the proposal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec instructions proposal --change "<name>" --json
|
openspec instructions proposal --change "<name>" --json
|
||||||
```
|
```
|
||||||
|
|
||||||
Then write the content to `openspec/changes/<name>/proposal.md`.
|
Then write the content to `openspec/changes/<name>/proposal.md`.
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -248,6 +260,7 @@ Next up: specs.
|
|||||||
## Phase 6: Specs
|
## Phase 6: Specs
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Specs
|
## Specs
|
||||||
|
|
||||||
@@ -257,6 +270,7 @@ For a small task like this, we might only need one spec file.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:** Create the spec file:
|
**DO:** Create the spec file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
mkdir -p openspec/changes/<name>/specs/<capability-name>
|
||||||
```
|
```
|
||||||
@@ -292,6 +306,7 @@ Save to `openspec/changes/<name>/specs/<capability>/spec.md`.
|
|||||||
## Phase 7: Design
|
## Phase 7: Design
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
@@ -337,6 +352,7 @@ Save to `openspec/changes/<name>/design.md`.
|
|||||||
## Phase 8: Tasks
|
## Phase 8: Tasks
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@@ -375,6 +391,7 @@ Save to `openspec/changes/<name>/tasks.md`.
|
|||||||
## Phase 9: Apply (Implementation)
|
## Phase 9: Apply (Implementation)
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
@@ -409,6 +426,7 @@ The change is implemented! One more step—let's archive it.
|
|||||||
## Phase 10: Archive
|
## Phase 10: Archive
|
||||||
|
|
||||||
**EXPLAIN:**
|
**EXPLAIN:**
|
||||||
|
|
||||||
```
|
```
|
||||||
## Archiving
|
## Archiving
|
||||||
|
|
||||||
@@ -418,11 +436,13 @@ Archived changes become your project's decision history—you can always find th
|
|||||||
```
|
```
|
||||||
|
|
||||||
**DO:**
|
**DO:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openspec archive "<name>"
|
openspec archive "<name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
**SHOW:**
|
**SHOW:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
Archived to: `openspec/changes/archive/YYYY-MM-DD-<name>/`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: "OPSX: Sync"
|
name: 'OPSX: Sync'
|
||||||
description: Sync delta specs from a change to main specs
|
description: Sync delta specs from a change to main specs
|
||||||
category: Workflow
|
category: Workflow
|
||||||
tags: [workflow, specs, experimental]
|
tags: [workflow, specs, experimental]
|
||||||
@@ -43,28 +43,28 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|||||||
|
|
||||||
c. **Apply changes intelligently**:
|
c. **Apply changes intelligently**:
|
||||||
|
|
||||||
**ADDED Requirements:**
|
**ADDED Requirements:**
|
||||||
- If requirement doesn't exist in main spec → add it
|
- If requirement doesn't exist in main spec → add it
|
||||||
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
- If requirement already exists → update it to match (treat as implicit MODIFIED)
|
||||||
|
|
||||||
**MODIFIED Requirements:**
|
**MODIFIED Requirements:**
|
||||||
- Find the requirement in main spec
|
- Find the requirement in main spec
|
||||||
- Apply the changes - this can be:
|
- Apply the changes - this can be:
|
||||||
- Adding new scenarios (don't need to copy existing ones)
|
- Adding new scenarios (don't need to copy existing ones)
|
||||||
- Modifying existing scenarios
|
- Modifying existing scenarios
|
||||||
- Changing the requirement description
|
- Changing the requirement description
|
||||||
- Preserve scenarios/content not mentioned in the delta
|
- Preserve scenarios/content not mentioned in the delta
|
||||||
|
|
||||||
**REMOVED Requirements:**
|
**REMOVED Requirements:**
|
||||||
- Remove the entire requirement block from main spec
|
- Remove the entire requirement block from main spec
|
||||||
|
|
||||||
**RENAMED Requirements:**
|
**RENAMED Requirements:**
|
||||||
- Find the FROM requirement, rename to TO
|
- Find the FROM requirement, rename to TO
|
||||||
|
|
||||||
d. **Create new main spec** if capability doesn't exist yet:
|
d. **Create new main spec** if capability doesn't exist yet:
|
||||||
- Create `openspec/specs/<capability>/spec.md`
|
- Create `openspec/specs/<capability>/spec.md`
|
||||||
- Add Purpose section (can be brief, mark as TBD)
|
- Add Purpose section (can be brief, mark as TBD)
|
||||||
- Add Requirements section with the ADDED requirements
|
- Add Requirements section with the ADDED requirements
|
||||||
|
|
||||||
4. **Show summary**
|
4. **Show summary**
|
||||||
|
|
||||||
@@ -78,16 +78,20 @@ This is an **agent-driven** operation - you will read delta specs and directly e
|
|||||||
## ADDED Requirements
|
## ADDED Requirements
|
||||||
|
|
||||||
### Requirement: New Feature
|
### Requirement: New Feature
|
||||||
|
|
||||||
The system SHALL do something new.
|
The system SHALL do something new.
|
||||||
|
|
||||||
#### Scenario: Basic case
|
#### Scenario: Basic case
|
||||||
|
|
||||||
- **WHEN** user does X
|
- **WHEN** user does X
|
||||||
- **THEN** system does Y
|
- **THEN** system does Y
|
||||||
|
|
||||||
## MODIFIED Requirements
|
## MODIFIED Requirements
|
||||||
|
|
||||||
### Requirement: Existing Feature
|
### Requirement: Existing Feature
|
||||||
|
|
||||||
#### Scenario: New scenario to add
|
#### Scenario: New scenario to add
|
||||||
|
|
||||||
- **WHEN** user does A
|
- **WHEN** user does A
|
||||||
- **THEN** system does B
|
- **THEN** system does B
|
||||||
|
|
||||||
@@ -104,8 +108,9 @@ The system SHALL do something new.
|
|||||||
**Key Principle: Intelligent Merging**
|
**Key Principle: Intelligent Merging**
|
||||||
|
|
||||||
Unlike programmatic merging, you can apply **partial updates**:
|
Unlike programmatic merging, you can apply **partial updates**:
|
||||||
|
|
||||||
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
|
||||||
- The delta represents *intent*, not a wholesale replacement
|
- The delta represents _intent_, not a wholesale replacement
|
||||||
- Use your judgment to merge changes sensibly
|
- Use your judgment to merge changes sensibly
|
||||||
|
|
||||||
**Output On Success**
|
**Output On Success**
|
||||||
@@ -127,6 +132,7 @@ Main specs are now updated. The change remains active - archive when implementat
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Guardrails**
|
**Guardrails**
|
||||||
|
|
||||||
- Read both delta and main specs before making changes
|
- Read both delta and main specs before making changes
|
||||||
- Preserve existing content not mentioned in delta
|
- Preserve existing content not mentioned in delta
|
||||||
- If something is unclear, ask for clarification
|
- If something is unclear, ask for clarification
|
||||||
|
|||||||
Loaded 100 of 313 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user