Skip to content

feat(config): classify team_group with optional SQL - #4

Merged
midagedev merged 3 commits into
mainfrom
feat/group-query
Aug 18, 2026
Merged

feat(config): classify team_group with optional SQL#4
midagedev merged 3 commits into
mainfrom
feat/group-query

Conversation

@midagedev

@midagedev midagedev commented Aug 18, 2026

Copy link
Copy Markdown
Owner

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

  • New config key groupQuery: one SELECT/WITH returning (issue key, group).
  • Runs when the derived view is rebuilt (config or sync version), never on a keystroke.
  • Empty group string means unclassified (stop). NULL or a missing key falls through to groupRules, then the assignee's member group.
  • REGEXP is registered on the mirror.
  • Writes, PRAGMA, ATTACH, and multi-statement payloads are rejected on save.
  • Team-exportable. Settings PUT is omit-to-preserve so older clients cannot wipe a stored query.
  • Settings → Teams textarea, and 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/gadak
  • bash scripts/scan-internal.sh

midagedev and others added 3 commits August 18, 2026 10:53
…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>
@midagedev

Copy link
Copy Markdown
Owner Author

리뷰 완료 — 커밋 2개 추가하고 main을 병합했습니다.

검수 결과

검증 설계는 견고합니다. 저장 경로 2곳(gadak config set, Settings PUT)과 실행 지점(GroupQueryHits)에서 각각 검증하므로, 손으로 고친 config.json도 막힙니다. ; 검사는 문자열 리터럴 안의 세미콜론까지 거부하는 과잉 엄격이지만 fail-closed라 문제 없습니다. 신뢰 경계도 넓히지 않습니다 — gadak sql이 이미 같은 사용자에게 같은 클래스의 SELECT를 허용합니다.

groupQuery를 포인터로 두어 생략 시 보존하는 처리가 특히 좋습니다. 빈 필드가 "전체"나 "삭제"를 뜻하는 것은 실제로 사고를 낸 적 있는 부류입니다.

고친 것 2건 (c5dbc6f)

① 잘못된 쿼리가 앱을 죽입니다 (실측). 저장 시점엔 SELECT인지만 보고 테이블·컬럼 존재는 아무도 확인하지 않습니다. 그래서 필드명이 바뀌거나 오타가 있으면 읽기 경로에서 터지는데, buildView가 에러를 그대로 올려 bootstrap이 500이 되고 앱이 아예 안 뜹니다. 쿼리를 고쳐야 할 설정 다이얼로그가 그 앱 안에 있으니 UI로는 복구할 수 없습니다.

실측:

$ curl -o /dev/null -w "%{http_code}" .../bootstrap/    → 500
server: GET /api/v1/issues/bootstrap/: groupQuery: SQL logic error: no such table: no_such_table (1)

분류만 잃고 넘어가게 했습니다. groupRules → 담당자 그룹이라는 폴백은 결과에 키가 없을 때 이미 쓰이는 것과 같고, 바로 옆 handleWriteMeta가 "degrade rather than block the boot"으로 같은 판단을 이미 하고 있습니다.

TestBootstrapSurvivesABrokenGroupQuery 추가. FAIL-first: 수정 전 소스에서 status 500으로 실패함을 확인했습니다.

② REGEXP가 행마다 재컴파일합니다. SQLite는 이 함수를 행당 1회 호출하고 groupQuery는 미러 전체를 읽으니, 같은 패턴 몇 개를 리빌드마다 수만 번 컴파일하게 됩니다. sync.Map 캐시를 넣었습니다. (Go의 regexp는 RE2라 백트래킹 폭발은 원래 없습니다.)

게이트 (병합 후 상태, 리드 실측)

  • go build / go vet / go test ./... -count=1 → rc=0, 25패키지 초록
  • tools/doc-checks.sh → 통과
  • npm run typecheck → 4243파일 0에러
  • npm run test:unit → 30파일 309건 통과
  • scripts/scan-internal.sh → 통과

Windows 잡 실패는 이 PR과 무관한 제 CI 버그였고(e03c075), main 병합으로 해소됩니다.

@midagedev
midagedev merged commit 53f4cc7 into main Aug 18, 2026
11 of 12 checks passed
midagedev added a commit that referenced this pull request Aug 18, 2026
`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>
midagedev added a commit that referenced this pull request Aug 18, 2026
…-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>
@midagedev
midagedev deleted the feat/group-query branch August 20, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant