Compare commits
158 Commits
977795ffb1
...
renovate/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38e9d48b0c | ||
| b12106d3bf | |||
| d0ed6790ef | |||
| 92372b6a59 | |||
| 7817ad182b | |||
| 9483e9b1f7 | |||
| 75e6548403 | |||
| d4a1f0dc23 | |||
| 3d7efb14f7 | |||
| 2f8b911af8 | |||
| e9791de4e2 | |||
| 3b4cc7fbb9 | |||
| 9c0e9249ce | |||
| 5082ec1333 | |||
| 35b488a8be | |||
| b067c0ef1e | |||
|
|
42686502d8 | ||
| 51ab99fc61 | |||
| d52f51d6e1 | |||
| c1760ae376 | |||
| 6d51327e56 | |||
| 96044ae1ed | |||
| f972a41e45 | |||
| 13b01dfba8 | |||
| fd8724db8f | |||
| 8885dbd722 | |||
| c51eacb261 | |||
| c450849e4d | |||
| e01d5ee642 | |||
| d333ab3d39 | |||
| 541017965f | |||
| 981920f004 | |||
| 3908c89998 | |||
| bf0f4ffb7f | |||
| 58043d1507 | |||
|
|
264c4ec21f | ||
| 6d7a55fdb3 | |||
| a8aacf4ee9 | |||
| 0a404ecde3 | |||
| 01f9e3dac1 | |||
| ad607afe83 | |||
| f0424223de | |||
| 7ab9068c14 | |||
| 41bb17d5c9 | |||
|
|
a44b938f08 | ||
|
|
7477a953c5 | ||
|
|
7fb296b47f | ||
|
|
8ab7d345c8 | ||
|
|
cf2139f229 | ||
|
|
79f33d659c | ||
| e5b71f8fb8 | |||
|
|
60649ae4de | ||
| e90aefae15 | |||
|
|
622932418d | ||
| a1855ff8d6 | |||
| 4bfaee685c | |||
| 2a6a658df9 | |||
| 37d378ca59 | |||
| 0441ca0c33 | |||
|
|
e6711b33d4 | ||
| 6b3a06a72c | |||
| 448e801ca3 | |||
| 751201617d | |||
| fa34223c10 | |||
| e6ea9405a6 | |||
| 32f96e4c6f | |||
| e6c4a21f65 | |||
| 831ffc071a | |||
| 5dd7cb3fb8 | |||
| 64816558c1 | |||
| 019ead7be3 | |||
| 29974704d0 | |||
| 877c869a22 | |||
| a9743025a7 | |||
| 9f82275c63 | |||
| e203ecf687 | |||
| aa3ea04bfc | |||
|
|
27ca8ab4b8 | ||
| 752d153cd4 | |||
| 763811fce6 | |||
| d7ed28e036 | |||
| a52d0cd1d3 | |||
| 373f3671f6 | |||
| 8f78c6cd45 | |||
| fe291e36e4 | |||
| e56998b17c | |||
| 1b3eafa8d1 | |||
| 061d507825 | |||
| d79a19ca15 | |||
| 2da36058ae | |||
| 90bfd12bf3 | |||
| 4d6df8d16b | |||
| be1c5062a2 | |||
| d9136481d8 | |||
| e248a2ee06 | |||
| fc77248c38 | |||
| a625e34fe4 | |||
| 4828d06aba | |||
| cac2903807 | |||
|
|
210118bf9a | ||
| 9a78ebd9b0 | |||
| 5f50ea991b | |||
| fd9175925e | |||
| 63108f4eb5 | |||
| cd71110514 | |||
| 76b48d8b61 | |||
| e5d0dd5f8f | |||
| e77e479e2a | |||
| 80d79c3596 | |||
| 7efe932621 | |||
| a56a26b1f0 | |||
| 906ba99b75 | |||
| da08752642 | |||
| 014b3b0171 | |||
| 33aff5bff5 | |||
| 6de0769d70 | |||
| 6a16255984 | |||
| 2ce3ce0d05 | |||
|
|
ca651d4c05 | ||
|
|
1e065bef18 | ||
|
|
6e655597d7 | ||
|
|
e10b88ee5f | ||
|
|
465fc2178f | ||
|
|
9e48debca7 | ||
|
|
fc344d3ca0 | ||
|
|
e04a86399c | ||
|
|
0069747e68 | ||
| 1e7b0cb56c | |||
|
|
d248bcd944 | ||
| 074f4ef1ee | |||
| 1afdd0a462 | |||
| 6aeb4b8bca | |||
| 0b2b84dafc | |||
| 562631f151 | |||
| 9cf199dd9f | |||
| b2d37d9269 | |||
| ffea279b54 | |||
| 4cfac860aa | |||
| e3ca613210 | |||
| 14f11875a4 | |||
| a029e951b8 | |||
| 84feeb9997 | |||
| f3d4b5fa17 | |||
| c80074093c | |||
| 830ca55f20 | |||
| eeadaf58c7 | |||
| 387445e089 | |||
| c2bbb78b7b | |||
| 91e566efea | |||
| b8421274b4 | |||
| 747ed18945 | |||
| e9110ea143 | |||
| bd84f4e355 | |||
| 23b264e66e | |||
| cb0bcad145 | |||
| e8184be12f | |||
| a7303aa859 | |||
|
|
f5df2d7290 |
184
.claude/commands/speckit.analyze.md
Normal file
184
.claude/commands/speckit.analyze.md
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Goal
|
||||
|
||||
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
|
||||
|
||||
**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Initialize Analysis Context
|
||||
|
||||
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths:
|
||||
|
||||
- SPEC = FEATURE_DIR/spec.md
|
||||
- PLAN = FEATURE_DIR/plan.md
|
||||
- TASKS = FEATURE_DIR/tasks.md
|
||||
|
||||
Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command).
|
||||
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
### 2. Load Artifacts (Progressive Disclosure)
|
||||
|
||||
Load only the minimal necessary context from each artifact:
|
||||
|
||||
**From spec.md:**
|
||||
|
||||
- Overview/Context
|
||||
- Functional Requirements
|
||||
- Non-Functional Requirements
|
||||
- User Stories
|
||||
- Edge Cases (if present)
|
||||
|
||||
**From plan.md:**
|
||||
|
||||
- Architecture/stack choices
|
||||
- Data Model references
|
||||
- Phases
|
||||
- Technical constraints
|
||||
|
||||
**From tasks.md:**
|
||||
|
||||
- Task IDs
|
||||
- Descriptions
|
||||
- Phase grouping
|
||||
- Parallel markers [P]
|
||||
- Referenced file paths
|
||||
|
||||
**From constitution:**
|
||||
|
||||
- Load `.specify/memory/constitution.md` for principle validation
|
||||
|
||||
### 3. Build Semantic Models
|
||||
|
||||
Create internal representations (do not include raw artifacts in output):
|
||||
|
||||
- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`)
|
||||
- **User story/action inventory**: Discrete user actions with acceptance criteria
|
||||
- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases)
|
||||
- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements
|
||||
|
||||
### 4. Detection Passes (Token-Efficient Analysis)
|
||||
|
||||
Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary.
|
||||
|
||||
#### A. Duplication Detection
|
||||
|
||||
- Identify near-duplicate requirements
|
||||
- Mark lower-quality phrasing for consolidation
|
||||
|
||||
#### B. Ambiguity Detection
|
||||
|
||||
- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria
|
||||
- Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.)
|
||||
|
||||
#### C. Underspecification
|
||||
|
||||
- Requirements with verbs but missing object or measurable outcome
|
||||
- User stories missing acceptance criteria alignment
|
||||
- Tasks referencing files or components not defined in spec/plan
|
||||
|
||||
#### D. Constitution Alignment
|
||||
|
||||
- Any requirement or plan element conflicting with a MUST principle
|
||||
- Missing mandated sections or quality gates from constitution
|
||||
|
||||
#### E. Coverage Gaps
|
||||
|
||||
- Requirements with zero associated tasks
|
||||
- Tasks with no mapped requirement/story
|
||||
- Non-functional requirements not reflected in tasks (e.g., performance, security)
|
||||
|
||||
#### F. Inconsistency
|
||||
|
||||
- Terminology drift (same concept named differently across files)
|
||||
- Data entities referenced in plan but absent in spec (or vice versa)
|
||||
- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note)
|
||||
- Conflicting requirements (e.g., one requires Next.js while other specifies Vue)
|
||||
|
||||
### 5. Severity Assignment
|
||||
|
||||
Use this heuristic to prioritize findings:
|
||||
|
||||
- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality
|
||||
- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion
|
||||
- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case
|
||||
- **LOW**: Style/wording improvements, minor redundancy not affecting execution order
|
||||
|
||||
### 6. Produce Compact Analysis Report
|
||||
|
||||
Output a Markdown report (no file writes) with the following structure:
|
||||
|
||||
## Specification Analysis Report
|
||||
|
||||
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|
||||
|----|----------|----------|-------------|---------|----------------|
|
||||
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
|
||||
|
||||
(Add one row per finding; generate stable IDs prefixed by category initial.)
|
||||
|
||||
**Coverage Summary Table:**
|
||||
|
||||
| Requirement Key | Has Task? | Task IDs | Notes |
|
||||
|-----------------|-----------|----------|-------|
|
||||
|
||||
**Constitution Alignment Issues:** (if any)
|
||||
|
||||
**Unmapped Tasks:** (if any)
|
||||
|
||||
**Metrics:**
|
||||
|
||||
- Total Requirements
|
||||
- Total Tasks
|
||||
- Coverage % (requirements with >=1 task)
|
||||
- Ambiguity Count
|
||||
- Duplication Count
|
||||
- Critical Issues Count
|
||||
|
||||
### 7. Provide Next Actions
|
||||
|
||||
At end of report, output a concise Next Actions block:
|
||||
|
||||
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
|
||||
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
|
||||
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
|
||||
|
||||
### 8. Offer Remediation
|
||||
|
||||
Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.)
|
||||
|
||||
## Operating Principles
|
||||
|
||||
### Context Efficiency
|
||||
|
||||
- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation
|
||||
- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis
|
||||
- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow
|
||||
- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts
|
||||
|
||||
### Analysis Guidelines
|
||||
|
||||
- **NEVER modify files** (this is read-only analysis)
|
||||
- **NEVER hallucinate missing sections** (if absent, report them accurately)
|
||||
- **Prioritize constitution violations** (these are always CRITICAL)
|
||||
- **Use examples over exhaustive rules** (cite specific instances, not generic patterns)
|
||||
- **Report zero issues gracefully** (emit success report with coverage statistics)
|
||||
|
||||
## Context
|
||||
|
||||
$ARGUMENTS
|
||||
295
.claude/commands/speckit.checklist.md
Normal file
295
.claude/commands/speckit.checklist.md
Normal file
@@ -0,0 +1,295 @@
|
||||
---
|
||||
description: Generate a custom checklist for the current feature based on user requirements.
|
||||
---
|
||||
|
||||
## Checklist Purpose: "Unit Tests for English"
|
||||
|
||||
**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain.
|
||||
|
||||
**NOT for verification/testing**:
|
||||
|
||||
- ❌ NOT "Verify the button clicks correctly"
|
||||
- ❌ NOT "Test error handling works"
|
||||
- ❌ NOT "Confirm the API returns 200"
|
||||
- ❌ NOT checking if code/implementation matches the spec
|
||||
|
||||
**FOR requirements quality validation**:
|
||||
|
||||
- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness)
|
||||
- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity)
|
||||
- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency)
|
||||
- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage)
|
||||
- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases)
|
||||
|
||||
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Execution Steps
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list.
|
||||
- All file paths must be absolute.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
|
||||
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
|
||||
- Only ask about information that materially changes checklist content
|
||||
- Be skipped individually if already unambiguous in `$ARGUMENTS`
|
||||
- Prefer precision over breadth
|
||||
|
||||
Generation algorithm:
|
||||
1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts").
|
||||
2. Cluster signals into candidate focus areas (max 4) ranked by relevance.
|
||||
3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit.
|
||||
4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria.
|
||||
5. Formulate questions chosen from these archetypes:
|
||||
- Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?")
|
||||
- Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?")
|
||||
- Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?")
|
||||
- Audience framing (e.g., "Will this be used by the author only or peers during PR review?")
|
||||
- Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?")
|
||||
- Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?")
|
||||
|
||||
Question formatting rules:
|
||||
- If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters
|
||||
- Limit to A–E options maximum; omit table if a free-form answer is clearer
|
||||
- Never ask the user to restate what they already said
|
||||
- Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope."
|
||||
|
||||
Defaults when interaction impossible:
|
||||
- Depth: Standard
|
||||
- Audience: Reviewer (PR) if code-related; Author otherwise
|
||||
- Focus: Top 2 relevance clusters
|
||||
|
||||
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
|
||||
|
||||
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
|
||||
- Derive checklist theme (e.g., security, review, deploy, ux)
|
||||
- Consolidate explicit must-have items mentioned by user
|
||||
- Map focus selections to category scaffolding
|
||||
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
|
||||
|
||||
4. **Load feature context**: Read from FEATURE_DIR:
|
||||
- spec.md: Feature requirements and scope
|
||||
- plan.md (if exists): Technical details, dependencies
|
||||
- tasks.md (if exists): Implementation tasks
|
||||
|
||||
**Context Loading Strategy**:
|
||||
- Load only necessary portions relevant to active focus areas (avoid full-file dumping)
|
||||
- Prefer summarizing long sections into concise scenario/requirement bullets
|
||||
- Use progressive disclosure: add follow-on retrieval only if gaps detected
|
||||
- If source docs are large, generate interim summary items instead of embedding raw text
|
||||
|
||||
5. **Generate checklist** - Create "Unit Tests for Requirements":
|
||||
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
|
||||
- Generate unique checklist filename:
|
||||
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
|
||||
- Format: `[domain].md`
|
||||
- File handling behavior:
|
||||
- If file does NOT exist: Create new file and number items starting from CHK001
|
||||
- If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016)
|
||||
- Never delete or replace existing checklist content - always preserve and append
|
||||
|
||||
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
|
||||
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
|
||||
- **Completeness**: Are all necessary requirements present?
|
||||
- **Clarity**: Are requirements unambiguous and specific?
|
||||
- **Consistency**: Do requirements align with each other?
|
||||
- **Measurability**: Can requirements be objectively verified?
|
||||
- **Coverage**: Are all scenarios/edge cases addressed?
|
||||
|
||||
**Category Structure** - Group items by requirement quality dimensions:
|
||||
- **Requirement Completeness** (Are all necessary requirements documented?)
|
||||
- **Requirement Clarity** (Are requirements specific and unambiguous?)
|
||||
- **Requirement Consistency** (Do requirements align without conflicts?)
|
||||
- **Acceptance Criteria Quality** (Are success criteria measurable?)
|
||||
- **Scenario Coverage** (Are all flows/cases addressed?)
|
||||
- **Edge Case Coverage** (Are boundary conditions defined?)
|
||||
- **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?)
|
||||
- **Dependencies & Assumptions** (Are they documented and validated?)
|
||||
- **Ambiguities & Conflicts** (What needs clarification?)
|
||||
|
||||
**HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**:
|
||||
|
||||
❌ **WRONG** (Testing implementation):
|
||||
- "Verify landing page displays 3 episode cards"
|
||||
- "Test hover states work on desktop"
|
||||
- "Confirm logo click navigates home"
|
||||
|
||||
✅ **CORRECT** (Testing requirements quality):
|
||||
- "Are the exact number and layout of featured episodes specified?" [Completeness]
|
||||
- "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity]
|
||||
- "Are hover state requirements consistent across all interactive elements?" [Consistency]
|
||||
- "Are keyboard navigation requirements defined for all interactive UI?" [Coverage]
|
||||
- "Is the fallback behavior specified when logo image fails to load?" [Edge Cases]
|
||||
- "Are loading states defined for asynchronous episode data?" [Completeness]
|
||||
- "Does the spec define visual hierarchy for competing UI elements?" [Clarity]
|
||||
|
||||
**ITEM STRUCTURE**:
|
||||
Each item should follow this pattern:
|
||||
- Question format asking about requirement quality
|
||||
- Focus on what's WRITTEN (or not written) in the spec/plan
|
||||
- Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.]
|
||||
- Reference spec section `[Spec §X.Y]` when checking existing requirements
|
||||
- Use `[Gap]` marker when checking for missing requirements
|
||||
|
||||
**EXAMPLES BY QUALITY DIMENSION**:
|
||||
|
||||
Completeness:
|
||||
- "Are error handling requirements defined for all API failure modes? [Gap]"
|
||||
- "Are accessibility requirements specified for all interactive elements? [Completeness]"
|
||||
- "Are mobile breakpoint requirements defined for responsive layouts? [Gap]"
|
||||
|
||||
Clarity:
|
||||
- "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]"
|
||||
- "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]"
|
||||
- "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]"
|
||||
|
||||
Consistency:
|
||||
- "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]"
|
||||
- "Are card component requirements consistent between landing and detail pages? [Consistency]"
|
||||
|
||||
Coverage:
|
||||
- "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]"
|
||||
- "Are concurrent user interaction scenarios addressed? [Coverage, Gap]"
|
||||
- "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]"
|
||||
|
||||
Measurability:
|
||||
- "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]"
|
||||
- "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]"
|
||||
|
||||
**Scenario Classification & Coverage** (Requirements Quality Focus):
|
||||
- Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios
|
||||
- For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?"
|
||||
- If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]"
|
||||
- Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]"
|
||||
|
||||
**Traceability Requirements**:
|
||||
- MINIMUM: ≥80% of items MUST include at least one traceability reference
|
||||
- Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]`
|
||||
- If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]"
|
||||
|
||||
**Surface & Resolve Issues** (Requirements Quality Problems):
|
||||
Ask questions about the requirements themselves:
|
||||
- Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]"
|
||||
- Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]"
|
||||
- Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]"
|
||||
- Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]"
|
||||
- Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]"
|
||||
|
||||
**Content Consolidation**:
|
||||
- Soft cap: If raw candidate items > 40, prioritize by risk/impact
|
||||
- Merge near-duplicates checking the same requirement aspect
|
||||
- If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]"
|
||||
|
||||
**🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test:
|
||||
- ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior
|
||||
- ❌ References to code execution, user actions, system behavior
|
||||
- ❌ "Displays correctly", "works properly", "functions as expected"
|
||||
- ❌ "Click", "navigate", "render", "load", "execute"
|
||||
- ❌ Test cases, test plans, QA procedures
|
||||
- ❌ Implementation details (frameworks, APIs, algorithms)
|
||||
|
||||
**✅ REQUIRED PATTERNS** - These test requirements quality:
|
||||
- ✅ "Are [requirement type] defined/specified/documented for [scenario]?"
|
||||
- ✅ "Is [vague term] quantified/clarified with specific criteria?"
|
||||
- ✅ "Are requirements consistent between [section A] and [section B]?"
|
||||
- ✅ "Can [requirement] be objectively measured/verified?"
|
||||
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
|
||||
- ✅ "Does the spec define [missing aspect]?"
|
||||
|
||||
6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
|
||||
|
||||
7. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize:
|
||||
- Focus areas selected
|
||||
- Depth level
|
||||
- Actor/timing
|
||||
- Any explicit user-specified must-have items incorporated
|
||||
|
||||
**Important**: Each `/speckit.checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows:
|
||||
|
||||
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
|
||||
- Simple, memorable filenames that indicate checklist purpose
|
||||
- Easy identification and navigation in the `checklists/` folder
|
||||
|
||||
To avoid clutter, use descriptive types and clean up obsolete checklists when done.
|
||||
|
||||
## Example Checklist Types & Sample Items
|
||||
|
||||
**UX Requirements Quality:** `ux.md`
|
||||
|
||||
Sample items (testing the requirements, NOT the implementation):
|
||||
|
||||
- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]"
|
||||
- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]"
|
||||
- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]"
|
||||
- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]"
|
||||
- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]"
|
||||
- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]"
|
||||
|
||||
**API Requirements Quality:** `api.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are error response formats specified for all failure scenarios? [Completeness]"
|
||||
- "Are rate limiting requirements quantified with specific thresholds? [Clarity]"
|
||||
- "Are authentication requirements consistent across all endpoints? [Consistency]"
|
||||
- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]"
|
||||
- "Is versioning strategy documented in requirements? [Gap]"
|
||||
|
||||
**Performance Requirements Quality:** `performance.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are performance requirements quantified with specific metrics? [Clarity]"
|
||||
- "Are performance targets defined for all critical user journeys? [Coverage]"
|
||||
- "Are performance requirements under different load conditions specified? [Completeness]"
|
||||
- "Can performance requirements be objectively measured? [Measurability]"
|
||||
- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]"
|
||||
|
||||
**Security Requirements Quality:** `security.md`
|
||||
|
||||
Sample items:
|
||||
|
||||
- "Are authentication requirements specified for all protected resources? [Coverage]"
|
||||
- "Are data protection requirements defined for sensitive information? [Completeness]"
|
||||
- "Is the threat model documented and requirements aligned to it? [Traceability]"
|
||||
- "Are security requirements consistent with compliance obligations? [Consistency]"
|
||||
- "Are security failure/breach response requirements defined? [Gap, Exception Flow]"
|
||||
|
||||
## Anti-Examples: What NOT To Do
|
||||
|
||||
**❌ WRONG - These test implementation, not requirements:**
|
||||
|
||||
```markdown
|
||||
- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001]
|
||||
- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003]
|
||||
- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010]
|
||||
- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005]
|
||||
```
|
||||
|
||||
**✅ CORRECT - These test requirements quality:**
|
||||
|
||||
```markdown
|
||||
- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001]
|
||||
- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003]
|
||||
- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010]
|
||||
- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005]
|
||||
- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap]
|
||||
- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001]
|
||||
```
|
||||
|
||||
**Key Differences:**
|
||||
|
||||
- Wrong: Tests if the system works correctly
|
||||
- Correct: Tests if the requirements are written correctly
|
||||
- Wrong: Verification of behavior
|
||||
- Correct: Validation of requirement quality
|
||||
- Wrong: "Does it do X?"
|
||||
- Correct: "Is X clearly specified?"
|
||||
181
.claude/commands/speckit.clarify.md
Normal file
181
.claude/commands/speckit.clarify.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
|
||||
handoffs:
|
||||
- label: Build Technical Plan
|
||||
agent: speckit.plan
|
||||
prompt: Create a plan for the spec. I am building with...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
|
||||
|
||||
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
|
||||
|
||||
Execution steps:
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
|
||||
- `FEATURE_DIR`
|
||||
- `FEATURE_SPEC`
|
||||
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
|
||||
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
|
||||
|
||||
Functional Scope & Behavior:
|
||||
- Core user goals & success criteria
|
||||
- Explicit out-of-scope declarations
|
||||
- User roles / personas differentiation
|
||||
|
||||
Domain & Data Model:
|
||||
- Entities, attributes, relationships
|
||||
- Identity & uniqueness rules
|
||||
- Lifecycle/state transitions
|
||||
- Data volume / scale assumptions
|
||||
|
||||
Interaction & UX Flow:
|
||||
- Critical user journeys / sequences
|
||||
- Error/empty/loading states
|
||||
- Accessibility or localization notes
|
||||
|
||||
Non-Functional Quality Attributes:
|
||||
- Performance (latency, throughput targets)
|
||||
- Scalability (horizontal/vertical, limits)
|
||||
- Reliability & availability (uptime, recovery expectations)
|
||||
- Observability (logging, metrics, tracing signals)
|
||||
- Security & privacy (authN/Z, data protection, threat assumptions)
|
||||
- Compliance / regulatory constraints (if any)
|
||||
|
||||
Integration & External Dependencies:
|
||||
- External services/APIs and failure modes
|
||||
- Data import/export formats
|
||||
- Protocol/versioning assumptions
|
||||
|
||||
Edge Cases & Failure Handling:
|
||||
- Negative scenarios
|
||||
- Rate limiting / throttling
|
||||
- Conflict resolution (e.g., concurrent edits)
|
||||
|
||||
Constraints & Tradeoffs:
|
||||
- Technical constraints (language, storage, hosting)
|
||||
- Explicit tradeoffs or rejected alternatives
|
||||
|
||||
Terminology & Consistency:
|
||||
- Canonical glossary terms
|
||||
- Avoided synonyms / deprecated terms
|
||||
|
||||
Completion Signals:
|
||||
- Acceptance criteria testability
|
||||
- Measurable Definition of Done style indicators
|
||||
|
||||
Misc / Placeholders:
|
||||
- TODO markers / unresolved decisions
|
||||
- Ambiguous adjectives ("robust", "intuitive") lacking quantification
|
||||
|
||||
For each category with Partial or Missing status, add a candidate question opportunity unless:
|
||||
- Clarification would not materially change implementation or validation strategy
|
||||
- Information is better deferred to planning phase (note internally)
|
||||
|
||||
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
|
||||
- Maximum of 5 total questions across the whole session.
|
||||
- Each question must be answerable with EITHER:
|
||||
- A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
|
||||
- A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words").
|
||||
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
|
||||
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
|
||||
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
|
||||
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
|
||||
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
|
||||
|
||||
4. Sequential questioning loop (interactive):
|
||||
- Present EXACTLY ONE question at a time.
|
||||
- For multiple‑choice questions:
|
||||
- **Analyze all options** and determine the **most suitable option** based on:
|
||||
- Best practices for the project type
|
||||
- Common patterns in similar implementations
|
||||
- Risk reduction (security, performance, maintainability)
|
||||
- Alignment with any explicit project goals or constraints visible in the spec
|
||||
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
|
||||
- Format as: `**Recommended:** Option [X] - <reasoning>`
|
||||
- Then render all options as a Markdown table:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| A | <Option A description> |
|
||||
| B | <Option B description> |
|
||||
| C | <Option C description> (add D/E as needed up to 5) |
|
||||
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
|
||||
|
||||
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
|
||||
- For short‑answer style (no meaningful discrete options):
|
||||
- Provide your **suggested answer** based on best practices and context.
|
||||
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
|
||||
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
|
||||
- After the user answers:
|
||||
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
|
||||
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
|
||||
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
|
||||
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
|
||||
- Stop asking further questions when:
|
||||
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
|
||||
- User signals completion ("done", "good", "no more"), OR
|
||||
- You reach 5 asked questions.
|
||||
- Never reveal future queued questions in advance.
|
||||
- If no valid questions exist at start, immediately report no critical ambiguities.
|
||||
|
||||
5. Integration after EACH accepted answer (incremental update approach):
|
||||
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
|
||||
- For the first integrated answer in this session:
|
||||
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
|
||||
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
|
||||
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
|
||||
- Then immediately apply the clarification to the most appropriate section(s):
|
||||
- Functional ambiguity → Update or add a bullet in Functional Requirements.
|
||||
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
|
||||
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
|
||||
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
|
||||
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
|
||||
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
|
||||
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
|
||||
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
|
||||
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
|
||||
- Keep each inserted clarification minimal and testable (avoid narrative drift).
|
||||
|
||||
6. Validation (performed after EACH write plus final pass):
|
||||
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
|
||||
- Total asked (accepted) questions ≤ 5.
|
||||
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
|
||||
- No contradictory earlier statement remains (scan for now-invalid alternative choices removed).
|
||||
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
|
||||
- Terminology consistency: same canonical term used across all updated sections.
|
||||
|
||||
7. Write the updated spec back to `FEATURE_SPEC`.
|
||||
|
||||
8. Report completion (after questioning loop ends or early termination):
|
||||
- Number of questions asked & answered.
|
||||
- Path to updated spec.
|
||||
- Sections touched (list names).
|
||||
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
|
||||
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
|
||||
- Suggested next command.
|
||||
|
||||
Behavior rules:
|
||||
|
||||
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
|
||||
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
|
||||
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
|
||||
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
|
||||
- Respect user early termination signals ("stop", "done", "proceed").
|
||||
- If no questions asked due to full coverage, output a compact coverage summary (all categories Clear) then suggest advancing.
|
||||
- If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale.
|
||||
|
||||
Context for prioritization: $ARGUMENTS
|
||||
84
.claude/commands/speckit.constitution.md
Normal file
84
.claude/commands/speckit.constitution.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
|
||||
handoffs:
|
||||
- label: Build Specification
|
||||
agent: speckit.specify
|
||||
prompt: Implement the feature specification based on the updated constitution. I want to build...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
|
||||
|
||||
**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
|
||||
|
||||
Follow this execution flow:
|
||||
|
||||
1. Load the existing constitution at `.specify/memory/constitution.md`.
|
||||
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
|
||||
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
|
||||
|
||||
2. Collect/derive values for placeholders:
|
||||
- If user input (conversation) supplies a value, use it.
|
||||
- Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded).
|
||||
- For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous.
|
||||
- `CONSTITUTION_VERSION` must increment according to semantic versioning rules:
|
||||
- MAJOR: Backward incompatible governance/principle removals or redefinitions.
|
||||
- MINOR: New principle/section added or materially expanded guidance.
|
||||
- PATCH: Clarifications, wording, typo fixes, non-semantic refinements.
|
||||
- If version bump type ambiguous, propose reasoning before finalizing.
|
||||
|
||||
3. Draft the updated constitution content:
|
||||
- Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left).
|
||||
- Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance.
|
||||
- Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious.
|
||||
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
|
||||
|
||||
4. Consistency propagation checklist (convert prior checklist into active validations):
|
||||
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
|
||||
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
|
||||
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
|
||||
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
|
||||
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
|
||||
|
||||
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
|
||||
- Version change: old → new
|
||||
- List of modified principles (old title → new title if renamed)
|
||||
- Added sections
|
||||
- Removed sections
|
||||
- Templates requiring updates (✅ updated / ⚠ pending) with file paths
|
||||
- Follow-up TODOs if any placeholders intentionally deferred.
|
||||
|
||||
6. Validation before final output:
|
||||
- No remaining unexplained bracket tokens.
|
||||
- Version line matches report.
|
||||
- Dates ISO format YYYY-MM-DD.
|
||||
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
|
||||
|
||||
7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite).
|
||||
|
||||
8. Output a final summary to the user with:
|
||||
- New version and bump rationale.
|
||||
- Any files flagged for manual follow-up.
|
||||
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
|
||||
|
||||
Formatting & Style Requirements:
|
||||
|
||||
- Use Markdown headings exactly as in the template (do not demote/promote levels).
|
||||
- Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks.
|
||||
- Keep a single blank line between sections.
|
||||
- Avoid trailing whitespace.
|
||||
|
||||
If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps.
|
||||
|
||||
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
|
||||
|
||||
Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file.
|
||||
135
.claude/commands/speckit.implement.md
Normal file
135
.claude/commands/speckit.implement.md
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
description: Execute the implementation plan by processing and executing all tasks defined in tasks.md
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
|
||||
- Scan all checklist files in the checklists/ directory
|
||||
- For each checklist, count:
|
||||
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
|
||||
- Completed items: Lines matching `- [X]` or `- [x]`
|
||||
- Incomplete items: Lines matching `- [ ]`
|
||||
- Create a status table:
|
||||
|
||||
```text
|
||||
| Checklist | Total | Completed | Incomplete | Status |
|
||||
|-----------|-------|-----------|------------|--------|
|
||||
| ux.md | 12 | 12 | 0 | ✓ PASS |
|
||||
| test.md | 8 | 5 | 3 | ✗ FAIL |
|
||||
| security.md | 6 | 6 | 0 | ✓ PASS |
|
||||
```
|
||||
|
||||
- Calculate overall status:
|
||||
- **PASS**: All checklists have 0 incomplete items
|
||||
- **FAIL**: One or more checklists have incomplete items
|
||||
|
||||
- **If any checklist is incomplete**:
|
||||
- Display the table with incomplete item counts
|
||||
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
|
||||
- Wait for user response before continuing
|
||||
- If user says "no" or "wait" or "stop", halt execution
|
||||
- If user says "yes" or "proceed" or "continue", proceed to step 3
|
||||
|
||||
- **If all checklists are complete**:
|
||||
- Display the table showing all checklists passed
|
||||
- Automatically proceed to step 3
|
||||
|
||||
3. Load and analyze the implementation context:
|
||||
- **REQUIRED**: Read tasks.md for the complete task list and execution plan
|
||||
- **REQUIRED**: Read plan.md for tech stack, architecture, and file structure
|
||||
- **IF EXISTS**: Read data-model.md for entities and relationships
|
||||
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
|
||||
- **IF EXISTS**: Read research.md for technical decisions and constraints
|
||||
- **IF EXISTS**: Read quickstart.md for integration scenarios
|
||||
|
||||
4. **Project Setup Verification**:
|
||||
- **REQUIRED**: Create/verify ignore files based on actual project setup:
|
||||
|
||||
**Detection & Creation Logic**:
|
||||
- Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so):
|
||||
|
||||
```sh
|
||||
git rev-parse --git-dir 2>/dev/null
|
||||
```
|
||||
|
||||
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
|
||||
- Check if .eslintrc* exists → create/verify .eslintignore
|
||||
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
|
||||
- Check if .prettierrc* exists → create/verify .prettierignore
|
||||
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
|
||||
- Check if terraform files (*.tf) exist → create/verify .terraformignore
|
||||
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
|
||||
|
||||
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
|
||||
**If ignore file missing**: Create with full pattern set for detected technology
|
||||
|
||||
**Common Patterns by Technology** (from plan.md tech stack):
|
||||
- **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*`
|
||||
- **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/`
|
||||
- **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/`
|
||||
- **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/`
|
||||
- **Go**: `*.exe`, `*.test`, `vendor/`, `*.out`
|
||||
- **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/`
|
||||
- **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env`
|
||||
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
|
||||
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
|
||||
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
|
||||
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `autom4te.cache/`, `config.status`, `config.log`, `.idea/`, `*.log`, `.env*`
|
||||
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
|
||||
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
|
||||
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
|
||||
|
||||
**Tool-Specific Patterns**:
|
||||
- **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/`
|
||||
- **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js`
|
||||
- **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
|
||||
- **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl`
|
||||
- **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt`
|
||||
|
||||
5. Parse tasks.md structure and extract:
|
||||
- **Task phases**: Setup, Tests, Core, Integration, Polish
|
||||
- **Task dependencies**: Sequential vs parallel execution rules
|
||||
- **Task details**: ID, description, file paths, parallel markers [P]
|
||||
- **Execution flow**: Order and dependency requirements
|
||||
|
||||
6. Execute implementation following the task plan:
|
||||
- **Phase-by-phase execution**: Complete each phase before moving to the next
|
||||
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
|
||||
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
|
||||
- **File-based coordination**: Tasks affecting the same files must run sequentially
|
||||
- **Validation checkpoints**: Verify each phase completion before proceeding
|
||||
|
||||
7. Implementation execution rules:
|
||||
- **Setup first**: Initialize project structure, dependencies, configuration
|
||||
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
|
||||
- **Core development**: Implement models, services, CLI commands, endpoints
|
||||
- **Integration work**: Database connections, middleware, logging, external services
|
||||
- **Polish and validation**: Unit tests, performance optimization, documentation
|
||||
|
||||
8. Progress tracking and error handling:
|
||||
- Report progress after each completed task
|
||||
- Halt execution if any non-parallel task fails
|
||||
- For parallel tasks [P], continue with successful tasks, report failed ones
|
||||
- Provide clear error messages with context for debugging
|
||||
- Suggest next steps if implementation cannot proceed
|
||||
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
|
||||
|
||||
9. Completion validation:
|
||||
- Verify all required tasks are completed
|
||||
- Check that implemented features match the original specification
|
||||
- Validate that tests pass and coverage meets requirements
|
||||
- Confirm the implementation follows the technical plan
|
||||
- Report final status with summary of completed work
|
||||
|
||||
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
|
||||
90
.claude/commands/speckit.plan.md
Normal file
90
.claude/commands/speckit.plan.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
|
||||
handoffs:
|
||||
- label: Create Tasks
|
||||
agent: speckit.tasks
|
||||
prompt: Break the plan into tasks
|
||||
send: true
|
||||
- label: Create Checklist
|
||||
agent: speckit.checklist
|
||||
prompt: Create a checklist for the following domain...
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Load context**: Read FEATURE_SPEC and `.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied).
|
||||
|
||||
3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
|
||||
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
|
||||
- Fill Constitution Check section from constitution
|
||||
- Evaluate gates (ERROR if violations unjustified)
|
||||
- Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
|
||||
- Phase 1: Generate data-model.md, contracts/, quickstart.md
|
||||
- Phase 1: Update agent context by running the agent script
|
||||
- Re-evaluate Constitution Check post-design
|
||||
|
||||
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0: Outline & Research
|
||||
|
||||
1. **Extract unknowns from Technical Context** above:
|
||||
- For each NEEDS CLARIFICATION → research task
|
||||
- For each dependency → best practices task
|
||||
- For each integration → patterns task
|
||||
|
||||
2. **Generate and dispatch research agents**:
|
||||
|
||||
```text
|
||||
For each unknown in Technical Context:
|
||||
Task: "Research {unknown} for {feature context}"
|
||||
For each technology choice:
|
||||
Task: "Find best practices for {tech} in {domain}"
|
||||
```
|
||||
|
||||
3. **Consolidate findings** in `research.md` using format:
|
||||
- Decision: [what was chosen]
|
||||
- Rationale: [why chosen]
|
||||
- Alternatives considered: [what else evaluated]
|
||||
|
||||
**Output**: research.md with all NEEDS CLARIFICATION resolved
|
||||
|
||||
### Phase 1: Design & Contracts
|
||||
|
||||
**Prerequisites:** `research.md` complete
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships
|
||||
- Validation rules from requirements
|
||||
- State transitions if applicable
|
||||
|
||||
2. **Define interface contracts** (if project has external interfaces) → `/contracts/`:
|
||||
- Identify what interfaces the project exposes to users or other systems
|
||||
- Document the contract format appropriate for the project type
|
||||
- Examples: public APIs for libraries, command schemas for CLI tools, endpoints for web services, grammars for parsers, UI contracts for applications
|
||||
- Skip if project is purely internal (build scripts, one-off tools, etc.)
|
||||
|
||||
3. **Agent context update**:
|
||||
- Run `.specify/scripts/bash/update-agent-context.sh claude`
|
||||
- These scripts detect which AI agent is in use
|
||||
- Update the appropriate agent-specific context file
|
||||
- Add only new technology from current plan
|
||||
- Preserve manual additions between markers
|
||||
|
||||
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
|
||||
|
||||
## Key rules
|
||||
|
||||
- Use absolute paths
|
||||
- ERROR on gate failures or unresolved clarifications
|
||||
258
.claude/commands/speckit.specify.md
Normal file
258
.claude/commands/speckit.specify.md
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
description: Create or update the feature specification from a natural language feature description.
|
||||
handoffs:
|
||||
- label: Build Technical Plan
|
||||
agent: speckit.plan
|
||||
prompt: Create a plan for the spec. I am building with...
|
||||
- label: Clarify Spec Requirements
|
||||
agent: speckit.clarify
|
||||
prompt: Clarify specification requirements
|
||||
send: true
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
|
||||
|
||||
Given that feature description, do this:
|
||||
|
||||
1. **Generate a concise short name** (2-4 words) for the branch:
|
||||
- Analyze the feature description and extract the most meaningful keywords
|
||||
- Create a 2-4 word short name that captures the essence of the feature
|
||||
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
|
||||
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
|
||||
- Keep it concise but descriptive enough to understand the feature at a glance
|
||||
- Examples:
|
||||
- "I want to add user authentication" → "user-auth"
|
||||
- "Implement OAuth2 integration for the API" → "oauth2-api-integration"
|
||||
- "Create a dashboard for analytics" → "analytics-dashboard"
|
||||
- "Fix payment processing timeout bug" → "fix-payment-timeout"
|
||||
|
||||
2. **Check for existing branches before creating new one**:
|
||||
|
||||
a. First, fetch all remote branches to ensure we have the latest information:
|
||||
|
||||
```bash
|
||||
git fetch --all --prune
|
||||
```
|
||||
|
||||
b. Find the highest feature number across all sources for the short-name:
|
||||
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
|
||||
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
|
||||
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
|
||||
|
||||
c. Determine the next available number:
|
||||
- Extract all numbers from all three sources
|
||||
- Find the highest number N
|
||||
- Use N+1 for the new branch number
|
||||
|
||||
d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name:
|
||||
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
|
||||
- Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"`
|
||||
- PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
|
||||
|
||||
**IMPORTANT**:
|
||||
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
|
||||
- Only match branches/directories with the exact short-name pattern
|
||||
- If no existing branches/directories found with this short-name, start with number 1
|
||||
- You must only ever run this script once per feature
|
||||
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
|
||||
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
|
||||
|
||||
3. Load `.specify/templates/spec-template.md` to understand required sections.
|
||||
|
||||
4. Follow this execution flow:
|
||||
|
||||
1. Parse user description from Input
|
||||
If empty: ERROR "No feature description provided"
|
||||
2. Extract key concepts from description
|
||||
Identify: actors, actions, data, constraints
|
||||
3. For unclear aspects:
|
||||
- Make informed guesses based on context and industry standards
|
||||
- Only mark with [NEEDS CLARIFICATION: specific question] if:
|
||||
- The choice significantly impacts feature scope or user experience
|
||||
- Multiple reasonable interpretations exist with different implications
|
||||
- No reasonable default exists
|
||||
- **LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total**
|
||||
- Prioritize clarifications by impact: scope > security/privacy > user experience > technical details
|
||||
4. Fill User Scenarios & Testing section
|
||||
If no clear user flow: ERROR "Cannot determine user scenarios"
|
||||
5. Generate Functional Requirements
|
||||
Each requirement must be testable
|
||||
Use reasonable defaults for unspecified details (document assumptions in Assumptions section)
|
||||
6. Define Success Criteria
|
||||
Create measurable, technology-agnostic outcomes
|
||||
Include both quantitative metrics (time, performance, volume) and qualitative measures (user satisfaction, task completion)
|
||||
Each criterion must be verifiable without implementation details
|
||||
7. Identify Key Entities (if data involved)
|
||||
8. Return: SUCCESS (spec ready for planning)
|
||||
|
||||
5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
|
||||
|
||||
6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
|
||||
|
||||
a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items:
|
||||
|
||||
```markdown
|
||||
# Specification Quality Checklist: [FEATURE NAME]
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: [DATE]
|
||||
**Feature**: [Link to spec.md]
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [ ] No implementation details (languages, frameworks, APIs)
|
||||
- [ ] Focused on user value and business needs
|
||||
- [ ] Written for non-technical stakeholders
|
||||
- [ ] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [ ] No [NEEDS CLARIFICATION] markers remain
|
||||
- [ ] Requirements are testable and unambiguous
|
||||
- [ ] Success criteria are measurable
|
||||
- [ ] Success criteria are technology-agnostic (no implementation details)
|
||||
- [ ] All acceptance scenarios are defined
|
||||
- [ ] Edge cases are identified
|
||||
- [ ] Scope is clearly bounded
|
||||
- [ ] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [ ] All functional requirements have clear acceptance criteria
|
||||
- [ ] User scenarios cover primary flows
|
||||
- [ ] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [ ] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
|
||||
```
|
||||
|
||||
b. **Run Validation Check**: Review the spec against each checklist item:
|
||||
- For each item, determine if it passes or fails
|
||||
- Document specific issues found (quote relevant spec sections)
|
||||
|
||||
c. **Handle Validation Results**:
|
||||
|
||||
- **If all items pass**: Mark checklist complete and proceed to step 6
|
||||
|
||||
- **If items fail (excluding [NEEDS CLARIFICATION])**:
|
||||
1. List the failing items and specific issues
|
||||
2. Update the spec to address each issue
|
||||
3. Re-run validation until all items pass (max 3 iterations)
|
||||
4. If still failing after 3 iterations, document remaining issues in checklist notes and warn user
|
||||
|
||||
- **If [NEEDS CLARIFICATION] markers remain**:
|
||||
1. Extract all [NEEDS CLARIFICATION: ...] markers from the spec
|
||||
2. **LIMIT CHECK**: If more than 3 markers exist, keep only the 3 most critical (by scope/security/UX impact) and make informed guesses for the rest
|
||||
3. For each clarification needed (max 3), present options to user in this format:
|
||||
|
||||
```markdown
|
||||
## Question [N]: [Topic]
|
||||
|
||||
**Context**: [Quote relevant spec section]
|
||||
|
||||
**What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
|
||||
|
||||
**Suggested Answers**:
|
||||
|
||||
| Option | Answer | Implications |
|
||||
|--------|--------|--------------|
|
||||
| A | [First suggested answer] | [What this means for the feature] |
|
||||
| B | [Second suggested answer] | [What this means for the feature] |
|
||||
| C | [Third suggested answer] | [What this means for the feature] |
|
||||
| Custom | Provide your own answer | [Explain how to provide custom input] |
|
||||
|
||||
**Your choice**: _[Wait for user response]_
|
||||
```
|
||||
|
||||
4. **CRITICAL - Table Formatting**: Ensure markdown tables are properly formatted:
|
||||
- Use consistent spacing with pipes aligned
|
||||
- Each cell should have spaces around content: `| Content |` not `|Content|`
|
||||
- Header separator must have at least 3 dashes: `|--------|`
|
||||
- Test that the table renders correctly in markdown preview
|
||||
5. Number questions sequentially (Q1, Q2, Q3 - max 3 total)
|
||||
6. Present all questions together before waiting for responses
|
||||
7. Wait for user to respond with their choices for all questions (e.g., "Q1: A, Q2: Custom - [details], Q3: B")
|
||||
8. Update the spec by replacing each [NEEDS CLARIFICATION] marker with the user's selected or provided answer
|
||||
9. Re-run validation after all clarifications are resolved
|
||||
|
||||
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
|
||||
|
||||
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
|
||||
|
||||
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
## Quick Guidelines
|
||||
|
||||
- Focus on **WHAT** users need and **WHY**.
|
||||
- Avoid HOW to implement (no tech stack, APIs, code structure).
|
||||
- Written for business stakeholders, not developers.
|
||||
- DO NOT create any checklists that are embedded in the spec. That will be a separate command.
|
||||
|
||||
### Section Requirements
|
||||
|
||||
- **Mandatory sections**: Must be completed for every feature
|
||||
- **Optional sections**: Include only when relevant to the feature
|
||||
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
|
||||
|
||||
### For AI Generation
|
||||
|
||||
When creating this spec from a user prompt:
|
||||
|
||||
1. **Make informed guesses**: Use context, industry standards, and common patterns to fill gaps
|
||||
2. **Document assumptions**: Record reasonable defaults in the Assumptions section
|
||||
3. **Limit clarifications**: Maximum 3 [NEEDS CLARIFICATION] markers - use only for critical decisions that:
|
||||
- Significantly impact feature scope or user experience
|
||||
- Have multiple reasonable interpretations with different implications
|
||||
- Lack any reasonable default
|
||||
4. **Prioritize clarifications**: scope > security/privacy > user experience > technical details
|
||||
5. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
|
||||
6. **Common areas needing clarification** (only if no reasonable default exists):
|
||||
- Feature scope and boundaries (include/exclude specific use cases)
|
||||
- User types and permissions (if multiple conflicting interpretations possible)
|
||||
- Security/compliance requirements (when legally/financially significant)
|
||||
|
||||
**Examples of reasonable defaults** (don't ask about these):
|
||||
|
||||
- Data retention: Industry-standard practices for the domain
|
||||
- Performance targets: Standard web/mobile app expectations unless specified
|
||||
- Error handling: User-friendly messages with appropriate fallbacks
|
||||
- Authentication method: Standard session-based or OAuth2 for web apps
|
||||
- Integration patterns: Use project-appropriate patterns (REST/GraphQL for web services, function calls for libraries, CLI args for tools, etc.)
|
||||
|
||||
### Success Criteria Guidelines
|
||||
|
||||
Success criteria must be:
|
||||
|
||||
1. **Measurable**: Include specific metrics (time, percentage, count, rate)
|
||||
2. **Technology-agnostic**: No mention of frameworks, languages, databases, or tools
|
||||
3. **User-focused**: Describe outcomes from user/business perspective, not system internals
|
||||
4. **Verifiable**: Can be tested/validated without knowing implementation details
|
||||
|
||||
**Good examples**:
|
||||
|
||||
- "Users can complete checkout in under 3 minutes"
|
||||
- "System supports 10,000 concurrent users"
|
||||
- "95% of searches return results in under 1 second"
|
||||
- "Task completion rate improves by 40%"
|
||||
|
||||
**Bad examples** (implementation-focused):
|
||||
|
||||
- "API response time is under 200ms" (too technical, use "Users see results instantly")
|
||||
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
|
||||
- "React components render efficiently" (framework-specific)
|
||||
- "Redis cache hit rate above 80%" (technology-specific)
|
||||
137
.claude/commands/speckit.tasks.md
Normal file
137
.claude/commands/speckit.tasks.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
|
||||
handoffs:
|
||||
- label: Analyze For Consistency
|
||||
agent: speckit.analyze
|
||||
prompt: Run a project analysis for consistency
|
||||
send: true
|
||||
- label: Implement Project
|
||||
agent: speckit.implement
|
||||
prompt: Start the implementation in phases
|
||||
send: true
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **Load design documents**: Read from FEATURE_DIR:
|
||||
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
|
||||
- **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios)
|
||||
- Note: Not all projects have all documents. Generate tasks based on what's available.
|
||||
|
||||
3. **Execute task generation workflow**:
|
||||
- Load plan.md and extract tech stack, libraries, project structure
|
||||
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
|
||||
- If data-model.md exists: Extract entities and map to user stories
|
||||
- If contracts/ exists: Map interface contracts to user stories
|
||||
- If research.md exists: Extract decisions for setup tasks
|
||||
- Generate tasks organized by user story (see Task Generation Rules below)
|
||||
- Generate dependency graph showing user story completion order
|
||||
- Create parallel execution examples per user story
|
||||
- Validate task completeness (each user story has all needed tasks, independently testable)
|
||||
|
||||
4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
|
||||
- Correct feature name from plan.md
|
||||
- Phase 1: Setup tasks (project initialization)
|
||||
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)
|
||||
- Phase 3+: One phase per user story (in priority order from spec.md)
|
||||
- Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks
|
||||
- Final Phase: Polish & cross-cutting concerns
|
||||
- All tasks must follow the strict checklist format (see Task Generation Rules below)
|
||||
- Clear file paths for each task
|
||||
- Dependencies section showing story completion order
|
||||
- Parallel execution examples per story
|
||||
- Implementation strategy section (MVP first, incremental delivery)
|
||||
|
||||
5. **Report**: Output path to generated tasks.md and summary:
|
||||
- Total task count
|
||||
- Task count per user story
|
||||
- Parallel opportunities identified
|
||||
- Independent test criteria for each story
|
||||
- Suggested MVP scope (typically just User Story 1)
|
||||
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
|
||||
|
||||
Context for task generation: $ARGUMENTS
|
||||
|
||||
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
|
||||
|
||||
## Task Generation Rules
|
||||
|
||||
**CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
|
||||
|
||||
**Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature specification or if user requests TDD approach.
|
||||
|
||||
### Checklist Format (REQUIRED)
|
||||
|
||||
Every task MUST strictly follow this format:
|
||||
|
||||
```text
|
||||
- [ ] [TaskID] [P?] [Story?] Description with file path
|
||||
```
|
||||
|
||||
**Format Components**:
|
||||
|
||||
1. **Checkbox**: ALWAYS start with `- [ ]` (markdown checkbox)
|
||||
2. **Task ID**: Sequential number (T001, T002, T003...) in execution order
|
||||
3. **[P] marker**: Include ONLY if task is parallelizable (different files, no dependencies on incomplete tasks)
|
||||
4. **[Story] label**: REQUIRED for user story phase tasks only
|
||||
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
|
||||
- Setup phase: NO story label
|
||||
- Foundational phase: NO story label
|
||||
- User Story phases: MUST have story label
|
||||
- Polish phase: NO story label
|
||||
5. **Description**: Clear action with exact file path
|
||||
|
||||
**Examples**:
|
||||
|
||||
- ✅ CORRECT: `- [ ] T001 Create project structure per implementation plan`
|
||||
- ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py`
|
||||
- ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py`
|
||||
- ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py`
|
||||
- ❌ WRONG: `- [ ] Create User model` (missing ID and Story label)
|
||||
- ❌ WRONG: `T001 [US1] Create model` (missing checkbox)
|
||||
- ❌ WRONG: `- [ ] [US1] Create User model` (missing Task ID)
|
||||
- ❌ WRONG: `- [ ] T001 [US1] Create model` (missing file path)
|
||||
|
||||
### Task Organization
|
||||
|
||||
1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION:
|
||||
- Each user story (P1, P2, P3...) gets its own phase
|
||||
- Map all related components to their story:
|
||||
- Models needed for that story
|
||||
- Services needed for that story
|
||||
- Interfaces/UI needed for that story
|
||||
- If tests requested: Tests specific to that story
|
||||
- Mark story dependencies (most stories should be independent)
|
||||
|
||||
2. **From Contracts**:
|
||||
- Map each interface contract → to the user story it serves
|
||||
- If tests requested: Each interface contract → contract test task [P] before implementation in that story's phase
|
||||
|
||||
3. **From Data Model**:
|
||||
- Map each entity to the user story(ies) that need it
|
||||
- If entity serves multiple stories: Put in earliest story or Setup phase
|
||||
- Relationships → service layer tasks in appropriate story phase
|
||||
|
||||
4. **From Setup/Infrastructure**:
|
||||
- Shared infrastructure → Setup phase (Phase 1)
|
||||
- Foundational/blocking tasks → Foundational phase (Phase 2)
|
||||
- Story-specific setup → within that story's phase
|
||||
|
||||
### Phase Structure
|
||||
|
||||
- **Phase 1**: Setup (project initialization)
|
||||
- **Phase 2**: Foundational (blocking prerequisites - MUST complete before user stories)
|
||||
- **Phase 3+**: User Stories in priority order (P1, P2, P3...)
|
||||
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
|
||||
- Each phase should be a complete, independently testable increment
|
||||
- **Final Phase**: Polish & Cross-Cutting Concerns
|
||||
30
.claude/commands/speckit.taskstoissues.md
Normal file
30
.claude/commands/speckit.taskstoissues.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
|
||||
tools: ['github/github-mcp-server/issue_write']
|
||||
---
|
||||
|
||||
## User Input
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
## Outline
|
||||
|
||||
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
1. From the executed script, extract the path to **tasks**.
|
||||
1. Get the Git remote by running:
|
||||
|
||||
```bash
|
||||
git config --get remote.origin.url
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
|
||||
|
||||
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
|
||||
|
||||
> [!CAUTION]
|
||||
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
|
||||
@@ -16,7 +16,7 @@ cd "$CLAUDE_PROJECT_DIR/frontend"
|
||||
ERRORS=""
|
||||
|
||||
# Type-check
|
||||
if OUTPUT=$(npx vue-tsc --noEmit 2>&1); then
|
||||
if OUTPUT=$(npm run type-check 2>&1); then
|
||||
:
|
||||
else
|
||||
ERRORS+="Type-check failed:\n$OUTPUT\n\n"
|
||||
|
||||
22
.claude/hooks/openapi-validate.sh
Executable file
22
.claude/hooks/openapi-validate.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Read hook input from stdin (JSON with tool_input.file_path)
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
|
||||
|
||||
# Only run for OpenAPI spec files
|
||||
case "$FILE_PATH" in
|
||||
*/openapi/*.yaml|*/openapi/*.yml) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
cd "$CLAUDE_PROJECT_DIR/backend"
|
||||
|
||||
# Run validation (zero-config: structural validity only)
|
||||
if OUTPUT=$(npx @redocly/cli@latest lint src/main/resources/openapi/api.yaml --format=stylish 2>&1); then
|
||||
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ OpenAPI spec validation passed."}}'
|
||||
else
|
||||
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
|
||||
fi
|
||||
@@ -14,7 +14,7 @@ fi
|
||||
|
||||
# Check for uncommitted changes in backend/frontend source
|
||||
HAS_BACKEND=$(git status --porcelain backend/src/ 2>/dev/null | head -1)
|
||||
HAS_FRONTEND=$(git status --porcelain frontend/src/ 2>/dev/null | head -1)
|
||||
HAS_FRONTEND=$(git status --porcelain frontend/src/ frontend/e2e/ 2>/dev/null | head -1)
|
||||
|
||||
# Nothing changed -- skip
|
||||
if [[ -z "$HAS_BACKEND" && -z "$HAS_FRONTEND" ]]; then
|
||||
@@ -26,7 +26,7 @@ PASSED=""
|
||||
|
||||
# Run backend tests if Java sources changed
|
||||
if [[ -n "$HAS_BACKEND" ]]; then
|
||||
if OUTPUT=$(cd backend && ./mvnw test -q 2>&1); then
|
||||
if OUTPUT=$(cd backend && ./mvnw verify -q 2>&1); then
|
||||
PASSED+="✓ Backend tests passed. "
|
||||
else
|
||||
# Filter: only [ERROR] lines, skip Maven boilerplate
|
||||
@@ -38,9 +38,15 @@ fi
|
||||
# Run frontend tests if TS/Vue sources changed
|
||||
if [[ -n "$HAS_FRONTEND" ]]; then
|
||||
if OUTPUT=$(cd frontend && npm run test:unit -- --run 2>&1); then
|
||||
PASSED+="✓ Frontend tests passed. "
|
||||
PASSED+="✓ Frontend unit tests passed. "
|
||||
else
|
||||
ERRORS+="Frontend tests failed:\n$OUTPUT\n\n"
|
||||
ERRORS+="Frontend unit tests failed:\n$OUTPUT\n\n"
|
||||
fi
|
||||
|
||||
if OUTPUT=$(cd frontend && npm run test:e2e 2>&1); then
|
||||
PASSED+="✓ Frontend E2E tests passed. "
|
||||
else
|
||||
ERRORS+="Frontend E2E tests failed:\n$OUTPUT\n\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/frontend-check.sh\"",
|
||||
"timeout": 120
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/openapi-validate.sh\"",
|
||||
"timeout": 120
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
94
.claude/skills/merge-pr/SKILL.md
Normal file
94
.claude/skills/merge-pr/SKILL.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: merge-pr
|
||||
description: Create a Gitea pull request, monitor CI pipeline status, and merge when green. Use this skill when the user asks to "create a PR", "merge the PR", "ship it", "make it ready to merge", or when you need to open a pull request and wait for CI before merging. Also use when asked to check CI/PR status on Gitea.
|
||||
---
|
||||
|
||||
# Merge PR
|
||||
|
||||
Create a pull request on Gitea, monitor the CI pipeline via the Actions API, and merge once all jobs pass.
|
||||
|
||||
## Why this skill exists
|
||||
|
||||
The Gitea MCP pull request API does not return CI status directly. To know if a PR is ready to merge, you must cross-reference the PR's `head.sha` with the Actions runs API, find the matching run, and check job conclusions. This skill encodes that workflow so it doesn't have to be rediscovered.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The Gitea MCP tools must be available. The key tools are:
|
||||
|
||||
- `mcp__gitea__pull_request_write` (method: `create`, `merge`)
|
||||
- `mcp__gitea__pull_request_read` (method: `get`)
|
||||
- `mcp__gitea__actions_run_read` (methods: `list_runs`, `list_run_jobs`)
|
||||
|
||||
If these tools are not yet loaded, use ToolSearch to discover and load them before proceeding.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Create the PR
|
||||
|
||||
Use `mcp__gitea__pull_request_write` with method `create`. Include a clear title, body with summary and test plan, head branch, and base branch (usually `master`).
|
||||
|
||||
Save the returned `head.sha` — you need it to find the CI run.
|
||||
|
||||
### 2. Find the CI run for the PR
|
||||
|
||||
The Actions API has no direct "get CI status for PR" call. Instead:
|
||||
|
||||
```
|
||||
mcp__gitea__actions_run_read(method: "list_runs", owner, repo, perPage: 5)
|
||||
```
|
||||
|
||||
Find the run whose `head_sha` matches the PR's `head.sha`. This is the CI run triggered by the push that the PR points to. If the branch was force-pushed or new commits were added, always match against the latest `head.sha` from a fresh `get` on the PR.
|
||||
|
||||
### 3. Monitor job status
|
||||
|
||||
Once you have the run ID:
|
||||
|
||||
```
|
||||
mcp__gitea__actions_run_read(method: "list_run_jobs", owner, repo, run_id: <id>)
|
||||
```
|
||||
|
||||
This returns all jobs with their `status` (queued/in_progress/completed) and `conclusion` (success/failure/skipped/null).
|
||||
|
||||
Present a status table to the user:
|
||||
|
||||
| Job | Status |
|
||||
|-----|--------|
|
||||
| backend-test | success |
|
||||
| frontend-test | in_progress |
|
||||
| frontend-e2e | queued |
|
||||
|
||||
If jobs are still running, wait ~30 seconds and check again. Don't poll in a tight loop.
|
||||
|
||||
### 4. Handle failures
|
||||
|
||||
If any job has `conclusion: failure`:
|
||||
- Use `mcp__gitea__actions_run_read` with method `get_job_log_preview` to fetch the failing job's log
|
||||
- Report the failure to the user with relevant log output
|
||||
- Do NOT attempt to merge
|
||||
|
||||
### 5. Merge when green
|
||||
|
||||
Once all jobs show `conclusion: success` (or `skipped` for conditional jobs like `build-and-publish`):
|
||||
|
||||
```
|
||||
mcp__gitea__pull_request_write(
|
||||
method: "merge",
|
||||
owner, repo,
|
||||
index: <pr_number>,
|
||||
merge_style: "merge",
|
||||
delete_branch: true
|
||||
)
|
||||
```
|
||||
|
||||
Ask the user for confirmation before merging. They may want to review the PR in the web UI first.
|
||||
|
||||
### 6. Post-merge cleanup
|
||||
|
||||
After a successful merge, suggest:
|
||||
- `git checkout master && git pull origin master`
|
||||
- `git branch -d <feature-branch>` (local cleanup)
|
||||
- Tagging a release if appropriate (see `/release` skill)
|
||||
|
||||
## Abbreviated flow
|
||||
|
||||
When the user just wants a quick status check (e.g. "how's the PR?"), skip straight to steps 2-3: find the run by SHA, show the job status table.
|
||||
66
.claude/skills/release/SKILL.md
Normal file
66
.claude/skills/release/SKILL.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: release
|
||||
description: Create a SemVer release tag and push it to trigger the CI/CD pipeline. Use this skill when the user says "release", "tag a release", "publish version X.Y.Z", "create a release", or mentions pushing a version tag. Also trigger when the user says "/release".
|
||||
---
|
||||
|
||||
# Release
|
||||
|
||||
Create a SemVer git tag and push it to the Gitea remote, triggering the CI/CD pipeline which builds and publishes a Docker image.
|
||||
|
||||
## How it works
|
||||
|
||||
The project uses a Gitea Actions pipeline (`.gitea/workflows/ci.yaml`) with three jobs:
|
||||
|
||||
- **backend-test**: JDK 25, `./mvnw -B verify`
|
||||
- **frontend-test**: Node 24, lint + type-check + tests + build
|
||||
- **build-and-publish**: Docker image build + push (only on SemVer tags)
|
||||
|
||||
When a tag matching `X.Y.Z` is pushed, `build-and-publish` runs after both test jobs pass. It publishes the Docker image to the Gitea container registry with four rolling tags: `X.Y.Z`, `X.Y`, `X`, and `latest`.
|
||||
|
||||
## Arguments
|
||||
|
||||
The user provides a version string as argument, e.g. `/release 0.2.0`. If no version is provided, ask for one.
|
||||
|
||||
## Workflow
|
||||
|
||||
Execute these steps in order. Stop and report if any check fails.
|
||||
|
||||
### 1. Validate the version
|
||||
|
||||
The argument must be a valid SemVer string: `X.Y.Z` where X, Y, Z are non-negative integers. Reject anything else (no `v` prefix, no pre-release suffixes).
|
||||
|
||||
### 2. Check for existing tag
|
||||
|
||||
Run `git tag -l <version>`. If the tag already exists locally or on the remote, stop and tell the user.
|
||||
|
||||
### 3. Check working tree is clean
|
||||
|
||||
Run `git status --porcelain`. If there is any output, stop and warn the user about uncommitted changes. List what's dirty.
|
||||
|
||||
### 4. Check all commits are pushed
|
||||
|
||||
Compare `git rev-parse HEAD` with `git rev-parse @{upstream}`. If they differ, stop and warn the user that there are unpushed local commits. Show `git log @{upstream}..HEAD --oneline` so they can see what's pending.
|
||||
|
||||
### 5. Confirm with the user
|
||||
|
||||
Before creating the tag, show a summary and ask for confirmation:
|
||||
- Version to tag: `<version>`
|
||||
- Commit being tagged: short hash + subject line
|
||||
- What will happen: pipeline runs tests, then builds and publishes `<registry>/<owner>/fete:<version>` (plus rolling tags)
|
||||
|
||||
### 6. Create and push the tag
|
||||
|
||||
```bash
|
||||
git tag <version>
|
||||
git push origin <version>
|
||||
```
|
||||
|
||||
### 7. Report success
|
||||
|
||||
Tell the user:
|
||||
- Tag `<version>` pushed successfully
|
||||
- The CI/CD pipeline is now running
|
||||
- They can watch the progress in the Gitea Actions UI
|
||||
- Once complete, the image will be available as `docker pull <registry>/<owner>/fete:<version>`
|
||||
|
||||
Do not wait for the pipeline to finish or poll its status.
|
||||
@@ -7,10 +7,10 @@ jobs:
|
||||
backend-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 25
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
frontend-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node 24
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -46,12 +46,41 @@ jobs:
|
||||
- name: Production build
|
||||
run: cd frontend && npm run build
|
||||
|
||||
frontend-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd frontend && npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: cd frontend && npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E tests
|
||||
run: cd frontend && npm run test:e2e
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
build-and-publish:
|
||||
needs: [backend-test, frontend-test]
|
||||
needs: [backend-test, frontend-test, frontend-e2e]
|
||||
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '.')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Parse SemVer tag
|
||||
id: semver
|
||||
@@ -74,19 +103,35 @@ jobs:
|
||||
REGISTRY="${REGISTRY#http://}"
|
||||
REPO="${{ github.repository }}"
|
||||
IMAGE="${REGISTRY}/${REPO}"
|
||||
buildah bud -t "${IMAGE}:${{ steps.semver.outputs.full }}" .
|
||||
buildah tag "${IMAGE}:${{ steps.semver.outputs.full }}" \
|
||||
"${IMAGE}:${{ steps.semver.outputs.minor }}" \
|
||||
"${IMAGE}:${{ steps.semver.outputs.major }}" \
|
||||
"${IMAGE}:latest"
|
||||
docker build -t "${IMAGE}:${{ steps.semver.outputs.full }}" .
|
||||
docker tag "${IMAGE}:${{ steps.semver.outputs.full }}" "${IMAGE}:${{ steps.semver.outputs.minor }}"
|
||||
docker tag "${IMAGE}:${{ steps.semver.outputs.full }}" "${IMAGE}:${{ steps.semver.outputs.major }}"
|
||||
docker tag "${IMAGE}:${{ steps.semver.outputs.full }}" "${IMAGE}:latest"
|
||||
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Push to registry
|
||||
run: |
|
||||
buildah login -u "${{ github.repository_owner }}" \
|
||||
-p "${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"${IMAGE%%/*}"
|
||||
buildah push "${IMAGE}:${{ steps.semver.outputs.full }}"
|
||||
buildah push "${IMAGE}:${{ steps.semver.outputs.minor }}"
|
||||
buildah push "${IMAGE}:${{ steps.semver.outputs.major }}"
|
||||
buildah push "${IMAGE}:latest"
|
||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login -u "${{ github.repository_owner }}" --password-stdin "${IMAGE%%/*}"
|
||||
docker push "${IMAGE}:${{ steps.semver.outputs.full }}"
|
||||
docker push "${IMAGE}:${{ steps.semver.outputs.minor }}"
|
||||
docker push "${IMAGE}:${{ steps.semver.outputs.major }}"
|
||||
docker push "${IMAGE}:latest"
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
PREV_TAG=$(git tag --sort=-v:refname | sed -n '2p')
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
git log --oneline --no-merges > RELEASE_NOTES.md
|
||||
else
|
||||
git log --oneline --no-merges "${PREV_TAG}..HEAD" > RELEASE_NOTES.md
|
||||
fi
|
||||
echo "Container image: \`${IMAGE}:${{ steps.semver.outputs.full }}\`" >> RELEASE_NOTES.md
|
||||
|
||||
- name: Create Gitea release
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: v${{ steps.semver.outputs.full }}
|
||||
body_path: RELEASE_NOTES.md
|
||||
token: ${{ github.token }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -9,8 +9,13 @@ Thumbs.db
|
||||
|
||||
# Claude Code (machine-local)
|
||||
.claude/settings.local.json
|
||||
.mcp.json
|
||||
.rodney/
|
||||
.agent-tests/
|
||||
.ralph/*/iteration-*.jsonl
|
||||
|
||||
# Test results (Playwright artifacts)
|
||||
test-results/
|
||||
|
||||
# Java/Maven
|
||||
*.class
|
||||
@@ -49,6 +54,9 @@ npm-debug.log*
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Spring Boot local profile (developer-specific, not committed)
|
||||
backend/src/main/resources/application-local.properties
|
||||
|
||||
# Editor swap files
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
3
.ralph/speckit-migration/answers.md
Normal file
3
.ralph/speckit-migration/answers.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Answers
|
||||
|
||||
<!-- Human answers to open questions. Ralph processes these one per iteration. -->
|
||||
11
.ralph/speckit-migration/chief-wiggum.md
Normal file
11
.ralph/speckit-migration/chief-wiggum.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Chief Wiggum's Notes
|
||||
|
||||
<!-- This file is written by the Chief Wiggum session. Ralph reads it but never modifies it. -->
|
||||
|
||||
## Action Required
|
||||
|
||||
(No action items.)
|
||||
|
||||
## Observations
|
||||
|
||||
(No observations.)
|
||||
298
.ralph/speckit-migration/instructions.md
Normal file
298
.ralph/speckit-migration/instructions.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Ralph Loop — Migrate project artifacts to spec-kit format
|
||||
|
||||
Ralph migrates existing project documentation from `spec/`, `docs/agents/`, and `CLAUDE.md` into the spec-kit directory structure. Cross-cutting project knowledge goes to `.specify/memory/`. Feature-specific docs go to `specs/[feature-name]/` with `spec.md`, `research.md`, and `plan.md` files following the spec-kit templates.
|
||||
|
||||
Each iteration migrates exactly ONE file. No batching.
|
||||
|
||||
## CRITICAL RULE: One Task Per Iteration
|
||||
|
||||
You MUST perform exactly ONE task per iteration. Not two, not "a few small ones", not "all remaining items". ONE.
|
||||
|
||||
After completing your single task:
|
||||
1. Append a short summary of what you did to `{{RUN_DIR}}/progress.txt`.
|
||||
2. Stop. Do not look for more work. Do not "while I'm at it" anything.
|
||||
|
||||
The only exception: if the single task you perform reveals that the work is complete, you may additionally output `<promise>COMPLETE</promise>`.
|
||||
|
||||
## Startup: Read Project State
|
||||
|
||||
At the start of every iteration, read these files in order:
|
||||
|
||||
1. `{{RUN_DIR}}/progress.txt` — what previous iterations did (your memory across iterations).
|
||||
2. `{{RUN_DIR}}/chief-wiggum.md` — notes from Chief Wiggum. Items under `## Action Required` have highest priority.
|
||||
3. `{{RUN_DIR}}/answers.md` — check if the human answered any open questions.
|
||||
4. `{{RUN_DIR}}/questions.md` — open and resolved questions.
|
||||
5. `CLAUDE.md` — project statutes and principles.
|
||||
6. `.specify/memory/constitution.md` — the project constitution (already migrated).
|
||||
7. `.specify/templates/spec-template.md` — target format for feature specs.
|
||||
|
||||
Do NOT read all source files upfront. Only read the specific source file needed for the current task.
|
||||
|
||||
## Task Selection (Priority Order)
|
||||
|
||||
Pick the FIRST applicable task from this list. Do that ONE task, then stop.
|
||||
|
||||
### Priority 1: Chief Wiggum action items
|
||||
If `{{RUN_DIR}}/chief-wiggum.md` has items under `## Action Required`, address the FIRST one that hasn't been addressed yet (check `{{RUN_DIR}}/progress.txt`). Do NOT modify `{{RUN_DIR}}/chief-wiggum.md`.
|
||||
|
||||
### Priority 2: Process answers
|
||||
If `{{RUN_DIR}}/answers.md` contains an answer, process it. Remove the processed entry from `{{RUN_DIR}}/answers.md`.
|
||||
|
||||
### Priority 3: Execute next migration task
|
||||
Check `{{RUN_DIR}}/progress.txt` to see which tasks are already done. Find the first task from the Migration Task List (below) that has NOT been logged as completed in progress.txt. Execute that one task, then stop.
|
||||
|
||||
### Priority 4: Update CLAUDE.md references
|
||||
After all migration tasks are complete, update path references in `CLAUDE.md` to point to the new locations. Specifically:
|
||||
- Change `spec/design-system.md` references to `.specify/memory/design-system.md`
|
||||
- Change `spec/` references to `specs/` or `.specify/memory/` as appropriate
|
||||
- Change `docs/agents/research/` and `docs/agents/plan/` references to `specs/[feature]/`
|
||||
- Update the "Agent Documentation" section to reflect the new structure
|
||||
- Do NOT change project statutes, build commands, or anything not related to file paths
|
||||
|
||||
### Priority 5: Cleanup
|
||||
After CLAUDE.md is updated, delete the now-empty old directories and migrated files:
|
||||
- Remove `Ideen.md` (migrated to `.specify/memory/ideen.md`)
|
||||
- Remove `spec/` directory (all content has been migrated)
|
||||
- Remove `docs/agents/` directory (all content has been migrated)
|
||||
- Do NOT remove `docs/` itself if other content exists there
|
||||
|
||||
### Priority 6: Complete
|
||||
If all migration tasks, CLAUDE.md update, and cleanup are done:
|
||||
Output `<promise>COMPLETE</promise>` and stop.
|
||||
|
||||
---
|
||||
|
||||
## Migration Task List
|
||||
|
||||
Execute these in order, one per iteration. For each task, read the source file, transform it to the target format, and write the result.
|
||||
|
||||
### Phase A: Cross-cutting project docs to `.specify/memory/`
|
||||
|
||||
These files contain project-level knowledge that isn't specific to one feature. Copy them to `.specify/memory/` with minimal reformatting. Preserve all content — do not summarize or cut.
|
||||
|
||||
**A0** — `Ideen.md` -> `.specify/memory/ideen.md`
|
||||
Copy as-is. This is the original brainstorming/founding document of the project (in German). Preserve completely.
|
||||
|
||||
**A1** — `spec/personas.md` -> `.specify/memory/personas.md`
|
||||
Copy as-is. No format changes needed.
|
||||
|
||||
**A2** — `spec/design-system.md` -> `.specify/memory/design-system.md`
|
||||
Copy as-is. No format changes needed.
|
||||
|
||||
**A3** — `spec/implementation-phases.md` -> `.specify/memory/implementation-phases.md`
|
||||
Copy as-is. No format changes needed.
|
||||
|
||||
### Phase B: Cross-cutting research to `.specify/memory/research/`
|
||||
|
||||
These research docs cover topics that apply across multiple features. Move them with date prefix removed from filename.
|
||||
|
||||
**B1** — `docs/agents/research/2026-03-04-backpressure-agentic-coding.md` -> `.specify/memory/research/backpressure-agentic-coding.md`
|
||||
Copy as-is.
|
||||
|
||||
**B2** — `docs/agents/research/2026-03-04-api-first-approach.md` -> `.specify/memory/research/api-first-approach.md`
|
||||
Copy as-is.
|
||||
|
||||
**B3** — `docs/agents/research/2026-03-04-datetime-best-practices.md` -> `.specify/memory/research/datetime-best-practices.md`
|
||||
Copy as-is.
|
||||
|
||||
**B4** — `docs/agents/research/2026-03-04-rfc9457-problem-details.md` -> `.specify/memory/research/rfc9457-problem-details.md`
|
||||
Copy as-is.
|
||||
|
||||
**B5** — `docs/agents/research/2026-03-04-sans-serif-fonts.md` -> `.specify/memory/research/sans-serif-fonts.md`
|
||||
Copy as-is.
|
||||
|
||||
**B6** — `docs/agents/research/2026-03-04-openapi-validation-pipeline.md` -> `.specify/memory/research/openapi-validation-pipeline.md`
|
||||
Copy as-is.
|
||||
|
||||
**B7** — `docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md` -> `.specify/memory/research/e2e-testing-playwright.md`
|
||||
Copy as-is.
|
||||
|
||||
### Phase C: Cross-cutting plans to `.specify/memory/plans/`
|
||||
|
||||
**C1** — `docs/agents/plan/2026-03-04-backpressure-agentic-coding.md` -> `.specify/memory/plans/backpressure-agentic-coding.md`
|
||||
Copy as-is.
|
||||
|
||||
**C2** — `docs/agents/plan/2026-03-05-e2e-testing-playwright-setup.md` -> `.specify/memory/plans/e2e-testing-playwright-setup.md`
|
||||
Copy as-is.
|
||||
|
||||
### Phase D: Setup tasks — extract specs + relocate research/plans
|
||||
|
||||
Each setup task from `spec/setup-tasks.md` becomes its own feature directory under `specs/`. Read the setup-tasks.md file, extract the section for the specific task, and reformat it into a `spec.md` following the spec-kit template structure. Then relocate the corresponding research and plan docs.
|
||||
|
||||
For the spec.md conversion: map the existing acceptance criteria to the spec-kit "User Scenarios & Testing" section as acceptance scenarios. Map the task description to "Requirements". Keep all checkmark states (completed criteria). Add a note at the top indicating this is a setup task (infrastructure), not a user-facing feature.
|
||||
|
||||
**D1** — Extract T-1 from `spec/setup-tasks.md` -> `specs/t-01-monorepo-setup/spec.md`
|
||||
Read `spec/setup-tasks.md`, extract the T-1 section, reformat to spec-kit spec template.
|
||||
|
||||
**D2** — `docs/agents/research/2026-03-04-t1-monorepo-setup.md` -> `specs/t-01-monorepo-setup/research.md`
|
||||
Copy as-is.
|
||||
|
||||
**D3** — `docs/agents/plan/2026-03-04-t1-monorepo-setup.md` -> `specs/t-01-monorepo-setup/plan.md`
|
||||
Copy as-is.
|
||||
|
||||
**D4** — Extract T-2 from `spec/setup-tasks.md` -> `specs/t-02-docker-deployment/spec.md`
|
||||
|
||||
**D5** — `docs/agents/research/2026-03-04-spa-springboot-docker-patterns.md` -> `specs/t-02-docker-deployment/research.md`
|
||||
Copy as-is.
|
||||
|
||||
**D6** — `docs/agents/plan/2026-03-04-t2-docker-deployment.md` -> `specs/t-02-docker-deployment/plan.md`
|
||||
Copy as-is.
|
||||
|
||||
**D7** — Extract T-3 from `spec/setup-tasks.md` -> `specs/t-03-cicd-pipeline/spec.md`
|
||||
|
||||
**D8** — `docs/agents/research/2026-03-04-t3-cicd-pipeline.md` -> `specs/t-03-cicd-pipeline/research.md`
|
||||
Copy as-is.
|
||||
|
||||
**D9** — `docs/agents/plan/2026-03-04-t3-cicd-pipeline.md` -> `specs/t-03-cicd-pipeline/plan.md`
|
||||
Copy as-is.
|
||||
|
||||
**D10** — Extract T-4 from `spec/setup-tasks.md` -> `specs/t-04-dev-infrastructure/spec.md`
|
||||
|
||||
**D11** — `docs/agents/research/2026-03-04-t4-development-infrastructure.md` -> `specs/t-04-dev-infrastructure/research.md`
|
||||
Copy as-is.
|
||||
|
||||
**D12** — `docs/agents/plan/2026-03-04-t4-development-infrastructure.md` -> `specs/t-04-dev-infrastructure/plan.md`
|
||||
Copy as-is.
|
||||
|
||||
**D13** — Extract T-5 from `spec/setup-tasks.md` -> `specs/t-05-jpa-database/spec.md`
|
||||
T-5 may not have research/plan docs yet. Only create the spec.md.
|
||||
|
||||
### Phase E: User stories — extract specs
|
||||
|
||||
Each user story from `spec/userstories.md` becomes its own feature directory. Read the userstories.md file, extract the section for the specific user story, and reformat it into a `spec.md` following the spec-kit template structure. Map acceptance criteria to acceptance scenarios. Map the story description to requirements. Preserve all checkmark states.
|
||||
|
||||
**E1** — Extract US-1 from `spec/userstories.md` -> `specs/us-01-create-event/spec.md`
|
||||
|
||||
**E2** — `docs/agents/research/2026-03-04-us1-create-event.md` -> `specs/us-01-create-event/research.md`
|
||||
Copy as-is.
|
||||
|
||||
**E3** — `docs/agents/plan/2026-03-04-us1-create-event.md` -> `specs/us-01-create-event/plan.md`
|
||||
Copy as-is.
|
||||
|
||||
**E4** — `docs/agents/plan/2026-03-05-us1-review-fixes.md` -> `specs/us-01-create-event/plan-review-fixes.md`
|
||||
Copy as-is. This is a supplementary plan doc for US-1.
|
||||
|
||||
**E5** — `docs/agents/plan/2026-03-05-us1-post-review-fixes.md` -> `specs/us-01-create-event/plan-post-review-fixes.md`
|
||||
Copy as-is. This is a supplementary plan doc for US-1.
|
||||
|
||||
**E6** — Extract US-2 from `spec/userstories.md` -> `specs/us-02-view-event/spec.md`
|
||||
|
||||
**E7** — Extract US-3 from `spec/userstories.md` -> `specs/us-03-rsvp/spec.md`
|
||||
|
||||
**E8** — Extract US-4 from `spec/userstories.md` -> `specs/us-04-guest-list/spec.md`
|
||||
|
||||
**E9** — Extract US-5 from `spec/userstories.md` -> `specs/us-05-edit-event/spec.md`
|
||||
|
||||
**E10** — Extract US-6 from `spec/userstories.md` -> `specs/us-06-calendar-export/spec.md`
|
||||
|
||||
**E11** — Extract US-7 from `spec/userstories.md` -> `specs/us-07-local-event-overview/spec.md`
|
||||
|
||||
**E12** — Extract US-8 from `spec/userstories.md` -> `specs/us-08-comments/spec.md`
|
||||
|
||||
**E13** — Extract US-9 from `spec/userstories.md` -> `specs/us-09-reminders/spec.md`
|
||||
|
||||
**E14** — Extract US-10a from `spec/userstories.md` -> `specs/us-10a-organizer-updates/spec.md`
|
||||
|
||||
**E15** — Extract US-10b from `spec/userstories.md` -> `specs/us-10b-guest-notifications/spec.md`
|
||||
|
||||
**E16** — Extract US-11 from `spec/userstories.md` -> `specs/us-11-qr-code/spec.md`
|
||||
|
||||
**E17** — Extract US-12 from `spec/userstories.md` -> `specs/us-12-recurring-events/spec.md`
|
||||
|
||||
**E18** — Extract US-13 from `spec/userstories.md` -> `specs/us-13-plus-one/spec.md`
|
||||
|
||||
**E19** — Extract US-14 from `spec/userstories.md` -> `specs/us-14-waitlist/spec.md`
|
||||
|
||||
**E20** — Extract US-15 from `spec/userstories.md` -> `specs/us-15-color-themes/spec.md`
|
||||
|
||||
**E21** — Extract US-16 from `spec/userstories.md` -> `specs/us-16-header-image/spec.md`
|
||||
|
||||
**E22** — Extract US-17 from `spec/userstories.md` -> `specs/us-17-dark-mode/spec.md`
|
||||
|
||||
**E23** — Extract US-18 from `spec/userstories.md` -> `specs/us-18-cancel-event/spec.md`
|
||||
|
||||
**E24** — Extract US-19 from `spec/userstories.md` -> `specs/us-19-delete-event/spec.md`
|
||||
|
||||
---
|
||||
|
||||
## Spec Conversion Guidelines
|
||||
|
||||
When converting a user story or setup task to a spec-kit `spec.md`, use this structure:
|
||||
|
||||
```markdown
|
||||
# Feature Specification: [Title]
|
||||
|
||||
**Feature**: `[id-kebab-case]`
|
||||
**Created**: 2026-03-06
|
||||
**Status**: [Draft | Approved | Implemented]
|
||||
**Source**: Migrated from spec/userstories.md (or spec/setup-tasks.md)
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 - [Title] (Priority: P1)
|
||||
|
||||
[Story description from the original]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** ..., **When** ..., **Then** ...
|
||||
[Map each acceptance criterion to a Given/When/Then scenario]
|
||||
|
||||
### Edge Cases
|
||||
|
||||
[Extract from original if present, otherwise mark as [NEEDS EXPANSION]]
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
[Map the story's requirements and acceptance criteria to FR-XXX items]
|
||||
|
||||
## Success Criteria
|
||||
|
||||
[Derive from acceptance criteria. Mark as [NEEDS EXPANSION] if not obvious]
|
||||
```
|
||||
|
||||
For setup tasks that are already completed (all criteria checked off), set Status to `Implemented`.
|
||||
For user stories not yet started, set Status to `Draft`.
|
||||
For US-1 (in progress), set Status to `Approved`.
|
||||
|
||||
## File Ownership
|
||||
|
||||
Respect these boundaries strictly:
|
||||
|
||||
| File | Owner | You may... |
|
||||
|------|-------|------------|
|
||||
| `{{RUN_DIR}}/progress.txt` | Ralph | Read and append |
|
||||
| `{{RUN_DIR}}/questions.md` | Ralph | Read and write |
|
||||
| `{{RUN_DIR}}/answers.md` | Human | **Read only.** Only remove entries you have already processed. |
|
||||
| `{{RUN_DIR}}/chief-wiggum.md` | Chief Wiggum | **Read only.** Never modify. |
|
||||
| `CLAUDE.md` | Ralph (Priority 4 only) | Read always. Write ONLY during Priority 4 (update references). |
|
||||
| `Ideen.md` | Source | **Read only.** Never modify original. |
|
||||
| `spec/*` | Source | **Read only.** Never modify originals. |
|
||||
| `docs/agents/**` | Source | **Read only.** Never modify originals. |
|
||||
| `.specify/memory/**` | Ralph | Read and write (create new files). |
|
||||
| `.specify/templates/**` | System | **Read only.** |
|
||||
| `specs/**` | Ralph | Read and write (create new files). |
|
||||
|
||||
## Handling Uncertainty
|
||||
|
||||
If you encounter ambiguity (e.g. a user story doesn't map cleanly to the spec-kit template, or you're unsure where a cross-cutting research doc belongs), add a question to `{{RUN_DIR}}/questions.md` in this format:
|
||||
|
||||
```markdown
|
||||
## Open
|
||||
|
||||
### Q[N]: [Short question]
|
||||
**Context**: [What you were trying to do]
|
||||
**Options**: [A] ... [B] ...
|
||||
```
|
||||
|
||||
Then skip that task and move to the next one. Come back to skipped tasks after the question is answered.
|
||||
|
||||
## Rules
|
||||
|
||||
- NEVER delete or modify source files (`spec/`, `docs/agents/`). Only create new files. Cleanup happens in Priority 5 after everything is verified.
|
||||
- NEVER invent content. If the original doesn't have enough detail for a section, mark it `[NEEDS EXPANSION]`.
|
||||
- ALWAYS preserve acceptance criteria checkmark states (`- [x]` vs `- [ ]`).
|
||||
- Keep file content in English (project statute).
|
||||
- Do not add emojis.
|
||||
6
.ralph/speckit-migration/meta.md
Normal file
6
.ralph/speckit-migration/meta.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Run: speckit-migration
|
||||
|
||||
- **Created**: 2026-03-06
|
||||
- **Description**: (add description here)
|
||||
- **Model**: sonnet
|
||||
- **Max iterations**: 55
|
||||
146
.ralph/speckit-migration/progress.txt
Normal file
146
.ralph/speckit-migration/progress.txt
Normal file
@@ -0,0 +1,146 @@
|
||||
# Ralph Loop Progress Log
|
||||
# Each iteration appends its findings and decisions here.
|
||||
|
||||
## Iteration 1 — Task A0
|
||||
Migrated `Ideen.md` -> `.specify/memory/ideen.md`. Copied content as-is (German brainstorming/founding document). No modifications.
|
||||
|
||||
## Iteration 2 — Task A1
|
||||
Migrated `spec/personas.md` -> `.specify/memory/personas.md`. Copied content as-is. No modifications.
|
||||
|
||||
## Iteration 3 — Task A2
|
||||
Migrated `spec/design-system.md` -> `.specify/memory/design-system.md`. Copied content as-is. No modifications.
|
||||
|
||||
## Iteration 4 — Task A3
|
||||
Migrated `spec/implementation-phases.md` -> `.specify/memory/implementation-phases.md`. Copied content as-is. No modifications.
|
||||
|
||||
## Iteration 5 — Task B1
|
||||
Migrated `docs/agents/research/2026-03-04-backpressure-agentic-coding.md` -> `.specify/memory/research/backpressure-agentic-coding.md`. Copied content as-is. No modifications.
|
||||
|
||||
## Chief Wiggum — Bulk copy (B2-B7, C1-C2, D2-D3, D5-D6, D8-D9, D11-D12, E2-E5)
|
||||
All remaining copy-as-is tasks completed manually. Files copied:
|
||||
- B2-B7: 6 cross-cutting research docs -> `.specify/memory/research/`
|
||||
- C1-C2: 2 cross-cutting plan docs -> `.specify/memory/plans/`
|
||||
- D2,D3: T-1 research+plan -> `specs/t-01-monorepo-setup/`
|
||||
- D5,D6: T-2 research+plan -> `specs/t-02-docker-deployment/`
|
||||
- D8,D9: T-3 research+plan -> `specs/t-03-cicd-pipeline/`
|
||||
- D11,D12: T-4 research+plan -> `specs/t-04-dev-infrastructure/`
|
||||
- E2: US-1 research -> `specs/us-01-create-event/research.md`
|
||||
- E3: US-1 plan -> `specs/us-01-create-event/plan.md`
|
||||
- E4: US-1 review fixes plan -> `specs/us-01-create-event/plan-review-fixes.md`
|
||||
- E5: US-1 post-review fixes plan -> `specs/us-01-create-event/plan-post-review-fixes.md`
|
||||
All target directories created.
|
||||
|
||||
NEXT TASK: D4, D7, D10, D13 (remaining setup task spec extractions)
|
||||
Then: E1, E6-E24 (user story spec extractions)
|
||||
Then: Priority 4 (CLAUDE.md update)
|
||||
Then: Priority 5 (cleanup)
|
||||
|
||||
## Iteration 6 — Task D1
|
||||
Extracted T-1 from `spec/setup-tasks.md` -> `specs/t-01-monorepo-setup/spec.md`. All 6 acceptance criteria checked (Implemented). Reformatted to spec-kit template with acceptance scenarios, functional requirements, and success criteria.
|
||||
|
||||
## Iteration 7 — Task D4
|
||||
Extracted T-2 from `spec/setup-tasks.md` -> `specs/t-02-docker-deployment/spec.md`. All 5 acceptance criteria checked (Implemented). Included scope note from the 2026-03-04 addendum (database config deferred to T-4). Reformatted to spec-kit template.
|
||||
|
||||
## Iteration 8 — Task D7
|
||||
Extracted T-3 from `spec/setup-tasks.md` -> `specs/t-03-cicd-pipeline/spec.md`. All 6 acceptance criteria checked (Implemented). Included Q-5 resolution note about Gitea Actions. Reformatted to spec-kit template.
|
||||
|
||||
## Iteration 9 — Task D10
|
||||
Extracted T-4 from `spec/setup-tasks.md` -> `specs/t-04-dev-infrastructure/spec.md`. All 9 acceptance criteria checked (Implemented). Included addendum about absorption of T-2 scope. Reformatted to spec-kit template with 5 setup task stories covering: database connectivity, env var config, backend test infra, frontend test infra, SPA router.
|
||||
|
||||
## Iteration 10 — Task D13
|
||||
Extracted T-5 from `spec/setup-tasks.md` -> `specs/t-05-jpa-database/spec.md`. All 8 acceptance criteria checked (Implemented). T-5 covers API-first tooling setup (OpenAPI code generation for backend + frontend, minimal spec, API client). No research or plan docs exist for this task. Added note about directory name vs. task content mismatch.
|
||||
|
||||
## Iteration 11 — Task E1
|
||||
Extracted US-1 from `spec/userstories.md` -> `specs/us-01-create-event/spec.md`. Status: Approved (in progress). All 8 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (create with required fields P1, optional fields P2, expiry validation P2), acceptance scenarios, 12 functional requirements, key entities, and 5 success criteria.
|
||||
|
||||
## Iteration 12 — Task E6
|
||||
Extracted US-2 from `spec/userstories.md` -> `specs/us-02-view-event/spec.md`. Status: Draft. All 8 acceptance criteria unchecked. Reformatted to spec-kit template with 4 user stories (view event details P1, expired event P2, cancelled event P2 deferred, event not found P2), acceptance scenarios, 8 functional requirements, key entities, and 5 success criteria.
|
||||
|
||||
## Iteration 13 — Task E7
|
||||
Extracted US-3 from `spec/userstories.md` -> `specs/us-03-rsvp/spec.md`. Status: Draft. All 11 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (submit RSVP P1, re-RSVP from same device P2, RSVP blocked on expired/cancelled events P2), 12 functional requirements, 1 key entity, and 5 success criteria. Noted deduplication mechanism as NEEDS EXPANSION (implementation detail).
|
||||
|
||||
## Iteration 14 — Task E8
|
||||
Extracted US-4 from `spec/userstories.md` -> `specs/us-04-guest-list/spec.md`. Status: Draft. All 7 acceptance criteria unchecked. Reformatted to spec-kit template with 1 user story (view and manage RSVPs P1), 6 acceptance scenarios, 8 functional requirements, 2 key entities, and 5 success criteria.
|
||||
|
||||
## Iteration 15 — Task E9
|
||||
Extracted US-5 from `spec/userstories.md` -> `specs/us-05-edit-event/spec.md`. Status: Draft. All 8 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (edit event details P1, expiry date future-date validation P2, organizer token authentication P2), 4 edge cases, 8 functional requirements, 2 key entities, and 5 success criteria. Noted that visual highlighting of changes is deferred to US-9.
|
||||
|
||||
## Iteration 16 — Task E10
|
||||
Extracted US-6 from `spec/userstories.md` -> `specs/us-06-bookmark-event/spec.md`. Status: Draft. All 7 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (bookmark without RSVP P1, bookmark independent of RSVP state P2, bookmark on expired events P2), 3 edge cases, 8 functional requirements, 1 key entity, and 5 success criteria.
|
||||
NOTE: Migration task list labeled this directory "us-06-calendar-export" but US-6 in userstories.md is "Bookmark an event". Calendar export is US-8. Directory was created as "us-06-bookmark-event" to reflect actual content. The instruction label appears to be an error from an earlier version of the story numbering.
|
||||
|
||||
## Iteration 17 — Task E11
|
||||
Extracted US-7 from `spec/userstories.md` -> `specs/us-07-local-event-overview/spec.md`. Status: Draft. All 11 acceptance criteria unchecked. Reformatted to spec-kit template with 4 user stories (view tracked events P1, distinguish past events P2, remove entry P2, handle deleted event P2), 11 functional requirements, 1 key entity (LocalEventEntry), and 5 success criteria. Noted edge cases for localStorage unavailability, duplicate entries, stale cached data, and large list handling.
|
||||
|
||||
## Iteration 18 — Task E12
|
||||
Extracted US-8 from `spec/userstories.md` -> `specs/us-08-calendar-export/spec.md`. Status: Draft. All 9 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (.ics download P1, webcal:// subscription P2, cancelled event STATUS:CANCELLED P3 deferred until US-18), 3 edge cases, 9 functional requirements, 1 key entity (CalendarFeed — virtual, no independent storage), and 5 success criteria.
|
||||
NOTE: Migration task list labeled this directory "us-08-comments" but US-8 in userstories.md is "Add event to calendar". Directory created as "us-08-calendar-export" to reflect actual content. The label mismatch is consistent with the earlier E10/E16 issue (story numbering shifted).
|
||||
|
||||
## Iteration 20 — Task E14
|
||||
Extracted US-10a from `spec/userstories.md` -> `specs/us-10a-organizer-updates/spec.md`. Status: Draft. All 8 acceptance criteria unchecked. Reformatted to spec-kit template with 2 user stories (post and manage update messages P1, block posting after expiry P2), 4 edge cases, 9 functional requirements, 1 key entity (UpdateMessage), and 5 success criteria. Noted that cancellation does not block posting (only expiry does), which is intentional per story notes.
|
||||
|
||||
## Iteration 19 — Task E13
|
||||
Extracted US-9 from `spec/userstories.md` -> `specs/us-09-highlight-changes/spec.md`. Status: Draft. All 8 acceptance criteria unchecked. Reformatted to spec-kit template with 4 user stories (guest sees highlight for changed fields P1, no highlight on first visit P2, highlight clears after viewing P2, only most recent edit tracked P3), 3 edge cases, 10 functional requirements, 2 key entities (EditMetadata server-side, last_seen_at client-side localStorage), and 5 success criteria.
|
||||
NOTE: Migration task list specified directory "us-09-reminders" but US-9 in userstories.md is "Highlight changed event details". Directory created as "us-09-highlight-changes" to match actual content. Consistent with corrections in iterations 16 and 18.
|
||||
|
||||
## Iteration 22 — Task E16
|
||||
Extracted US-11 from `spec/userstories.md` -> `specs/us-11-qr-code/spec.md`. Status: Draft. All 7 acceptance criteria unchecked. Reformatted to spec-kit template with 1 user story (display and download QR code P1), 7 acceptance scenarios, 8 functional requirements, 1 key entity (QRCode — virtual, generated on demand, not persisted), and 5 success criteria. Confirmed server-side generation requirement (no external QR service) is the key constraint, consistent with privacy statutes.
|
||||
|
||||
## Iteration 21 — Task E15
|
||||
Extracted US-10b from `spec/userstories.md` -> `specs/us-10b-guest-notifications/spec.md`. Status: Draft. All 5 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (unread update indicator P1, first visit no indicator P2, no server read-tracking P1), 3 edge cases, 7 functional requirements, 1 key entity (UpdateReadState — client-side only, localStorage), and 5 success criteria. Noted that `updates_last_seen_at` key is distinct from the `last_seen_at` key used in US-9.
|
||||
|
||||
## Iteration 24 — Task E18
|
||||
Extracted US-13 from `spec/userstories.md` -> `specs/us-13-instance-limit/spec.md`. Status: Draft. All 8 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (enforce cap on creation P1, no limit when unset P2, server-side enforcement only P2), 4 edge cases (MAX_ACTIVE_EVENTS=0, non-integer value, race condition, expired events excluded), 8 functional requirements, 1 key entity (active event count), and 5 success criteria.
|
||||
NOTE: Migration task list labeled this directory "us-13-plus-one" but US-13 in userstories.md is "Limit the number of active events per instance". Directory created as "us-13-instance-limit" to match actual content. Consistent with corrections in iterations 16, 18, 19, and 23.
|
||||
|
||||
## Iteration 25 — Task E19
|
||||
Extracted US-14 from `spec/userstories.md` -> `specs/us-14-pwa/spec.md`. Status: Draft. All 7 acceptance criteria unchecked. Reformatted to spec-kit template with 2 user stories (install app on device P1, serve valid manifest P1), 3 edge cases, 9 functional requirements, 2 key entities (Web App Manifest, Service Worker — both purely frontend), and 5 success criteria.
|
||||
NOTE: Migration task list labeled this directory "us-14-waitlist" but US-14 in userstories.md is "Install as Progressive Web App". Directory created as "us-14-pwa" to match actual content. Consistent with corrections in iterations 16, 18, 19, 23, and 24.
|
||||
|
||||
## Iteration 26 — Task E20
|
||||
Extracted US-15 from `spec/userstories.md` -> `specs/us-15-color-themes/spec.md`. Status: Draft. All 7 acceptance criteria unchecked. Reformatted to spec-kit template with 2 user stories (select theme during creation P1, update theme during editing P2), 3 edge cases (removed theme fallback, dark/light mode interaction, legacy events), 8 functional requirements, 1 key entity (ColorTheme — value type on Event), and 5 success criteria.
|
||||
|
||||
## Iteration 27 — Task E21
|
||||
Extracted US-16 from `spec/userstories.md` -> `specs/us-16-header-image/spec.md`. Status: Draft. All 11 acceptance criteria unchecked. Reformatted to spec-kit template with 4 user stories (select/search image P1, event page renders image P1, graceful degradation without API key P2, image deleted with event P2), 4 edge cases, 10 functional requirements, 1 key entity (HeaderImage — stored on disk, deleted with event), and 5 success criteria. Noted server-proxy requirement as the key privacy constraint (guests never contact Unsplash directly).
|
||||
|
||||
## Iteration 23 — Task E17
|
||||
Extracted US-12 from `spec/userstories.md` -> `specs/us-12-data-deletion/spec.md`. Status: Draft. All 7 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (automatic cleanup P1, expiry date extension delays deletion P2, no early deletion P2), 4 edge cases, 8 functional requirements, 2 key entities (Event expiry_date, Cleanup Job), and 5 success criteria.
|
||||
NOTE: Migration task list labeled this directory "us-12-recurring-events" but US-12 in userstories.md is "Automatic data deletion after expiry date". Directory created as "us-12-data-deletion" to match actual content. Consistent with corrections in iterations 16, 18, and 19.
|
||||
|
||||
## Iteration 28 — Task E22
|
||||
Extracted US-17 from `spec/userstories.md` -> `specs/us-17-dark-mode/spec.md`. Status: Draft. All 8 acceptance criteria unchecked. Reformatted to spec-kit template with 5 user stories (system preference on first visit P1, manual toggle with localStorage persistence P1, toggle accessible from any page P2, dark/light mode does not affect event-level themes P2, WCAG AA contrast for both modes P1), 3 edge cases, 8 functional requirements, 1 key entity (DarkLightPreference — client-side only, localStorage), and 5 success criteria.
|
||||
|
||||
## Iteration 29 — Task E23
|
||||
Extracted US-18 from `spec/userstories.md` -> `specs/us-18-cancel-event/spec.md`. Status: Draft. All 12 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (cancel with optional message P1, adjust expiry date during cancellation P2, edit cancellation message after cancellation P3), 5 edge cases (includes one NEEDS EXPANSION for cancellation on already-expired events), 11 functional requirements, 1 key entity (CancellationState — value type on Event, not a separate entity), and 5 success criteria.
|
||||
|
||||
## Iteration 30 — Task E24
|
||||
Extracted US-19 from `spec/userstories.md` -> `specs/us-19-delete-event/spec.md`. Status: Draft. All 10 acceptance criteria unchecked. Reformatted to spec-kit template with 3 user stories (immediately delete an active event P1, delete a cancelled or expired event P2, reject deletion without valid organizer token P2), 4 edge cases, 9 functional requirements, 2 key entities (Event, OrganizerToken), and 5 success criteria.
|
||||
NOTE: spec/userstories.md also contains US-20 (404 page) which is not listed in the migration task list. This story was added post-US-1 review and is not included in E1-E24. No action taken on US-20 — it is out of scope for the task list as defined.
|
||||
|
||||
NEXT TASK: Priority 4 — Update CLAUDE.md references (all E tasks now complete).
|
||||
|
||||
## Iteration 31 — Priority 4: Update CLAUDE.md references
|
||||
Updated 4 path references in `CLAUDE.md`:
|
||||
1. Methodology section: `spec/setup-tasks.md` or `spec/userstories.md` -> `specs/[feature]/spec.md`
|
||||
2. Design section: `spec/design-system.md` -> `.specify/memory/design-system.md`
|
||||
3. Agent Documentation section: replaced `docs/agents/research/` and `docs/agents/plan/` with new structure (`specs/[feature]/` for feature-specific, `.specify/memory/research/` and `.specify/memory/plans/` for cross-cutting)
|
||||
4. Ralph Loops section: `spec/` -> `specs/` and `.specify/memory/` (cross-cutting docs)
|
||||
|
||||
NEXT TASK: Priority 5 — Cleanup (delete migrated source files and empty old directories).
|
||||
|
||||
## Iteration 32 — Priority 5: Cleanup (blocked by sandbox)
|
||||
|
||||
Attempted to delete migrated source files. All `rm` commands were blocked by the sandbox permission mode (even though files are within the allowed working directory).
|
||||
|
||||
All migration content is complete. The following files/directories still need to be manually deleted by the user:
|
||||
|
||||
```bash
|
||||
rm /home/nitrix/Development/fete/Ideen.md
|
||||
rm -rf /home/nitrix/Development/fete/spec/
|
||||
rm -rf /home/nitrix/Development/fete/docs/agents/
|
||||
rmdir /home/nitrix/Development/fete/docs/ 2>/dev/null || true
|
||||
```
|
||||
|
||||
`docs/` only contains `docs/agents/` so it will be empty after removal and can be deleted.
|
||||
|
||||
All migration tasks (A0-A3, B1-B7, C1-C2, D1-D13, E1-E24) are COMPLETE. CLAUDE.md references have been updated (Priority 4). Only the cleanup of source files remains, requiring manual execution.
|
||||
9
.ralph/speckit-migration/questions.md
Normal file
9
.ralph/speckit-migration/questions.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Questions
|
||||
|
||||
## Open
|
||||
|
||||
(No open questions.)
|
||||
|
||||
## Resolved
|
||||
|
||||
(No resolved questions.)
|
||||
136
.specify/memory/constitution.md
Normal file
136
.specify/memory/constitution.md
Normal file
@@ -0,0 +1,136 @@
|
||||
<!--
|
||||
Sync Impact Report
|
||||
==================
|
||||
Version change: 0.0.0 (template) -> 1.0.0
|
||||
Modified principles: N/A (initial adoption)
|
||||
Added sections:
|
||||
- 6 Core Principles (Privacy, Methodology, API-First, Quality, Dependencies, Accessibility)
|
||||
- Tech Stack & Constraints
|
||||
- Development Workflow
|
||||
- Governance
|
||||
Removed sections: N/A
|
||||
Templates requiring updates:
|
||||
- .specify/templates/plan-template.md: OK (Constitution Check section already generic)
|
||||
- .specify/templates/spec-template.md: OK (no constitution-specific references)
|
||||
- .specify/templates/tasks-template.md: OK (no constitution-specific references)
|
||||
- .specify/templates/checklist-template.md: OK (no constitution-specific references)
|
||||
- .specify/templates/commands/*.md: N/A (no command files exist)
|
||||
Follow-up TODOs: none
|
||||
-->
|
||||
|
||||
# fete Constitution
|
||||
|
||||
## Core Principles
|
||||
|
||||
### I. Privacy by Design
|
||||
|
||||
Privacy is a design constraint, not a feature. It shapes every decision from
|
||||
the start.
|
||||
|
||||
- The system MUST NOT include analytics, telemetry, or tracking of any kind.
|
||||
- The server MUST NOT log PII or IP addresses.
|
||||
- Every feature MUST critically evaluate what data is necessary; only data
|
||||
absolutely required for functionality may be stored.
|
||||
- External dependencies that phone home (CDNs, Google Fonts, tracking-capable
|
||||
libraries) MUST NOT be used.
|
||||
|
||||
### II. Test-Driven Methodology
|
||||
|
||||
Development follows a strict Research -> Spec -> Test -> Implement -> Review
|
||||
sequence. No shortcuts.
|
||||
|
||||
- Tests MUST be written before implementation (TDD). The Red -> Green ->
|
||||
Refactor cycle is strictly enforced.
|
||||
- No implementation code may be written without a specification.
|
||||
- E2E tests are mandatory for every frontend user story.
|
||||
- When a setup task or user story is completed, its acceptance criteria MUST
|
||||
be checked off in the corresponding spec file before committing.
|
||||
|
||||
### III. API-First Development
|
||||
|
||||
The OpenAPI spec (`backend/src/main/resources/openapi/api.yaml`) is the
|
||||
single source of truth for the REST API contract.
|
||||
|
||||
- Endpoints and schemas MUST be defined in the spec first.
|
||||
- Backend interfaces and frontend types MUST be generated from the spec
|
||||
before writing implementation code.
|
||||
- Response schemas MUST include `example:` fields for mock generation and
|
||||
documentation.
|
||||
|
||||
### IV. Simplicity & Quality
|
||||
|
||||
KISS and grugbrain. Engineer it properly, but do not over-engineer.
|
||||
|
||||
- No workarounds. Always fix the root cause, even if it takes longer.
|
||||
- Technical debt MUST be addressed immediately; it MUST NOT accumulate.
|
||||
- Refactoring is permitted freely as long as it does not alter the
|
||||
fundamental architecture.
|
||||
- Every line of code MUST be intentional and traceable to a requirement.
|
||||
No vibe coding.
|
||||
|
||||
### V. Dependency Discipline
|
||||
|
||||
Every dependency is a deliberate, justified decision.
|
||||
|
||||
- A dependency MUST provide substantial value and a significant portion of
|
||||
its features MUST actually be used.
|
||||
- Dependencies MUST be actively maintained and open source (copyleft is
|
||||
acceptable under GPL).
|
||||
- Dependencies that phone home or compromise user privacy MUST NOT be
|
||||
introduced.
|
||||
|
||||
### VI. Accessibility
|
||||
|
||||
Accessibility is a baseline requirement, not an afterthought.
|
||||
|
||||
- All frontend components MUST meet WCAG AA contrast requirements.
|
||||
- Semantic HTML and ARIA attributes MUST be used where appropriate.
|
||||
- The UI MUST be operable via keyboard navigation.
|
||||
|
||||
## Tech Stack & Constraints
|
||||
|
||||
- **Backend:** Java 25 (LTS, SDKMAN), Spring Boot 3.5.x, Maven with wrapper
|
||||
- **Frontend:** Vue 3, TypeScript, Vue Router, Vite, Vitest, ESLint, Prettier
|
||||
- **Testing:** Playwright + MSW for E2E, Vitest for unit tests, JUnit for
|
||||
backend
|
||||
- **Architecture:** Hexagonal (single Maven module, package-level separation),
|
||||
base package `de.fete`
|
||||
- **State management:** Composition API (`ref`/`reactive`) + localStorage;
|
||||
no Pinia
|
||||
- **Database:** No JPA until setup task T-4 is reached
|
||||
- **Design system:** Electric Dusk + Sora (see `.specify/memory/design-system.md`)
|
||||
- **Deployment:** Dockerfile provided; docker-compose example in README
|
||||
|
||||
## Development Workflow
|
||||
|
||||
- Document integrity: when a decision is revised, add an addendum with
|
||||
rationale. Never rewrite or delete the original decision.
|
||||
- The visual design system in `.specify/memory/design-system.md` is authoritative. All
|
||||
frontend implementation MUST follow it.
|
||||
- Feature specs, research, and plans live in `specs/NNN-feature-name/`
|
||||
(spec-kit format). Cross-cutting research goes to
|
||||
`.specify/memory/research/`, cross-cutting plans to
|
||||
`.specify/memory/plans/`.
|
||||
- Conversation and brainstorming in German; code, comments, commits, and
|
||||
documentation in English.
|
||||
- Documentation lives in the README. No wiki, no elaborate docs site.
|
||||
|
||||
## Governance
|
||||
|
||||
This constitution supersedes all ad-hoc practices. It is the authoritative
|
||||
reference for project principles and constraints.
|
||||
|
||||
- **Amendment procedure:** Amendments require documentation of the change,
|
||||
rationale, and an updated version number. The original text MUST be
|
||||
preserved via addendum, not overwritten.
|
||||
- **Versioning:** The constitution follows semantic versioning. MAJOR for
|
||||
principle removals or redefinitions, MINOR for additions or material
|
||||
expansions, PATCH for clarifications and wording fixes.
|
||||
- **Compliance review:** All code changes and architectural decisions MUST
|
||||
be verified against these principles. The plan template's "Constitution
|
||||
Check" gate enforces this before implementation begins.
|
||||
- **Agent governance:** The agent works autonomously on implementation tasks.
|
||||
Architectural decisions, fundamental design questions, tech stack choices,
|
||||
and dependency selections MUST be proposed and approved before proceeding.
|
||||
|
||||
**Version**: 1.0.0 | **Ratified**: 2026-03-06 | **Last Amended**: 2026-03-06
|
||||
85
.specify/memory/design-system.md
Normal file
85
.specify/memory/design-system.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Design System
|
||||
|
||||
This document defines the visual design language for fete. All frontend implementation must follow these specifications.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Mobile-first / App-native feel** — not a classic website. Think installed app, not browser page.
|
||||
- **Desktop:** centered narrow column (max ~480px), gradient background fills the rest.
|
||||
- **Generous whitespace** — elements breathe, nothing cramped.
|
||||
- **WCAG AA contrast** as baseline for all color choices.
|
||||
- **Accessibility is a baseline requirement** — not an afterthought (per project statutes).
|
||||
|
||||
## Color Palette: Electric Dusk
|
||||
|
||||
Chosen for best balance of style, broad appeal, and accessibility.
|
||||
|
||||
| Role | Hex | Description |
|
||||
|--------------------|-----------|--------------------|
|
||||
| Gradient Start | `#F06292` | Pink |
|
||||
| Gradient Mid | `#AB47BC` | Purple |
|
||||
| Gradient End | `#5C6BC0` | Indigo blue |
|
||||
| Accent (CTAs) | `#FF7043` | Deep orange |
|
||||
| Text (light mode) | `#1C1C1E` | Near black |
|
||||
| Text (dark mode) | `#FFFFFF` | White |
|
||||
| Surface (light) | `#FFF5F8` | Pinkish white |
|
||||
| Surface (dark) | `#1B1730` | Deep indigo-black |
|
||||
| Card (light) | `#FFFFFF` | White |
|
||||
| Card (dark) | `#2A2545` | Muted indigo |
|
||||
|
||||
### Primary Gradient
|
||||
|
||||
```css
|
||||
background: linear-gradient(135deg, #F06292 0%, #AB47BC 50%, #5C6BC0 100%);
|
||||
```
|
||||
|
||||
### Usage Rules
|
||||
|
||||
- Gradient for hero/splash areas and page backgrounds — not as direct text background for body copy.
|
||||
- Cards and content areas use solid surface colors with high-contrast text.
|
||||
- Accent color (`#FF7043`) for primary action buttons with dark text (`#1C1C1E`).
|
||||
- White text on gradient mid/end passes WCAG AA (4.82:1 and 4.86:1).
|
||||
- White text on gradient start passes AA-large (3.06:1) — use for headings 18px+ only.
|
||||
|
||||
## Typography: Sora
|
||||
|
||||
Contemporary geometric sans-serif with slightly rounded terminals. Modern and friendly without being childish.
|
||||
|
||||
- **Font:** Sora
|
||||
- **License:** SIL Open Font License 1.1 (OFL)
|
||||
- **Source:** https://github.com/sora-xor/sora-font
|
||||
- **Format:** Self-hosted WOFF2. No external CDN. No Google Fonts.
|
||||
- **Weights:** 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold), 800 (ExtraBold)
|
||||
|
||||
### Weight Usage
|
||||
|
||||
| Context | Weight | Size guideline |
|
||||
|------------------|--------|-----------------|
|
||||
| Body text | 400 | 0.85–1rem |
|
||||
| Labels | 600–700| 0.8–0.9rem |
|
||||
| Headlines | 700–800| 1.2–1.6rem |
|
||||
| Buttons | 700–800| 1rem |
|
||||
| Small/meta text | 400–500| 0.75–0.85rem |
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Card-Style Form Fields
|
||||
|
||||
- Rounded corners (`border-radius: 14px`)
|
||||
- Generous padding (`0.9rem 1rem`)
|
||||
- White/card-colored background on gradient pages
|
||||
- Subtle shadow (`box-shadow: 0 2px 8px rgba(0,0,0,0.1)`)
|
||||
- Bold label (font-weight 700), regular-weight input text
|
||||
|
||||
### Buttons
|
||||
|
||||
- Rounded corners matching card fields (`border-radius: 14px`)
|
||||
- Accent color background with dark text
|
||||
- Bold/ExtraBold weight (700–800)
|
||||
- Subtle shadow for depth
|
||||
|
||||
### Layout
|
||||
|
||||
- Mobile: full-width content with horizontal padding (~1.2rem)
|
||||
- Desktop: centered column, max-width ~480px, gradient background fills viewport
|
||||
- Vertical spacing between elements: ~0.75rem (compact), ~1.2rem (sections)
|
||||
147
.specify/memory/ideen.md
Normal file
147
.specify/memory/ideen.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# fete
|
||||
|
||||
## Grundsätze
|
||||
* Soll als PWA im Browser laufen
|
||||
* Damit es sich wie eine normale app anfühlt
|
||||
* Soll ein kleiner Helfer sein, den man schnell mal nutzen kann
|
||||
* Ich will es mir selbst hosten können
|
||||
* Das schließt eigentlich schon AI features aus und das ist okay
|
||||
* Privacy als first class citizen
|
||||
* Schon das Produktdesign muss mit privacy im sinn entworfen werden
|
||||
* Keine Registrierung, kein login notwendig (nur z.B. einen code, um den "raum" zu finden oder so)
|
||||
* Alternativ könnten etwaige anfallende Daten auch im local storage gespeichert werden
|
||||
|
||||
## Die Idee
|
||||
Eine alternative zu Facebook Event Gruppen oder Telegram Gruppen, in denen eine Veranstaltung bekanntgegeben wird und Teilnahmen bestätigt werden
|
||||
|
||||
### Zielbild
|
||||
Person erstellt via App eine Veranstaltung und schickt seine Freunden irgendwie via Link eine Einladung. Freunde können zurückmelden, ob sie kommen oder nicht.
|
||||
|
||||
## Gedankensammlung
|
||||
* So ne Art Landingpage zu jedem Event
|
||||
* Ein Link pro Event, den man z.B. in ne WhatsApp-Gruppe werfen kann
|
||||
* Was, wie, wann, wo?
|
||||
* Irgendwie auch Designbar, sofern man das will
|
||||
* RSVP: "Ich komme" (mit Name) / "Ich komme nicht" (optional mit Name)
|
||||
* Wird serverseitig gespeichert + im LocalStorage gemerkt
|
||||
* Duplikatschutz: Kein perfekter Schutz ohne Accounts, aber gegen versehentliche Doppeleinträge reicht Gerätebindung via LocalStorage
|
||||
* Gegen malicious actors (Fake-Namen spammen etc.) kann man ohne Accounts wenig machen — akzeptables Risiko (vgl. spliit)
|
||||
* "Veranstaltung merken/folgen": Rein lokal, kein Serverkontakt, kein Name nötig
|
||||
* Löst das Multi-Geräte-Problem: Am Handy zugesagt, am Laptop einfach "Folgen" drücken
|
||||
* Auch nützlich für Unentschlossene, die sich das Event erstmal merken wollen
|
||||
* View für den Veranstalter:
|
||||
* Updaten der Veranstaltung
|
||||
* Einsicht angemeldete Gäste, kann bei Bedarf Einträge entfernen
|
||||
* Featureideen:
|
||||
* Organisator kann einstellen, ob Attendee-Namensliste öffentlich auf der Event-Seite sichtbar ist (default: nur für Organisator). Wenn öffentlich, muss im RSVP-Bottom-Sheet eine Warnung angezeigt werden, dass der Name öffentlich sichtbar sein wird.
|
||||
* Link-Previews (OpenGraph Meta-Tags): Generische OG-Tags mit App-Branding (z.B. "fete — Du wurdest eingeladen") damit geteilte Links in WhatsApp/Signal/Telegram hübsch aussehen. Keine Event-Daten an Crawler aus Privacy-Gründen. → Eigene User Story.
|
||||
* Kalender-Integration: .ics-Download + optional webcal:// für Live-Updates bei Änderungen
|
||||
* Änderungen zum ursprünglichen Inhalt (z.b. geändertes datum/ort) werden iwi hervorgehoben
|
||||
* Veranstalter kann Updatenachrichten im Event posten, pro Device wird via LocalStorage gemerkt was man schon gesehen hat (Badge/Hervorhebung für neue Updates)
|
||||
* QR Code generieren (z.B. für Plakate/Flyer)
|
||||
* Ablaufdatum als Pflichtfeld, nach dem alle gespeicherten Daten gelöscht werden
|
||||
* Übersichtsliste im LocalStorage: Alle Events die man zugesagt oder gemerkt hat (vgl. spliit)
|
||||
* RSVP editieren: Gast kann seine bestehende Zusage bearbeiten (Name ändern via PUT mit rsvpToken) oder zurückziehen (DELETE mit rsvpToken). Bottom Sheet öffnet sich im Edit-Mode mit pre-filled Name + "Zusage zurückziehen"-Button. Später ergänzen: "Absagen und merken" (Kombination mit 011-bookmark-event). Ausgelagert aus 008-rsvp um den Scope klein zu halten.
|
||||
* Organizer-Gästeliste: Namensliste der Zusagen nur für Organisator sichtbar (über Organizer-Link). Gehört thematisch zu 009-guest-list, nicht zu 008-rsvp.
|
||||
* Sicherheit/Missbrauchsschutz:
|
||||
* Nicht-erratbare Event-Tokens (z.B. UUIDs)
|
||||
* Event-Erstellung ist offen, kein Login/Passwort/Invite-Code nötig
|
||||
* Max aktive Events als serverseitige Konfiguration (env variable)
|
||||
* Honeypot-Felder in Formularen (verstecktes Feld das nur Bots ausfüllen → Anfrage ignorieren)
|
||||
* Abgrenzungskriterien
|
||||
* KEIN Chat
|
||||
* KEIN Discovery Feature über die App: Ohne Zugangslink geht nichts
|
||||
* KEINE Planung des Events! Also kein "wer macht/bringt was?", "was machen wir überhaupt?"
|
||||
|
||||
## Getroffene Designentscheidungen
|
||||
|
||||
Die folgenden Punkte wurden in Diskussionen bereits geklärt und sind verbindlich.
|
||||
|
||||
* RSVP-System:
|
||||
* Ein Link pro Event (NICHT individuelle Einladungslinks pro Person — zu umständlich für den Veranstalter)
|
||||
* Gäste geben beim RSVP einen Namen an, das reicht
|
||||
* Duplikate durch versehentliche Mehrfachanmeldung: LocalStorage-Gerätebindung reicht als Schutz
|
||||
* Bewusste Doppelanmeldung/Spam: Akzeptables Risiko, Veranstalter kann Einträge manuell löschen
|
||||
* Geräte-Sync ohne Account ist nicht sauber lösbar und das ist okay
|
||||
* Missbrauchsschutz:
|
||||
* Rate Limiting: Bewusst rausgelassen — zu viel Infra-Overhead für den Scope
|
||||
* Captcha: Bewusst rausgelassen — entweder Privacy-Problem (Google) oder hässlich
|
||||
* Admin-Passwort/Invite-Code für Event-Erstellung: Bewusst rausgelassen — die App soll organisch weitergegeben werden können
|
||||
* Erfahrungswert: Spliit-Instanz läuft auch komplett offen ohne nennenswerte Probleme
|
||||
* Stattdessen pragmatische Maßnahmen: Nicht-erratbare Tokens, Ablaufdatum als Pflichtfeld, Max Events per Konfiguration, Honeypot-Felder
|
||||
* Zielgruppe:
|
||||
* Primär Freundeskreise, nicht die breite Öffentlichkeit
|
||||
* Trotzdem: Die App hängt im Internet, also muss man grundlegende Absicherung haben
|
||||
* Architektur (bereits entschieden):
|
||||
* SPA + RESTful API Backend, kein SSR
|
||||
* Datenbank: PostgreSQL, wird separat gehostet (nicht im App-Container — der Hoster betreibt seinen eigenen Postgres)
|
||||
* Organizer-Authentifizierung: Zwei separate UUIDs pro Event — ein öffentliches Event-Token (in der URL, für Gäste) und ein geheimes Organizer-Token (in localStorage, für Verwaltung). Interne DB-ID ist ein Implementierungsdetail.
|
||||
* App wird als einzelner Docker-Container ausgeliefert, verbindet sich per Konfiguration (env variable) mit der externen Postgres-Instanz
|
||||
* Techstack:
|
||||
* Backend: Java (neuste LTS Version), Spring Boot, Maven, Hexagonal/Onion Architecture
|
||||
* Frontend: Vue 3 (mit Vite als Bundler, TypeScript, Vue Router)
|
||||
* Architekturentscheidungen die NOCH NICHT getroffen wurden (hier darf nichts eigenmächtig entschieden werden!):
|
||||
* (derzeit keine offenen Architekturentscheidungen)
|
||||
|
||||
## Nicht umgesetzte Feature-Ideen (ehemals Specs 009–026)
|
||||
### 010 – Event bearbeiten
|
||||
Organisator kann Titel, Beschreibung, Datum, Ort und Ablaufdatum ändern.
|
||||
* Formular vorausgefüllt mit aktuellen Werten
|
||||
* Ablaufdatum muss in der Zukunft liegen
|
||||
* Ohne Organizer-Token kein Edit-UI sichtbar
|
||||
|
||||
### 013 – Kalender-Export
|
||||
.ics-Download (RFC 5545) mit Event-Details, optional webcal:// für Live-Updates.
|
||||
* Stabile UID aus Event-Token (Re-Import aktualisiert statt dupliziert)
|
||||
* Bei Absage: STATUS:CANCELLED im .ics
|
||||
* Kein externer Kalenderservice kontaktiert
|
||||
|
||||
### 014 – Änderungen hervorheben
|
||||
Geänderte Felder werden visuell hervorgehoben, wenn der Gast seit der letzten Änderung nicht mehr auf der Seite war.
|
||||
* Server trackt `last_edited_at` + geänderte Feldnamen
|
||||
* Client speichert `last_seen_at` in localStorage
|
||||
* Privacy-freundlich: kein serverseitiges Read-Tracking
|
||||
|
||||
### 015 – Organisator-Updates
|
||||
Organisator kann Textnachrichten im Event posten (Pinnwand-Stil).
|
||||
* Chronologisch sortiert, löschbar durch Organisator
|
||||
* Nach Ablauf kein Posting mehr möglich
|
||||
* Ohne Organizer-Token kein Compose-UI
|
||||
|
||||
### 016 – Gast-Benachrichtigungen
|
||||
Badge/Indikator bei ungelesenen Organisator-Updates, rein clientseitig via localStorage.
|
||||
* Eigener Timestamp `updates_last_seen_at` (getrennt von Feld-Änderungen)
|
||||
* Kein Indikator beim ersten Besuch
|
||||
* Kein serverseitiges Tracking (Privacy)
|
||||
|
||||
### 017 – QR-Code
|
||||
Event-Seite zeigt QR-Code mit der öffentlichen Event-URL.
|
||||
* Serverseitig generiert (kein externer QR-Service)
|
||||
* Download als SVG oder hochauflösendes PNG
|
||||
|
||||
### 020 – PWA
|
||||
Web App Manifest + Service Worker für Installierbarkeit und Offline-Caching.
|
||||
* Standalone-Modus ohne Browser-Chrome
|
||||
* Icon + Name auf Home-Screen
|
||||
* Alle Assets selbstgehostet
|
||||
|
||||
### 021 – Farbthemen
|
||||
Organisator wählt bei Erstellung ein vordefiniertes Farbthema für die Event-Seite.
|
||||
* Nur auf der Gast-Seite angewendet (nicht global)
|
||||
* Änderbar beim Bearbeiten
|
||||
* Unabhängig von Dark/Light Mode
|
||||
|
||||
### 022 – Headerbild
|
||||
Organisator sucht Headerbild über integrierte Unsplash-Suche.
|
||||
* Serverseitig geproxied (Client kontaktiert nie Unsplash)
|
||||
* Bild lokal gespeichert + Unsplash-Attribution
|
||||
* Feature deaktiviert wenn kein API-Key konfiguriert
|
||||
|
||||
### 026 – 404-Seite
|
||||
Catch-all Route für ungültige Pfade mit "Seite nicht gefunden" und Link zur Startseite.
|
||||
* Folgt dem Design System (Electric Dusk + Sora)
|
||||
* WCAG AA konform
|
||||
* Verhindert leere Seiten bei Fehlnavigation
|
||||
|
||||
### 027 - Update der EventListe
|
||||
* Irgendwie ein update der event liste, wenn man sie betritt oder wenn man mit touch die seite nach unten zieht (hier müssen wir noch überlegen, wie wir mit den verschiedenen update fällen umgehen und wie wir das update überhaupt requesten. Ich meine sowas wie: was ist, wenn das event nicht mehr gefunden wurde?)
|
||||
123
.specify/memory/implementation-phases.md
Normal file
123
.specify/memory/implementation-phases.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Implementation Order
|
||||
|
||||
Sequential implementation order for all user stories. No parallelization — one story at a time.
|
||||
|
||||
## Progress Tracker
|
||||
|
||||
- [ ] US-1 Create event
|
||||
- [ ] US-2 View event page
|
||||
- [ ] US-3 RSVP
|
||||
- [ ] US-5 Edit event
|
||||
- [ ] US-4 Manage guest list
|
||||
- [ ] US-18 Cancel event
|
||||
- [ ] US-19 Delete event
|
||||
- [ ] US-12 Auto-cleanup after expiry
|
||||
- [ ] US-13 Limit active events
|
||||
- [ ] US-6 Bookmark event
|
||||
- [ ] US-7 Local event overview
|
||||
- [ ] US-17 Dark/light mode
|
||||
- [ ] US-8 Calendar integration
|
||||
- [ ] US-11 QR code
|
||||
- [ ] US-9 Change highlights
|
||||
- [ ] US-10a Update messages
|
||||
- [ ] US-10b New-update indicator
|
||||
- [ ] US-15 Color themes
|
||||
- [ ] US-16 Unsplash header images
|
||||
- [ ] US-14 PWA install
|
||||
|
||||
## Prerequisites
|
||||
|
||||
All setup tasks (T-1 through T-5) are complete.
|
||||
|
||||
## Order Rationale
|
||||
|
||||
### Increment 1: Minimal Viable Event — US-1, US-2, US-3
|
||||
|
||||
The vertical slice. After these three stories, the app is usable: an organizer creates an event, shares the link, guests view it and RSVP.
|
||||
|
||||
| # | Story | Depends on | Delivers |
|
||||
|---|-------|------------|----------|
|
||||
| 1 | US-1: Create event | T-4 | Event creation with tokens, localStorage |
|
||||
| 2 | US-2: View event page | US-1 | Public event page with attendee list, expired state |
|
||||
| 3 | US-3: RSVP | US-2 | Attend/decline flow, localStorage dedup |
|
||||
|
||||
### Increment 2: Organizer Toolset — US-5, US-4
|
||||
|
||||
The organizer needs to correct mistakes and moderate spam before the app goes to real users.
|
||||
|
||||
| # | Story | Depends on | Delivers |
|
||||
|---|-------|------------|----------|
|
||||
| 4 | US-5: Edit event | US-1 | Edit all fields, expiry-must-be-future constraint |
|
||||
| 5 | US-4: Manage guest list | US-1 | View RSVPs, delete spam entries |
|
||||
|
||||
US-5 before US-4: US-9 (change highlights) depends on US-5, so getting it done early unblocks Phase 3 work.
|
||||
|
||||
### Increment 3: Event Lifecycle — US-18, US-19, US-12, US-13
|
||||
|
||||
Complete lifecycle management. After this increment, the privacy guarantee is enforced and abuse prevention is in place.
|
||||
|
||||
| # | Story | Depends on | Delivers | Activates deferred ACs |
|
||||
|---|-------|------------|----------|----------------------|
|
||||
| 6 | US-18: Cancel event | US-1 | One-way cancellation with optional message, expiry adjustment | US-2 AC5, US-3 AC11 |
|
||||
| 7 | US-19: Delete event | US-1 | Immediate permanent deletion, localStorage cleanup | US-2 AC6 (partial) |
|
||||
| 8 | US-12: Auto-cleanup | US-1 | Scheduled deletion after expiry, silent logging | US-2 AC6 (complete) |
|
||||
| 9 | US-13: Event limit | US-1 | `MAX_ACTIVE_EVENTS` env var, server-side enforcement | — |
|
||||
|
||||
When implementing US-18, US-19, and US-12: immediately activate their deferred ACs in US-2 and US-3 (cancelled state display, RSVP blocking, event-not-found handling). These stories exist at this point — no reason to defer further.
|
||||
|
||||
### Increment 4: App Shell — US-6, US-7, US-17
|
||||
|
||||
The app gets a home screen. Users can find their events without the original link.
|
||||
|
||||
| # | Story | Depends on | Delivers |
|
||||
|---|-------|------------|----------|
|
||||
| 10 | US-6: Bookmark event | US-2 | Client-only bookmark, no server contact |
|
||||
| 11 | US-7: Local event overview | — | Root page `/` with all tracked events from localStorage |
|
||||
| 12 | US-17: Dark/light mode | — | System preference detection, manual toggle, localStorage persistence |
|
||||
|
||||
US-6 before US-7: bookmarking populates localStorage entries that the overview displays. Without US-6, the overview only shows created and RSVPed events.
|
||||
|
||||
US-17 here (not in a late phase): event color themes (US-15) must account for dark/light mode. Having it in place before US-15 avoids rework.
|
||||
|
||||
### Increment 5: Rich Event Page — US-8, US-11, US-9, US-10a, US-10b
|
||||
|
||||
Features that enrich the event page for guests and organizers.
|
||||
|
||||
| # | Story | Depends on | Delivers |
|
||||
|---|-------|------------|----------|
|
||||
| 13 | US-8: Calendar .ics + webcal | US-2 | RFC 5545 download, webcal subscription, STATUS:CANCELLED support |
|
||||
| 14 | US-11: QR code | US-2 | Server-generated QR, SVG/PNG download |
|
||||
| 15 | US-9: Change highlights | US-2, US-5 | Field-level change indicators, localStorage-based read tracking |
|
||||
| 16 | US-10a: Update messages | US-1, US-2 | Organizer posts, reverse-chronological display, delete capability |
|
||||
| 17 | US-10b: New-update indicator | US-10a | localStorage-based unread badge |
|
||||
|
||||
US-8 benefits from US-18 being complete: `STATUS:CANCELLED` in .ics can be implemented directly instead of deferred.
|
||||
|
||||
US-9 benefits from US-5 being complete (increment 2): no dependency waiting.
|
||||
|
||||
### Increment 6: Visual Polish & PWA — US-15, US-16, US-14
|
||||
|
||||
Final layer: visual customization and native app feel.
|
||||
|
||||
| # | Story | Depends on | Delivers |
|
||||
|---|-------|------------|----------|
|
||||
| 18 | US-15: Color themes | US-1, US-2 | Predefined theme picker, event-scoped styling |
|
||||
| 19 | US-16: Unsplash images | US-1, US-2 | Server-proxied search, local storage, attribution |
|
||||
| 20 | US-14: PWA | T-4 | Manifest, service worker, installability |
|
||||
|
||||
US-15 before US-16: themes are self-contained, Unsplash adds external API complexity.
|
||||
|
||||
US-14 last: PWA caching is most effective when the app has all its pages and assets. Service worker strategy can cover everything in one pass.
|
||||
|
||||
Note: US-12 AC2 (delete stored header images on expiry) remains deferred until US-16 is implemented. When implementing US-16, activate this AC in US-12.
|
||||
|
||||
## Deferred AC Activation Schedule
|
||||
|
||||
| When implementing | Activate deferred AC in | AC description |
|
||||
|-------------------|------------------------|----------------|
|
||||
| US-18 (#6) | US-2 AC5 | Cancelled state display |
|
||||
| US-18 (#6) | US-3 AC11 | RSVP blocked on cancelled event |
|
||||
| US-18 (#6) | US-8 AC9 | STATUS:CANCELLED in .ics (if US-8 not yet done — in this order, US-8 comes later, so implement directly) |
|
||||
| US-19 (#7) | US-2 AC6 | Event not found (organizer deletion) |
|
||||
| US-12 (#8) | US-2 AC6 | Event not found (expiry deletion) |
|
||||
| US-16 (#19) | US-12 AC2 | Delete stored header images on expiry |
|
||||
504
.specify/memory/plans/e2e-testing-playwright-setup.md
Normal file
504
.specify/memory/plans/e2e-testing-playwright-setup.md
Normal file
@@ -0,0 +1,504 @@
|
||||
---
|
||||
date: 2026-03-05T10:29:08+00:00
|
||||
git_commit: ffea279b54ad84be09bd0e82b3ed9c89a95fc606
|
||||
branch: master
|
||||
topic: "E2E Testing with Playwright — Setup & Initial Tests"
|
||||
tags: [plan, e2e, playwright, testing, frontend, msw]
|
||||
status: draft
|
||||
---
|
||||
|
||||
# E2E Testing with Playwright — Setup & Initial Tests
|
||||
|
||||
## Overview
|
||||
|
||||
Set up Playwright E2E testing infrastructure for the fete Vue 3 frontend with mocked backend (via `@msw/playwright` + `@msw/source`), write initial smoke and US-1 event-creation tests, and integrate into CI.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
- **Vitest** is configured and already excludes `e2e/**` (`vitest.config.ts:10`)
|
||||
- **Three routes** exist: `/` (Home), `/create` (EventCreate), `/events/:token` (EventStub)
|
||||
- **No E2E framework** installed — no Playwright, no MSW
|
||||
- **OpenAPI spec** at `backend/src/main/resources/openapi/api.yaml` defines `POST /events` with `CreateEventRequest` and `CreateEventResponse` schemas
|
||||
- **`CreateEventResponse` lacks `example:` fields** — required for `@msw/source` mock generation
|
||||
- **CI pipeline** (`.gitea/workflows/ci.yaml`) has backend-test and frontend-test jobs but no E2E step
|
||||
- **`.gitignore`** does not include Playwright output directories
|
||||
|
||||
### Key Discoveries:
|
||||
- `frontend/vitest.config.ts:10` — `e2e/**` already excluded from Vitest
|
||||
- `frontend/vite.config.ts:18-25` — Dev proxy forwards `/api` → `localhost:8080`
|
||||
- `frontend/package.json:8` — `dev` script runs `generate:api` first, then Vite
|
||||
- `frontend/src/views/EventCreateView.vue` — Full form with client-side validation, API call via `openapi-fetch`, redirect to event stub on success
|
||||
- `frontend/src/views/EventStubView.vue` — Shows "Event created!" confirmation with shareable link
|
||||
- `frontend/src/views/HomeView.vue` — Empty state with "Create Event" CTA
|
||||
|
||||
## Desired End State
|
||||
|
||||
After this plan is complete:
|
||||
- Playwright is installed and configured with Chromium-only
|
||||
- `@msw/playwright` + `@msw/source` provide automatic API mocking from the OpenAPI spec
|
||||
- A smoke test verifies the app loads and basic navigation works
|
||||
- A US-1 E2E test covers the full event creation flow (form fill → mocked API → redirect → stub page)
|
||||
- `npm run test:e2e` runs all E2E tests locally
|
||||
- CI runs E2E tests after unit tests, uploading the report as artifact on failure
|
||||
- OpenAPI response schemas include `example:` fields for mock generation
|
||||
|
||||
### Verification:
|
||||
```bash
|
||||
cd frontend && npm run test:e2e # all E2E tests pass locally
|
||||
```
|
||||
|
||||
## What We're NOT Doing
|
||||
|
||||
- Firefox/WebKit browser testing — Chromium only for now
|
||||
- Page Object Model pattern — premature with <5 tests
|
||||
- CI caching of Playwright browser binaries — separate concern
|
||||
- Full-stack E2E tests with real Spring Boot backend
|
||||
- E2E tests for US-2 through US-20 — only US-1 flow + smoke test
|
||||
- Service worker / PWA testing
|
||||
- `data-testid` attributes — using accessible locators (`getByRole`, `getByLabel`) where possible
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
Install Playwright and MSW packages, configure Playwright to spawn the Vite dev server, set up MSW to auto-generate handlers from the OpenAPI spec, then write two test files: a smoke test and a US-1 event creation flow test. Finally, add an E2E step to CI.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Playwright Infrastructure
|
||||
|
||||
### Overview
|
||||
Install dependencies, create configuration, add npm scripts, update `.gitignore`.
|
||||
|
||||
### Changes Required:
|
||||
|
||||
#### [x] 1. Install npm packages
|
||||
**Command**: `cd frontend && npm install --save-dev @playwright/test @msw/playwright @msw/source msw`
|
||||
|
||||
Four packages:
|
||||
- `@playwright/test` — Playwright test runner
|
||||
- `msw` — Mock Service Worker core
|
||||
- `@msw/playwright` — Playwright integration for MSW (intercepts at network level via `page.route()`)
|
||||
- `@msw/source` — Reads OpenAPI spec and generates MSW request handlers
|
||||
|
||||
#### [x] 2. Install Chromium browser binary
|
||||
**Command**: `cd frontend && npx playwright install --with-deps chromium`
|
||||
|
||||
Only Chromium — saves ~2 min vs installing all browsers. `--with-deps` installs OS-level libraries.
|
||||
|
||||
#### [x] 3. Create `playwright.config.ts`
|
||||
**File**: `frontend/playwright.config.ts`
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? 'github' : 'html',
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
stdout: 'pipe',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Key decisions per research doc:
|
||||
- `testDir: './e2e'` — separate from Vitest unit tests
|
||||
- `forbidOnly: !!process.env.CI` — prevents `.only` in CI
|
||||
- `workers: 1` in CI — avoids shared-state flakiness
|
||||
- `reuseExistingServer` locally — fast iteration when `npm run dev` is already running
|
||||
|
||||
#### [x] 4. Add npm scripts to `package.json`
|
||||
**File**: `frontend/package.json`
|
||||
|
||||
Add to `"scripts"`:
|
||||
```json
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug"
|
||||
```
|
||||
|
||||
#### [x] 5. Update `.gitignore`
|
||||
**File**: `frontend/.gitignore`
|
||||
|
||||
Append:
|
||||
```
|
||||
# Playwright
|
||||
playwright-report/
|
||||
test-results/
|
||||
```
|
||||
|
||||
#### [x] 6. Create `e2e/` directory
|
||||
**Command**: `mkdir -p frontend/e2e`
|
||||
|
||||
### Success Criteria:
|
||||
|
||||
#### Automated Verification:
|
||||
- [ ] `cd frontend && npx playwright --version` outputs a version
|
||||
- [ ] `cd frontend && npx playwright test --list` runs without error (shows 0 tests initially)
|
||||
- [ ] `npm run test:e2e` script exists in package.json
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] `playwright-report/` and `test-results/` are in `.gitignore`
|
||||
- [ ] No unintended changes to existing config files
|
||||
|
||||
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: OpenAPI Response Examples
|
||||
|
||||
### Overview
|
||||
Add `example:` fields to `CreateEventResponse` properties so `@msw/source` can generate realistic mock responses.
|
||||
|
||||
### Changes Required:
|
||||
|
||||
#### [x] 1. Add examples to `CreateEventResponse`
|
||||
**File**: `backend/src/main/resources/openapi/api.yaml`
|
||||
|
||||
Update `CreateEventResponse` properties to include `example:` fields:
|
||||
|
||||
```yaml
|
||||
CreateEventResponse:
|
||||
type: object
|
||||
required:
|
||||
- eventToken
|
||||
- organizerToken
|
||||
- title
|
||||
- dateTime
|
||||
- expiryDate
|
||||
properties:
|
||||
eventToken:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Public token for the event URL
|
||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
organizerToken:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Secret token for organizer access
|
||||
example: "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
|
||||
title:
|
||||
type: string
|
||||
example: "Summer BBQ"
|
||||
dateTime:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2026-03-15T20:00:00+01:00"
|
||||
expiryDate:
|
||||
type: string
|
||||
format: date
|
||||
example: "2026-06-15"
|
||||
```
|
||||
|
||||
### Success Criteria:
|
||||
|
||||
#### Automated Verification:
|
||||
- [ ] `cd backend && ./mvnw compile` succeeds (OpenAPI codegen still works)
|
||||
- [ ] `cd frontend && npm run generate:api` succeeds (TypeScript types regenerate)
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] All response schema properties have `example:` fields
|
||||
|
||||
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: MSW Integration
|
||||
|
||||
### Overview
|
||||
Set up `@msw/source` to read the OpenAPI spec and generate MSW handlers, and configure `@msw/playwright` to intercept network requests in E2E tests.
|
||||
|
||||
### Changes Required:
|
||||
|
||||
#### [x] 1. Create MSW setup helper
|
||||
**File**: `frontend/e2e/msw-setup.ts`
|
||||
|
||||
```typescript
|
||||
import { fromOpenApi } from '@msw/source'
|
||||
import { createWorkerFixture } from '@msw/playwright'
|
||||
import { test as base, expect } from '@playwright/test'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const specPath = path.resolve(__dirname, '../../backend/src/main/resources/openapi/api.yaml')
|
||||
|
||||
// Generate MSW handlers from the OpenAPI spec.
|
||||
// These return example values defined in the spec by default.
|
||||
const handlers = await fromOpenApi(specPath)
|
||||
|
||||
// Create a Playwright fixture that intercepts network requests via page.route()
|
||||
// and delegates them to MSW handlers.
|
||||
export const test = base.extend(createWorkerFixture(handlers))
|
||||
export { expect }
|
||||
```
|
||||
|
||||
This module:
|
||||
- Reads the OpenAPI spec at test startup
|
||||
- Generates MSW request handlers that return `example:` values by default
|
||||
- Exports a `test` fixture with MSW network interception built in
|
||||
- Tests import `{ test, expect }` from this file instead of `@playwright/test`
|
||||
|
||||
#### [x] 2. Verify MSW integration works
|
||||
Write a minimal test in Phase 4 that uses the fixture — if the import chain works and a test passes, MSW is correctly configured.
|
||||
|
||||
### Success Criteria:
|
||||
|
||||
#### Automated Verification:
|
||||
- [ ] `frontend/e2e/msw-setup.ts` type-checks (no TS errors)
|
||||
- [ ] Import path to OpenAPI spec resolves correctly
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] MSW helper is clean and minimal
|
||||
|
||||
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: E2E Tests
|
||||
|
||||
### Overview
|
||||
Write two test files: a smoke test for basic app functionality and a US-1 event creation flow test.
|
||||
|
||||
### Changes Required:
|
||||
|
||||
#### [x] 1. Smoke test
|
||||
**File**: `frontend/e2e/smoke.spec.ts`
|
||||
|
||||
```typescript
|
||||
import { test, expect } from './msw-setup'
|
||||
|
||||
test.describe('Smoke', () => {
|
||||
test('home page loads and shows branding', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('heading', { name: 'fete' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('home page has create event CTA', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('link', { name: /create event/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('navigating to /create shows the creation form', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.getByRole('link', { name: /create event/i }).click()
|
||||
await expect(page).toHaveURL('/create')
|
||||
await expect(page.getByLabel(/title/i)).toBeVisible()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### [x] 2. US-1 event creation flow test
|
||||
**File**: `frontend/e2e/event-create.spec.ts`
|
||||
|
||||
```typescript
|
||||
import { test, expect } from './msw-setup'
|
||||
|
||||
test.describe('US-1: Create an event', () => {
|
||||
test('shows validation errors for empty required fields', async ({ page }) => {
|
||||
await page.goto('/create')
|
||||
|
||||
await page.getByRole('button', { name: /create event/i }).click()
|
||||
|
||||
await expect(page.getByText('Title is required.')).toBeVisible()
|
||||
await expect(page.getByText('Date and time are required.')).toBeVisible()
|
||||
await expect(page.getByText('Expiry date is required.')).toBeVisible()
|
||||
})
|
||||
|
||||
test('creates an event and redirects to stub page', async ({ page }) => {
|
||||
await page.goto('/create')
|
||||
|
||||
// Fill the form
|
||||
await page.getByLabel(/title/i).fill('Summer BBQ')
|
||||
await page.getByLabel(/description/i).fill('Bring your own drinks')
|
||||
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
|
||||
await page.getByLabel(/location/i).fill('Central Park')
|
||||
await page.getByLabel(/expiry/i).fill('2026-06-15')
|
||||
|
||||
// Submit — MSW returns the OpenAPI example response
|
||||
await page.getByRole('button', { name: /create event/i }).click()
|
||||
|
||||
// Should redirect to the event stub page
|
||||
await expect(page).toHaveURL(/\/events\/.+/)
|
||||
await expect(page.getByText('Event created!')).toBeVisible()
|
||||
})
|
||||
|
||||
test('stores event data in localStorage after creation', async ({ page }) => {
|
||||
await page.goto('/create')
|
||||
|
||||
await page.getByLabel(/title/i).fill('Summer BBQ')
|
||||
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
|
||||
await page.getByLabel(/expiry/i).fill('2026-06-15')
|
||||
|
||||
await page.getByRole('button', { name: /create event/i }).click()
|
||||
await expect(page).toHaveURL(/\/events\/.+/)
|
||||
|
||||
// Verify localStorage was populated
|
||||
const storage = await page.evaluate(() => {
|
||||
const raw = localStorage.getItem('fete_events')
|
||||
return raw ? JSON.parse(raw) : null
|
||||
})
|
||||
expect(storage).not.toBeNull()
|
||||
expect(storage).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ title: 'Summer BBQ' }),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
test('shows server error on API failure', async ({ page, network }) => {
|
||||
// Override the default MSW handler to return a 400 error
|
||||
await network.use(
|
||||
// Exact override syntax depends on @msw/playwright API —
|
||||
// may need adjustment based on actual package API
|
||||
)
|
||||
|
||||
await page.goto('/create')
|
||||
await page.getByLabel(/title/i).fill('Test')
|
||||
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
|
||||
await page.getByLabel(/expiry/i).fill('2026-06-15')
|
||||
|
||||
await page.getByRole('button', { name: /create event/i }).click()
|
||||
|
||||
// Should show error message, not redirect
|
||||
await expect(page.getByRole('alert')).toBeVisible()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Note on the server error test:** The exact override syntax for `network.use()` depends on the `@msw/playwright` API. During implementation, this will need to be adapted to the actual package API. The pattern is: override the `POST /api/events` handler to return a 400/500 response.
|
||||
|
||||
### Success Criteria:
|
||||
|
||||
#### Automated Verification:
|
||||
- [ ] `cd frontend && npm run test:e2e` passes — all tests green
|
||||
- [ ] No TypeScript errors in test files
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] Tests cover: home page rendering, navigation, form validation, successful creation flow, localStorage persistence
|
||||
- [ ] Test names are descriptive and map to acceptance criteria
|
||||
|
||||
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: CI Integration
|
||||
|
||||
### Overview
|
||||
Add a Playwright E2E test step to the Gitea Actions CI pipeline.
|
||||
|
||||
### Changes Required:
|
||||
|
||||
#### [x] 1. Add E2E job to CI workflow
|
||||
**File**: `.gitea/workflows/ci.yaml`
|
||||
|
||||
Add a new job `frontend-e2e` after the existing `frontend-test` job:
|
||||
|
||||
```yaml
|
||||
frontend-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd frontend && npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: cd frontend && npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run E2E tests
|
||||
run: cd frontend && npm run test:e2e
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 30
|
||||
```
|
||||
|
||||
#### [x] 2. Add E2E to the `needs` array of `build-and-publish`
|
||||
**File**: `.gitea/workflows/ci.yaml`
|
||||
|
||||
Update the `build-and-publish` job:
|
||||
```yaml
|
||||
build-and-publish:
|
||||
needs: [backend-test, frontend-test, frontend-e2e]
|
||||
```
|
||||
|
||||
This ensures Docker images are only published if E2E tests also pass.
|
||||
|
||||
### Success Criteria:
|
||||
|
||||
#### Automated Verification:
|
||||
- [ ] CI YAML is valid (no syntax errors)
|
||||
- [ ] `frontend-e2e` job uses `chromium` only (no full browser install)
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] E2E job runs independently from backend-test (no unnecessary dependency)
|
||||
- [ ] `build-and-publish` requires all three test jobs
|
||||
- [ ] Report artifact is uploaded even on test failure (`!cancelled()`)
|
||||
|
||||
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### E2E Tests (this plan):
|
||||
- Smoke test: app loads, branding visible, navigation works
|
||||
- US-1 happy path: fill form → submit → redirect → stub page
|
||||
- US-1 validation: empty required fields show errors
|
||||
- US-1 localStorage: event data persisted after creation
|
||||
- US-1 error handling: API failure shows error message
|
||||
|
||||
### Existing Unit/Component Tests (unchanged):
|
||||
- `useEventStorage.spec.ts` — 6 tests
|
||||
- `EventCreateView.spec.ts` — 11 tests
|
||||
- `EventStubView.spec.ts` — 8 tests
|
||||
|
||||
### Future:
|
||||
- Each new user story adds its own E2E tests
|
||||
- Page Object Model when test suite grows beyond 5-10 tests
|
||||
- Cross-browser testing (Firefox/WebKit) as needed
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Chromium-only keeps install time and test runtime low
|
||||
- `reuseExistingServer` in local dev avoids restarting Vite per test run
|
||||
- Single worker in CI prevents flakiness from parallel state issues
|
||||
- MSW intercepts at network level — no real backend needed, fast test execution
|
||||
|
||||
## References
|
||||
|
||||
- Research: `docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md`
|
||||
- OpenAPI spec: `backend/src/main/resources/openapi/api.yaml`
|
||||
- Existing views: `frontend/src/views/EventCreateView.vue`, `EventStubView.vue`, `HomeView.vue`
|
||||
- CI pipeline: `.gitea/workflows/ci.yaml`
|
||||
- Vitest config: `frontend/vitest.config.ts`
|
||||
107
.specify/memory/research/datetime-best-practices.md
Normal file
107
.specify/memory/research/datetime-best-practices.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
date: 2026-03-04T21:15:50+00:00
|
||||
git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71
|
||||
branch: master
|
||||
topic: "Date/Time Handling Best Practices for the fete Stack"
|
||||
tags: [research, datetime, java, postgresql, openapi, typescript]
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Research: Date/Time Handling Best Practices
|
||||
|
||||
## Research Question
|
||||
|
||||
What are the best practices for handling dates and times across the full fete stack (Java 25 / Spring Boot 3.5.x / PostgreSQL / OpenAPI 3.1 / Vue 3 / TypeScript)?
|
||||
|
||||
## Summary
|
||||
|
||||
The project has two distinct date/time concepts: **event date/time** (when something happens) and **expiry date** (after which data is deleted). These map to different types at every layer. The recommendations align Java types, PostgreSQL column types, OpenAPI formats, and TypeScript representations into a consistent stack-wide approach.
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### Type Mapping Across the Stack
|
||||
|
||||
| Concept | Java | PostgreSQL | OpenAPI | TypeScript | Example |
|
||||
|---------|------|------------|---------|------------|---------|
|
||||
| Event date/time | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-15T20:00:00+01:00` |
|
||||
| Expiry date | `LocalDate` | `date` | `string`, `format: date` | `string` | `2026-06-15` |
|
||||
| Audit timestamps (createdAt, etc.) | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-04T14:22:00Z` |
|
||||
|
||||
### Event Date/Time: `OffsetDateTime` + `timestamptz`
|
||||
|
||||
**Why `OffsetDateTime`, not `LocalDateTime`:**
|
||||
|
||||
- PostgreSQL best practice explicitly recommends `timestamptz` over `timestamp` — the PostgreSQL wiki says ["don't use `timestamp`"](https://wiki.postgresql.org/wiki/Don't_Do_This). `timestamptz` maps naturally to `OffsetDateTime`.
|
||||
- Hibernate 6 (Spring Boot 3.5.x) has native `OffsetDateTime` ↔ `timestamptz` support. `LocalDateTime` requires extra care to avoid silent timezone bugs at the JDBC driver level.
|
||||
- An ISO 8601 string with offset (`2026-03-15T20:00:00+01:00`) is unambiguous in the API. A bare `LocalDateTime` string forces the client to guess the timezone.
|
||||
- The OpenAPI `date-time` format and `openapi-generator` default to `OffsetDateTime` in Java — no custom type mappings needed.
|
||||
|
||||
**Why not `ZonedDateTime`:** Carries IANA zone IDs (e.g. `Europe/Berlin`) which add complexity without value for this use case. Worse JDBC support than `OffsetDateTime`.
|
||||
|
||||
**How PostgreSQL stores it:** `timestamptz` does **not** store the timezone. It converts input to UTC and stores UTC. On retrieval, it converts to the session's timezone setting. The offset is preserved in the Java `OffsetDateTime` via the JDBC driver.
|
||||
|
||||
**Practical flow:** The frontend sends the offset based on the organizer's browser locale. The server stores UTC. Display-side conversion happens in the frontend.
|
||||
|
||||
### Expiry Date: `LocalDate` + `date`
|
||||
|
||||
The expiry date is a calendar-date concept ("after which day should data be deleted"), not a point-in-time. A cleanup job runs periodically and deletes events where `expiryDate < today`. Sub-day precision adds no value and complicates the UX.
|
||||
|
||||
### Jackson Serialization (Spring Boot 3.5.x)
|
||||
|
||||
Spring Boot 3.x auto-configures `jackson-datatype-jsr310` (JavaTimeModule) and disables `WRITE_DATES_AS_TIMESTAMPS` by default:
|
||||
|
||||
- `OffsetDateTime` serializes to `"2026-03-15T20:00:00+01:00"` (ISO 8601 string)
|
||||
- `LocalDate` serializes to `"2026-06-15"`
|
||||
|
||||
No additional configuration needed. For explicitness, can add to `application.properties`:
|
||||
```properties
|
||||
spring.jackson.serialization.write-dates-as-timestamps=false
|
||||
```
|
||||
|
||||
### Hibernate 6 Configuration
|
||||
|
||||
With Hibernate 6, `OffsetDateTime` maps to `timestamptz` using the `NATIVE` timezone storage strategy by default on PostgreSQL. Can be made explicit:
|
||||
|
||||
```properties
|
||||
spring.jpa.properties.hibernate.timezone.default_storage=NATIVE
|
||||
```
|
||||
|
||||
This tells Hibernate to use the database's native `TIMESTAMP WITH TIME ZONE` type directly.
|
||||
|
||||
### OpenAPI Schema Definitions
|
||||
|
||||
```yaml
|
||||
# Event date/time
|
||||
eventDateTime:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2026-03-15T20:00:00+01:00"
|
||||
|
||||
# Expiry date
|
||||
expiryDate:
|
||||
type: string
|
||||
format: date
|
||||
example: "2026-06-15"
|
||||
```
|
||||
|
||||
**Code-generation mapping (defaults, no customization needed):**
|
||||
|
||||
| OpenAPI format | Java type (openapi-generator) | TypeScript type (openapi-typescript) |
|
||||
|---------------|-------------------------------|--------------------------------------|
|
||||
| `date-time` | `java.time.OffsetDateTime` | `string` |
|
||||
| `date` | `java.time.LocalDate` | `string` |
|
||||
|
||||
### Frontend (TypeScript)
|
||||
|
||||
`openapi-typescript` generates `string` for both `format: date-time` and `format: date`. This is correct — JSON has no native date type, so dates travel as strings. Parsing to `Date` objects happens explicitly at the application boundary when needed (e.g. for display formatting).
|
||||
|
||||
## Sources
|
||||
|
||||
- [PostgreSQL Wiki: Don't Do This](https://wiki.postgresql.org/wiki/Don't_Do_This) — recommends `timestamptz` over `timestamp`
|
||||
- [PostgreSQL Docs: Date/Time Types](https://www.postgresql.org/docs/current/datatype-datetime.html)
|
||||
- [Thorben Janssen: Hibernate 6 OffsetDateTime and ZonedDateTime](https://thorben-janssen.com/hibernate-6-offsetdatetime-and-zoneddatetime/)
|
||||
- [Baeldung: OffsetDateTime Serialization With Jackson](https://www.baeldung.com/java-jackson-offsetdatetime)
|
||||
- [Baeldung: Map Date Types With OpenAPI Generator](https://www.baeldung.com/openapi-map-date-types)
|
||||
- [Baeldung: ZonedDateTime vs OffsetDateTime](https://www.baeldung.com/java-zoneddatetime-offsetdatetime)
|
||||
- [Reflectoring: Handling Timezones in Spring Boot](https://reflectoring.io/spring-timezones/)
|
||||
- [openapi-typescript documentation](https://openapi-ts.dev/)
|
||||
273
.specify/memory/research/e2e-testing-playwright.md
Normal file
273
.specify/memory/research/e2e-testing-playwright.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
date: 2026-03-05T10:14:52+00:00
|
||||
git_commit: ffea279b54ad84be09bd0e82b3ed9c89a95fc606
|
||||
branch: master
|
||||
topic: "End-to-End Testing for Vue 3 with Playwright"
|
||||
tags: [research, e2e, playwright, testing, frontend]
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Research: End-to-End Testing for Vue 3 with Playwright
|
||||
|
||||
## Research Question
|
||||
|
||||
How to set up and structure end-to-end tests for the fete Vue 3 + Vite frontend using Playwright?
|
||||
|
||||
## Summary
|
||||
|
||||
Playwright is Vue 3's officially recommended E2E testing framework. It integrates with Vite projects through a `webServer` config block (no Vite plugin needed), supports Chromium/Firefox/WebKit under a single API, and is fully free including parallelism. The fete project's existing vitest.config.ts already excludes `e2e/**`, making the integration path clean.
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### 1. Current Frontend Test Infrastructure
|
||||
|
||||
The project uses **Vitest 4.0.18** with jsdom for unit/component tests:
|
||||
|
||||
- **Config:** `frontend/vitest.config.ts` — merges with vite.config, uses jsdom environment, bail on first failure
|
||||
- **Exclusion:** Already excludes `e2e/**` from Vitest's test discovery (`vitest.config.ts:10`)
|
||||
- **Existing tests:** 3 test files with ~25 tests total:
|
||||
- `src/composables/__tests__/useEventStorage.spec.ts` (6 tests)
|
||||
- `src/views/__tests__/EventCreateView.spec.ts` (11 tests)
|
||||
- `src/views/__tests__/EventStubView.spec.ts` (8 tests)
|
||||
- **No E2E framework** is currently configured
|
||||
|
||||
### 2. Why Playwright
|
||||
|
||||
Vue's official testing guide ([vuejs.org/guide/scaling-up/testing](https://vuejs.org/guide/scaling-up/testing)) positions Playwright as the primary E2E recommendation. Key advantages over Cypress:
|
||||
|
||||
| Dimension | Playwright | Cypress |
|
||||
|---|---|---|
|
||||
| Browser support | Chromium, Firefox, WebKit | Chrome-family, Firefox (WebKit experimental) |
|
||||
| Parallelism | Free, native | Requires paid Cypress Cloud |
|
||||
| Architecture | Out-of-process (CDP/BiDi) | In-browser (same process) |
|
||||
| Speed | 35-45% faster in parallel | Slower at scale |
|
||||
| Pricing | 100% free, Apache 2.0 | Cloud features cost money |
|
||||
| Privacy | No account, no cloud dependency | Cloud service integration |
|
||||
|
||||
Playwright aligns with fete's privacy constraints (no cloud dependency, no account required).
|
||||
|
||||
### 3. Playwright + Vite Integration
|
||||
|
||||
Playwright does **not** use a Vite plugin. Integration is purely through process management:
|
||||
|
||||
1. Playwright reads `webServer.command` and spawns the Vite dev server
|
||||
2. Polls `webServer.url` until ready
|
||||
3. Runs tests against `use.baseURL`
|
||||
4. Kills the server after all tests finish
|
||||
|
||||
The existing Vite dev proxy (`/api` → `localhost:8080`) works transparently — E2E tests can hit the real backend or intercept via `page.route()` mocks.
|
||||
|
||||
Note: `@playwright/experimental-ct-vue` exists for component-level testing (mounting individual Vue components without a server), but is still experimental and is a different category from E2E.
|
||||
|
||||
### 4. Installation
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install --save-dev @playwright/test
|
||||
npx playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
Using `npm init playwright@latest` generates scaffolding automatically, but for an existing project manual setup is cleaner.
|
||||
|
||||
### 5. Project Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
playwright.config.ts # Playwright config
|
||||
e2e/ # E2E test directory
|
||||
home.spec.ts
|
||||
event-create.spec.ts
|
||||
event-view.spec.ts
|
||||
fixtures/ # shared test fixtures (optional)
|
||||
helpers/ # page object models (optional)
|
||||
playwright-report/ # generated HTML report (gitignored)
|
||||
test-results/ # generated artifacts (gitignored)
|
||||
```
|
||||
|
||||
The `e2e/` directory is already excluded from Vitest via `vitest.config.ts:10`.
|
||||
|
||||
### 6. Recommended playwright.config.ts
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? 'github' : 'html',
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
// Uncomment for cross-browser coverage:
|
||||
// { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
// { name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
stdout: 'pipe',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Key decisions:
|
||||
- `testDir: './e2e'` — separates E2E from Vitest unit tests
|
||||
- `forbidOnly: !!process.env.CI` — prevents `test.only` from shipping to CI
|
||||
- `workers: process.env.CI ? 1 : undefined` — single worker in CI avoids shared-state flakiness; locally uses all cores
|
||||
- `reporter: 'github'` — GitHub Actions annotations in CI
|
||||
- `command: 'npm run dev'` — runs `generate:api` first (via the existing npm script), then starts Vite
|
||||
- `reuseExistingServer: !process.env.CI` — reuses running dev server locally for fast iteration
|
||||
|
||||
### 7. package.json Scripts
|
||||
|
||||
```json
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug"
|
||||
```
|
||||
|
||||
### 8. .gitignore Additions
|
||||
|
||||
```
|
||||
playwright-report/
|
||||
test-results/
|
||||
```
|
||||
|
||||
### 9. TypeScript Configuration
|
||||
|
||||
The existing `tsconfig.app.json` excludes `src/**/__tests__/*`. Since E2E tests live in `e2e/` (outside `src/`), they are already excluded from the app build.
|
||||
|
||||
A separate `tsconfig` for E2E tests is not strictly required — Playwright's own TypeScript support handles it. If needed, a minimal `e2e/tsconfig.json` can extend `tsconfig.node.json`.
|
||||
|
||||
### 10. Vue-Specific Testing Patterns
|
||||
|
||||
**Router navigation:**
|
||||
```typescript
|
||||
await page.goto('/events/abc-123')
|
||||
await page.waitForURL('/events/abc-123') // confirms SPA router resolved
|
||||
```
|
||||
|
||||
**Waiting for reactive content (auto-retry):**
|
||||
```typescript
|
||||
await expect(page.getByRole('heading', { name: 'My Event' })).toBeVisible()
|
||||
// Playwright auto-retries assertions for up to the configured timeout
|
||||
```
|
||||
|
||||
**URL assertions:**
|
||||
```typescript
|
||||
await expect(page).toHaveURL(/\/events\/.+/)
|
||||
```
|
||||
|
||||
**API mocking (for isolated E2E tests):**
|
||||
```typescript
|
||||
await page.route('/api/events/**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ title: 'Test Event', date: '2026-04-01' }),
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Locator strategy — prefer accessible locators:**
|
||||
```typescript
|
||||
page.getByRole('button', { name: 'RSVP' }) // best
|
||||
page.getByLabel('Event Title') // form fields
|
||||
page.getByTestId('event-card') // data-testid fallback
|
||||
page.locator('.some-class') // last resort
|
||||
```
|
||||
|
||||
### 11. CI Integration
|
||||
|
||||
**GitHub Actions workflow:**
|
||||
```yaml
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
# --with-deps installs OS-level libraries (libglib, libnss, etc.)
|
||||
# Specify 'chromium' to save ~2min vs installing all browsers
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npx playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 30
|
||||
```
|
||||
|
||||
**Docker:** Use official images `mcr.microsoft.com/playwright:v1.x.x-noble` (Ubuntu 24.04). Alpine is unsupported (browsers need glibc). Key flag: `--ipc=host` prevents Chromium memory exhaustion. The Playwright Docker image version must match the `@playwright/test` package version exactly.
|
||||
|
||||
For the fete project, E2E tests run as a separate CI step, not inside the app's Dockerfile.
|
||||
|
||||
### 12. Integration with Existing Backend
|
||||
|
||||
Two approaches for E2E tests:
|
||||
|
||||
1. **Mocked backend** (via `page.route()`): Fast, isolated, no backend dependency. Good for frontend-only testing.
|
||||
2. **Real backend**: Start Spring Boot alongside Vite. Tests hit `/api` through the Vite proxy. More realistic but requires Java in CI. Could use Docker Compose.
|
||||
|
||||
The Vite proxy config (`vite.config.ts:19-23`) already forwards `/api` to `localhost:8080`, so both approaches work without changes.
|
||||
|
||||
## Code References
|
||||
|
||||
- `frontend/vitest.config.ts:10` — E2E exclusion pattern already in place
|
||||
- `frontend/vite.config.ts:19-23` — API proxy configuration for backend integration
|
||||
- `frontend/package.json:8-9` — `dev` script runs `generate:api` before Vite
|
||||
- `frontend/src/router/index.ts` — Route definitions (Home, Create, Event views)
|
||||
- `frontend/src/api/client.ts` — openapi-fetch client using `/api` base URL
|
||||
- `frontend/tsconfig.app.json` — App TypeScript config (excludes test files)
|
||||
|
||||
## Architecture Documentation
|
||||
|
||||
### Test Pyramid in fete
|
||||
|
||||
| Layer | Framework | Directory | Purpose |
|
||||
|---|---|---|---|
|
||||
| Unit | Vitest + jsdom | `src/**/__tests__/` | Composables, isolated logic |
|
||||
| Component | Vitest + @vue/test-utils | `src/**/__tests__/` | Vue component behavior |
|
||||
| E2E | Playwright (proposed) | `e2e/` | Full browser, user flows |
|
||||
| Visual | browser-interactive-testing skill | `.agent-tests/` | Agent-driven screenshots |
|
||||
|
||||
### Decision Points for Implementation
|
||||
|
||||
1. **Start with Chromium only** — add Firefox/WebKit later if needed
|
||||
2. **Use `npm run dev`** as webServer command (includes API type generation)
|
||||
3. **API mocking by default** — use `page.route()` for E2E isolation; full-stack tests as a separate concern
|
||||
4. **`data-testid` attributes** on key interactive elements for stable selectors
|
||||
5. **Page Object Model** recommended once the test suite grows beyond 5-10 tests
|
||||
|
||||
## Sources
|
||||
|
||||
- [Testing | Vue.js](https://vuejs.org/guide/scaling-up/testing) — official E2E recommendation
|
||||
- [Installation | Playwright](https://playwright.dev/docs/intro)
|
||||
- [webServer | Playwright](https://playwright.dev/docs/test-webserver) — Vite integration
|
||||
- [CI Intro | Playwright](https://playwright.dev/docs/ci-intro)
|
||||
- [Docker | Playwright](https://playwright.dev/docs/docker)
|
||||
- [Cypress vs Playwright 2026 | BugBug](https://bugbug.io/blog/test-automation-tools/cypress-vs-playwright/)
|
||||
- [Playwright vs Cypress | Katalon](https://katalon.com/resources-center/blog/playwright-vs-cypress)
|
||||
|
||||
## Decisions (2026-03-05)
|
||||
|
||||
- **Mocked backend only** — E2E tests use `page.route()` to mock API responses. No real Spring Boot backend in E2E.
|
||||
- **Mocking stack:** `@msw/playwright` + `@msw/source` — reads OpenAPI spec at runtime, generates MSW handlers, per-test overrides via `network.use()`.
|
||||
- **US-1 flows first** — Event creation is the only implemented user story; E2E tests cover that flow.
|
||||
- **No CI caching yet** — Playwright browser binaries are not cached; CI runner needs reconfiguration first.
|
||||
- **E2E tests are part of frontend tasks** — every frontend user story includes E2E test coverage going forward.
|
||||
- **OpenAPI examples mandatory** — all response schemas in the OpenAPI spec must include `example:` fields (required for `@msw/source` mock generation).
|
||||
37
.specify/memory/research/modern-ui-effects.md
Normal file
37
.specify/memory/research/modern-ui-effects.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Modern UI Effects Research (2025-2026)
|
||||
|
||||
## Liquid Glass (Apple WWDC 2025)
|
||||
Evolved glassmorphism with directional lighting. Three-layer approach: highlight, shadow, illumination.
|
||||
- `backdrop-filter: blur(20px) saturate(1.5)` — higher saturation than basic glass
|
||||
- `inset 0 1px 0 rgba(255,255,255,0.15)` — top highlight (light direction)
|
||||
- `inset 0 -1px 0 rgba(0,0,0,0.1)` — bottom shadow
|
||||
- Outer drop shadow for depth: `0 8px 32px rgba(0,0,0,0.3)`
|
||||
- Advanced: SVG `feTurbulence` + `feSpecularLighting` for refraction (Chromium only)
|
||||
- Browser support: `backdrop-filter` ~88%, Firefox since v103
|
||||
|
||||
## Aurora / Gradient Mesh Backgrounds
|
||||
Stacked animated radial gradients simulating northern lights. Pairs well with glass cards on dark backgrounds.
|
||||
- Multiple `radial-gradient(ellipse ...)` layers with partial opacity
|
||||
- Animated via `background-position` shift (GPU-friendly)
|
||||
- `@property` rule enables direct gradient color animation (broad support since 2024)
|
||||
- Best for ambient background movement, not for content areas
|
||||
|
||||
## Animated Glow Borders
|
||||
Rotating `conic-gradient` borders with blur halo. Striking on dark backgrounds.
|
||||
- Outer wrapper with `conic-gradient(from var(--angle), color1, color2, color3, color1)`
|
||||
- `::before` pseudo with `filter: blur(12px)` and `opacity: 0.5` for glow halo
|
||||
- `@property --angle` trick to animate custom property inside `conic-gradient`
|
||||
- Use sparingly — best for single highlight elements (FAB, CTA), not all cards
|
||||
|
||||
## Modern Neumorphism (2025-2026 revision)
|
||||
Subtler than the original trend. Higher contrast, less extreme extrusion, combined with accent colors.
|
||||
- Light and dark shadow pair: `6px 6px 12px rgba(0,0,0,0.5)` + `-6px -6px 12px rgba(60,50,80,0.15)`
|
||||
- `border: 1px solid rgba(255,255,255,0.05)` for definition
|
||||
- Works on dark backgrounds with slightly lighter "uplift" shadow direction
|
||||
- Better suited for interactive elements (buttons, toggles) than content cards
|
||||
|
||||
## Sources
|
||||
- Apple Liquid Glass CSS: dev.to/gruszdev, dev.to/kevinbism, css-tricks.com, kube.io
|
||||
- Aurora: dev.to/oobleck, daltonwalsh.com, github.com/mattnewdavid
|
||||
- Glow borders: frontendmasters.com (Kevin Powell), docode.co.in
|
||||
- Trends overview: medium.com/design-bootcamp, index.dev, bighuman.com
|
||||
215
.specify/memory/research/openapi-validation-pipeline.md
Normal file
215
.specify/memory/research/openapi-validation-pipeline.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
date: "2026-03-04T22:27:37.933286+00:00"
|
||||
git_commit: 91e566efea0cbf53ba06a29b63317b7435609bd8
|
||||
branch: master
|
||||
topic: "Automatic OpenAPI Validation Pipelines for Backpressure Hooks"
|
||||
tags: [research, openapi, validation, hooks, backpressure, linting]
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Research: Automatic OpenAPI Validation Pipelines
|
||||
|
||||
## Research Question
|
||||
|
||||
What automatic validation pipelines exist for OpenAPI specs that can be integrated into the current Claude Code backpressure hook setup, running after the OpenAPI spec has been modified?
|
||||
|
||||
## Summary
|
||||
|
||||
The project already has a PostToolUse hook system that runs backend compile checks and frontend lint/type-checks after Edit/Write operations. Adding OpenAPI spec validation requires a new hook script that triggers specifically when `api.yaml` is modified. Several CLI tools support OpenAPI 3.1.0 validation — **Redocly CLI** is the strongest fit given the existing Node.js toolchain, MIT license, active maintenance, and zero-config baseline.
|
||||
|
||||
## Current Backpressure Setup
|
||||
|
||||
### Hook Architecture (`.claude/settings.json`)
|
||||
|
||||
The project uses Claude Code hooks for automated quality gates:
|
||||
|
||||
| Hook Event | Trigger | Scripts |
|
||||
|---|---|---|
|
||||
| `PostToolUse` | `Edit\|Write` tool calls | `backend-compile-check.sh`, `frontend-check.sh` |
|
||||
| `Stop` | Agent attempts to stop | `run-tests.sh` |
|
||||
|
||||
### How Hooks Work
|
||||
|
||||
Each hook script:
|
||||
1. Reads JSON from stdin containing `tool_input.file_path`
|
||||
2. Pattern-matches the file path to decide if it should run
|
||||
3. Executes validation (compile, lint, type-check, test)
|
||||
4. Returns JSON with either success message or failure details
|
||||
5. On failure: outputs `hookSpecificOutput` with error context (PostToolUse) or `{"decision":"block"}` (Stop)
|
||||
|
||||
### Existing Pattern for File Matching
|
||||
|
||||
```bash
|
||||
# backend-compile-check.sh — matches Java files
|
||||
case "$FILE_PATH" in
|
||||
*/backend/src/*.java|backend/src/*.java) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# frontend-check.sh — matches TS/Vue files
|
||||
case "$FILE_PATH" in
|
||||
*/frontend/src/*.ts|*/frontend/src/*.vue|frontend/src/*.ts|frontend/src/*.vue) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
An OpenAPI validation hook would use the same pattern:
|
||||
```bash
|
||||
case "$FILE_PATH" in
|
||||
*/openapi/api.yaml|*/openapi/*.yaml) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
### Existing OpenAPI Tooling in the Project
|
||||
|
||||
- **Backend:** `openapi-generator-maven-plugin` v7.20.0 generates Spring interfaces from `api.yaml` (`pom.xml:149-178`)
|
||||
- **Frontend:** `openapi-typescript` v7.13.0 generates TypeScript types; `openapi-fetch` v0.17.0 provides type-safe client
|
||||
- **No validation/linting tools** currently installed — no Redocly, Spectral, or other linter config exists
|
||||
|
||||
## Tool Evaluation
|
||||
|
||||
### Redocly CLI (`@redocly/cli`)
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| OpenAPI 3.1 | Full support |
|
||||
| Install | `npm install -g @redocly/cli` or `npx @redocly/cli@latest` |
|
||||
| CLI | `redocly lint api.yaml` |
|
||||
| License | MIT |
|
||||
| Maintenance | Very active — latest v2.20.3 (2026-03-03), daily/weekly releases |
|
||||
| GitHub | ~1.4k stars (Redocly ecosystem: 24k+ combined) |
|
||||
|
||||
**Checks:** Structural validity against OAS schema, configurable linting rules (naming, descriptions, operation IDs, security), style/consistency enforcement. Built-in rulesets: `minimal`, `recommended`, `recommended-strict`. Zero-config baseline works immediately. Custom rules via `redocly.yaml`.
|
||||
|
||||
**Fit for this project:** Node.js already in the toolchain (frontend). `npx` form requires no permanent install. MIT license compatible with GPL-3.0. The `@redocly/openapi-core` package is already present as a transitive dependency of `openapi-typescript` in `node_modules`.
|
||||
|
||||
### Spectral (`@stoplight/spectral-cli`)
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| OpenAPI 3.1 | Full support (since v6.x) |
|
||||
| Install | `npm install -g @stoplight/spectral-cli` |
|
||||
| CLI | `spectral lint api.yaml` |
|
||||
| License | Apache 2.0 |
|
||||
| Maintenance | Active — latest v6.15.0 (2025-04-22), slower cadence |
|
||||
| GitHub | ~3k stars |
|
||||
|
||||
**Checks:** Schema compliance, missing descriptions/tags/operationIds, contact/license metadata. Highly extensible custom rulesets via YAML/JS. Configurable severity levels.
|
||||
|
||||
**Fit for this project:** Well-established industry standard. Apache 2.0 compatible with GPL. Less actively maintained than Redocly (10 months since last release). Heavier custom ruleset system may be over-engineered for current needs.
|
||||
|
||||
### Vacuum (`daveshanley/vacuum`)
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| OpenAPI 3.1 | Full support (via libopenapi) |
|
||||
| Install | `brew install daveshanley/vacuum/vacuum` or Go binary |
|
||||
| CLI | `vacuum lint api.yaml` |
|
||||
| License | MIT |
|
||||
| Maintenance | Active — latest release 2025-12-22 |
|
||||
| GitHub | ~1k stars |
|
||||
|
||||
**Checks:** Structural validation, Spectral-compatible rulesets, OWASP security checks, naming conventions, descriptions/examples/tags. Single Go binary — no runtime dependencies.
|
||||
|
||||
**Fit for this project:** Zero-dependency binary is appealing for CI. However, adds a non-Node.js tool dependency when the project already has Node.js. Spectral ruleset compatibility is a plus for portability.
|
||||
|
||||
### oasdiff (`oasdiff/oasdiff`)
|
||||
|
||||
| Attribute | Value |
|
||||
|---|---|
|
||||
| OpenAPI 3.1 | Beta |
|
||||
| Install | `brew install oasdiff` or Go binary |
|
||||
| CLI | `oasdiff breaking base.yaml revision.yaml` |
|
||||
| License | Apache 2.0 |
|
||||
| Maintenance | Active — latest v1.11.10 (2026-02-05) |
|
||||
| GitHub | ~1.1k stars |
|
||||
|
||||
**Checks:** 300+ breaking change detection rules (paths, parameters, schemas, security, headers, enums). Requires two spec versions to compare — not a standalone validator.
|
||||
|
||||
**Fit for this project:** Different category — detects breaking changes between spec versions, not structural validity. Useful as a CI-only check comparing `HEAD~1` vs `HEAD`. OAS 3.1 support is still beta.
|
||||
|
||||
### Not Recommended
|
||||
|
||||
- **swagger-cli:** Abandoned, no OAS 3.1 support
|
||||
- **IBM OpenAPI Validator:** Active but opinionated IBM-specific rules add configuration overhead for no benefit
|
||||
|
||||
## Tool Comparison Matrix
|
||||
|
||||
| Tool | OAS 3.1 | License | Last Release | Stars | Runtime | Category |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **Redocly CLI** | Full | MIT | 2026-03-03 | ~1.4k | Node.js | Lint + validate |
|
||||
| **Spectral** | Full | Apache 2.0 | 2025-04-22 | ~3k | Node.js | Lint |
|
||||
| **Vacuum** | Full | MIT | 2025-12-22 | ~1k | Go binary | Lint + validate |
|
||||
| **oasdiff** | Beta | Apache 2.0 | 2026-02-05 | ~1.1k | Go binary | Breaking changes |
|
||||
|
||||
## Integration Pattern
|
||||
|
||||
### Hook Script Structure
|
||||
|
||||
An OpenAPI validation hook would follow the existing pattern in `.claude/hooks/`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
|
||||
|
||||
# Only run for OpenAPI spec files
|
||||
case "$FILE_PATH" in
|
||||
*/openapi/*.yaml|*/openapi/*.yml) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
cd "$CLAUDE_PROJECT_DIR/backend"
|
||||
|
||||
# Run validation
|
||||
if OUTPUT=$(npx @redocly/cli@latest lint src/main/resources/openapi/api.yaml --format=stylish 2>&1); then
|
||||
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ OpenAPI spec validation passed."}}'
|
||||
else
|
||||
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
|
||||
fi
|
||||
```
|
||||
|
||||
### Registration in `.claude/settings.json`
|
||||
|
||||
The hook would be added to the existing `PostToolUse` array alongside the compile and lint hooks:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/openapi-validate.sh\"",
|
||||
"timeout": 120
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration (Optional)
|
||||
|
||||
A `redocly.yaml` in the project root or `backend/` directory can customize rules:
|
||||
|
||||
```yaml
|
||||
extends:
|
||||
- recommended
|
||||
|
||||
rules:
|
||||
operation-operationId: error
|
||||
tag-description: warn
|
||||
no-ambiguous-paths: error
|
||||
```
|
||||
|
||||
## Code References
|
||||
|
||||
- `.claude/settings.json:1-32` — Hook configuration (PostToolUse + Stop events)
|
||||
- `.claude/hooks/backend-compile-check.sh` — Java file detection pattern + compile check
|
||||
- `.claude/hooks/frontend-check.sh` — TS/Vue file detection pattern + type-check + lint
|
||||
- `.claude/hooks/run-tests.sh` — Stop hook with test execution and block/approve logic
|
||||
- `backend/pom.xml:149-178` — openapi-generator-maven-plugin configuration
|
||||
- `backend/src/main/resources/openapi/api.yaml` — The OpenAPI 3.1.0 spec to validate
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the validation use a pinned version (`npx @redocly/cli@1.x.x`) or latest? Pinned is more reproducible; latest gets rule updates automatically.
|
||||
- Should a `redocly.yaml` config be added immediately with the `recommended` ruleset, or start with zero-config (structural validation only) and add rules incrementally?
|
||||
- Is breaking change detection (oasdiff) desirable as a separate CI check, or is structural validation sufficient for now?
|
||||
202
.specify/memory/research/rfc9457-problem-details.md
Normal file
202
.specify/memory/research/rfc9457-problem-details.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
date: 2026-03-04T21:15:50+00:00
|
||||
git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71
|
||||
branch: master
|
||||
topic: "RFC 9457 Problem Details for HTTP API Error Responses"
|
||||
tags: [research, error-handling, rfc9457, spring-boot, openapi]
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Research: RFC 9457 Problem Details
|
||||
|
||||
## Research Question
|
||||
|
||||
How should the fete API structure error responses? What does RFC 9457 (Problem Details) specify, and how does it integrate with Spring Boot 3.5.x, OpenAPI 3.1, and openapi-fetch?
|
||||
|
||||
## Summary
|
||||
|
||||
RFC 9457 (successor to RFC 7807) defines a standard JSON format (`application/problem+json`) for machine-readable HTTP API errors. Spring Boot 3.x has first-class support via `ProblemDetail`, `ErrorResponseException`, and `ResponseEntityExceptionHandler`. The recommended approach is a single `@RestControllerAdvice` that handles all exceptions consistently — no `spring.mvc.problemdetails.enabled` property, no fallback to legacy error format.
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### RFC 9457 Format
|
||||
|
||||
Standard fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `type` | URI | Identifies the problem type. Defaults to `about:blank`. |
|
||||
| `title` | string | Short, human-readable summary. Should not change between occurrences. |
|
||||
| `status` | int | HTTP status code. |
|
||||
| `detail` | string | Human-readable explanation specific to this occurrence. |
|
||||
| `instance` | URI | Identifies the specific occurrence (e.g. correlation ID). |
|
||||
|
||||
Extension members (additional JSON properties) are explicitly permitted. This is the mechanism for validation errors, error codes, etc.
|
||||
|
||||
**Key rule:** With `type: "about:blank"`, the `title` must match the HTTP status phrase exactly. Use a custom `type` URI when providing a custom `title`.
|
||||
|
||||
### Spring Boot 3.x Built-in Support
|
||||
|
||||
- **`ProblemDetail`** — container class for the five standard fields + a `properties` Map for extensions.
|
||||
- **`ErrorResponseException`** — base class for custom exceptions that carry their own `ProblemDetail`.
|
||||
- **`ResponseEntityExceptionHandler`** — `@ControllerAdvice` base class that handles all Spring MVC exceptions and renders them as `application/problem+json`.
|
||||
- **`ProblemDetailJacksonMixin`** — automatically unwraps the `properties` Map as top-level JSON fields during serialization.
|
||||
|
||||
### Recommended Configuration
|
||||
|
||||
Use a single `@RestControllerAdvice` extending `ResponseEntityExceptionHandler`. Do **not** use the `spring.mvc.problemdetails.enabled` property.
|
||||
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
||||
// All Spring MVC exceptions are handled automatically.
|
||||
// Add @ExceptionHandler methods for domain exceptions here.
|
||||
// Add a catch-all for Exception.class to prevent legacy error format.
|
||||
}
|
||||
```
|
||||
|
||||
Reasons to avoid the property-based approach:
|
||||
1. No place to add custom `@ExceptionHandler` methods.
|
||||
2. Having both the property AND a custom `ResponseEntityExceptionHandler` bean causes a conflict.
|
||||
3. The property ignores `server.error.include-*` properties.
|
||||
|
||||
### Validation Errors (Field-Level)
|
||||
|
||||
Spring deliberately does **not** include field-level validation errors in `ProblemDetail` by default (security rationale). Override `handleMethodArgumentNotValid`:
|
||||
|
||||
```java
|
||||
@Override
|
||||
protected ResponseEntity<Object> handleMethodArgumentNotValid(
|
||||
MethodArgumentNotValidException ex,
|
||||
HttpHeaders headers,
|
||||
HttpStatusCode status,
|
||||
WebRequest request) {
|
||||
|
||||
ProblemDetail problemDetail = ex.getBody();
|
||||
problemDetail.setTitle("Validation Failed");
|
||||
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
|
||||
|
||||
List<Map<String, String>> fieldErrors = ex.getBindingResult()
|
||||
.getFieldErrors()
|
||||
.stream()
|
||||
.map(fe -> Map.of(
|
||||
"field", fe.getField(),
|
||||
"message", fe.getDefaultMessage()
|
||||
))
|
||||
.toList();
|
||||
|
||||
problemDetail.setProperty("fieldErrors", fieldErrors);
|
||||
return handleExceptionInternal(ex, problemDetail, headers, status, request);
|
||||
}
|
||||
```
|
||||
|
||||
Resulting response:
|
||||
```json
|
||||
{
|
||||
"type": "urn:problem-type:validation-error",
|
||||
"title": "Validation Failed",
|
||||
"status": 400,
|
||||
"detail": "Invalid request content.",
|
||||
"instance": "/api/events",
|
||||
"fieldErrors": [
|
||||
{ "field": "title", "message": "must not be blank" },
|
||||
{ "field": "expiryDate", "message": "must be a future date" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI Schema Definition
|
||||
|
||||
```yaml
|
||||
components:
|
||||
schemas:
|
||||
ProblemDetail:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
format: uri
|
||||
default: "about:blank"
|
||||
title:
|
||||
type: string
|
||||
status:
|
||||
type: integer
|
||||
detail:
|
||||
type: string
|
||||
instance:
|
||||
type: string
|
||||
format: uri
|
||||
additionalProperties: true
|
||||
|
||||
ValidationProblemDetail:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ProblemDetail'
|
||||
- type: object
|
||||
properties:
|
||||
fieldErrors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- field
|
||||
- message
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Validation failed
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationProblemDetail'
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/problem+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetail'
|
||||
```
|
||||
|
||||
Use media type `application/problem+json` in response definitions. Set `additionalProperties: true` on the base schema.
|
||||
|
||||
### Frontend Consumption (openapi-fetch)
|
||||
|
||||
openapi-fetch uses a discriminated union for responses:
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client.POST('/api/events', { body: eventData })
|
||||
|
||||
if (error) {
|
||||
// `error` is typed from the OpenAPI error response schema
|
||||
console.log(error.title) // "Validation Failed"
|
||||
console.log(error.fieldErrors) // [{ field: "title", message: "..." }]
|
||||
return
|
||||
}
|
||||
|
||||
// `data` is the typed success response
|
||||
```
|
||||
|
||||
The `error` object is already typed from the generated schema — no manual type assertions needed for defined error shapes.
|
||||
|
||||
### Known Pitfalls
|
||||
|
||||
| Pitfall | Description | Mitigation |
|
||||
|---------|-------------|------------|
|
||||
| **Inconsistent formats** | Exceptions escaping to Spring Boot's `BasicErrorController` return legacy format (`timestamp`, `error`, `path`), not Problem Details. | Add a catch-all `@ExceptionHandler(Exception.class)` in the `@RestControllerAdvice`. |
|
||||
| **`server.error.include-*` ignored** | When Problem Details is active, these properties have no effect. | Control content via `ProblemDetail` directly. |
|
||||
| **Validation errors hidden by default** | Spring returns only `"Invalid request content."` without field details. | Override `handleMethodArgumentNotValid` explicitly. |
|
||||
| **Content negotiation** | `application/problem+json` is only returned when the client accepts it. `openapi-fetch` sends `Accept: application/json` which Spring considers compatible. | No action needed for SPA clients. |
|
||||
| **`about:blank` semantics** | With `type: "about:blank"`, `title` must match the HTTP status phrase. Custom titles require a custom `type` URI. | Use `urn:problem-type:*` URIs for custom problem types. |
|
||||
|
||||
## Sources
|
||||
|
||||
- [RFC 9457 Full Text](https://www.rfc-editor.org/rfc/rfc9457.html)
|
||||
- [Spring Framework Docs: Error Responses](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html)
|
||||
- [Swagger Blog: Problem Details RFC 9457](https://swagger.io/blog/problem-details-rfc9457-doing-api-errors-well/)
|
||||
- [Baeldung: Returning Errors Using ProblemDetail](https://www.baeldung.com/spring-boot-return-errors-problemdetail)
|
||||
- [SivaLabs: Spring Boot 3 Error Reporting](https://www.sivalabs.in/blog/spring-boot-3-error-reporting-using-problem-details/)
|
||||
- [Spring Boot Issue #43850: Render global errors as Problem Details](https://github.com/spring-projects/spring-boot/issues/43850)
|
||||
404
.specify/memory/research/sans-serif-fonts.md
Normal file
404
.specify/memory/research/sans-serif-fonts.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# Research: Modern Sans-Serif Fonts for Mobile-First PWA
|
||||
|
||||
**Date:** 2026-03-04
|
||||
**Context:** Selecting a primary typeface for fete, a privacy-focused PWA for event announcements and RSVPs. The font must be open-source with permissive licensing, modern geometric/neo-grotesque style, excellent mobile readability, and strong weight range.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Based on research of 9 candidate fonts, **6 meet all requirements** for self-hosting and redistribution under permissive licenses. Two do not qualify:
|
||||
|
||||
- **General Sans**: Proprietary (ITF Free Font License, non-commercial personal use only)
|
||||
- **Satoshi**: License ambiguity; sources conflict between full OFL and ITF restrictions
|
||||
|
||||
The remaining **6 fonts are fully open-source** and suitable for the project:
|
||||
|
||||
| Font | License | Design | Weights | Status |
|
||||
|------|---------|--------|---------|--------|
|
||||
| Inter | OFL-1.1 | Neo-grotesque, humanist | 9 (Thin–Black) | ✅ Recommended |
|
||||
| Plus Jakarta Sans | OFL-1.1 | Geometric, modern | 7 (ExtraLight–ExtraBold) | ✅ Recommended |
|
||||
| Outfit | OFL-1.1 | Geometric | 9 (Thin–Black) | ✅ Recommended |
|
||||
| Space Grotesk | OFL-1.1 | Neo-grotesque, distinctive | 5 (Light–Bold) | ✅ Recommended |
|
||||
| Manrope | OFL-1.1 | Geometric, humanist | 7 (ExtraLight–ExtraBold) | ✅ Recommended |
|
||||
| DM Sans | OFL-1.1 | Geometric, low-contrast | 9 (Thin–Black) | ✅ Recommended |
|
||||
| Sora | OFL-1.1 | Geometric | 8 (Thin–ExtraBold) | ✅ Recommended |
|
||||
|
||||
---
|
||||
|
||||
## Detailed Candidate Analysis
|
||||
|
||||
### 1. Inter
|
||||
|
||||
**License:** SIL Open Font License 1.1 (OFL-1.1)
|
||||
|
||||
**Download Location:**
|
||||
- **Official:** https://github.com/rsms/inter (releases page)
|
||||
- **NPM:** `inter-ui` package
|
||||
- **Homebrew:** `font-inter`
|
||||
- **Official CDN:** https://rsms.me/inter/inter.css
|
||||
|
||||
**Design Character:** Neo-grotesque with humanist touches. High x-height for enhanced legibility on screens. Geometric letterforms with open apertures. Designed specifically for UI and on-screen use.
|
||||
|
||||
**Available Weights:** 9 weights from Thin (100) to Black (900), each with italic variant. Also available as a variable font with weight axis.
|
||||
|
||||
**Notable Apps/Products:**
|
||||
- **UX/Design tools:** Figma, Notion, Pixar Presto
|
||||
- **OS:** Elementary OS, GNOME
|
||||
- **Web:** GitLab, ISO, Mozilla, NASA
|
||||
- **Why:** Chosen by product teams valuing clarity and modern minimalism; default choice for UI designers
|
||||
|
||||
**Mobile Suitability:** Excellent. Specifically engineered for screen readability with high x-height and open apertures. Performs well at 14–16px body text.
|
||||
|
||||
**Distinctive Strengths:**
|
||||
- Purpose-built for digital interfaces
|
||||
- Exceptional clarity in dense UI layouts
|
||||
- Strong brand identity (recognizable across tech products)
|
||||
- Extensive OpenType features
|
||||
|
||||
**Weakness:** Very widely used; less distinctive for a bold brand identity. Considered the "safe" choice.
|
||||
|
||||
---
|
||||
|
||||
### 2. Plus Jakarta Sans
|
||||
|
||||
**License:** SIL Open Font License 1.1 (OFL-1.1)
|
||||
|
||||
**Download Location:**
|
||||
- **Official Repository:** https://github.com/tokotype/PlusJakartaSans
|
||||
- **Source Files:** `sources/`, compiled fonts in `fonts/` directory
|
||||
- **Designer Contact:** mail@tokotype.com (Gumpita Rahayu, Tokotype)
|
||||
- **Latest Version:** 2.7.1 (May 2023)
|
||||
- **Build Command:** `gftools builder sources/builder.yaml`
|
||||
|
||||
**Design Character:** Geometric sans-serif with modern, clean-cut forms. Inspired by Neuzeit Grotesk and Futura but with contemporary refinement. Slightly taller x-height for clear spacing between caps and lowercase. Open counters and balanced spacing for legibility across sizes. **Bold, distinctive look** with personality.
|
||||
|
||||
**Available Weights:** 7 weights from ExtraLight (200) to ExtraBold (800), with matching italics.
|
||||
|
||||
**Notable Apps/Products:**
|
||||
- Original commission: Jakarta Provincial Government's "+Jakarta City of Collaboration" program (2020)
|
||||
- Now widely used in: Branding projects, modern web design, UI design
|
||||
- **Why:** Chosen for fresh, contemporary feel without generic blandness
|
||||
|
||||
**Mobile Suitability:** Excellent. Designed with mobile UI in mind. Clean letterforms render crisply on small screens.
|
||||
|
||||
**Distinctive Strengths:**
|
||||
- **Stylistic sets:** Sharp, Straight, and Swirl variants add design flexibility
|
||||
- Modern geometric with Indonesian design heritage (unique perspective)
|
||||
- Excellent for branding (not generic like Inter)
|
||||
- OpenType features for sophisticated typography
|
||||
- Well-maintained, active development
|
||||
|
||||
**Weakness:** Less ubiquitous than Inter; smaller ecosystem of design tool integrations.
|
||||
|
||||
---
|
||||
|
||||
### 3. Outfit
|
||||
|
||||
**License:** SIL Open Font License 1.1 (OFL-1.1)
|
||||
|
||||
**Download Location:**
|
||||
- **Official Repository:** https://github.com/Outfitio/Outfit-Fonts
|
||||
- **Fonts Directory:** `/fonts` in repository
|
||||
- **OFL Text:** `OFL.txt` in repository
|
||||
- **Designer:** Rodrigo Fuenzalida (originally for Outfit.io)
|
||||
- **Status:** Repository archived Feb 25, 2025 (read-only, downloads remain accessible)
|
||||
|
||||
**Design Character:** Geometric sans-serif with warm, friendly appearance. Generous x-height, balanced spacing, low contrast. Nine static weights plus variable font with weight axis.
|
||||
|
||||
**Available Weights:** 9 weights from Thin (100) to Black (900). No italics.
|
||||
|
||||
**Notable Apps/Products:**
|
||||
- Originally created for Outfit.io platform
|
||||
- Good readability for body text (≈16px) and strong headline presence
|
||||
- Used in design tools (Figma integration)
|
||||
|
||||
**Mobile Suitability:** Good. Geometric forms and generous spacing work well on mobile, though low contrast may require careful pairing with sufficient color contrast.
|
||||
|
||||
**Distinctive Strengths:**
|
||||
- Full weight range (Thin–Black)
|
||||
- Variable font option for granular weight control
|
||||
- Stylistic alternates and rare ligatures
|
||||
- Accessible character set
|
||||
|
||||
**Weakness:** Archived repository; no active development. Low contrast design requires careful color/contrast pairing for accessibility.
|
||||
|
||||
---
|
||||
|
||||
### 4. Space Grotesk
|
||||
|
||||
**License:** SIL Open Font License 1.1 (OFL-1.1)
|
||||
|
||||
**Download Location:**
|
||||
- **Official Repository:** https://github.com/floriankarsten/space-grotesk
|
||||
- **Official Site:** https://fonts.floriankarsten.com/space-grotesk
|
||||
- **Designer:** Florian Karsten
|
||||
- **Variants:** Variable font with weight axis
|
||||
|
||||
**Design Character:** Neo-grotesque with distinctive personality. Proportional variant of Space Mono (Colophon Foundry, 2016). Retains Space Mono's idiosyncratic details while optimizing for improved readability. Bold, tech-forward aesthetic with monowidth heritage visible in character design.
|
||||
|
||||
**Available Weights:** 5 weights—Light (300), Regular (400), Medium (500), SemiBold (600), Bold (700). No italics.
|
||||
|
||||
**Notable Apps/Products:**
|
||||
- Modern tech companies and startups seeking distinctive branding
|
||||
- Popular in neo-brutalist web design
|
||||
- Good for headlines and display use
|
||||
|
||||
**Mobile Suitability:** Good. Clean proportional forms with distinctive character. Works well for headlines; body text at 14px+ is readable.
|
||||
|
||||
**Distinctive Strengths:**
|
||||
- **Bold, tech-forward personality** — immediately recognizable
|
||||
- Heritage from Space Mono adds character without looking dated
|
||||
- Excellent OpenType support (old-style figures, tabular figures, superscript, subscript, fractions, stylistic alternates)
|
||||
- **Supports extended language coverage:** Latin, Vietnamese, Pinyin, Central/South-Eastern European
|
||||
|
||||
**Weakness:** Only 5 weights (lightest is 300, no Thin). Fewer weight options than Inter or DM Sans.
|
||||
|
||||
---
|
||||
|
||||
### 5. Manrope
|
||||
|
||||
**License:** SIL Open Font License 1.1 (OFL-1.1)
|
||||
|
||||
**Download Location:**
|
||||
- **Official Repository:** https://github.com/sharanda/manrope
|
||||
- **Designer:** Mikhail Sharanda (2018), converted to variable by Mirko Velimirovic (2019)
|
||||
- **Alternative Sources:** Multiple community forks on GitHub, npm packages
|
||||
- **NPM Package:** `@fontsource/manrope`, `@fontsource-variable/manrope`
|
||||
|
||||
**Design Character:** Modern geometric sans-serif blending geometric shapes with humanistic elements. Semi-condensed structure with clean, contemporary feel. Geometric digits, packed with OpenType features.
|
||||
|
||||
**Available Weights:** 7 weights from ExtraLight (200) to ExtraBold (800). Available as variable font.
|
||||
|
||||
**Notable Apps/Products:**
|
||||
- Widely used in modern design systems
|
||||
- Popular in product/SaaS design
|
||||
- Good for both UI and branding
|
||||
|
||||
**Mobile Suitability:** Excellent. Clean geometric design with humanistic touches; balanced proportions work well on mobile.
|
||||
|
||||
**Distinctive Strengths:**
|
||||
- Geometric + humanistic blend (best of both worlds)
|
||||
- Well-maintained active project
|
||||
- Variable font available
|
||||
- Strong design community around the font
|
||||
|
||||
**Weakness:** None significant; solid all-around choice.
|
||||
|
||||
---
|
||||
|
||||
### 6. DM Sans
|
||||
|
||||
**License:** SIL Open Font License 1.1 (OFL-1.1)
|
||||
|
||||
**Download Location:**
|
||||
- **Official Repository:** https://github.com/googlefonts/dm-fonts
|
||||
- **Releases Page:** https://github.com/googlefonts/dm-fonts/releases
|
||||
- **Google Fonts:** https://fonts.google.com/specimen/DM+Sans
|
||||
- **Design:** Commissioned from Colophon Foundry; Creative Direction: MultiAdaptor & DeepMind
|
||||
|
||||
**Design Character:** Low-contrast geometric sans-serif optimized for text at smaller sizes. Part of the DM suite (DM Sans, DM Serif Text, DM Serif Display). Designed for clarity and efficiency in dense typography.
|
||||
|
||||
**Available Weights:** 9 weights from Thin (100) to Black (900), each with italic variant.
|
||||
|
||||
**Notable Apps/Products:**
|
||||
- DeepMind products (by commission)
|
||||
- Tech companies favoring geometric clarity
|
||||
- Professional and commercial products requiring text legibility
|
||||
|
||||
**Mobile Suitability:** Excellent. Specifically optimized for small text sizes; low contrast minimizes visual noise on mobile screens.
|
||||
|
||||
**Distinctive Strengths:**
|
||||
- **Optimized for small text** — superior at 12–14px
|
||||
- Full weight range (Thin–Black)
|
||||
- Active Google Fonts maintenance
|
||||
- Italic variants (unlike Outfit or Space Grotesk)
|
||||
- Commissioned by reputable team (DeepMind)
|
||||
|
||||
**Weakness:** Low contrast may feel less bold on headlines without careful sizing/weight adjustment.
|
||||
|
||||
---
|
||||
|
||||
### 7. Sora
|
||||
|
||||
**License:** SIL Open Font License 1.1 (OFL-1.1)
|
||||
|
||||
**Download Location:**
|
||||
- **Official Repository:** https://github.com/sora-xor/sora-font
|
||||
- **GitHub Releases:** Direct TTF/OTF downloads available
|
||||
- **NPM Packages:** `@fontsource/sora`, `@fontsource-variable/sora`
|
||||
- **Original Purpose:** Custom typeface for SORA decentralized autonomous economy
|
||||
|
||||
**Design Character:** Geometric sans-serif with contemporary, clean aesthetic. Available as both static fonts and variable font. Designed as a branding solution for decentralized systems.
|
||||
|
||||
**Available Weights:** 8 weights from Thin (100) to ExtraBold (800), each with italic variant. Variable font available.
|
||||
|
||||
**Notable Apps/Products:**
|
||||
- Sora (XOR) decentralized projects
|
||||
- Crypto/blockchain projects using modern typography
|
||||
- Web3 products seeking distinctive branding
|
||||
|
||||
**Mobile Suitability:** Good. Clean geometric forms render well on mobile; italics available for emphasis.
|
||||
|
||||
**Distinctive Strengths:**
|
||||
- Full weight range with italics
|
||||
- Variable font option
|
||||
- Designed for digital-first branding
|
||||
- GitHub-native distribution
|
||||
|
||||
**Weakness:** Less established than Inter or DM Sans in mainstream product design; smaller ecosystem.
|
||||
|
||||
---
|
||||
|
||||
## Rejected Candidates
|
||||
|
||||
### General Sans
|
||||
|
||||
**Status:** ❌ Does not meet licensing requirements
|
||||
|
||||
**License:** ITF Free Font License (proprietary, non-commercial personal use only)
|
||||
|
||||
**Why Rejected:** This is a **paid commercial font** distributed by the Indian Type Foundry (not open-source). The ITF Free Font License permits personal use only; commercial use requires a separate paid license. Does not meet the "open-source with permissive license" requirement.
|
||||
|
||||
**Designer:** Frode Helland (published by Indian Type Foundry)
|
||||
|
||||
---
|
||||
|
||||
### Satoshi
|
||||
|
||||
**Status:** ⚠️ License ambiguity — conflicting sources
|
||||
|
||||
**Documented License:**
|
||||
- Some sources claim SIL Open Font License (OFL-1.1)
|
||||
- Other sources indicate ITF Free Font License (personal use only) similar to General Sans
|
||||
|
||||
**Design:** Swiss-style modernist sans-serif (Light to Black, 5–10 weights)
|
||||
|
||||
**Download:** Fontshare (Indian Type Foundry's free font service)
|
||||
|
||||
**Why Not Recommended:** The license status is unclear. While Fontshare advertises "free for personal and commercial use," the font's origin (Indian Type Foundry) and conflicting license documentation create uncertainty. For a privacy-focused project with clear open-source requirements, Satoshi's ambiguous licensing creates unnecessary legal risk. Better alternatives with unambiguous OFL-1.1 licensing are available.
|
||||
|
||||
**Recommendation:** If clarity is needed, contact Fontshare/ITF directly. For now, exclude from consideration to reduce licensing complexity.
|
||||
|
||||
---
|
||||
|
||||
## Comparative Table: Qualified Fonts
|
||||
|
||||
| Metric | Inter | Plus Jakarta Sans | Outfit | Space Grotesk | Manrope | DM Sans | Sora |
|
||||
|--------|-------|-------------------|--------|---------------|---------|---------|------|
|
||||
| **License** | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 |
|
||||
| **Weights** | 9 | 7 | 9 | 5 | 7 | 9 | 8 |
|
||||
| **Italics** | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
|
||||
| **Variable Font** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
|
||||
| **Design** | Neo-grotesque | Geometric | Geometric | Neo-grotesque | Geo + Humanist | Geometric | Geometric |
|
||||
| **Personality** | Generic/Safe | Bold/Fresh | Warm/Friendly | Tech-Forward | Balanced | Efficient/Clean | Contemporary |
|
||||
| **Mobile Text** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| **Distinctiveness** | Low | High | Medium | High | High | Medium | Medium |
|
||||
| **Ecosystem** | Very Large | Growing | Medium | Growing | Growing | Large | Small |
|
||||
| **Active Dev** | ✅ Yes | ✅ Yes | ❌ Archived | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### For Bold App-Native Branding
|
||||
|
||||
**Primary Choice: Plus Jakarta Sans**
|
||||
|
||||
**Rationale:**
|
||||
- Fully open-source (OFL-1.1) with unambiguous licensing
|
||||
- Bold, modern geometric aesthetic suitable for app branding
|
||||
- Stylistic sets (Sharp, Straight, Swirl) provide design flexibility
|
||||
- Well-maintained by Tokotype with clear development history
|
||||
- Strong presence in modern UI/web design
|
||||
- Excellent mobile readability with thoughtful character spacing
|
||||
- Indonesian design heritage adds unique perspective (not generic)
|
||||
|
||||
**Alternative: Space Grotesk**
|
||||
|
||||
If you prefer **even more distinctive character:**
|
||||
- Neo-grotesque with tech-forward personality
|
||||
- Smaller weight range (5 weights) but strong identity
|
||||
- Popular in contemporary design circles
|
||||
- Good for headlines; pair with a more neutral font for body text if needed
|
||||
|
||||
---
|
||||
|
||||
### For Safe, Professional UI
|
||||
|
||||
**Primary Choice: Inter or DM Sans**
|
||||
|
||||
**Inter if:**
|
||||
- Maximum ecosystem and tool support desired
|
||||
- Designing for broad recognition and trust
|
||||
- Team already familiar with Inter (widespread in tech)
|
||||
|
||||
**DM Sans if:**
|
||||
- Emphasis on small text legibility (optimized for 12–14px)
|
||||
- Prefer italic variants
|
||||
- Want active maintenance from Google Fonts community
|
||||
|
||||
---
|
||||
|
||||
### For Balanced Approach
|
||||
|
||||
**Manrope**
|
||||
|
||||
- Geometric + humanistic blend (versatile)
|
||||
- Excellent mobile performance
|
||||
- Strong weight range (7 weights)
|
||||
- Underrated choice; often overlooked for bolder options but delivers polish
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes for Self-Hosting
|
||||
|
||||
All recommended fonts can be self-hosted:
|
||||
|
||||
1. **Download:** Clone repository or download from releases page
|
||||
2. **Generate Web Formats:** Use FontForge, FontTools, or online converters to generate WOFF2 (required for modern browsers)
|
||||
3. **CSS:** Include via `@font-face` with local file paths
|
||||
4. **License:** Include `LICENSE.txt` or `OFL.txt` in the distribution
|
||||
|
||||
Example self-hosted CSS:
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'Plus Jakarta Sans';
|
||||
src: url('/fonts/PlusJakartaSans-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Privacy Considerations
|
||||
|
||||
All selected fonts are self-hosted open-source projects with no telemetry, no external CDN dependencies, and no tracking. Fully compliant with the project's privacy-first principles.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Inter, Plus Jakarta Sans, and Space Grotesk** are the strongest candidates. The choice depends on brand positioning:
|
||||
|
||||
- **Generic + Safe → Inter**
|
||||
- **Bold + Modern → Plus Jakarta Sans**
|
||||
- **Tech-Forward + Distinctive → Space Grotesk**
|
||||
|
||||
All seven recommended fonts meet the strict licensing, openness, mobile readability, and weight-range requirements. Any of them are viable; the decision is primarily aesthetic.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Inter Font GitHub Repository](https://github.com/rsms/inter)
|
||||
- [Plus Jakarta Sans GitHub Repository](https://github.com/tokotype/PlusJakartaSans)
|
||||
- [Outfit Fonts GitHub Repository](https://github.com/Outfitio/Outfit-Fonts)
|
||||
- [Space Grotesk GitHub Repository](https://github.com/floriankarsten/space-grotesk)
|
||||
- [Manrope GitHub Repository](https://github.com/sharanda/manrope)
|
||||
- [DM Fonts GitHub Repository](https://github.com/googlefonts/dm-fonts)
|
||||
- [Sora Font GitHub Repository](https://github.com/sora-xor/sora-font)
|
||||
- [SIL Open Font License](https://openfontlicense.org/)
|
||||
- [Google Fonts (reference)](https://fonts.google.com)
|
||||
- [Fontshare (reference)](https://www.fontshare.com)
|
||||
166
.specify/scripts/bash/check-prerequisites.sh
Executable file
166
.specify/scripts/bash/check-prerequisites.sh
Executable file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Consolidated prerequisite checking script
|
||||
#
|
||||
# This script provides unified prerequisite checking for Spec-Driven Development workflow.
|
||||
# It replaces the functionality previously spread across multiple scripts.
|
||||
#
|
||||
# Usage: ./check-prerequisites.sh [OPTIONS]
|
||||
#
|
||||
# OPTIONS:
|
||||
# --json Output in JSON format
|
||||
# --require-tasks Require tasks.md to exist (for implementation phase)
|
||||
# --include-tasks Include tasks.md in AVAILABLE_DOCS list
|
||||
# --paths-only Only output path variables (no validation)
|
||||
# --help, -h Show help message
|
||||
#
|
||||
# OUTPUTS:
|
||||
# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
|
||||
# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
|
||||
# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
|
||||
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
JSON_MODE=false
|
||||
REQUIRE_TASKS=false
|
||||
INCLUDE_TASKS=false
|
||||
PATHS_ONLY=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--require-tasks)
|
||||
REQUIRE_TASKS=true
|
||||
;;
|
||||
--include-tasks)
|
||||
INCLUDE_TASKS=true
|
||||
;;
|
||||
--paths-only)
|
||||
PATHS_ONLY=true
|
||||
;;
|
||||
--help|-h)
|
||||
cat << 'EOF'
|
||||
Usage: check-prerequisites.sh [OPTIONS]
|
||||
|
||||
Consolidated prerequisite checking for Spec-Driven Development workflow.
|
||||
|
||||
OPTIONS:
|
||||
--json Output in JSON format
|
||||
--require-tasks Require tasks.md to exist (for implementation phase)
|
||||
--include-tasks Include tasks.md in AVAILABLE_DOCS list
|
||||
--paths-only Only output path variables (no prerequisite validation)
|
||||
--help, -h Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
# Check task prerequisites (plan.md required)
|
||||
./check-prerequisites.sh --json
|
||||
|
||||
# Check implementation prerequisites (plan.md + tasks.md required)
|
||||
./check-prerequisites.sh --json --require-tasks --include-tasks
|
||||
|
||||
# Get feature paths only (no validation)
|
||||
./check-prerequisites.sh --paths-only
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Source common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get feature paths and validate branch
|
||||
eval $(get_feature_paths)
|
||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||
|
||||
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
||||
if $PATHS_ONLY; then
|
||||
if $JSON_MODE; then
|
||||
# Minimal JSON paths payload (no validation performed)
|
||||
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
||||
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
|
||||
else
|
||||
echo "REPO_ROOT: $REPO_ROOT"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
echo "FEATURE_DIR: $FEATURE_DIR"
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "TASKS: $TASKS"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate required directories and files
|
||||
if [[ ! -d "$FEATURE_DIR" ]]; then
|
||||
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
|
||||
echo "Run /speckit.specify first to create the feature structure." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$IMPL_PLAN" ]]; then
|
||||
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
|
||||
echo "Run /speckit.plan first to create the implementation plan." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for tasks.md if required
|
||||
if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
|
||||
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
|
||||
echo "Run /speckit.tasks first to create the task list." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build list of available documents
|
||||
docs=()
|
||||
|
||||
# Always check these optional docs
|
||||
[[ -f "$RESEARCH" ]] && docs+=("research.md")
|
||||
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
|
||||
|
||||
# Check contracts directory (only if it exists and has files)
|
||||
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
|
||||
docs+=("contracts/")
|
||||
fi
|
||||
|
||||
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
|
||||
|
||||
# Include tasks.md if requested and it exists
|
||||
if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
|
||||
docs+=("tasks.md")
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_MODE; then
|
||||
# Build JSON array of documents
|
||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
||||
json_docs="[]"
|
||||
else
|
||||
json_docs=$(printf '"%s",' "${docs[@]}")
|
||||
json_docs="[${json_docs%,}]"
|
||||
fi
|
||||
|
||||
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
|
||||
else
|
||||
# Text output
|
||||
echo "FEATURE_DIR:$FEATURE_DIR"
|
||||
echo "AVAILABLE_DOCS:"
|
||||
|
||||
# Show status of each potential document
|
||||
check_file "$RESEARCH" "research.md"
|
||||
check_file "$DATA_MODEL" "data-model.md"
|
||||
check_dir "$CONTRACTS_DIR" "contracts/"
|
||||
check_file "$QUICKSTART" "quickstart.md"
|
||||
|
||||
if $INCLUDE_TASKS; then
|
||||
check_file "$TASKS" "tasks.md"
|
||||
fi
|
||||
fi
|
||||
156
.specify/scripts/bash/common.sh
Executable file
156
.specify/scripts/bash/common.sh
Executable file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
# Common functions and variables for all scripts
|
||||
|
||||
# Get repository root, with fallback for non-git repositories
|
||||
get_repo_root() {
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
git rev-parse --show-toplevel
|
||||
else
|
||||
# Fall back to script location for non-git repos
|
||||
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
(cd "$script_dir/../../.." && pwd)
|
||||
fi
|
||||
}
|
||||
|
||||
# Get current branch, with fallback for non-git repositories
|
||||
get_current_branch() {
|
||||
# First check if SPECIFY_FEATURE environment variable is set
|
||||
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
|
||||
echo "$SPECIFY_FEATURE"
|
||||
return
|
||||
fi
|
||||
|
||||
# Then check git if available
|
||||
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
return
|
||||
fi
|
||||
|
||||
# For non-git repos, try to find the latest feature directory
|
||||
local repo_root=$(get_repo_root)
|
||||
local specs_dir="$repo_root/specs"
|
||||
|
||||
if [[ -d "$specs_dir" ]]; then
|
||||
local latest_feature=""
|
||||
local highest=0
|
||||
|
||||
for dir in "$specs_dir"/*; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
local dirname=$(basename "$dir")
|
||||
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
|
||||
local number=${BASH_REMATCH[1]}
|
||||
number=$((10#$number))
|
||||
if [[ "$number" -gt "$highest" ]]; then
|
||||
highest=$number
|
||||
latest_feature=$dirname
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$latest_feature" ]]; then
|
||||
echo "$latest_feature"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "main" # Final fallback
|
||||
}
|
||||
|
||||
# Check if we have git available
|
||||
has_git() {
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_feature_branch() {
|
||||
local branch="$1"
|
||||
local has_git_repo="$2"
|
||||
|
||||
# For non-git repos, we can't enforce branch naming but still provide output
|
||||
if [[ "$has_git_repo" != "true" ]]; then
|
||||
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
|
||||
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
||||
echo "Feature branches should be named like: 001-feature-name" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
get_feature_dir() { echo "$1/specs/$2"; }
|
||||
|
||||
# Find feature directory by numeric prefix instead of exact branch match
|
||||
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
|
||||
find_feature_dir_by_prefix() {
|
||||
local repo_root="$1"
|
||||
local branch_name="$2"
|
||||
local specs_dir="$repo_root/specs"
|
||||
|
||||
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
|
||||
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
|
||||
# If branch doesn't have numeric prefix, fall back to exact match
|
||||
echo "$specs_dir/$branch_name"
|
||||
return
|
||||
fi
|
||||
|
||||
local prefix="${BASH_REMATCH[1]}"
|
||||
|
||||
# Search for directories in specs/ that start with this prefix
|
||||
local matches=()
|
||||
if [[ -d "$specs_dir" ]]; then
|
||||
for dir in "$specs_dir"/"$prefix"-*; do
|
||||
if [[ -d "$dir" ]]; then
|
||||
matches+=("$(basename "$dir")")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Handle results
|
||||
if [[ ${#matches[@]} -eq 0 ]]; then
|
||||
# No match found - return the branch name path (will fail later with clear error)
|
||||
echo "$specs_dir/$branch_name"
|
||||
elif [[ ${#matches[@]} -eq 1 ]]; then
|
||||
# Exactly one match - perfect!
|
||||
echo "$specs_dir/${matches[0]}"
|
||||
else
|
||||
# Multiple matches - this shouldn't happen with proper naming convention
|
||||
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
||||
echo "Please ensure only one spec directory exists per numeric prefix." >&2
|
||||
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
|
||||
fi
|
||||
}
|
||||
|
||||
get_feature_paths() {
|
||||
local repo_root=$(get_repo_root)
|
||||
local current_branch=$(get_current_branch)
|
||||
local has_git_repo="false"
|
||||
|
||||
if has_git; then
|
||||
has_git_repo="true"
|
||||
fi
|
||||
|
||||
# Use prefix-based lookup to support multiple branches per spec
|
||||
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
|
||||
|
||||
cat <<EOF
|
||||
REPO_ROOT='$repo_root'
|
||||
CURRENT_BRANCH='$current_branch'
|
||||
HAS_GIT='$has_git_repo'
|
||||
FEATURE_DIR='$feature_dir'
|
||||
FEATURE_SPEC='$feature_dir/spec.md'
|
||||
IMPL_PLAN='$feature_dir/plan.md'
|
||||
TASKS='$feature_dir/tasks.md'
|
||||
RESEARCH='$feature_dir/research.md'
|
||||
DATA_MODEL='$feature_dir/data-model.md'
|
||||
QUICKSTART='$feature_dir/quickstart.md'
|
||||
CONTRACTS_DIR='$feature_dir/contracts'
|
||||
EOF
|
||||
}
|
||||
|
||||
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||
|
||||
313
.specify/scripts/bash/create-new-feature.sh
Executable file
313
.specify/scripts/bash/create-new-feature.sh
Executable file
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
JSON_MODE=false
|
||||
SHORT_NAME=""
|
||||
BRANCH_NUMBER=""
|
||||
ARGS=()
|
||||
i=1
|
||||
while [ $i -le $# ]; do
|
||||
arg="${!i}"
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--short-name)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
# Check if the next argument is another option (starts with --)
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --short-name requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
SHORT_NAME="$next_arg"
|
||||
;;
|
||||
--number)
|
||||
if [ $((i + 1)) -gt $# ]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
i=$((i + 1))
|
||||
next_arg="${!i}"
|
||||
if [[ "$next_arg" == --* ]]; then
|
||||
echo 'Error: --number requires a value' >&2
|
||||
exit 1
|
||||
fi
|
||||
BRANCH_NUMBER="$next_arg"
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --json Output in JSON format"
|
||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
|
||||
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
|
||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to find the repository root by searching for existing project markers
|
||||
find_repo_root() {
|
||||
local dir="$1"
|
||||
while [ "$dir" != "/" ]; do
|
||||
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir="$(dirname "$dir")"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to get highest number from specs directory
|
||||
get_highest_from_specs() {
|
||||
local specs_dir="$1"
|
||||
local highest=0
|
||||
|
||||
if [ -d "$specs_dir" ]; then
|
||||
for dir in "$specs_dir"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
dirname=$(basename "$dir")
|
||||
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to get highest number from git branches
|
||||
get_highest_from_branches() {
|
||||
local highest=0
|
||||
|
||||
# Get all branches (local and remote)
|
||||
branches=$(git branch -a 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$branches" ]; then
|
||||
while IFS= read -r branch; do
|
||||
# Clean branch name: remove leading markers and remote prefixes
|
||||
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
||||
|
||||
# Extract feature number if branch matches pattern ###-*
|
||||
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
||||
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
||||
number=$((10#$number))
|
||||
if [ "$number" -gt "$highest" ]; then
|
||||
highest=$number
|
||||
fi
|
||||
fi
|
||||
done <<< "$branches"
|
||||
fi
|
||||
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Function to check existing branches (local and remote) and return next available number
|
||||
check_existing_branches() {
|
||||
local specs_dir="$1"
|
||||
|
||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||
git fetch --all --prune 2>/dev/null || true
|
||||
|
||||
# Get highest number from ALL branches (not just matching short name)
|
||||
local highest_branch=$(get_highest_from_branches)
|
||||
|
||||
# Get highest number from ALL specs (not just matching short name)
|
||||
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
||||
|
||||
# Take the maximum of both
|
||||
local max_num=$highest_branch
|
||||
if [ "$highest_spec" -gt "$max_num" ]; then
|
||||
max_num=$highest_spec
|
||||
fi
|
||||
|
||||
# Return next number
|
||||
echo $((max_num + 1))
|
||||
}
|
||||
|
||||
# Function to clean and format a branch name
|
||||
clean_branch_name() {
|
||||
local name="$1"
|
||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||
}
|
||||
|
||||
# Resolve repository root. Prefer git information when available, but fall back
|
||||
# to searching for repository markers so the workflow still functions in repositories that
|
||||
# were initialised with --no-git.
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
HAS_GIT=true
|
||||
else
|
||||
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
HAS_GIT=false
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
SPECS_DIR="$REPO_ROOT/specs"
|
||||
mkdir -p "$SPECS_DIR"
|
||||
|
||||
# Function to generate branch name with stop word filtering and length filtering
|
||||
generate_branch_name() {
|
||||
local description="$1"
|
||||
|
||||
# Common stop words to filter out
|
||||
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
|
||||
|
||||
# Convert to lowercase and split into words
|
||||
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
|
||||
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
||||
local meaningful_words=()
|
||||
for word in $clean_name; do
|
||||
# Skip empty words
|
||||
[ -z "$word" ] && continue
|
||||
|
||||
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
||||
if ! echo "$word" | grep -qiE "$stop_words"; then
|
||||
if [ ${#word} -ge 3 ]; then
|
||||
meaningful_words+=("$word")
|
||||
elif echo "$description" | grep -q "\b${word^^}\b"; then
|
||||
# Keep short words if they appear as uppercase in original (likely acronyms)
|
||||
meaningful_words+=("$word")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# If we have meaningful words, use first 3-4 of them
|
||||
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
||||
local max_words=3
|
||||
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
||||
|
||||
local result=""
|
||||
local count=0
|
||||
for word in "${meaningful_words[@]}"; do
|
||||
if [ $count -ge $max_words ]; then break; fi
|
||||
if [ -n "$result" ]; then result="$result-"; fi
|
||||
result="$result$word"
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo "$result"
|
||||
else
|
||||
# Fallback to original logic if no meaningful words found
|
||||
local cleaned=$(clean_branch_name "$description")
|
||||
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate branch name
|
||||
if [ -n "$SHORT_NAME" ]; then
|
||||
# Use provided short name, just clean it up
|
||||
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
||||
else
|
||||
# Generate from description with smart filtering
|
||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||
fi
|
||||
|
||||
# Determine branch number
|
||||
if [ -z "$BRANCH_NUMBER" ]; then
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
# Check existing branches on remotes
|
||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
||||
else
|
||||
# Fall back to local directory check
|
||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names
|
||||
# Validate and truncate if necessary
|
||||
MAX_BRANCH_LENGTH=244
|
||||
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
||||
# Calculate how much we need to trim from suffix
|
||||
# Account for: feature number (3) + hyphen (1) = 4 chars
|
||||
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
|
||||
|
||||
# Truncate suffix at word boundary if possible
|
||||
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
||||
# Remove trailing hyphen if truncation created one
|
||||
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
||||
|
||||
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
||||
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
||||
|
||||
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
||||
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||
fi
|
||||
|
||||
if [ "$HAS_GIT" = true ]; then
|
||||
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
||||
# Check if branch already exists
|
||||
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
||||
exit 1
|
||||
else
|
||||
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||
fi
|
||||
|
||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
|
||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||
|
||||
# Set the SPECIFY_FEATURE environment variable for the current session
|
||||
export SPECIFY_FEATURE="$BRANCH_NAME"
|
||||
|
||||
if $JSON_MODE; then
|
||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
||||
else
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "SPEC_FILE: $SPEC_FILE"
|
||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
||||
fi
|
||||
61
.specify/scripts/bash/setup-plan.sh
Executable file
61
.specify/scripts/bash/setup-plan.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
JSON_MODE=false
|
||||
ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json)
|
||||
JSON_MODE=true
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--json]"
|
||||
echo " --json Output results in JSON format"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Get script directory and load common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths and variables from common functions
|
||||
eval $(get_feature_paths)
|
||||
|
||||
# Check if we're on a proper feature branch (only for git repos)
|
||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||
|
||||
# Ensure the feature directory exists
|
||||
mkdir -p "$FEATURE_DIR"
|
||||
|
||||
# Copy plan template if it exists
|
||||
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
|
||||
if [[ -f "$TEMPLATE" ]]; then
|
||||
cp "$TEMPLATE" "$IMPL_PLAN"
|
||||
echo "Copied plan template to $IMPL_PLAN"
|
||||
else
|
||||
echo "Warning: Plan template not found at $TEMPLATE"
|
||||
# Create a basic plan file if template doesn't exist
|
||||
touch "$IMPL_PLAN"
|
||||
fi
|
||||
|
||||
# Output results
|
||||
if $JSON_MODE; then
|
||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
||||
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
|
||||
else
|
||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||
echo "SPECS_DIR: $FEATURE_DIR"
|
||||
echo "BRANCH: $CURRENT_BRANCH"
|
||||
echo "HAS_GIT: $HAS_GIT"
|
||||
fi
|
||||
|
||||
829
.specify/scripts/bash/update-agent-context.sh
Executable file
829
.specify/scripts/bash/update-agent-context.sh
Executable file
@@ -0,0 +1,829 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Update agent context files with information from plan.md
|
||||
#
|
||||
# This script maintains AI agent context files by parsing feature specifications
|
||||
# and updating agent-specific configuration files with project information.
|
||||
#
|
||||
# MAIN FUNCTIONS:
|
||||
# 1. Environment Validation
|
||||
# - Verifies git repository structure and branch information
|
||||
# - Checks for required plan.md files and templates
|
||||
# - Validates file permissions and accessibility
|
||||
#
|
||||
# 2. Plan Data Extraction
|
||||
# - Parses plan.md files to extract project metadata
|
||||
# - Identifies language/version, frameworks, databases, and project types
|
||||
# - Handles missing or incomplete specification data gracefully
|
||||
#
|
||||
# 3. Agent File Management
|
||||
# - Creates new agent context files from templates when needed
|
||||
# - Updates existing agent files with new project information
|
||||
# - Preserves manual additions and custom configurations
|
||||
# - Supports multiple AI agent formats and directory structures
|
||||
#
|
||||
# 4. Content Generation
|
||||
# - Generates language-specific build/test commands
|
||||
# - Creates appropriate project directory structures
|
||||
# - Updates technology stacks and recent changes sections
|
||||
# - Maintains consistent formatting and timestamps
|
||||
#
|
||||
# 5. Multi-Agent Support
|
||||
# - Handles agent-specific file paths and naming conventions
|
||||
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity
|
||||
# - Can update single agents or all existing agent files
|
||||
# - Creates default Claude file if no agent files exist
|
||||
#
|
||||
# Usage: ./update-agent-context.sh [agent_type]
|
||||
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli
|
||||
# Leave empty to update all existing agent files
|
||||
|
||||
set -e
|
||||
|
||||
# Enable strict error handling
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
#==============================================================================
|
||||
# Configuration and Global Variables
|
||||
#==============================================================================
|
||||
|
||||
# Get script directory and load common functions
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get all paths and variables from common functions
|
||||
eval $(get_feature_paths)
|
||||
|
||||
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
|
||||
AGENT_TYPE="${1:-}"
|
||||
|
||||
# Agent-specific file paths
|
||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
||||
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
||||
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
|
||||
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
||||
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
||||
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
||||
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
|
||||
KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
|
||||
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
|
||||
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
|
||||
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
|
||||
QODER_FILE="$REPO_ROOT/QODER.md"
|
||||
AMP_FILE="$REPO_ROOT/AGENTS.md"
|
||||
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
||||
KIRO_FILE="$REPO_ROOT/AGENTS.md"
|
||||
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
||||
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
||||
|
||||
# Template file
|
||||
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
||||
|
||||
# Global variables for parsed plan data
|
||||
NEW_LANG=""
|
||||
NEW_FRAMEWORK=""
|
||||
NEW_DB=""
|
||||
NEW_PROJECT_TYPE=""
|
||||
|
||||
#==============================================================================
|
||||
# Utility Functions
|
||||
#==============================================================================
|
||||
|
||||
log_info() {
|
||||
echo "INFO: $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "✓ $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "ERROR: $1" >&2
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo "WARNING: $1" >&2
|
||||
}
|
||||
|
||||
# Cleanup function for temporary files
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
rm -f /tmp/agent_update_*_$$
|
||||
rm -f /tmp/manual_additions_$$
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
# Set up cleanup trap
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
#==============================================================================
|
||||
# Validation Functions
|
||||
#==============================================================================
|
||||
|
||||
validate_environment() {
|
||||
# Check if we have a current branch/feature (git or non-git)
|
||||
if [[ -z "$CURRENT_BRANCH" ]]; then
|
||||
log_error "Unable to determine current feature"
|
||||
if [[ "$HAS_GIT" == "true" ]]; then
|
||||
log_info "Make sure you're on a feature branch"
|
||||
else
|
||||
log_info "Set SPECIFY_FEATURE environment variable or create a feature first"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if plan.md exists
|
||||
if [[ ! -f "$NEW_PLAN" ]]; then
|
||||
log_error "No plan.md found at $NEW_PLAN"
|
||||
log_info "Make sure you're working on a feature with a corresponding spec directory"
|
||||
if [[ "$HAS_GIT" != "true" ]]; then
|
||||
log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if template exists (needed for new files)
|
||||
if [[ ! -f "$TEMPLATE_FILE" ]]; then
|
||||
log_warning "Template file not found at $TEMPLATE_FILE"
|
||||
log_warning "Creating new agent files will fail"
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Plan Parsing Functions
|
||||
#==============================================================================
|
||||
|
||||
extract_plan_field() {
|
||||
local field_pattern="$1"
|
||||
local plan_file="$2"
|
||||
|
||||
grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \
|
||||
head -1 | \
|
||||
sed "s|^\*\*${field_pattern}\*\*: ||" | \
|
||||
sed 's/^[ \t]*//;s/[ \t]*$//' | \
|
||||
grep -v "NEEDS CLARIFICATION" | \
|
||||
grep -v "^N/A$" || echo ""
|
||||
}
|
||||
|
||||
parse_plan_data() {
|
||||
local plan_file="$1"
|
||||
|
||||
if [[ ! -f "$plan_file" ]]; then
|
||||
log_error "Plan file not found: $plan_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$plan_file" ]]; then
|
||||
log_error "Plan file is not readable: $plan_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Parsing plan data from $plan_file"
|
||||
|
||||
NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file")
|
||||
NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file")
|
||||
NEW_DB=$(extract_plan_field "Storage" "$plan_file")
|
||||
NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file")
|
||||
|
||||
# Log what we found
|
||||
if [[ -n "$NEW_LANG" ]]; then
|
||||
log_info "Found language: $NEW_LANG"
|
||||
else
|
||||
log_warning "No language information found in plan"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_FRAMEWORK" ]]; then
|
||||
log_info "Found framework: $NEW_FRAMEWORK"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
|
||||
log_info "Found database: $NEW_DB"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_PROJECT_TYPE" ]]; then
|
||||
log_info "Found project type: $NEW_PROJECT_TYPE"
|
||||
fi
|
||||
}
|
||||
|
||||
format_technology_stack() {
|
||||
local lang="$1"
|
||||
local framework="$2"
|
||||
local parts=()
|
||||
|
||||
# Add non-empty parts
|
||||
[[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang")
|
||||
[[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework")
|
||||
|
||||
# Join with proper formatting
|
||||
if [[ ${#parts[@]} -eq 0 ]]; then
|
||||
echo ""
|
||||
elif [[ ${#parts[@]} -eq 1 ]]; then
|
||||
echo "${parts[0]}"
|
||||
else
|
||||
# Join multiple parts with " + "
|
||||
local result="${parts[0]}"
|
||||
for ((i=1; i<${#parts[@]}; i++)); do
|
||||
result="$result + ${parts[i]}"
|
||||
done
|
||||
echo "$result"
|
||||
fi
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Template and Content Generation Functions
|
||||
#==============================================================================
|
||||
|
||||
get_project_structure() {
|
||||
local project_type="$1"
|
||||
|
||||
if [[ "$project_type" == *"web"* ]]; then
|
||||
echo "backend/\\nfrontend/\\ntests/"
|
||||
else
|
||||
echo "src/\\ntests/"
|
||||
fi
|
||||
}
|
||||
|
||||
get_commands_for_language() {
|
||||
local lang="$1"
|
||||
|
||||
case "$lang" in
|
||||
*"Python"*)
|
||||
echo "cd src && pytest && ruff check ."
|
||||
;;
|
||||
*"Rust"*)
|
||||
echo "cargo test && cargo clippy"
|
||||
;;
|
||||
*"JavaScript"*|*"TypeScript"*)
|
||||
echo "npm test \\&\\& npm run lint"
|
||||
;;
|
||||
*)
|
||||
echo "# Add commands for $lang"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_language_conventions() {
|
||||
local lang="$1"
|
||||
echo "$lang: Follow standard conventions"
|
||||
}
|
||||
|
||||
create_new_agent_file() {
|
||||
local target_file="$1"
|
||||
local temp_file="$2"
|
||||
local project_name="$3"
|
||||
local current_date="$4"
|
||||
|
||||
if [[ ! -f "$TEMPLATE_FILE" ]]; then
|
||||
log_error "Template not found at $TEMPLATE_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$TEMPLATE_FILE" ]]; then
|
||||
log_error "Template file is not readable: $TEMPLATE_FILE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Creating new agent context file from template..."
|
||||
|
||||
if ! cp "$TEMPLATE_FILE" "$temp_file"; then
|
||||
log_error "Failed to copy template file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Replace template placeholders
|
||||
local project_structure
|
||||
project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")
|
||||
|
||||
local commands
|
||||
commands=$(get_commands_for_language "$NEW_LANG")
|
||||
|
||||
local language_conventions
|
||||
language_conventions=$(get_language_conventions "$NEW_LANG")
|
||||
|
||||
# Perform substitutions with error checking using safer approach
|
||||
# Escape special characters for sed by using a different delimiter or escaping
|
||||
local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g')
|
||||
local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g')
|
||||
local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g')
|
||||
|
||||
# Build technology stack and recent change strings conditionally
|
||||
local tech_stack
|
||||
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
|
||||
tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)"
|
||||
elif [[ -n "$escaped_lang" ]]; then
|
||||
tech_stack="- $escaped_lang ($escaped_branch)"
|
||||
elif [[ -n "$escaped_framework" ]]; then
|
||||
tech_stack="- $escaped_framework ($escaped_branch)"
|
||||
else
|
||||
tech_stack="- ($escaped_branch)"
|
||||
fi
|
||||
|
||||
local recent_change
|
||||
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
|
||||
recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework"
|
||||
elif [[ -n "$escaped_lang" ]]; then
|
||||
recent_change="- $escaped_branch: Added $escaped_lang"
|
||||
elif [[ -n "$escaped_framework" ]]; then
|
||||
recent_change="- $escaped_branch: Added $escaped_framework"
|
||||
else
|
||||
recent_change="- $escaped_branch: Added"
|
||||
fi
|
||||
|
||||
local substitutions=(
|
||||
"s|\[PROJECT NAME\]|$project_name|"
|
||||
"s|\[DATE\]|$current_date|"
|
||||
"s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|"
|
||||
"s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g"
|
||||
"s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|"
|
||||
"s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|"
|
||||
"s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|"
|
||||
)
|
||||
|
||||
for substitution in "${substitutions[@]}"; do
|
||||
if ! sed -i.bak -e "$substitution" "$temp_file"; then
|
||||
log_error "Failed to perform substitution: $substitution"
|
||||
rm -f "$temp_file" "$temp_file.bak"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Convert \n sequences to actual newlines
|
||||
newline=$(printf '\n')
|
||||
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
|
||||
|
||||
# Clean up backup files
|
||||
rm -f "$temp_file.bak" "$temp_file.bak2"
|
||||
|
||||
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
|
||||
if [[ "$target_file" == *.mdc ]]; then
|
||||
local frontmatter_file
|
||||
frontmatter_file=$(mktemp) || return 1
|
||||
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
|
||||
cat "$temp_file" >> "$frontmatter_file"
|
||||
mv "$frontmatter_file" "$temp_file"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
update_existing_agent_file() {
|
||||
local target_file="$1"
|
||||
local current_date="$2"
|
||||
|
||||
log_info "Updating existing agent context file..."
|
||||
|
||||
# Use a single temporary file for atomic update
|
||||
local temp_file
|
||||
temp_file=$(mktemp) || {
|
||||
log_error "Failed to create temporary file"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Process the file in one pass
|
||||
local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")
|
||||
local new_tech_entries=()
|
||||
local new_change_entry=""
|
||||
|
||||
# Prepare new technology entries
|
||||
if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then
|
||||
new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)")
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then
|
||||
new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)")
|
||||
fi
|
||||
|
||||
# Prepare new change entry
|
||||
if [[ -n "$tech_stack" ]]; then
|
||||
new_change_entry="- $CURRENT_BRANCH: Added $tech_stack"
|
||||
elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then
|
||||
new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
|
||||
fi
|
||||
|
||||
# Check if sections exist in the file
|
||||
local has_active_technologies=0
|
||||
local has_recent_changes=0
|
||||
|
||||
if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then
|
||||
has_active_technologies=1
|
||||
fi
|
||||
|
||||
if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then
|
||||
has_recent_changes=1
|
||||
fi
|
||||
|
||||
# Process file line by line
|
||||
local in_tech_section=false
|
||||
local in_changes_section=false
|
||||
local tech_entries_added=false
|
||||
local changes_entries_added=false
|
||||
local existing_changes_count=0
|
||||
local file_ended=false
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
# Handle Active Technologies section
|
||||
if [[ "$line" == "## Active Technologies" ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
in_tech_section=true
|
||||
continue
|
||||
elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
|
||||
# Add new tech entries before closing the section
|
||||
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
echo "$line" >> "$temp_file"
|
||||
in_tech_section=false
|
||||
continue
|
||||
elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then
|
||||
# Add new tech entries before empty line in tech section
|
||||
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
echo "$line" >> "$temp_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Handle Recent Changes section
|
||||
if [[ "$line" == "## Recent Changes" ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
# Add new change entry right after the heading
|
||||
if [[ -n "$new_change_entry" ]]; then
|
||||
echo "$new_change_entry" >> "$temp_file"
|
||||
fi
|
||||
in_changes_section=true
|
||||
changes_entries_added=true
|
||||
continue
|
||||
elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
in_changes_section=false
|
||||
continue
|
||||
elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then
|
||||
# Keep only first 2 existing changes
|
||||
if [[ $existing_changes_count -lt 2 ]]; then
|
||||
echo "$line" >> "$temp_file"
|
||||
((existing_changes_count++))
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# Update timestamp
|
||||
if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
|
||||
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
|
||||
else
|
||||
echo "$line" >> "$temp_file"
|
||||
fi
|
||||
done < "$target_file"
|
||||
|
||||
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
|
||||
if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
|
||||
# If sections don't exist, add them at the end of the file
|
||||
if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
|
||||
echo "" >> "$temp_file"
|
||||
echo "## Active Technologies" >> "$temp_file"
|
||||
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
|
||||
tech_entries_added=true
|
||||
fi
|
||||
|
||||
if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then
|
||||
echo "" >> "$temp_file"
|
||||
echo "## Recent Changes" >> "$temp_file"
|
||||
echo "$new_change_entry" >> "$temp_file"
|
||||
changes_entries_added=true
|
||||
fi
|
||||
|
||||
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
|
||||
if [[ "$target_file" == *.mdc ]]; then
|
||||
if ! head -1 "$temp_file" | grep -q '^---'; then
|
||||
local frontmatter_file
|
||||
frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
|
||||
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
|
||||
cat "$temp_file" >> "$frontmatter_file"
|
||||
mv "$frontmatter_file" "$temp_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Move temp file to target atomically
|
||||
if ! mv "$temp_file" "$target_file"; then
|
||||
log_error "Failed to update target file"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
#==============================================================================
|
||||
# Main Agent File Update Function
|
||||
#==============================================================================
|
||||
|
||||
update_agent_file() {
|
||||
local target_file="$1"
|
||||
local agent_name="$2"
|
||||
|
||||
if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then
|
||||
log_error "update_agent_file requires target_file and agent_name parameters"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Updating $agent_name context file: $target_file"
|
||||
|
||||
local project_name
|
||||
project_name=$(basename "$REPO_ROOT")
|
||||
local current_date
|
||||
current_date=$(date +%Y-%m-%d)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
local target_dir
|
||||
target_dir=$(dirname "$target_file")
|
||||
if [[ ! -d "$target_dir" ]]; then
|
||||
if ! mkdir -p "$target_dir"; then
|
||||
log_error "Failed to create directory: $target_dir"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$target_file" ]]; then
|
||||
# Create new file from template
|
||||
local temp_file
|
||||
temp_file=$(mktemp) || {
|
||||
log_error "Failed to create temporary file"
|
||||
return 1
|
||||
}
|
||||
|
||||
if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then
|
||||
if mv "$temp_file" "$target_file"; then
|
||||
log_success "Created new $agent_name context file"
|
||||
else
|
||||
log_error "Failed to move temporary file to $target_file"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_error "Failed to create new agent file"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Update existing file
|
||||
if [[ ! -r "$target_file" ]]; then
|
||||
log_error "Cannot read existing file: $target_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -w "$target_file" ]]; then
|
||||
log_error "Cannot write to existing file: $target_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if update_existing_agent_file "$target_file" "$current_date"; then
|
||||
log_success "Updated existing $agent_name context file"
|
||||
else
|
||||
log_error "Failed to update existing agent file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Agent Selection and Processing
|
||||
#==============================================================================
|
||||
|
||||
update_specific_agent() {
|
||||
local agent_type="$1"
|
||||
|
||||
case "$agent_type" in
|
||||
claude)
|
||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||
;;
|
||||
gemini)
|
||||
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||
;;
|
||||
copilot)
|
||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||
;;
|
||||
cursor-agent)
|
||||
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
||||
;;
|
||||
qwen)
|
||||
update_agent_file "$QWEN_FILE" "Qwen Code"
|
||||
;;
|
||||
opencode)
|
||||
update_agent_file "$AGENTS_FILE" "opencode"
|
||||
;;
|
||||
codex)
|
||||
update_agent_file "$AGENTS_FILE" "Codex CLI"
|
||||
;;
|
||||
windsurf)
|
||||
update_agent_file "$WINDSURF_FILE" "Windsurf"
|
||||
;;
|
||||
kilocode)
|
||||
update_agent_file "$KILOCODE_FILE" "Kilo Code"
|
||||
;;
|
||||
auggie)
|
||||
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
|
||||
;;
|
||||
roo)
|
||||
update_agent_file "$ROO_FILE" "Roo Code"
|
||||
;;
|
||||
codebuddy)
|
||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||
;;
|
||||
qodercli)
|
||||
update_agent_file "$QODER_FILE" "Qoder CLI"
|
||||
;;
|
||||
amp)
|
||||
update_agent_file "$AMP_FILE" "Amp"
|
||||
;;
|
||||
shai)
|
||||
update_agent_file "$SHAI_FILE" "SHAI"
|
||||
;;
|
||||
kiro-cli)
|
||||
update_agent_file "$KIRO_FILE" "Kiro CLI"
|
||||
;;
|
||||
agy)
|
||||
update_agent_file "$AGY_FILE" "Antigravity"
|
||||
;;
|
||||
bob)
|
||||
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||
;;
|
||||
generic)
|
||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown agent type '$agent_type'"
|
||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_all_existing_agents() {
|
||||
local found_agent=false
|
||||
|
||||
# Check each possible agent file and update if it exists
|
||||
if [[ -f "$CLAUDE_FILE" ]]; then
|
||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$GEMINI_FILE" ]]; then
|
||||
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$COPILOT_FILE" ]]; then
|
||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$CURSOR_FILE" ]]; then
|
||||
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$QWEN_FILE" ]]; then
|
||||
update_agent_file "$QWEN_FILE" "Qwen Code"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$AGENTS_FILE" ]]; then
|
||||
update_agent_file "$AGENTS_FILE" "Codex/opencode"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$WINDSURF_FILE" ]]; then
|
||||
update_agent_file "$WINDSURF_FILE" "Windsurf"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$KILOCODE_FILE" ]]; then
|
||||
update_agent_file "$KILOCODE_FILE" "Kilo Code"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$AUGGIE_FILE" ]]; then
|
||||
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$ROO_FILE" ]]; then
|
||||
update_agent_file "$ROO_FILE" "Roo Code"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$CODEBUDDY_FILE" ]]; then
|
||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$SHAI_FILE" ]]; then
|
||||
update_agent_file "$SHAI_FILE" "SHAI"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$QODER_FILE" ]]; then
|
||||
update_agent_file "$QODER_FILE" "Qoder CLI"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$KIRO_FILE" ]]; then
|
||||
update_agent_file "$KIRO_FILE" "Kiro CLI"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
if [[ -f "$AGY_FILE" ]]; then
|
||||
update_agent_file "$AGY_FILE" "Antigravity"
|
||||
found_agent=true
|
||||
fi
|
||||
if [[ -f "$BOB_FILE" ]]; then
|
||||
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||
found_agent=true
|
||||
fi
|
||||
|
||||
# If no agent files exist, create a default Claude file
|
||||
if [[ "$found_agent" == false ]]; then
|
||||
log_info "No existing agent files found, creating default Claude file..."
|
||||
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||
fi
|
||||
}
|
||||
print_summary() {
|
||||
echo
|
||||
log_info "Summary of changes:"
|
||||
|
||||
if [[ -n "$NEW_LANG" ]]; then
|
||||
echo " - Added language: $NEW_LANG"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_FRAMEWORK" ]]; then
|
||||
echo " - Added framework: $NEW_FRAMEWORK"
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
|
||||
echo " - Added database: $NEW_DB"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]"
|
||||
}
|
||||
|
||||
#==============================================================================
|
||||
# Main Execution
|
||||
#==============================================================================
|
||||
|
||||
main() {
|
||||
# Validate environment before proceeding
|
||||
validate_environment
|
||||
|
||||
log_info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
|
||||
|
||||
# Parse the plan file to extract project information
|
||||
if ! parse_plan_data "$NEW_PLAN"; then
|
||||
log_error "Failed to parse plan data"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Process based on agent type argument
|
||||
local success=true
|
||||
|
||||
if [[ -z "$AGENT_TYPE" ]]; then
|
||||
# No specific agent provided - update all existing agent files
|
||||
log_info "No agent specified, updating all existing agent files..."
|
||||
if ! update_all_existing_agents; then
|
||||
success=false
|
||||
fi
|
||||
else
|
||||
# Specific agent provided - update only that agent
|
||||
log_info "Updating specific agent: $AGENT_TYPE"
|
||||
if ! update_specific_agent "$AGENT_TYPE"; then
|
||||
success=false
|
||||
fi
|
||||
fi
|
||||
|
||||
# Print summary
|
||||
print_summary
|
||||
|
||||
if [[ "$success" == true ]]; then
|
||||
log_success "Agent context update completed successfully"
|
||||
exit 0
|
||||
else
|
||||
log_error "Agent context update completed with errors"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute main function if script is run directly
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
28
.specify/templates/agent-file-template.md
Normal file
28
.specify/templates/agent-file-template.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# [PROJECT NAME] Development Guidelines
|
||||
|
||||
Auto-generated from all feature plans. Last updated: [DATE]
|
||||
|
||||
## Active Technologies
|
||||
|
||||
[EXTRACTED FROM ALL PLAN.MD FILES]
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
[ACTUAL STRUCTURE FROM PLANS]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]
|
||||
|
||||
## Code Style
|
||||
|
||||
[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]
|
||||
|
||||
## Recent Changes
|
||||
|
||||
[LAST 3 FEATURES AND WHAT THEY ADDED]
|
||||
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
40
.specify/templates/checklist-template.md
Normal file
40
.specify/templates/checklist-template.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# [CHECKLIST TYPE] Checklist: [FEATURE NAME]
|
||||
|
||||
**Purpose**: [Brief description of what this checklist covers]
|
||||
**Created**: [DATE]
|
||||
**Feature**: [Link to spec.md or relevant documentation]
|
||||
|
||||
**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
|
||||
|
||||
<!--
|
||||
============================================================================
|
||||
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
|
||||
|
||||
The /speckit.checklist command MUST replace these with actual items based on:
|
||||
- User's specific checklist request
|
||||
- Feature requirements from spec.md
|
||||
- Technical context from plan.md
|
||||
- Implementation details from tasks.md
|
||||
|
||||
DO NOT keep these sample items in the generated checklist file.
|
||||
============================================================================
|
||||
-->
|
||||
|
||||
## [Category 1]
|
||||
|
||||
- [ ] CHK001 First checklist item with clear action
|
||||
- [ ] CHK002 Second checklist item
|
||||
- [ ] CHK003 Third checklist item
|
||||
|
||||
## [Category 2]
|
||||
|
||||
- [ ] CHK004 Another category item
|
||||
- [ ] CHK005 Item with specific criteria
|
||||
- [ ] CHK006 Final item in this category
|
||||
|
||||
## Notes
|
||||
|
||||
- Check items off as completed: `[x]`
|
||||
- Add comments or findings inline
|
||||
- Link to relevant resources or documentation
|
||||
- Items are numbered sequentially for easy reference
|
||||
50
.specify/templates/constitution-template.md
Normal file
50
.specify/templates/constitution-template.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# [PROJECT_NAME] Constitution
|
||||
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
|
||||
|
||||
## Core Principles
|
||||
|
||||
### [PRINCIPLE_1_NAME]
|
||||
<!-- Example: I. Library-First -->
|
||||
[PRINCIPLE_1_DESCRIPTION]
|
||||
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
|
||||
|
||||
### [PRINCIPLE_2_NAME]
|
||||
<!-- Example: II. CLI Interface -->
|
||||
[PRINCIPLE_2_DESCRIPTION]
|
||||
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
|
||||
|
||||
### [PRINCIPLE_3_NAME]
|
||||
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
|
||||
[PRINCIPLE_3_DESCRIPTION]
|
||||
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
|
||||
|
||||
### [PRINCIPLE_4_NAME]
|
||||
<!-- Example: IV. Integration Testing -->
|
||||
[PRINCIPLE_4_DESCRIPTION]
|
||||
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
|
||||
|
||||
### [PRINCIPLE_5_NAME]
|
||||
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
|
||||
[PRINCIPLE_5_DESCRIPTION]
|
||||
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
|
||||
|
||||
## [SECTION_2_NAME]
|
||||
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
|
||||
|
||||
[SECTION_2_CONTENT]
|
||||
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
|
||||
|
||||
## [SECTION_3_NAME]
|
||||
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
|
||||
|
||||
[SECTION_3_CONTENT]
|
||||
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
|
||||
|
||||
## Governance
|
||||
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
||||
|
||||
[GOVERNANCE_RULES]
|
||||
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
|
||||
|
||||
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
||||
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
|
||||
104
.specify/templates/plan-template.md
Normal file
104
.specify/templates/plan-template.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Implementation Plan: [FEATURE]
|
||||
|
||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||
|
||||
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
|
||||
|
||||
## Summary
|
||||
|
||||
[Extract from feature spec: primary requirement + technical approach from research]
|
||||
|
||||
## Technical Context
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the content in this section with the technical details
|
||||
for the project. The structure here is presented in advisory capacity to guide
|
||||
the iteration process.
|
||||
-->
|
||||
|
||||
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
|
||||
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
|
||||
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
|
||||
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
|
||||
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
|
||||
**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
|
||||
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
|
||||
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
|
||||
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
[Gates determined based on constitution file]
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/[###-feature]/
|
||||
├── plan.md # This file (/speckit.plan command output)
|
||||
├── research.md # Phase 0 output (/speckit.plan command)
|
||||
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
<!--
|
||||
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
|
||||
for this feature. Delete unused options and expand the chosen structure with
|
||||
real paths (e.g., apps/admin, packages/something). The delivered plan must
|
||||
not include Option labels.
|
||||
-->
|
||||
|
||||
```text
|
||||
# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
|
||||
src/
|
||||
├── models/
|
||||
├── services/
|
||||
├── cli/
|
||||
└── lib/
|
||||
|
||||
tests/
|
||||
├── contract/
|
||||
├── integration/
|
||||
└── unit/
|
||||
|
||||
# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── api/
|
||||
└── tests/
|
||||
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ └── services/
|
||||
└── tests/
|
||||
|
||||
# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
|
||||
api/
|
||||
└── [same as backend above]
|
||||
|
||||
ios/ or android/
|
||||
└── [platform-specific structure: feature modules, UI flows, platform tests]
|
||||
```
|
||||
|
||||
**Structure Decision**: [Document the selected structure and reference the real
|
||||
directories captured above]
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
115
.specify/templates/spec-template.md
Normal file
115
.specify/templates/spec-template.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Feature Specification: [FEATURE NAME]
|
||||
|
||||
**Feature Branch**: `[###-feature-name]`
|
||||
**Created**: [DATE]
|
||||
**Status**: Draft
|
||||
**Input**: User description: "$ARGUMENTS"
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
<!--
|
||||
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
|
||||
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
|
||||
you should still have a viable MVP (Minimum Viable Product) that delivers value.
|
||||
|
||||
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
|
||||
Think of each story as a standalone slice of functionality that can be:
|
||||
- Developed independently
|
||||
- Tested independently
|
||||
- Deployed independently
|
||||
- Demonstrated to users independently
|
||||
-->
|
||||
|
||||
### User Story 1 - [Brief Title] (Priority: P1)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
2. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - [Brief Title] (Priority: P2)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - [Brief Title] (Priority: P3)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
[Add more user stories as needed, each with an assigned priority]
|
||||
|
||||
### Edge Cases
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: The content in this section represents placeholders.
|
||||
Fill them out with the right edge cases.
|
||||
-->
|
||||
|
||||
- What happens when [boundary condition]?
|
||||
- How does system handle [error scenario]?
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: The content in this section represents placeholders.
|
||||
Fill them out with the right functional requirements.
|
||||
-->
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
|
||||
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
|
||||
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
|
||||
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
|
||||
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
|
||||
|
||||
*Example of marking unclear requirements:*
|
||||
|
||||
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
|
||||
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **[Entity 1]**: [What it represents, key attributes without implementation]
|
||||
- **[Entity 2]**: [What it represents, relationships to other entities]
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Define measurable success criteria.
|
||||
These must be technology-agnostic and measurable.
|
||||
-->
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
|
||||
- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
|
||||
- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
|
||||
- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
|
||||
251
.specify/templates/tasks-template.md
Normal file
251
.specify/templates/tasks-template.md
Normal file
@@ -0,0 +1,251 @@
|
||||
---
|
||||
|
||||
description: "Task list template for feature implementation"
|
||||
---
|
||||
|
||||
# Tasks: [FEATURE NAME]
|
||||
|
||||
**Input**: Design documents from `/specs/[###-feature-name]/`
|
||||
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
|
||||
|
||||
**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
|
||||
|
||||
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
|
||||
- Include exact file paths in descriptions
|
||||
|
||||
## Path Conventions
|
||||
|
||||
- **Single project**: `src/`, `tests/` at repository root
|
||||
- **Web app**: `backend/src/`, `frontend/src/`
|
||||
- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
|
||||
- Paths shown below assume single project - adjust based on plan.md structure
|
||||
|
||||
<!--
|
||||
============================================================================
|
||||
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
|
||||
|
||||
The /speckit.tasks command MUST replace these with actual tasks based on:
|
||||
- User stories from spec.md (with their priorities P1, P2, P3...)
|
||||
- Feature requirements from plan.md
|
||||
- Entities from data-model.md
|
||||
- Endpoints from contracts/
|
||||
|
||||
Tasks MUST be organized by user story so each story can be:
|
||||
- Implemented independently
|
||||
- Tested independently
|
||||
- Delivered as an MVP increment
|
||||
|
||||
DO NOT keep these sample tasks in the generated tasks.md file.
|
||||
============================================================================
|
||||
-->
|
||||
|
||||
## Phase 1: Setup (Shared Infrastructure)
|
||||
|
||||
**Purpose**: Project initialization and basic structure
|
||||
|
||||
- [ ] T001 Create project structure per implementation plan
|
||||
- [ ] T002 Initialize [language] project with [framework] dependencies
|
||||
- [ ] T003 [P] Configure linting and formatting tools
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
|
||||
|
||||
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
|
||||
|
||||
Examples of foundational tasks (adjust based on your project):
|
||||
|
||||
- [ ] T004 Setup database schema and migrations framework
|
||||
- [ ] T005 [P] Implement authentication/authorization framework
|
||||
- [ ] T006 [P] Setup API routing and middleware structure
|
||||
- [ ] T007 Create base models/entities that all stories depend on
|
||||
- [ ] T008 Configure error handling and logging infrastructure
|
||||
- [ ] T009 Setup environment configuration management
|
||||
|
||||
**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: [Brief description of what this story delivers]
|
||||
|
||||
**Independent Test**: [How to verify this story works on its own]
|
||||
|
||||
### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
|
||||
|
||||
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
|
||||
|
||||
- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
|
||||
- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
|
||||
- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
|
||||
- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
|
||||
- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
|
||||
- [ ] T016 [US1] Add validation and error handling
|
||||
- [ ] T017 [US1] Add logging for user story 1 operations
|
||||
|
||||
**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - [Title] (Priority: P2)
|
||||
|
||||
**Goal**: [Brief description of what this story delivers]
|
||||
|
||||
**Independent Test**: [How to verify this story works on its own]
|
||||
|
||||
### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
|
||||
|
||||
- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
|
||||
- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
|
||||
- [ ] T021 [US2] Implement [Service] in src/services/[service].py
|
||||
- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
|
||||
- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
|
||||
|
||||
**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - [Title] (Priority: P3)
|
||||
|
||||
**Goal**: [Brief description of what this story delivers]
|
||||
|
||||
**Independent Test**: [How to verify this story works on its own]
|
||||
|
||||
### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
|
||||
|
||||
- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
|
||||
- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
|
||||
- [ ] T027 [US3] Implement [Service] in src/services/[service].py
|
||||
- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
|
||||
|
||||
**Checkpoint**: All user stories should now be independently functional
|
||||
|
||||
---
|
||||
|
||||
[Add more user story phases as needed, following the same pattern]
|
||||
|
||||
---
|
||||
|
||||
## Phase N: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Improvements that affect multiple user stories
|
||||
|
||||
- [ ] TXXX [P] Documentation updates in docs/
|
||||
- [ ] TXXX Code cleanup and refactoring
|
||||
- [ ] TXXX Performance optimization across all stories
|
||||
- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
|
||||
- [ ] TXXX Security hardening
|
||||
- [ ] TXXX Run quickstart.md validation
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Setup (Phase 1)**: No dependencies - can start immediately
|
||||
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
|
||||
- **User Stories (Phase 3+)**: All depend on Foundational phase completion
|
||||
- User stories can then proceed in parallel (if staffed)
|
||||
- Or sequentially in priority order (P1 → P2 → P3)
|
||||
- **Polish (Final Phase)**: Depends on all desired user stories being complete
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
|
||||
- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
|
||||
- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Tests (if included) MUST be written and FAIL before implementation
|
||||
- Models before services
|
||||
- Services before endpoints
|
||||
- Core implementation before integration
|
||||
- Story complete before moving to next priority
|
||||
|
||||
### Parallel Opportunities
|
||||
|
||||
- All Setup tasks marked [P] can run in parallel
|
||||
- All Foundational tasks marked [P] can run in parallel (within Phase 2)
|
||||
- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
|
||||
- All tests for a user story marked [P] can run in parallel
|
||||
- Models within a story marked [P] can run in parallel
|
||||
- Different user stories can be worked on in parallel by different team members
|
||||
|
||||
---
|
||||
|
||||
## Parallel Example: User Story 1
|
||||
|
||||
```bash
|
||||
# Launch all tests for User Story 1 together (if tests requested):
|
||||
Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
|
||||
Task: "Integration test for [user journey] in tests/integration/test_[name].py"
|
||||
|
||||
# Launch all models for User Story 1 together:
|
||||
Task: "Create [Entity1] model in src/models/[entity1].py"
|
||||
Task: "Create [Entity2] model in src/models/[entity2].py"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### MVP First (User Story 1 Only)
|
||||
|
||||
1. Complete Phase 1: Setup
|
||||
2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
|
||||
3. Complete Phase 3: User Story 1
|
||||
4. **STOP and VALIDATE**: Test User Story 1 independently
|
||||
5. Deploy/demo if ready
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Complete Setup + Foundational → Foundation ready
|
||||
2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
|
||||
3. Add User Story 2 → Test independently → Deploy/Demo
|
||||
4. Add User Story 3 → Test independently → Deploy/Demo
|
||||
5. Each story adds value without breaking previous stories
|
||||
|
||||
### Parallel Team Strategy
|
||||
|
||||
With multiple developers:
|
||||
|
||||
1. Team completes Setup + Foundational together
|
||||
2. Once Foundational is done:
|
||||
- Developer A: User Story 1
|
||||
- Developer B: User Story 2
|
||||
- Developer C: User Story 3
|
||||
3. Stories complete and integrate independently
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- [P] tasks = different files, no dependencies
|
||||
- [Story] label maps task to specific user story for traceability
|
||||
- Each user story should be independently completable and testable
|
||||
- Verify tests fail before implementing
|
||||
- Commit after each task or logical group
|
||||
- Stop at any checkpoint to validate story independently
|
||||
- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
|
||||
82
CLAUDE.md
82
CLAUDE.md
@@ -2,70 +2,16 @@
|
||||
|
||||
A privacy-focused, self-hostable PWA for event announcements and RSVPs. Alternative to Facebook Events or Telegram groups — reduced to the essentials.
|
||||
|
||||
## Project Statutes
|
||||
## Constitution
|
||||
|
||||
These are the non-negotiable principles of this project. Every decision — architectural, technical, or design-related — must be consistent with them.
|
||||
Project principles, constraints, tech stack, and governance are defined in `.specify/memory/constitution.md`. That file is the single source of truth — read and follow it.
|
||||
|
||||
### Governance
|
||||
|
||||
- The agent works autonomously on implementation tasks.
|
||||
- When facing architectural decisions, fundamental design questions, tech stack choices, or dependency selections: advise, propose options, and ask for approval before proceeding.
|
||||
- Actively challenge decisions — including the developer's — when there are good reasons to. Don't be a yes-machine.
|
||||
- When encountering problems, attempt to solve them independently first. Only escalate when stuck.
|
||||
|
||||
### Methodology
|
||||
|
||||
- Follow Research → Spec → Test → Implement → Review. No shortcuts.
|
||||
- Never write implementation code without a specification.
|
||||
- Always write tests before implementation (TDD). Red → Green → Refactor.
|
||||
- Refactoring is permitted freely as long as it does not alter the fundamental architecture.
|
||||
- No vibe coding. Every line of code must be intentional and traceable to a requirement.
|
||||
- Document integrity: when a decision is revised (pivot), add an addendum with rationale — never rewrite or delete the original decision. Traceability over tidiness.
|
||||
- When a setup task or user story is completed, check off its acceptance criteria in the corresponding spec file (`spec/setup-tasks.md` or `spec/userstories.md`) before committing. Progress must be tracked — no silent completions.
|
||||
|
||||
### Privacy
|
||||
|
||||
- Privacy is a design constraint, not a feature. It shapes every decision from the start.
|
||||
- No analytics, no telemetry — not even self-hosted.
|
||||
- Never log PII or IP addresses on the server.
|
||||
- For every feature, critically evaluate what data is necessary. Only store what is absolutely required for functionality.
|
||||
- Never include external dependencies that phone home: no CDNs, no Google Fonts, no tracking-capable libraries.
|
||||
|
||||
### Quality
|
||||
|
||||
- KISS and grugbrain. Engineer it properly, but don't over-engineer.
|
||||
- No workarounds. Always fix the root cause, even if it takes longer.
|
||||
- Address technical debt immediately. Don't let it accumulate.
|
||||
- Accessibility is a baseline requirement, not an afterthought.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Every dependency is a deliberate, justified decision.
|
||||
- A dependency must provide substantial value and a significant portion of its features must actually be used.
|
||||
- Dependencies must be actively maintained and open source. Copyleft is fine — the project is GPL-licensed.
|
||||
- Never introduce a dependency that phones home or compromises user privacy.
|
||||
|
||||
### Language
|
||||
## Language
|
||||
|
||||
- Conversation and brainstorming: German.
|
||||
- Code, comments, commits, documentation: English — no exceptions.
|
||||
|
||||
### Deployment
|
||||
|
||||
- The project provides a Dockerfile. How and where it is deployed is the hoster's responsibility.
|
||||
- A docker-compose example in the README is sufficient.
|
||||
- Documentation lives in the README. No wiki, no elaborate docs site.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Backend:** Java 25 (LTS, installed via SDKMAN), Spring Boot 3.5.x, Maven with wrapper (`./mvnw`)
|
||||
- **Frontend:** Vue 3, TypeScript, Vue Router, Vite, Vitest, ESLint, Prettier
|
||||
- **Node.js:** 24 LTS (for Docker/CI; development tolerates newer versions)
|
||||
- **Base package:** `de.fete`, hexagonal architecture (single Maven module, package-level separation)
|
||||
- **No Pinia** — Composition API (`ref`/`reactive`) + localStorage is sufficient
|
||||
- **No JPA until T-4** — added when database infrastructure is ready
|
||||
|
||||
### Build Commands
|
||||
## Build Commands
|
||||
|
||||
| What | Command |
|
||||
|------------------|----------------------------------------|
|
||||
@@ -78,14 +24,15 @@ These are the non-negotiable principles of this project. Every decision — arch
|
||||
| Backend checkstyle | `cd backend && ./mvnw checkstyle:check` |
|
||||
| Backend full verify | `cd backend && ./mvnw verify` |
|
||||
|
||||
### Agent Documentation
|
||||
## Agent Documentation
|
||||
|
||||
- Research reports: `docs/agents/research/`
|
||||
- Implementation plans: `docs/agents/plan/`
|
||||
- Feature specs, research, and plans: `specs/[NNN-feature-name]/` (e.g. `specs/006-create-event/spec.md`, `research.md`, `plan.md`)
|
||||
- Cross-cutting research: `.specify/memory/research/`
|
||||
- Cross-cutting plans: `.specify/memory/plans/`
|
||||
- Agent test reports (browser verification): `.agent-tests/` (gitignored)
|
||||
- Use the `browser-interactive-testing` skill (rodney/showboat) for visual verification — this is an agent tool, not manual work.
|
||||
|
||||
### Skills
|
||||
## Skills
|
||||
|
||||
The following skills are available and should be used for their respective purposes:
|
||||
|
||||
@@ -96,9 +43,16 @@ The following skills are available and should be used for their respective purpo
|
||||
| `rpi-implement` | Approved plan ready for execution | Executes approved implementation plans phase by phase with automated and manual verification. |
|
||||
| `browser-interactive-testing` | Visual verification of web pages | Headless Chrome testing via rodney/showboat. Use for screenshots, browser automation, and visual test reports. |
|
||||
|
||||
### Ralph Loops
|
||||
## Ralph Loops
|
||||
|
||||
- Autonomous work is done via Ralph Loops. See [.claude/rules/ralph-loops.md](.claude/rules/ralph-loops.md) for documentation.
|
||||
- The loop runner is `ralph.sh`. Each run lives in its own directory under `.ralph/`.
|
||||
- Run directories contain: `instructions.md` (prompt), `chief-wiggum.md` (directives), `answers.md` (human answers), `questions.md` (Ralph's questions), `progress.txt` (iteration log), `meta.md` (metadata), `run.log` (execution log).
|
||||
- Project specifications (user stories, setup tasks, personas, etc.) live in `spec/`.
|
||||
- Project specifications (user stories, setup tasks, personas, etc.) live in `specs/` (feature dirs) and `.specify/memory/` (cross-cutting docs).
|
||||
|
||||
## Active Technologies
|
||||
- TypeScript 5.x, Vue 3 (Composition API) + openapi-fetch, Vue Router, Vite (018-cancel-event-list)
|
||||
- localStorage via `useEventStorage()` composable (018-cancel-event-list)
|
||||
|
||||
## Recent Changes
|
||||
- 018-cancel-event-list: Added TypeScript 5.x, Vue 3 (Composition API) + openapi-fetch, Vue Router, Vite
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Build frontend
|
||||
FROM node:24-alpine AS frontend-build
|
||||
FROM node:25-alpine AS frontend-build
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
@@ -10,17 +10,18 @@ COPY backend/src/main/resources/openapi/api.yaml \
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Build backend with frontend assets baked in
|
||||
FROM eclipse-temurin:25-jdk-alpine AS backend-build
|
||||
FROM eclipse-temurin:25.0.2_10-jdk-alpine AS backend-build
|
||||
WORKDIR /app/backend
|
||||
COPY backend/ ./
|
||||
COPY --from=frontend-build /app/frontend/dist src/main/resources/static/
|
||||
RUN ./mvnw -B -DskipTests -Dcheckstyle.skip -Dspotbugs.skip package
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM eclipse-temurin:25-jre-alpine
|
||||
FROM eclipse-temurin:25.0.2_10-jre-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=backend-build /app/backend/target/*.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENV SPRING_PROFILES_ACTIVE=prod
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
|
||||
108
README.md
108
README.md
@@ -1,6 +1,25 @@
|
||||
# fete
|
||||
<p align="center">
|
||||
<img src="frontend/public/og-image.png" alt="fete" width="100%" />
|
||||
</p>
|
||||
|
||||
A privacy-focused, self-hostable web app for event announcements and RSVPs. An alternative to Facebook Events or Telegram groups — reduced to the essentials.
|
||||
<p align="center">
|
||||
<strong>Privacy-focused, self-hostable event announcements and RSVPs.</strong><br>
|
||||
An alternative to Facebook Events or Telegram groups — reduced to the essentials.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/01-create-event.png" alt="Create Event" width="230" />
|
||||
|
||||
<img src="docs/screenshots/02-event-detail.png" alt="Event Detail" width="230" />
|
||||
|
||||
<img src="docs/screenshots/03-rsvp.png" alt="RSVP" width="230" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sub>Create events · Share with guests · Collect RSVPs</sub>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -47,6 +66,7 @@ A privacy-focused, self-hostable web app for event announcements and RSVPs. An a
|
||||
|
||||
- Java (latest LTS) + Maven wrapper (`./mvnw`, included)
|
||||
- Node.js (latest LTS) + npm
|
||||
- Docker (for running backend tests via Testcontainers)
|
||||
|
||||
### Project structure
|
||||
|
||||
@@ -64,8 +84,31 @@ fete/
|
||||
# Backend
|
||||
cd backend && ./mvnw test
|
||||
|
||||
# Frontend
|
||||
# Frontend unit tests
|
||||
cd frontend && npm run test:unit
|
||||
|
||||
# Frontend E2E tests (requires Chromium: npx playwright install chromium)
|
||||
cd frontend && npm run test:e2e
|
||||
```
|
||||
|
||||
### Running the backend locally
|
||||
|
||||
**Option A: Without external PostgreSQL (Testcontainers)**
|
||||
|
||||
```bash
|
||||
cd backend && ./mvnw spring-boot:test-run
|
||||
```
|
||||
|
||||
Starts the app with a Testcontainers-managed PostgreSQL that is created and destroyed automatically. Requires Docker.
|
||||
|
||||
**Option B: With external PostgreSQL**
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
cp src/main/resources/application-local.properties.example \
|
||||
src/main/resources/application-local.properties
|
||||
# Edit application-local.properties if your PostgreSQL uses different credentials
|
||||
./mvnw spring-boot:run -Dspring-boot.run.profiles=local
|
||||
```
|
||||
|
||||
### Building
|
||||
@@ -117,11 +160,12 @@ ArchUnit enforces hexagonal boundaries: domain must not depend on adapters, appl
|
||||
| TypeScript (strict) | `vue-tsc --noEmit` | Type errors |
|
||||
| oxlint + ESLint | `oxlint`, `eslint` | Lint violations |
|
||||
|
||||
**When the agent finishes** (Stop hook — only if `frontend/src/` has uncommitted changes):
|
||||
**When the agent finishes** (Stop hook — only if `frontend/src/` or `frontend/e2e/` has uncommitted changes):
|
||||
|
||||
| What | Command | Fails on |
|
||||
|---------------------|------------------------------|---------------------------------------|
|
||||
| Vitest | `npm run test:unit -- --run` | Test failures (fail-fast, stops at 1) |
|
||||
| Playwright | `npm run test:e2e` | E2E test failures |
|
||||
|
||||
**Not hooked** (run manually or via editor):
|
||||
|
||||
@@ -129,6 +173,62 @@ ArchUnit enforces hexagonal boundaries: domain must not depend on adapters, appl
|
||||
|---------------------|------------------|-------------------|
|
||||
| Prettier | `npm run format` | Formatting issues |
|
||||
|
||||
### OpenAPI Spec (YAML)
|
||||
|
||||
**After editing an `openapi/*.yaml` file** (PostToolUse hook):
|
||||
|
||||
| What | Command | Fails on |
|
||||
|---------------------|--------------------------|-----------------------------------|
|
||||
| Redocly CLI | `redocly lint api.yaml` | Structural and ruleset violations |
|
||||
|
||||
Validates the OpenAPI 3.1 spec against the Redocly `recommended` ruleset (with `security-defined` disabled, since endpoints are intentionally public). Runs via `npx @redocly/cli@latest`.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Compose
|
||||
|
||||
The app ships as a single Docker image. It requires an external PostgreSQL database.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: fete
|
||||
POSTGRES_USER: fete
|
||||
POSTGRES_PASSWORD: changeme
|
||||
volumes:
|
||||
- fete-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U fete"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
image: git.bahamut.nitrix.one/nitrix/fete:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
DATABASE_URL: jdbc:postgresql://db:5432/fete
|
||||
DATABASE_USERNAME: fete
|
||||
DATABASE_PASSWORD: changeme
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
fete-db:
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|---------------------|----------|---------|-----------------------------------|
|
||||
| `DATABASE_URL` | Yes | — | JDBC connection string |
|
||||
| `DATABASE_USERNAME` | Yes | — | Database username |
|
||||
| `DATABASE_PASSWORD` | Yes | — | Database password |
|
||||
|
||||
## License
|
||||
|
||||
GPL — see [LICENSE](LICENSE) for details.
|
||||
|
||||
94
WORKFLOW.md
Normal file
94
WORKFLOW.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Spec-Kit Workflow
|
||||
|
||||
How to take a feature from spec to working code.
|
||||
|
||||
## The Loop
|
||||
|
||||
```
|
||||
clarify → plan → tasks → analyze → implement
|
||||
```
|
||||
|
||||
Every step produces files in `specs/NNN-feature-name/`. Review after each step before moving on.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Clarify the spec (optional)
|
||||
|
||||
```
|
||||
/speckit.clarify
|
||||
```
|
||||
|
||||
Asks up to 5 targeted questions about gaps or ambiguities in the spec. Answers get folded back into `spec.md`. Skip this if the spec is already solid.
|
||||
|
||||
### 2. Create the implementation plan
|
||||
|
||||
```
|
||||
/speckit.plan
|
||||
```
|
||||
|
||||
Reads `spec.md`, checks it against the constitution, and produces:
|
||||
|
||||
| File | Content |
|
||||
|------|---------|
|
||||
| `plan.md` | Technical design, architecture decisions, contracts |
|
||||
| `research.md` | Background research (if needed) |
|
||||
| `data-model.md` | Entity definitions (if new entities are involved) |
|
||||
| `contracts/` | API contracts (if applicable) |
|
||||
|
||||
**Review this carefully.** The plan shapes everything downstream.
|
||||
|
||||
### 3. Generate the task list
|
||||
|
||||
```
|
||||
/speckit.tasks
|
||||
```
|
||||
|
||||
Reads the plan and generates `tasks.md` — an ordered, dependency-aware task list. Tasks marked `[P]` can run in parallel.
|
||||
|
||||
### 4. Check consistency (optional)
|
||||
|
||||
```
|
||||
/speckit.analyze
|
||||
```
|
||||
|
||||
Cross-checks spec, plan, and tasks for contradictions, gaps, or drift. Non-destructive — only reports, doesn't change files.
|
||||
|
||||
### 5. Implement
|
||||
|
||||
```
|
||||
/speckit.implement
|
||||
```
|
||||
|
||||
Executes the tasks from `tasks.md` phase by phase. Follows TDD: writes tests first, then implementation. Stops at checkpoints so you can verify.
|
||||
|
||||
## File structure
|
||||
|
||||
Each feature lives in its own directory:
|
||||
|
||||
```
|
||||
specs/
|
||||
007-view-event/
|
||||
spec.md # What and why (from /speckit.specify or migration)
|
||||
plan.md # How (from /speckit.plan)
|
||||
research.md # Background research (from /speckit.plan)
|
||||
data-model.md # Entity definitions (from /speckit.plan)
|
||||
contracts/ # API contracts (from /speckit.plan)
|
||||
tasks.md # Ordered task list (from /speckit.tasks)
|
||||
```
|
||||
|
||||
## Starting a brand new feature
|
||||
|
||||
If the feature doesn't have a spec yet:
|
||||
|
||||
```
|
||||
/speckit.specify
|
||||
```
|
||||
|
||||
Describe what you want in plain language. This creates the spec directory and `spec.md` from the template. Then continue with the loop above.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Don't skip the review.** Each step builds on the previous one. Garbage in, garbage out.
|
||||
- **The spec is the source of truth.** If something in the plan contradicts the spec, fix the spec first.
|
||||
- **You can re-run steps.** Changed the spec after planning? Run `/speckit.plan` again.
|
||||
- **Constitution governs everything.** Principles in `.specify/memory/constitution.md` override ad-hoc decisions.
|
||||
@@ -1,3 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
|
||||
|
||||
@@ -37,6 +37,22 @@
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
@@ -49,6 +65,24 @@
|
||||
<version>1.4.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -145,7 +179,7 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.6.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>add-openapi-sources</id>
|
||||
|
||||
5
backend/redocly.yaml
Normal file
5
backend/redocly.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
extends:
|
||||
- recommended
|
||||
|
||||
rules:
|
||||
security-defined: off
|
||||
@@ -7,4 +7,8 @@
|
||||
<Match>
|
||||
<Package name="de.fete.adapter.in.web.model"/>
|
||||
</Match>
|
||||
<!-- Constructor-injected Spring beans storing interfaces/proxies are not a real exposure risk -->
|
||||
<Match>
|
||||
<Bug pattern="EI_EXPOSE_REP2"/>
|
||||
</Match>
|
||||
</FindBugsFilter>
|
||||
|
||||
@@ -2,9 +2,11 @@ package de.fete;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/** Spring Boot entry point for the fete application. */
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class FeteApplication {
|
||||
|
||||
/** Starts the application. */
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package de.fete.adapter.in.web;
|
||||
|
||||
import de.fete.adapter.in.web.api.EventsApi;
|
||||
import de.fete.adapter.in.web.model.Attendee;
|
||||
import de.fete.adapter.in.web.model.CreateEventRequest;
|
||||
import de.fete.adapter.in.web.model.CreateEventResponse;
|
||||
import de.fete.adapter.in.web.model.CreateRsvpRequest;
|
||||
import de.fete.adapter.in.web.model.CreateRsvpResponse;
|
||||
import de.fete.adapter.in.web.model.GetAttendeesResponse;
|
||||
import de.fete.adapter.in.web.model.GetEventResponse;
|
||||
import de.fete.adapter.in.web.model.PatchEventRequest;
|
||||
import de.fete.application.service.exception.EventNotFoundException;
|
||||
import de.fete.application.service.exception.InvalidTimezoneException;
|
||||
import de.fete.domain.model.CreateEventCommand;
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.OrganizerToken;
|
||||
import de.fete.domain.model.Rsvp;
|
||||
import de.fete.domain.model.RsvpToken;
|
||||
import de.fete.domain.port.in.CancelRsvpUseCase;
|
||||
import de.fete.domain.port.in.CountAttendeesByEventUseCase;
|
||||
import de.fete.domain.port.in.CreateEventUseCase;
|
||||
import de.fete.domain.port.in.CreateRsvpUseCase;
|
||||
import de.fete.domain.port.in.GetAttendeesUseCase;
|
||||
import de.fete.domain.port.in.GetEventUseCase;
|
||||
import de.fete.domain.port.in.UpdateEventUseCase;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** REST controller for event operations. */
|
||||
@RestController
|
||||
public class EventController implements EventsApi {
|
||||
|
||||
private final CreateEventUseCase createEventUseCase;
|
||||
private final GetEventUseCase getEventUseCase;
|
||||
private final CreateRsvpUseCase createRsvpUseCase;
|
||||
private final CancelRsvpUseCase cancelRsvpUseCase;
|
||||
private final CountAttendeesByEventUseCase countAttendeesByEventUseCase;
|
||||
private final GetAttendeesUseCase getAttendeesUseCase;
|
||||
private final UpdateEventUseCase updateEventUseCase;
|
||||
|
||||
/** Creates a new controller with the given use cases. */
|
||||
public EventController(
|
||||
CreateEventUseCase createEventUseCase,
|
||||
GetEventUseCase getEventUseCase,
|
||||
CreateRsvpUseCase createRsvpUseCase,
|
||||
CancelRsvpUseCase cancelRsvpUseCase,
|
||||
CountAttendeesByEventUseCase countAttendeesByEventUseCase,
|
||||
GetAttendeesUseCase getAttendeesUseCase,
|
||||
UpdateEventUseCase updateEventUseCase) {
|
||||
this.createEventUseCase = createEventUseCase;
|
||||
this.getEventUseCase = getEventUseCase;
|
||||
this.createRsvpUseCase = createRsvpUseCase;
|
||||
this.cancelRsvpUseCase = cancelRsvpUseCase;
|
||||
this.countAttendeesByEventUseCase = countAttendeesByEventUseCase;
|
||||
this.getAttendeesUseCase = getAttendeesUseCase;
|
||||
this.updateEventUseCase = updateEventUseCase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<CreateEventResponse> createEvent(
|
||||
CreateEventRequest request) {
|
||||
ZoneId zoneId = parseTimezone(request.getTimezone());
|
||||
|
||||
var command = new CreateEventCommand(
|
||||
request.getTitle(),
|
||||
request.getDescription(),
|
||||
request.getDateTime(),
|
||||
zoneId,
|
||||
request.getLocation()
|
||||
);
|
||||
|
||||
Event event = createEventUseCase.createEvent(command);
|
||||
|
||||
var response = new CreateEventResponse();
|
||||
response.setEventToken(event.eventToken().value());
|
||||
response.setOrganizerToken(event.organizerToken().value());
|
||||
response.setTitle(event.title());
|
||||
response.setDateTime(event.dateTime());
|
||||
response.setTimezone(event.timezone().getId());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<GetEventResponse> getEvent(UUID eventToken) {
|
||||
var evtToken = new EventToken(eventToken);
|
||||
Event event = getEventUseCase.getByEventToken(evtToken)
|
||||
.orElseThrow(() -> new EventNotFoundException(eventToken));
|
||||
|
||||
var response = new GetEventResponse();
|
||||
response.setEventToken(event.eventToken().value());
|
||||
response.setTitle(event.title());
|
||||
response.setDescription(event.description());
|
||||
response.setDateTime(event.dateTime());
|
||||
response.setTimezone(event.timezone().getId());
|
||||
response.setLocation(event.location());
|
||||
response.setAttendeeCount(
|
||||
(int) countAttendeesByEventUseCase.countByEvent(evtToken));
|
||||
response.setCancelled(event.cancelled());
|
||||
response.setCancellationReason(event.cancellationReason());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> patchEvent(
|
||||
UUID eventToken, UUID organizerToken, PatchEventRequest request) {
|
||||
updateEventUseCase.cancelEvent(
|
||||
new EventToken(eventToken),
|
||||
new OrganizerToken(organizerToken),
|
||||
request.getCancelled(),
|
||||
request.getCancellationReason());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<GetAttendeesResponse> getAttendees(
|
||||
UUID eventToken, UUID organizerToken) {
|
||||
var evtToken = new EventToken(eventToken);
|
||||
var orgToken = new OrganizerToken(organizerToken);
|
||||
|
||||
List<String> names = getAttendeesUseCase
|
||||
.getAttendeeNames(evtToken, orgToken);
|
||||
|
||||
var attendees = names.stream()
|
||||
.map(name -> new Attendee().name(name))
|
||||
.toList();
|
||||
|
||||
var response = new GetAttendeesResponse();
|
||||
response.setAttendees(attendees);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<CreateRsvpResponse> createRsvp(
|
||||
UUID eventToken, CreateRsvpRequest createRsvpRequest) {
|
||||
var evtToken = new EventToken(eventToken);
|
||||
Rsvp rsvp = createRsvpUseCase.createRsvp(evtToken, createRsvpRequest.getName());
|
||||
|
||||
var response = new CreateRsvpResponse();
|
||||
response.setRsvpToken(rsvp.rsvpToken().value());
|
||||
response.setName(rsvp.name());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> cancelRsvp(UUID eventToken, UUID rsvpToken) {
|
||||
cancelRsvpUseCase.cancelRsvp(new EventToken(eventToken), new RsvpToken(rsvpToken));
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private static ZoneId parseTimezone(String timezone) {
|
||||
try {
|
||||
return ZoneId.of(timezone);
|
||||
} catch (DateTimeException e) {
|
||||
throw new InvalidTimezoneException(timezone);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package de.fete.adapter.in.web;
|
||||
|
||||
import de.fete.application.service.exception.EventAlreadyCancelledException;
|
||||
import de.fete.application.service.exception.EventCancelledException;
|
||||
import de.fete.application.service.exception.EventExpiredException;
|
||||
import de.fete.application.service.exception.EventNotFoundException;
|
||||
import de.fete.application.service.exception.ExpiryDateBeforeEventException;
|
||||
import de.fete.application.service.exception.ExpiryDateInPastException;
|
||||
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||
import de.fete.application.service.exception.InvalidTimezoneException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ProblemDetail;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
/** Global exception handler producing RFC 9457 Problem Details responses. */
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
||||
|
||||
@Override
|
||||
protected ResponseEntity<Object> handleMethodArgumentNotValid(
|
||||
MethodArgumentNotValidException ex,
|
||||
HttpHeaders headers,
|
||||
HttpStatusCode status,
|
||||
WebRequest request) {
|
||||
|
||||
ProblemDetail problemDetail = ex.getBody();
|
||||
problemDetail.setTitle("Validation Failed");
|
||||
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
|
||||
|
||||
List<Map<String, String>> fieldErrors = ex.getBindingResult()
|
||||
.getFieldErrors()
|
||||
.stream()
|
||||
.map(fe -> Map.of(
|
||||
"field", fe.getField(),
|
||||
"message", fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "invalid"
|
||||
))
|
||||
.toList();
|
||||
|
||||
problemDetail.setProperty("fieldErrors", fieldErrors);
|
||||
return handleExceptionInternal(ex, problemDetail, headers, status, request);
|
||||
}
|
||||
|
||||
/** Handles expiry date before event date. */
|
||||
@ExceptionHandler(ExpiryDateBeforeEventException.class)
|
||||
public ResponseEntity<ProblemDetail> handleExpiryDateBeforeEvent(
|
||||
ExpiryDateBeforeEventException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
problemDetail.setTitle("Invalid Expiry Date");
|
||||
problemDetail.setType(URI.create("urn:problem-type:expiry-date-before-event"));
|
||||
return ResponseEntity.badRequest()
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles expiry date validation failures. */
|
||||
@ExceptionHandler(ExpiryDateInPastException.class)
|
||||
public ResponseEntity<ProblemDetail> handleExpiryDateInPast(
|
||||
ExpiryDateInPastException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
problemDetail.setTitle("Invalid Expiry Date");
|
||||
problemDetail.setType(URI.create("urn:problem-type:expiry-date-in-past"));
|
||||
return ResponseEntity.badRequest()
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles attempt to cancel an already cancelled event. */
|
||||
@ExceptionHandler(EventAlreadyCancelledException.class)
|
||||
public ResponseEntity<ProblemDetail> handleEventAlreadyCancelled(
|
||||
EventAlreadyCancelledException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.CONFLICT, ex.getMessage());
|
||||
problemDetail.setTitle("Event Already Cancelled");
|
||||
problemDetail.setType(URI.create("urn:problem-type:event-already-cancelled"));
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles RSVP on cancelled event. */
|
||||
@ExceptionHandler(EventCancelledException.class)
|
||||
public ResponseEntity<ProblemDetail> handleEventCancelled(
|
||||
EventCancelledException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.CONFLICT, ex.getMessage());
|
||||
problemDetail.setTitle("Event Cancelled");
|
||||
problemDetail.setType(URI.create("urn:problem-type:event-cancelled"));
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles RSVP on expired event. */
|
||||
@ExceptionHandler(EventExpiredException.class)
|
||||
public ResponseEntity<ProblemDetail> handleEventExpired(
|
||||
EventExpiredException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.CONFLICT, ex.getMessage());
|
||||
problemDetail.setTitle("Event Expired");
|
||||
problemDetail.setType(URI.create("urn:problem-type:event-expired"));
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles invalid organizer token. */
|
||||
@ExceptionHandler(InvalidOrganizerTokenException.class)
|
||||
public ResponseEntity<ProblemDetail> handleInvalidOrganizerToken(
|
||||
InvalidOrganizerTokenException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.FORBIDDEN, ex.getMessage());
|
||||
problemDetail.setTitle("Forbidden");
|
||||
problemDetail.setType(URI.create("urn:problem-type:invalid-organizer-token"));
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles event not found. */
|
||||
@ExceptionHandler(EventNotFoundException.class)
|
||||
public ResponseEntity<ProblemDetail> handleEventNotFound(
|
||||
EventNotFoundException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.NOT_FOUND, ex.getMessage());
|
||||
problemDetail.setTitle("Event Not Found");
|
||||
problemDetail.setType(URI.create("urn:problem-type:event-not-found"));
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles invalid timezone. */
|
||||
@ExceptionHandler(InvalidTimezoneException.class)
|
||||
public ResponseEntity<ProblemDetail> handleInvalidTimezone(
|
||||
InvalidTimezoneException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
problemDetail.setTitle("Invalid Timezone");
|
||||
problemDetail.setType(URI.create("urn:problem-type:invalid-timezone"));
|
||||
return ResponseEntity.badRequest()
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Catches all unhandled exceptions. */
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ProblemDetail> handleAll(Exception ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"An unexpected error occurred.");
|
||||
problemDetail.setTitle("Internal Server Error");
|
||||
return ResponseEntity.internalServerError()
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
}
|
||||
188
backend/src/main/java/de/fete/adapter/in/web/SpaController.java
Normal file
188
backend/src/main/java/de/fete/adapter/in/web/SpaController.java
Normal file
@@ -0,0 +1,188 @@
|
||||
package de.fete.adapter.in.web;
|
||||
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.port.in.GetEventUseCase;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
/** Serves the SPA index.html with injected Open Graph and Twitter Card meta-tags. */
|
||||
@Controller
|
||||
public class SpaController {
|
||||
|
||||
private static final String PLACEHOLDER = "<!-- OG_META_TAGS -->";
|
||||
private static final int MAX_TITLE_LENGTH = 70;
|
||||
private static final int MAX_DESCRIPTION_LENGTH = 200;
|
||||
private static final String GENERIC_TITLE = "fete";
|
||||
private static final String GENERIC_DESCRIPTION =
|
||||
"Privacy-focused event planning. Create and share events without accounts.";
|
||||
private static final DateTimeFormatter DATE_FORMAT =
|
||||
DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy 'at' h:mm a", Locale.ENGLISH);
|
||||
|
||||
private final GetEventUseCase getEventUseCase;
|
||||
private String htmlTemplate;
|
||||
|
||||
/** Creates a new SpaController. */
|
||||
public SpaController(GetEventUseCase getEventUseCase) {
|
||||
this.getEventUseCase = getEventUseCase;
|
||||
}
|
||||
|
||||
/** Loads and caches the index.html template at startup. */
|
||||
@PostConstruct
|
||||
void loadTemplate() throws IOException {
|
||||
var resource = new ClassPathResource("/static/index.html");
|
||||
if (resource.exists()) {
|
||||
htmlTemplate = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
/** Serves SPA HTML with generic meta-tags for non-event routes. */
|
||||
@GetMapping(
|
||||
value = {"/", "/create", "/events"},
|
||||
produces = MediaType.TEXT_HTML_VALUE
|
||||
)
|
||||
@ResponseBody
|
||||
public String serveGenericPage(HttpServletRequest request) {
|
||||
if (htmlTemplate == null) {
|
||||
return "";
|
||||
}
|
||||
String baseUrl = getBaseUrl(request);
|
||||
return htmlTemplate.replace(PLACEHOLDER, renderTags(buildGenericMeta(baseUrl)));
|
||||
}
|
||||
|
||||
/** Serves SPA HTML with event-specific meta-tags. */
|
||||
@GetMapping(
|
||||
value = "/events/{eventToken}",
|
||||
produces = MediaType.TEXT_HTML_VALUE
|
||||
)
|
||||
@ResponseBody
|
||||
public String serveEventPage(@PathVariable String eventToken,
|
||||
HttpServletRequest request) {
|
||||
if (htmlTemplate == null) {
|
||||
return "";
|
||||
}
|
||||
String baseUrl = getBaseUrl(request);
|
||||
Map<String, String> meta = resolveEventMeta(eventToken, baseUrl);
|
||||
return htmlTemplate.replace(PLACEHOLDER, renderTags(meta));
|
||||
}
|
||||
|
||||
// --- Meta-tag composition ---
|
||||
|
||||
private Map<String, String> buildEventMeta(Event event, String baseUrl) {
|
||||
var tags = new LinkedHashMap<String, String>();
|
||||
String title = truncateTitle(event.title());
|
||||
String description = formatDescription(event);
|
||||
tags.put("og:title", title);
|
||||
tags.put("og:description", description);
|
||||
tags.put("og:url", baseUrl + "/events/" + event.eventToken().value());
|
||||
tags.put("og:type", "website");
|
||||
tags.put("og:site_name", GENERIC_TITLE);
|
||||
tags.put("og:image", baseUrl + "/og-image.png");
|
||||
tags.put("twitter:card", "summary");
|
||||
tags.put("twitter:title", title);
|
||||
tags.put("twitter:description", description);
|
||||
return tags;
|
||||
}
|
||||
|
||||
private Map<String, String> buildGenericMeta(String baseUrl) {
|
||||
var tags = new LinkedHashMap<String, String>();
|
||||
tags.put("og:title", GENERIC_TITLE);
|
||||
tags.put("og:description", GENERIC_DESCRIPTION);
|
||||
tags.put("og:url", baseUrl);
|
||||
tags.put("og:type", "website");
|
||||
tags.put("og:site_name", GENERIC_TITLE);
|
||||
tags.put("og:image", baseUrl + "/og-image.png");
|
||||
tags.put("twitter:card", "summary");
|
||||
tags.put("twitter:title", GENERIC_TITLE);
|
||||
tags.put("twitter:description", GENERIC_DESCRIPTION);
|
||||
return tags;
|
||||
}
|
||||
|
||||
private Map<String, String> resolveEventMeta(String token, String baseUrl) {
|
||||
try {
|
||||
UUID uuid = UUID.fromString(token);
|
||||
Optional<Event> event =
|
||||
getEventUseCase.getByEventToken(new EventToken(uuid));
|
||||
if (event.isPresent()) {
|
||||
return buildEventMeta(event.get(), baseUrl);
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Invalid UUID — fall back to generic
|
||||
}
|
||||
return buildGenericMeta(baseUrl);
|
||||
}
|
||||
|
||||
// --- Description formatting ---
|
||||
|
||||
private String truncateTitle(String title) {
|
||||
if (title.length() <= MAX_TITLE_LENGTH) {
|
||||
return title;
|
||||
}
|
||||
return title.substring(0, MAX_TITLE_LENGTH - 3) + "...";
|
||||
}
|
||||
|
||||
private String formatDescription(Event event) {
|
||||
ZonedDateTime zoned = event.dateTime().atZoneSameInstant(event.timezone());
|
||||
var sb = new StringBuilder();
|
||||
sb.append("📅 ").append(zoned.format(DATE_FORMAT));
|
||||
|
||||
if (event.location() != null && !event.location().isBlank()) {
|
||||
sb.append(" · 📍 ").append(event.location());
|
||||
}
|
||||
|
||||
if (event.description() != null && !event.description().isBlank()) {
|
||||
sb.append(" — ").append(event.description());
|
||||
}
|
||||
|
||||
String result = sb.toString();
|
||||
if (result.length() > MAX_DESCRIPTION_LENGTH) {
|
||||
return result.substring(0, MAX_DESCRIPTION_LENGTH - 3) + "...";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- HTML rendering ---
|
||||
|
||||
private String renderTags(Map<String, String> tags) {
|
||||
var sb = new StringBuilder();
|
||||
for (var entry : tags.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = escapeHtml(entry.getValue());
|
||||
String attr = key.startsWith("twitter:") ? "name" : "property";
|
||||
sb.append("<meta ").append(attr).append("=\"").append(key)
|
||||
.append("\" content=\"").append(value).append("\">\n");
|
||||
}
|
||||
return sb.toString().stripTrailing();
|
||||
}
|
||||
|
||||
private String escapeHtml(String input) {
|
||||
return input
|
||||
.replace("&", "&")
|
||||
.replace("\"", """)
|
||||
.replace("<", "<")
|
||||
.replace(">", ">");
|
||||
}
|
||||
|
||||
private String getBaseUrl(HttpServletRequest request) {
|
||||
return ServletUriComponentsBuilder.fromRequestUri(request)
|
||||
.replacePath("")
|
||||
.build()
|
||||
.toUriString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package de.fete.adapter.out.persistence;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/** JPA entity mapping to the events table. */
|
||||
@Entity
|
||||
@Table(name = "events")
|
||||
public class EventJpaEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "event_token", nullable = false, unique = true)
|
||||
private UUID eventToken;
|
||||
|
||||
@Column(name = "organizer_token", nullable = false, unique = true)
|
||||
private UUID organizerToken;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(length = 2000)
|
||||
private String description;
|
||||
|
||||
@Column(name = "date_time", nullable = false)
|
||||
private OffsetDateTime dateTime;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String timezone;
|
||||
|
||||
@Column(length = 500)
|
||||
private String location;
|
||||
|
||||
@Column(name = "expiry_date", nullable = false)
|
||||
private LocalDate expiryDate;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
@Column(name = "cancelled", nullable = false)
|
||||
private boolean cancelled;
|
||||
|
||||
@Column(name = "cancellation_reason", length = 2000)
|
||||
private String cancellationReason;
|
||||
|
||||
/** Returns the internal database ID. */
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Sets the internal database ID. */
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/** Returns the public event token. */
|
||||
public UUID getEventToken() {
|
||||
return eventToken;
|
||||
}
|
||||
|
||||
/** Sets the public event token. */
|
||||
public void setEventToken(UUID eventToken) {
|
||||
this.eventToken = eventToken;
|
||||
}
|
||||
|
||||
/** Returns the secret organizer token. */
|
||||
public UUID getOrganizerToken() {
|
||||
return organizerToken;
|
||||
}
|
||||
|
||||
/** Sets the secret organizer token. */
|
||||
public void setOrganizerToken(UUID organizerToken) {
|
||||
this.organizerToken = organizerToken;
|
||||
}
|
||||
|
||||
/** Returns the event title. */
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
/** Sets the event title. */
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/** Returns the event description. */
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/** Sets the event description. */
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/** Returns the event date and time. */
|
||||
public OffsetDateTime getDateTime() {
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
/** Sets the event date and time. */
|
||||
public void setDateTime(OffsetDateTime dateTime) {
|
||||
this.dateTime = dateTime;
|
||||
}
|
||||
|
||||
/** Returns the IANA timezone name. */
|
||||
public String getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
/** Sets the IANA timezone name. */
|
||||
public void setTimezone(String timezone) {
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
/** Returns the event location. */
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
/** Sets the event location. */
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
/** Returns the expiry date. */
|
||||
public LocalDate getExpiryDate() {
|
||||
return expiryDate;
|
||||
}
|
||||
|
||||
/** Sets the expiry date. */
|
||||
public void setExpiryDate(LocalDate expiryDate) {
|
||||
this.expiryDate = expiryDate;
|
||||
}
|
||||
|
||||
/** Returns the creation timestamp. */
|
||||
public OffsetDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
/** Sets the creation timestamp. */
|
||||
public void setCreatedAt(OffsetDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
/** Returns whether the event is cancelled. */
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/** Sets the cancelled flag. */
|
||||
public void setCancelled(boolean cancelled) {
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
/** Returns the cancellation reason. */
|
||||
public String getCancellationReason() {
|
||||
return cancellationReason;
|
||||
}
|
||||
|
||||
/** Sets the cancellation reason. */
|
||||
public void setCancellationReason(String cancellationReason) {
|
||||
this.cancellationReason = cancellationReason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.fete.adapter.out.persistence;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
/** Spring Data JPA repository for event entities. */
|
||||
public interface EventJpaRepository extends JpaRepository<EventJpaEntity, Long> {
|
||||
|
||||
/** Finds an event by its public event token. */
|
||||
Optional<EventJpaEntity> findByEventToken(UUID eventToken);
|
||||
|
||||
/** Deletes all events whose expiry date is before today. Returns the number of deleted rows. */
|
||||
@Modifying
|
||||
@Query(value = "DELETE FROM events WHERE expiry_date < CURRENT_DATE", nativeQuery = true)
|
||||
int deleteExpired();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package de.fete.adapter.out.persistence;
|
||||
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.OrganizerToken;
|
||||
import de.fete.domain.port.out.EventRepository;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** Persistence adapter implementing the EventRepository outbound port. */
|
||||
@Repository
|
||||
public class EventPersistenceAdapter implements EventRepository {
|
||||
|
||||
private final EventJpaRepository jpaRepository;
|
||||
|
||||
/** Creates a new adapter with the given JPA repository. */
|
||||
public EventPersistenceAdapter(EventJpaRepository jpaRepository) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Event save(Event event) {
|
||||
EventJpaEntity entity = toEntity(event);
|
||||
EventJpaEntity saved = jpaRepository.save(entity);
|
||||
return toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Event> findByEventToken(EventToken eventToken) {
|
||||
return jpaRepository.findByEventToken(eventToken.value()).map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteExpired() {
|
||||
return jpaRepository.deleteExpired();
|
||||
}
|
||||
|
||||
private EventJpaEntity toEntity(Event event) {
|
||||
var entity = new EventJpaEntity();
|
||||
entity.setId(event.id());
|
||||
entity.setEventToken(event.eventToken().value());
|
||||
entity.setOrganizerToken(event.organizerToken().value());
|
||||
entity.setTitle(event.title());
|
||||
entity.setDescription(event.description());
|
||||
entity.setDateTime(event.dateTime());
|
||||
entity.setTimezone(event.timezone().getId());
|
||||
entity.setLocation(event.location());
|
||||
entity.setExpiryDate(event.expiryDate());
|
||||
entity.setCreatedAt(event.createdAt());
|
||||
entity.setCancelled(event.cancelled());
|
||||
entity.setCancellationReason(event.cancellationReason());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Event toDomain(EventJpaEntity entity) {
|
||||
return new Event(
|
||||
entity.getId(),
|
||||
new EventToken(entity.getEventToken()),
|
||||
new OrganizerToken(entity.getOrganizerToken()),
|
||||
entity.getTitle(),
|
||||
entity.getDescription(),
|
||||
entity.getDateTime(),
|
||||
ZoneId.of(entity.getTimezone()),
|
||||
entity.getLocation(),
|
||||
entity.getExpiryDate(),
|
||||
entity.getCreatedAt(),
|
||||
entity.isCancelled(),
|
||||
entity.getCancellationReason());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.fete.adapter.out.persistence;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.UUID;
|
||||
|
||||
/** JPA entity mapping to the rsvps table. */
|
||||
@Entity
|
||||
@Table(name = "rsvps")
|
||||
public class RsvpJpaEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "rsvp_token", nullable = false, unique = true)
|
||||
private UUID rsvpToken;
|
||||
|
||||
@Column(name = "event_id", nullable = false)
|
||||
private Long eventId;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
/** Returns the internal database ID. */
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Sets the internal database ID. */
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/** Returns the RSVP token. */
|
||||
public UUID getRsvpToken() {
|
||||
return rsvpToken;
|
||||
}
|
||||
|
||||
/** Sets the RSVP token. */
|
||||
public void setRsvpToken(UUID rsvpToken) {
|
||||
this.rsvpToken = rsvpToken;
|
||||
}
|
||||
|
||||
/** Returns the event ID. */
|
||||
public Long getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
|
||||
/** Sets the event ID. */
|
||||
public void setEventId(Long eventId) {
|
||||
this.eventId = eventId;
|
||||
}
|
||||
|
||||
/** Returns the guest's display name. */
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Sets the guest's display name. */
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.fete.adapter.out.persistence;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/** Spring Data JPA repository for RSVP entities. */
|
||||
public interface RsvpJpaRepository extends JpaRepository<RsvpJpaEntity, Long> {
|
||||
|
||||
/** Finds an RSVP by its token. */
|
||||
java.util.Optional<RsvpJpaEntity> findByRsvpToken(UUID rsvpToken);
|
||||
|
||||
/** Counts RSVPs for the given event. */
|
||||
long countByEventId(Long eventId);
|
||||
|
||||
/** Finds all RSVPs for the given event, ordered by ID ascending. */
|
||||
java.util.List<RsvpJpaEntity> findAllByEventIdOrderByIdAsc(Long eventId);
|
||||
|
||||
/** Deletes an RSVP by event ID and RSVP token. Returns count of deleted rows. */
|
||||
long deleteByEventIdAndRsvpToken(Long eventId, UUID rsvpToken);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.fete.adapter.out.persistence;
|
||||
|
||||
import de.fete.domain.model.Rsvp;
|
||||
import de.fete.domain.model.RsvpToken;
|
||||
import de.fete.domain.port.out.RsvpRepository;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** Persistence adapter implementing the RsvpRepository outbound port. */
|
||||
@Repository
|
||||
public class RsvpPersistenceAdapter implements RsvpRepository {
|
||||
|
||||
private final RsvpJpaRepository jpaRepository;
|
||||
|
||||
/** Creates a new adapter with the given JPA repository. */
|
||||
public RsvpPersistenceAdapter(RsvpJpaRepository jpaRepository) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rsvp save(Rsvp rsvp) {
|
||||
RsvpJpaEntity entity = toEntity(rsvp);
|
||||
RsvpJpaEntity saved = jpaRepository.save(entity);
|
||||
return toDomain(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countByEventId(Long eventId) {
|
||||
return jpaRepository.countByEventId(eventId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Rsvp> findByEventId(Long eventId) {
|
||||
return jpaRepository.findAllByEventIdOrderByIdAsc(eventId).stream()
|
||||
.map(this::toDomain)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteByEventIdAndRsvpToken(Long eventId, RsvpToken rsvpToken) {
|
||||
return jpaRepository.deleteByEventIdAndRsvpToken(eventId, rsvpToken.value()) > 0;
|
||||
}
|
||||
|
||||
private RsvpJpaEntity toEntity(Rsvp rsvp) {
|
||||
var entity = new RsvpJpaEntity();
|
||||
entity.setId(rsvp.id());
|
||||
entity.setRsvpToken(rsvp.rsvpToken().value());
|
||||
entity.setEventId(rsvp.eventId());
|
||||
entity.setName(rsvp.name());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Rsvp toDomain(RsvpJpaEntity entity) {
|
||||
return new Rsvp(
|
||||
entity.getId(),
|
||||
new RsvpToken(entity.getRsvpToken()),
|
||||
entity.getEventId(),
|
||||
entity.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.fete.application.service;
|
||||
|
||||
import de.fete.application.service.exception.EventAlreadyCancelledException;
|
||||
import de.fete.application.service.exception.EventNotFoundException;
|
||||
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||
import de.fete.domain.model.CreateEventCommand;
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.OrganizerToken;
|
||||
import de.fete.domain.port.in.CreateEventUseCase;
|
||||
import de.fete.domain.port.in.GetEventUseCase;
|
||||
import de.fete.domain.port.in.UpdateEventUseCase;
|
||||
import de.fete.domain.port.out.EventRepository;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Application service implementing event creation and retrieval. */
|
||||
@Service
|
||||
public class EventService implements CreateEventUseCase, GetEventUseCase, UpdateEventUseCase {
|
||||
|
||||
private static final int EXPIRY_DAYS_AFTER_EVENT = 7;
|
||||
|
||||
private final EventRepository eventRepository;
|
||||
private final Clock clock;
|
||||
|
||||
/** Creates a new EventService with the given repository and clock. */
|
||||
public EventService(EventRepository eventRepository, Clock clock) {
|
||||
this.eventRepository = eventRepository;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Event createEvent(CreateEventCommand command) {
|
||||
LocalDate expiryDate = command.dateTime().toLocalDate().plusDays(EXPIRY_DAYS_AFTER_EVENT);
|
||||
|
||||
var event = new Event(
|
||||
null,
|
||||
EventToken.generate(),
|
||||
OrganizerToken.generate(),
|
||||
command.title(),
|
||||
command.description(),
|
||||
command.dateTime(),
|
||||
command.timezone(),
|
||||
command.location(),
|
||||
expiryDate,
|
||||
OffsetDateTime.now(clock),
|
||||
false,
|
||||
null);
|
||||
|
||||
return eventRepository.save(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Event> getByEventToken(EventToken eventToken) {
|
||||
return eventRepository.findByEventToken(eventToken);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void cancelEvent(
|
||||
EventToken eventToken, OrganizerToken organizerToken,
|
||||
Boolean cancelled, String reason) {
|
||||
if (!Boolean.TRUE.equals(cancelled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Event event = eventRepository.findByEventToken(eventToken)
|
||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||
|
||||
if (!event.organizerToken().equals(organizerToken)) {
|
||||
throw new InvalidOrganizerTokenException();
|
||||
}
|
||||
|
||||
if (event.cancelled()) {
|
||||
throw new EventAlreadyCancelledException(eventToken.value());
|
||||
}
|
||||
|
||||
eventRepository.save(event.withCancellation(true, reason));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.fete.application.service;
|
||||
|
||||
import de.fete.domain.port.out.EventRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Scheduled job that deletes events whose expiry date is in the past. */
|
||||
@Component
|
||||
public class ExpiredEventCleanupJob {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExpiredEventCleanupJob.class);
|
||||
|
||||
private final EventRepository eventRepository;
|
||||
|
||||
/** Creates a new cleanup job with the given event repository. */
|
||||
public ExpiredEventCleanupJob(EventRepository eventRepository) {
|
||||
this.eventRepository = eventRepository;
|
||||
}
|
||||
|
||||
/** Runs daily at 03:00 and deletes all expired events. */
|
||||
@Scheduled(cron = "0 0 3 * * *")
|
||||
@Transactional
|
||||
public void deleteExpiredEvents() {
|
||||
int deleted = eventRepository.deleteExpired();
|
||||
log.info("Expired event cleanup: deleted {} event(s)", deleted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package de.fete.application.service;
|
||||
|
||||
import de.fete.application.service.exception.EventCancelledException;
|
||||
import de.fete.application.service.exception.EventExpiredException;
|
||||
import de.fete.application.service.exception.EventNotFoundException;
|
||||
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.OrganizerToken;
|
||||
import de.fete.domain.model.Rsvp;
|
||||
import de.fete.domain.model.RsvpToken;
|
||||
import de.fete.domain.port.in.CancelRsvpUseCase;
|
||||
import de.fete.domain.port.in.CountAttendeesByEventUseCase;
|
||||
import de.fete.domain.port.in.CreateRsvpUseCase;
|
||||
import de.fete.domain.port.in.GetAttendeesUseCase;
|
||||
import de.fete.domain.port.out.EventRepository;
|
||||
import de.fete.domain.port.out.RsvpRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Application service implementing RSVP operations. */
|
||||
@Service
|
||||
public class RsvpService
|
||||
implements CreateRsvpUseCase, CancelRsvpUseCase, CountAttendeesByEventUseCase,
|
||||
GetAttendeesUseCase {
|
||||
|
||||
private final EventRepository eventRepository;
|
||||
private final RsvpRepository rsvpRepository;
|
||||
private final Clock clock;
|
||||
|
||||
/** Creates a new RsvpService. */
|
||||
public RsvpService(
|
||||
EventRepository eventRepository,
|
||||
RsvpRepository rsvpRepository,
|
||||
Clock clock) {
|
||||
this.eventRepository = eventRepository;
|
||||
this.rsvpRepository = rsvpRepository;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rsvp createRsvp(EventToken eventToken, String name) {
|
||||
Event event = eventRepository.findByEventToken(eventToken)
|
||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||
|
||||
if (event.cancelled()) {
|
||||
throw new EventCancelledException(eventToken.value());
|
||||
}
|
||||
|
||||
if (!event.expiryDate().isAfter(LocalDate.now(clock))) {
|
||||
throw new EventExpiredException(eventToken.value());
|
||||
}
|
||||
|
||||
var rsvp = new Rsvp(null, RsvpToken.generate(), event.id(), name.strip());
|
||||
|
||||
return rsvpRepository.save(rsvp);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void cancelRsvp(EventToken eventToken, RsvpToken rsvpToken) {
|
||||
eventRepository.findByEventToken(eventToken)
|
||||
.ifPresent(event ->
|
||||
rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countByEvent(EventToken eventToken) {
|
||||
Event event = eventRepository.findByEventToken(eventToken)
|
||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||
return rsvpRepository.countByEventId(event.id());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAttendeeNames(EventToken eventToken, OrganizerToken organizerToken) {
|
||||
Event event = eventRepository.findByEventToken(eventToken)
|
||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||
|
||||
if (!event.organizerToken().equals(organizerToken)) {
|
||||
throw new InvalidOrganizerTokenException();
|
||||
}
|
||||
|
||||
return rsvpRepository.findByEventId(event.id()).stream()
|
||||
.map(Rsvp::name)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Thrown when attempting to cancel an event that is already cancelled. */
|
||||
public class EventAlreadyCancelledException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given event token. */
|
||||
public EventAlreadyCancelledException(UUID eventToken) {
|
||||
super("Event is already cancelled: " + eventToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Thrown when an RSVP is attempted on a cancelled event. */
|
||||
public class EventCancelledException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given event token. */
|
||||
public EventCancelledException(UUID eventToken) {
|
||||
super("Event is cancelled: " + eventToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Thrown when an RSVP is attempted on an expired event. */
|
||||
public class EventExpiredException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given event token. */
|
||||
public EventExpiredException(UUID eventToken) {
|
||||
super("Event has expired: " + eventToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Thrown when an event cannot be found by its token. */
|
||||
public class EventNotFoundException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given event token. */
|
||||
public EventNotFoundException(UUID eventToken) {
|
||||
super("Event not found: " + eventToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/** Thrown when an event's expiry date is not after the event date. */
|
||||
public class ExpiryDateBeforeEventException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given dates. */
|
||||
public ExpiryDateBeforeEventException(LocalDate expiryDate, OffsetDateTime dateTime) {
|
||||
super("Expiry date " + expiryDate + " must be after event date " + dateTime.toLocalDate());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** Thrown when an event's expiry date is not in the future. */
|
||||
public class ExpiryDateInPastException extends RuntimeException {
|
||||
|
||||
private final LocalDate expiryDate;
|
||||
|
||||
/** Creates a new exception for the given invalid expiry date. */
|
||||
public ExpiryDateInPastException(LocalDate expiryDate) {
|
||||
super("Expiry date must be in the future: " + expiryDate);
|
||||
this.expiryDate = expiryDate;
|
||||
}
|
||||
|
||||
/** Returns the invalid expiry date. */
|
||||
public LocalDate getExpiryDate() {
|
||||
return expiryDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
/** Thrown when an invalid organizer token is provided. */
|
||||
public class InvalidOrganizerTokenException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for an invalid organizer token. */
|
||||
public InvalidOrganizerTokenException() {
|
||||
super("Invalid organizer token.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.fete.application.service.exception;
|
||||
|
||||
/** Thrown when an invalid IANA timezone ID is provided. */
|
||||
public class InvalidTimezoneException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given invalid timezone. */
|
||||
public InvalidTimezoneException(String timezone) {
|
||||
super("Invalid IANA timezone: " + timezone);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Application-layer exceptions thrown by service use case implementations.
|
||||
*/
|
||||
package de.fete.application.service.exception;
|
||||
@@ -1,40 +1,24 @@
|
||||
package de.fete.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
/** Configures API path prefix and SPA static resource serving. */
|
||||
/** Configures API path prefix. Static resources served by default Spring Boot handler. */
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
/** Provides a system clock bean for time-dependent services. */
|
||||
@Bean
|
||||
Clock clock() {
|
||||
return Clock.systemDefaultZone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(RestController.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
@Override
|
||||
protected Resource getResource(String resourcePath,
|
||||
Resource location) throws IOException {
|
||||
Resource requested = location.createRelative(resourcePath);
|
||||
if (requested.exists() && requested.isReadable()) {
|
||||
return requested;
|
||||
}
|
||||
Resource index = new ClassPathResource("/static/index.html");
|
||||
return (index.exists() && index.isReadable()) ? index : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.fete.domain.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/** Command carrying the data needed to create an event. */
|
||||
public record CreateEventCommand(
|
||||
String title,
|
||||
String description,
|
||||
OffsetDateTime dateTime,
|
||||
ZoneId timezone,
|
||||
String location
|
||||
) {}
|
||||
30
backend/src/main/java/de/fete/domain/model/Event.java
Normal file
30
backend/src/main/java/de/fete/domain/model/Event.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package de.fete.domain.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/** Domain entity representing an event. */
|
||||
public record Event(
|
||||
Long id,
|
||||
EventToken eventToken,
|
||||
OrganizerToken organizerToken,
|
||||
String title,
|
||||
String description,
|
||||
OffsetDateTime dateTime,
|
||||
ZoneId timezone,
|
||||
String location,
|
||||
LocalDate expiryDate,
|
||||
OffsetDateTime createdAt,
|
||||
boolean cancelled,
|
||||
String cancellationReason
|
||||
) {
|
||||
|
||||
/** Returns a copy of this event with cancellation applied. */
|
||||
public Event withCancellation(boolean cancelled, String cancellationReason) {
|
||||
return new Event(
|
||||
id, eventToken, organizerToken, title, description,
|
||||
dateTime, timezone, location, expiryDate, createdAt,
|
||||
cancelled, cancellationReason);
|
||||
}
|
||||
}
|
||||
18
backend/src/main/java/de/fete/domain/model/EventToken.java
Normal file
18
backend/src/main/java/de/fete/domain/model/EventToken.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package de.fete.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Type-safe wrapper for the public event token. */
|
||||
public record EventToken(UUID value) {
|
||||
|
||||
/** Validates that the token value is not null. */
|
||||
public EventToken {
|
||||
Objects.requireNonNull(value, "eventToken must not be null");
|
||||
}
|
||||
|
||||
/** Generates a new random event token. */
|
||||
public static EventToken generate() {
|
||||
return new EventToken(UUID.randomUUID());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.fete.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Type-safe wrapper for the secret organizer token. */
|
||||
public record OrganizerToken(UUID value) {
|
||||
|
||||
/** Validates that the token value is not null. */
|
||||
public OrganizerToken {
|
||||
Objects.requireNonNull(value, "organizerToken must not be null");
|
||||
}
|
||||
|
||||
/** Generates a new random organizer token. */
|
||||
public static OrganizerToken generate() {
|
||||
return new OrganizerToken(UUID.randomUUID());
|
||||
}
|
||||
}
|
||||
9
backend/src/main/java/de/fete/domain/model/Rsvp.java
Normal file
9
backend/src/main/java/de/fete/domain/model/Rsvp.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package de.fete.domain.model;
|
||||
|
||||
/** Domain entity representing an RSVP. */
|
||||
public record Rsvp(
|
||||
Long id,
|
||||
RsvpToken rsvpToken,
|
||||
Long eventId,
|
||||
String name
|
||||
) {}
|
||||
18
backend/src/main/java/de/fete/domain/model/RsvpToken.java
Normal file
18
backend/src/main/java/de/fete/domain/model/RsvpToken.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package de.fete.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Type-safe wrapper for the RSVP token. */
|
||||
public record RsvpToken(UUID value) {
|
||||
|
||||
/** Validates that the token value is not null. */
|
||||
public RsvpToken {
|
||||
Objects.requireNonNull(value, "rsvpToken must not be null");
|
||||
}
|
||||
|
||||
/** Generates a new random RSVP token. */
|
||||
public static RsvpToken generate() {
|
||||
return new RsvpToken(UUID.randomUUID());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.RsvpToken;
|
||||
|
||||
/** Inbound port for cancelling an RSVP. */
|
||||
public interface CancelRsvpUseCase {
|
||||
|
||||
/** Cancels the RSVP identified by the given tokens. Idempotent — no error if not found. */
|
||||
void cancelRsvp(EventToken eventToken, RsvpToken rsvpToken);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.EventToken;
|
||||
|
||||
/** Inbound port for counting attendees of an event. */
|
||||
public interface CountAttendeesByEventUseCase {
|
||||
|
||||
/** Counts the number of confirmed attendees for the given event. */
|
||||
long countByEvent(EventToken eventToken);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.CreateEventCommand;
|
||||
import de.fete.domain.model.Event;
|
||||
|
||||
/** Inbound port for creating a new event. */
|
||||
public interface CreateEventUseCase {
|
||||
|
||||
/** Creates an event from the given command and returns the persisted event. */
|
||||
Event createEvent(CreateEventCommand command);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.Rsvp;
|
||||
|
||||
/** Inbound port for creating a new RSVP. */
|
||||
public interface CreateRsvpUseCase {
|
||||
|
||||
/** Creates an RSVP for the given event and guest name. */
|
||||
Rsvp createRsvp(EventToken eventToken, String name);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.OrganizerToken;
|
||||
import java.util.List;
|
||||
|
||||
/** Inbound port for retrieving attendee names of an event. */
|
||||
public interface GetAttendeesUseCase {
|
||||
|
||||
/** Returns attendee names ordered by RSVP submission time. */
|
||||
List<String> getAttendeeNames(EventToken eventToken, OrganizerToken organizerToken);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.model.EventToken;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Inbound port for retrieving a public event by its token. */
|
||||
public interface GetEventUseCase {
|
||||
|
||||
/** Finds an event by its public event token. */
|
||||
Optional<Event> getByEventToken(EventToken eventToken);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.EventToken;
|
||||
import de.fete.domain.model.OrganizerToken;
|
||||
|
||||
/** Inbound port for updating an event. */
|
||||
public interface UpdateEventUseCase {
|
||||
|
||||
/** Cancels the event identified by the given token. */
|
||||
void cancelEvent(
|
||||
EventToken eventToken, OrganizerToken organizerToken,
|
||||
Boolean cancelled, String reason);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.fete.domain.port.out;
|
||||
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.model.EventToken;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Outbound port for persisting and retrieving events. */
|
||||
public interface EventRepository {
|
||||
|
||||
/** Persists the given event and returns it with generated fields populated. */
|
||||
Event save(Event event);
|
||||
|
||||
/** Finds an event by its public event token. */
|
||||
Optional<Event> findByEventToken(EventToken eventToken);
|
||||
|
||||
/** Deletes all events whose expiry date is in the past. Returns the number of deleted events. */
|
||||
int deleteExpired();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.fete.domain.port.out;
|
||||
|
||||
import de.fete.domain.model.Rsvp;
|
||||
import de.fete.domain.model.RsvpToken;
|
||||
import java.util.List;
|
||||
|
||||
/** Outbound port for persisting and querying RSVPs. */
|
||||
public interface RsvpRepository {
|
||||
|
||||
/** Persists the given RSVP and returns it with generated fields populated. */
|
||||
Rsvp save(Rsvp rsvp);
|
||||
|
||||
/** Counts the number of RSVPs for the given event. */
|
||||
long countByEventId(Long eventId);
|
||||
|
||||
/** Finds all RSVPs for the given event, ordered by ID ascending. */
|
||||
List<Rsvp> findByEventId(Long eventId);
|
||||
|
||||
/** Deletes an RSVP by event ID and RSVP token. Returns true if a record was deleted. */
|
||||
boolean deleteByEventIdAndRsvpToken(Long eventId, RsvpToken rsvpToken);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Local development database
|
||||
# Copy this file to application-local.properties and adjust as needed.
|
||||
# Start with: ./mvnw spring-boot:run -Dspring-boot.run.profiles=local
|
||||
spring.datasource.url=jdbc:postgresql://localhost:5432/fete
|
||||
spring.datasource.username=fete
|
||||
spring.datasource.password=fete
|
||||
4
backend/src/main/resources/application-prod.properties
Normal file
4
backend/src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
# Database (required)
|
||||
spring.datasource.url=${DATABASE_URL}
|
||||
spring.datasource.username=${DATABASE_USERNAME}
|
||||
spring.datasource.password=${DATABASE_PASSWORD}
|
||||
@@ -1,4 +1,15 @@
|
||||
spring.application.name=fete
|
||||
|
||||
# JPA
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
spring.jpa.open-in-view=false
|
||||
|
||||
# Liquibase
|
||||
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
|
||||
|
||||
# Proxy headers
|
||||
server.forward-headers-strategy=framework
|
||||
|
||||
# Actuator
|
||||
management.endpoints.web.exposure.include=health
|
||||
management.endpoint.health.show-details=never
|
||||
|
||||
12
backend/src/main/resources/db/changelog/000-baseline.xml
Normal file
12
backend/src/main/resources/db/changelog/000-baseline.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="000-baseline" author="nitrix">
|
||||
<comment>Baseline changeset — Liquibase tooling verification</comment>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="001-create-events-table" author="fete">
|
||||
<createTable tableName="events">
|
||||
<column name="id" type="bigserial" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="event_token" type="uuid">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="organizer_token" type="uuid">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="title" type="varchar(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="description" type="varchar(2000)"/>
|
||||
<column name="date_time" type="timestamptz">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="location" type="varchar(500)"/>
|
||||
<column name="expiry_date" type="date">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<createIndex tableName="events" indexName="idx_events_event_token">
|
||||
<column name="event_token"/>
|
||||
</createIndex>
|
||||
|
||||
<createIndex tableName="events" indexName="idx_events_expiry_date">
|
||||
<column name="expiry_date"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user