Skip to content

Commit bc72df2

Browse files
committed
fix: resolve merge conflicts
2 parents d1cb840 + 74139ed commit bc72df2

351 files changed

Lines changed: 32773 additions & 6194 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/plane-ticket-quality.skill.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
# Skill: High-Quality Plane Ticket Writing
2-
3-
## Purpose
1+
# High-Quality Ticket Creation
42

53
Create self-contained, implementation-ready Plane tickets with enough context
64
that a developer can start work immediately without chat follow-up.
@@ -99,7 +97,8 @@ evidence.
9997
4. Business impact is present and non-generic.
10098
5. No dates appear anywhere in the ticket body/title.
10199
6. No placeholder text remains.
102-
7. Ticket has enough implementation context (files/flows/data model) to start work immediately.
100+
7. Ticket has enough implementation context (files/flows/data model) to start
101+
work immediately.
103102
8. Acceptance criteria are testable and specific.
104103

105104
## Guardrails
@@ -180,12 +179,12 @@ or a private `.env` that stays untracked.
180179

181180
### Identifiers (this workspace)
182181

183-
| What | Value |
184-
|------|--------|
185-
| Host (UI + API) | `https://tasks.bobadilla.tech` |
186-
| API prefix | `/api/v1/` |
187-
| Workspace **slug** | `core` |
188-
| Project id (**UUID**) | `5da3caa1-cbba-4e0d-b9a9-0bcad39bba09` |
182+
| What | Value |
183+
| ----------------------------------------- | -------------------------------------- |
184+
| Host (UI + API) | `https://tasks.bobadilla.tech` |
185+
| API prefix | `/api/v1/` |
186+
| Workspace **slug** | `core` |
187+
| Project id (**UUID**) | `5da3caa1-cbba-4e0d-b9a9-0bcad39bba09` |
189188
| Example backlog **state** id (new issues) | `57725193-924f-4477-b9bc-b305e553efb9` |
190189

191190
Re-check **state** and **project** ids in the Plane UI if workflows were
@@ -203,8 +202,8 @@ https://tasks.bobadilla.tech/api/v1/workspaces/core/projects/5da3caa1-cbba-4e0d-
203202
- **`X-API-Key`**: personal access token (Plane); not `Authorization: Bearer …`
204203
for PAT-based calls.
205204
- **`Content-Type: application/json`** and **`Accept: application/json`**
206-
- **`User-Agent`**: use a normal browser string. A generic script UA can
207-
trigger Cloudflare **error 1010**; a desktop Chrome string is a safe default.
205+
- **`User-Agent`**: use a normal browser string. A generic script UA can trigger
206+
Cloudflare **error 1010**; a desktop Chrome string is a safe default.
208207

209208
Define once:
210209

.gitattributes

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
1-
# Exclude HTML from language statistics
2-
*.html linguist-detectable=false
3-
*.erb linguist-detectable=false
4-
5-
# Exclude shell scripts from language statistics
6-
*.sh linguist-detectable=false
7-
8-
# Mark Rails asset files as vendored
9-
apps/dashboard/app/assets/** linguist-vendored
10-
apps/dashboard/public/** linguist-vendored
11-
apps/dashboard/vendor/** linguist-vendored
12-
13-
# Mark Rails/Kamal deployment scripts as vendored
14-
apps/dashboard/bin/dev linguist-vendored
15-
apps/dashboard/bin/docker-entrypoint linguist-vendored
16-
apps/dashboard/.kamal/hooks/pre-build.sample linguist-vendored
17-
apps/dashboard/.kamal/hooks/post-deploy.sample linguist-vendored
18-
apps/dashboard/.kamal/hooks/pre-app-boot.sample linguist-vendored
19-
apps/dashboard/.kamal/hooks/docker-setup.sample linguist-vendored
20-
apps/dashboard/.kamal/hooks/post-app-boot.sample linguist-vendored
21-
apps/dashboard/.kamal/hooks/pre-proxy-reboot.sample linguist-vendored
22-
apps/dashboard/.kamal/hooks/post-proxy-reboot.sample linguist-vendored
23-
24-
**/node_modules/* linguist-vendored
25-
**/vendor/* linguist-vendored
26-
27-
# Mark auto-generated files as generated
28-
**/generated/* linguist-generated
1+
# Force LF line endings for Go and Go module files
2+
*.go text eol=lf
3+
*.mod text eol=lf
4+
*.sum text eol=lf
5+
6+
# Exclude HTML from language statistics
7+
*.html linguist-detectable=false
8+
*.erb linguist-detectable=false
9+
10+
# Exclude shell scripts from language statistics
11+
*.sh linguist-detectable=false
12+
13+
# Mark Rails asset files as vendored
14+
apps/dashboard/app/assets/** linguist-vendored
15+
apps/dashboard/public/** linguist-vendored
16+
apps/dashboard/vendor/** linguist-vendored
17+
18+
# Mark Rails/Kamal deployment scripts as vendored
19+
apps/dashboard/bin/dev linguist-vendored
20+
apps/dashboard/bin/docker-entrypoint linguist-vendored
21+
apps/dashboard/.kamal/hooks/pre-build.sample linguist-vendored
22+
apps/dashboard/.kamal/hooks/post-deploy.sample linguist-vendored
23+
apps/dashboard/.kamal/hooks/pre-app-boot.sample linguist-vendored
24+
apps/dashboard/.kamal/hooks/docker-setup.sample linguist-vendored
25+
apps/dashboard/.kamal/hooks/post-app-boot.sample linguist-vendored
26+
apps/dashboard/.kamal/hooks/pre-proxy-reboot.sample linguist-vendored
27+
apps/dashboard/.kamal/hooks/post-proxy-reboot.sample linguist-vendored
28+
29+
**/node_modules/* linguist-vendored
30+
**/vendor/* linguist-vendored
31+
32+
# Mark auto-generated files as generated
33+
**/generated/* linguist-generated

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"rubyLsp.bundleGemfile": "${workspaceFolder}/apps/dashboard/Gemfile",
2+
"rubyLsp.bundleGemfile": "${workspaceFolder}/apps/dashboard/Gemfile"
33
}

agents.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -275,15 +275,24 @@ Domain-driven design with feature modules:
275275
- `httpx/` - HTTP utilities
276276
- `middleware/` - Auth middleware
277277
- `reqredis/` - Redis connection
278-
- `services/` - Self-contained business domain modules, one directory per API category
279-
- `entertainment/` - Jokes, trivia, facts, horoscope, sudoku, emoji, quotes, advice (`/v1/entertainment/*`)
280-
- `finance/` - Exchange rates, crypto, BIN, IBAN, SWIFT, mortgage, commodities, inflation (`/v1/finance/*`)
278+
- `services/` - Self-contained business domain modules, one directory per API
279+
category
280+
- `entertainment/` - Jokes, trivia, facts, horoscope, sudoku, emoji, quotes,
281+
advice (`/v1/entertainment/*`)
282+
- `finance/` - Exchange rates, crypto, BIN, IBAN, SWIFT, mortgage,
283+
commodities, inflation (`/v1/finance/*`)
281284
- `health/` - Fitness exercises (`/v1/health/*`)
282-
- `networking/` - IP geolocation/ASN/VPN, WHOIS, domain info, MX lookup, disposable email (`/v1/networking/*`)
283-
- `places/` - Timezone, working days, holidays, geocoding, postal codes, cities (`/v1/places/*`)
284-
- `technology/` - QR/barcode, password, user agent, counter, random user, base64, number base, color, unit/format conversion, markdown (`/v1/technology/*`)
285-
- `text/` - Words/dictionary, lorem ipsum, spell check, thesaurus, sentiment, language detection, text similarity, email normalize (`/v1/text/*`)
286-
- `validation/` - Email validation, phone validation, profanity filter (`/v1/validation/*`)
285+
- `networking/` - IP geolocation/ASN/VPN, WHOIS, domain info, MX lookup,
286+
disposable email (`/v1/networking/*`)
287+
- `places/` - Timezone, working days, holidays, geocoding, postal codes,
288+
cities (`/v1/places/*`)
289+
- `technology/` - QR/barcode, password, user agent, counter, random user,
290+
base64, number base, color, unit/format conversion, markdown
291+
(`/v1/technology/*`)
292+
- `text/` - Words/dictionary, lorem ipsum, spell check, thesaurus, sentiment,
293+
language detection, text similarity, email normalize (`/v1/text/*`)
294+
- `validation/` - Email validation, phone validation, profanity filter
295+
(`/v1/validation/*`)
287296

288297
Each subdomain follows the same pattern: `service.go` (business logic),
289298
`transport_http.go` (HTTP handlers), `type.go` (data types), and parent

apps/api/app/healthz.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ type healthzResponse struct {
1616
Status string `json:"status"`
1717
}
1818

19-
func (healthzResponse) IsData() {}
20-
2119
func Healthz(pool dbPinger) http.HandlerFunc {
2220
return func(w http.ResponseWriter, r *http.Request) {
2321
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)

apps/api/app/routes_v1.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func registerV1Routes(ctx context.Context, r chi.Router, pool *pgxpool.Pool, rdb
7575

7676
if serviceEnabled(cfg, "text") {
7777
textRouter := chi.NewRouter()
78-
text.RegisterRoutes(textRouter, pool)
78+
text.RegisterRoutes(textRouter, pool, cfg)
7979
r.Mount("/text", textRouter)
8080
}
8181

apps/api/go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/golang-migrate/migrate/v4 v4.19.1
99
github.com/jackc/pgx/v5 v5.9.2
1010
github.com/redis/go-redis/v9 v9.19.0
11+
github.com/stretchr/testify v1.11.1
1112
)
1213

1314
require (
@@ -57,10 +58,8 @@ require (
5758
github.com/nyaruka/phonenumbers v1.7.4
5859
github.com/pemistahl/lingua-go v1.4.0
5960
github.com/ringsaturn/tzf v1.2.1
60-
github.com/sajari/fuzzy v1.0.0
6161
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
6262
github.com/spf13/cobra v1.10.2
63-
github.com/stretchr/testify v1.11.1
6463
github.com/yuin/goldmark v1.8.2
6564
gopkg.in/yaml.v3 v3.0.1
6665
)
@@ -75,4 +74,4 @@ require (
7574
golang.org/x/crypto v0.49.0 // indirect
7675
golang.org/x/sync v0.20.0 // indirect
7776
golang.org/x/text v0.35.0 // indirect
78-
)
77+
)

apps/api/platform/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Config struct {
1616
EnabledServices string // comma-separated list of services to mount; empty = all
1717
Environment string // "development" | "staging" | "production"
1818
SentryDSN string
19+
LanguageToolURL string // base URL for LanguageTool spell-check service
1920
}
2021

2122
func Load() Config {
@@ -33,6 +34,7 @@ func Load() Config {
3334
EnabledServices: envOrDefault("ENABLED_SERVICES", ""),
3435
Environment: envOrDefault("ENVIRONMENT", "development"),
3536
SentryDSN: envOrDefault("SENTRY_DSN", "https://df9023edf98442d4887d1040de81b768@issues.bobadilla.tech/1"),
37+
LanguageToolURL: envOrDefault("LANGUAGETOOL_URL", "http://localhost:8010"),
3638
}
3739
}
3840

apps/api/platform/db/interfaces.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package db
2+
3+
import (
4+
"context"
5+
6+
"github.com/jackc/pgx/v5"
7+
)
8+
9+
// Querier is the minimal DB interface for services that only need single-row queries.
10+
// *pgxpool.Pool satisfies this interface directly.
11+
type Querier interface {
12+
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
13+
}

apps/api/platform/httpx/handler.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
// and structured error responses. The request body is capped at 1 MB.
1616
//
1717
// Req is decoded from the JSON request body and validated via struct tags.
18-
// Res must implement Data (add an IsData() method to your response type).
1918
//
2019
// Error mapping:
2120
// - *ValidationFailure → 422 with {"error":"validation_failed","fields":[...]}
@@ -29,7 +28,7 @@ import (
2928
// return svc.CheckEmail(req.Email), nil
3029
// },
3130
// ))
32-
func Handle[Req any, Res Data](
31+
func Handle[Req any, Res any](
3332
fn func(ctx context.Context, req Req) (Res, error),
3433
) http.HandlerFunc {
3534
return func(w http.ResponseWriter, r *http.Request) {
@@ -78,31 +77,92 @@ func HandleBatch[Req any, Item any](
7877
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
7978

8079
var req Req
80+
8181
if err := BindAndValidate(r, &req); err != nil {
8282
if vf, ok := errors.AsType[*ValidationFailure](err); ok {
8383
ValidationError(w, vf)
8484
return
8585
}
86+
8687
Error(w, http.StatusBadRequest, "bad_request", cleanDecodeError(err))
8788
return
8889
}
8990

9091
res, err := fn(r.Context(), req)
92+
9193
if err != nil {
9294
if se, ok := errors.AsType[*svcerr.Error](err); ok {
9395
if se.Kind == svcerr.KindUpstream {
9496
sentry.CaptureException(err)
9597
}
98+
9699
Error(w, svcerr.HTTPStatus(se), se.Code, se.Message)
97100
return
98101
}
102+
99103
sentry.CaptureException(err)
104+
100105
Error(w, http.StatusInternalServerError, "internal_error", "internal server error")
101106
return
102107
}
103108

104109
res.Total = len(res.Results)
110+
105111
w.Header().Set("X-Usage-Count", strconv.Itoa(len(res.Results)))
112+
113+
JSON(w, http.StatusOK, res)
114+
}
115+
}
116+
117+
// HandleGet wraps a GET endpoint with automatic query-parameter binding,
118+
// validation, and structured error responses.
119+
//
120+
// Req is populated from URL query parameters using `query:"name"` struct tags
121+
// and validated via `validate:` tags. To apply defaults before binding, pass an
122+
// initialised Req value as the optional second argument — fields with no
123+
// matching query param keep their value from that initialiser.
124+
//
125+
// Error mapping is identical to Handle:
126+
// - *ValidationFailure → 422 with {"error":"validation_failed","fields":[...]}
127+
// - *svcerr.Error → mapped HTTP status with {"error":Code,"message":Message}
128+
// - any other error → 500 with {"error":"internal_error"}
129+
func HandleGet[Req any, Res any](
130+
fn func(ctx context.Context, req Req) (Res, error),
131+
defaults ...Req,
132+
) http.HandlerFunc {
133+
return func(w http.ResponseWriter, r *http.Request) {
134+
var req Req
135+
if len(defaults) > 0 {
136+
req = defaults[0]
137+
}
138+
139+
if err := BindQuery(r, &req); err != nil {
140+
if vf, ok := errors.AsType[*ValidationFailure](err); ok {
141+
ValidationError(w, vf)
142+
return
143+
}
144+
145+
Error(w, http.StatusBadRequest, "bad_request", err.Error())
146+
return
147+
}
148+
149+
res, err := fn(r.Context(), req)
150+
151+
if err != nil {
152+
if se, ok := errors.AsType[*svcerr.Error](err); ok {
153+
if se.Kind == svcerr.KindUpstream {
154+
sentry.CaptureException(err)
155+
}
156+
157+
Error(w, svcerr.HTTPStatus(se), se.Code, se.Message)
158+
return
159+
}
160+
161+
sentry.CaptureException(err)
162+
Error(w, http.StatusInternalServerError, "internal_error", "internal server error")
163+
return
164+
}
165+
106166
JSON(w, http.StatusOK, res)
107167
}
108168
}
@@ -121,6 +181,7 @@ func Guard[S any](svc *S, h http.HandlerFunc) http.HandlerFunc {
121181
Error(w, http.StatusInternalServerError, "internal_error", "service unavailable")
122182
}
123183
}
184+
124185
return h
125186
}
126187

0 commit comments

Comments
 (0)