feat(config): classify team_group with optional SQL - #4
Conversation
…e DSL groupRules stays three AND-lists. Classification that needs regex, exclusions, or custom fields is one read-only SELECT/WITH (groupQuery) run when the derived view is rebuilt. Empty group stops; NULL falls through to rules then the assignee's member group. REGEXP is registered on the mirror. The query is team-exportable and omit-to-preserve on settings PUT so older clients cannot wipe it.
groupQuery is checked for being a single SELECT when it is saved, and nothing checks that the tables and columns it names exist. So the first time a renamed field or a hand-edited config makes it wrong, the failure lands on the read path — where returning the error took bootstrap down with it. Measured on this branch: HTTP 500, and the app cannot load, which strands the person outside the settings dialog that would let them fix the query. Falling through costs the classification and nothing else: groupRules and the assignee's member group are the same fallbacks a key missing from the result already gets. It is also the rule this server already follows next door — write-meta degrades rather than blocking the boot. REGEXP now caches compiled patterns. SQLite calls the function once per row and a groupQuery reads the whole mirror, so the same few patterns were being compiled tens of thousands of times per rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
리뷰 완료 — 커밋 2개 추가하고 main을 병합했습니다. 검수 결과검증 설계는 견고합니다. 저장 경로 2곳(
고친 것 2건 (c5dbc6f)① 잘못된 쿼리가 앱을 죽입니다 (실측). 저장 시점엔 SELECT인지만 보고 테이블·컬럼 존재는 아무도 확인하지 않습니다. 그래서 필드명이 바뀌거나 오타가 있으면 읽기 경로에서 터지는데, 실측: 분류만 잃고 넘어가게 했습니다. groupRules → 담당자 그룹이라는 폴백은 결과에 키가 없을 때 이미 쓰이는 것과 같고, 바로 옆
② REGEXP가 행마다 재컴파일합니다. SQLite는 이 함수를 행당 1회 호출하고 groupQuery는 미러 전체를 읽으니, 같은 패턴 몇 개를 리빌드마다 수만 번 컴파일하게 됩니다. 게이트 (병합 후 상태, 리드 실측)
Windows 잡 실패는 이 PR과 무관한 제 CI 버그였고( |
`apt-get update` sat silent for 24 minutes on noble-security and consumed the desktop-linux job's entire 25-minute budget, so PR #4 reported a Linux *build* timeout before a single line had been compiled. The same job had passed three times on main within the hour — the mirror was the variable, and apt waits forever by default. Bound each request, retry three times, and cap the step at six minutes. A stuck mirror now costs six minutes and says "mirror trouble, not a build failure" instead of impersonating one — which matters because the wrong reading of this signal is to go looking at build-linux.sh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-272) (#23) `RESOLVED_STATUS_NAMES` held `resolved`/`closed`/`done`/`해결됨`/`종료`/`완료` and two readers used it whenever the server sent no category: `effectiveCategory` returned `done` on a name match, and HistoryTimeline's local `isReopen` painted a reopen badge from `isResolved(from) && !isResolved(to)`. The repository forbids exactly this. `status = 'In Progress'` is silently zero rows on a Korean account; `docs/STATE_OF_PLAY.md` hard-won #4 says a reopen is a done-category → non-done transition and *never* a name match. The set's own comment claimed it kept "only generic names that mean the same on every Jira" — `완료` is not a generic name, so the premise contradicted itself. It was not dead code. `internal/server/read.go:445` fills `from_category` from `view.categories[FromID]`, and `read.go:650` seeds that map only from status ids a *currently mirrored* issue occupies. Any status the issue passed through that nothing currently holds — a status removed from the workflow, for instance — arrives with no category, and the name decides. So this also broke on an English site with a custom done status ("Verified", "Shipped"), not just on a third language. Now: a real `new|inprogress|done` is trusted, anything else is `inprogress`, and no name is ever consulted. `isReopen` moved into `view-config.ts` as the single owner of the reopen rule and returns false when both categories are empty — an unpainted badge is a missing hint, while a wrongly painted one is a false claim about the issue's history. `matchesIdFirst`'s name fallback is untouched: that is the documented compatibility path for legacy saved views, a different decision from this one. Recurrence: `tools/doc-checks.sh` now matches `'해결됨'`, `'종료'` and `'완료'` as display-name literals in web logic. The old comment there claimed this table was "deliberately not matched (would fail the current tree; sibling issue)" — the round found that was only a comment, not a grep exclusion, so the real work was adding patterns that catch the table rather than removing an exemption. Lowercase `done`/`resolved`/`closed` stay out on purpose: they are also category keys and other-field values (a RangeField `resolved`, a GitHub PR `closed`). FAIL-first, run by the lead: reintroducing `new Set(['완료', '해결됨'])` into view-config.ts makes doc-checks fail and name the file and line; removing it returns exit 0. `effectiveCategory` also had no unit test at all before this — now 11 new cases cover the trusted values, the empty-category-with-Korean-name row, `'Done'`, a custom `'Shipped'`, and the reopen predicate. Debuggability: the failure was silent, so `missingStatusCategorySeen()` counts how often a category decision ran without a category. Integer increment, no per-row logging. Gates (lead, cold): svelte-check 4250 files / 0 errors · vitest 34 files / 347 tests (was 336) · doc-checks all passed · Playwright 240 passed. Co-authored-by: midagedev <midagedev@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Why
groupRules is three AND-lists (projects / labels / components). Real team classification needs exclusions, regex, and custom-field text. Growing that struct into a DSL would bake the next tenant's predicates into the schema.
Classification is a function of data already in the mirror, so the flexible surface is one read-only query — the same public interface as
gadak sql.What
groupQuery: one SELECT/WITH returning (issue key, group).gadak config set groupQuery.groupRules is unchanged.
Not this PR
No site URL, project key, or custom field id. Installation CASE stays in the query string and in
gadak team export.Test
go test ./internal/config ./internal/store ./internal/server ./internal/teamconfig ./cmd/gadakbash scripts/scan-internal.sh