From 3ab9d290c2177bf8c37504b31831b8e32d88c133 Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 02:34:50 +0900 Subject: [PATCH 01/36] hot fix --- .env.example | 11 +- Makefile | 31 +- apps/api/cmd/server/main.go | 8 +- apps/api/go.mod | 6 +- apps/api/go.sum | 4 + apps/api/internal/api/handler.go | 20 +- apps/api/internal/api/response.go | 144 ++++++++ apps/api/internal/config/config.go | 34 +- apps/api/internal/slack/modal.go | 17 + apps/api/internal/slack/socketmode.go | 477 ++++++++++++++++++++++++++ apps/worker/src/ai/triage.ts | 54 ++- apps/worker/src/index.ts | 12 +- apps/worker/src/slack/client.ts | 33 ++ apps/worker/src/workers/question.ts | 147 ++++++-- docs-host/.vitepress/config.mts | 107 ------ docs-host/index.html | 63 ++++ docs-host/package.json | 9 +- docs-host/server.js | 63 ++++ docs/README.md | 56 +++ docs/_sidebar.md | 22 ++ docs/detail/06_directory.md | 24 +- docs/detail/07_env-vars.md | 6 +- docs/index.md | 26 -- docs/setup/local.md | 18 +- docs/setup/slack.md | 45 ++- infra/docker/compose.yml | 1 - infra/docker/init-localstack.sh | 0 infra/slack/manifest.json | 12 +- pnpm-workspace.yaml | 1 - 29 files changed, 1166 insertions(+), 285 deletions(-) create mode 100644 apps/api/internal/api/response.go create mode 100644 apps/api/internal/slack/socketmode.go delete mode 100644 docs-host/.vitepress/config.mts create mode 100644 docs-host/index.html create mode 100644 docs-host/server.js create mode 100644 docs/README.md create mode 100644 docs/_sidebar.md delete mode 100644 docs/index.md mode change 100644 => 100755 infra/docker/init-localstack.sh diff --git a/.env.example b/.env.example index 46464bc..7446f0f 100644 --- a/.env.example +++ b/.env.example @@ -4,14 +4,15 @@ # .env は絶対にコミットしないこと # === Slack === -SLACK_BOT_TOKEN= -SLACK_SIGNING_SECRET= -SLACK_APP_ID= +SLACK_BOT_TOKEN= # xoxb-... (OAuth & Permissions) +SLACK_APP_TOKEN= # xapp-... (Basic Information → App-Level Tokens → connections:write) +SLACK_APP_ID= # A0XXXXXXX (setup.sh で使用) +SLACK_SIGNING_SECRET= # 不要になりましたが念のため残す SLACK_MENTOR_CHANNEL_ID= SLACK_PROGRESS_CHANNEL_ID= # === Database === -DATABASE_URL=postgres://postgres:postgres@localhost:5432/kcl_support_hub +DATABASE_URL=postgres://postgres:postgres@localhost:5432/kcl_support_hub?sslmode=disable # === AI (Bonsai / Ollama) === # ollama serve && ollama pull qwen2.5:7b を実行してから設定 @@ -34,7 +35,7 @@ ONYX_API_URL= ONYX_API_KEY= # === AWS === -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 # ローカル開発 (LocalStack) 用 AWS_ENDPOINT_URL=http://localhost:4566 AWS_ACCESS_KEY_ID=test diff --git a/Makefile b/Makefile index 5227b12..e788b27 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,12 @@ export .PHONY: help \ up down logs \ + init-sqs \ migrate migrate-down migrate-status \ dev-api dev-worker dev-web \ - docs docs-build \ + docs \ build \ - slack-setup slack-manifest \ + slack-manifest \ lint test \ setup @@ -22,6 +23,7 @@ help: @echo " make up Docker インフラ起動 (postgres + localstack)" @echo " make down Docker インフラ停止" @echo " make logs Docker ログ表示" + @echo " make init-sqs LocalStack SQS キュー作成 (up後に必要)" @echo "" @echo " DB" @echo " make migrate マイグレーション実行" @@ -33,12 +35,10 @@ help: @echo " make dev-worker TypeScript Worker 起動" @echo " make dev-web Next.js ダッシュボード起動 (port 3000)" @echo " make docs ドキュメントサーバー起動 (port 4000)" - @echo " make docs-build ドキュメントを静的ファイルにビルド" @echo "" @echo " Slack" - @echo " make slack-setup URL=https://xxxx.ngrok-free.app" - @echo " Slash Commands + Interactivity URL を一括更新" @echo " make slack-manifest manifest.json の内容を表示" + @echo " ※ Socket Mode 使用中のため ngrok / URL 設定不要" @echo "" @echo " ビルド / テスト" @echo " make build 全サービスをビルド" @@ -46,7 +46,7 @@ help: @echo " make test 全サービスのテスト" @echo "" @echo " 初回セットアップ一括" - @echo " make setup up + migrate をまとめて実行" + @echo " make setup up + init-sqs + migrate をまとめて実行" @echo "" # ----------------------------------------------- @@ -55,6 +55,9 @@ help: up: docker compose -f infra/docker/compose.yml up -d +init-sqs: + docker exec docker-localstack-1 bash /etc/localstack/init/ready.d/init-localstack.sh + down: docker compose -f infra/docker/compose.yml down @@ -83,26 +86,17 @@ dev-worker: cd apps/worker && pnpm dev dev-web: - cd apps/web && pnpm dev + cd apps/web && PORT=3000 pnpm dev # ----------------------------------------------- # ドキュメント # ----------------------------------------------- docs: - cd docs-host && pnpm dev - -docs-build: - cd docs-host && pnpm build + node docs-host/server.js # ----------------------------------------------- # Slack # ----------------------------------------------- -slack-setup: -ifndef URL - $(error URLを指定してください: make slack-setup URL=https://xxxx.ngrok-free.app) -endif - ./infra/slack/setup.sh "$(URL)" - slack-manifest: @cat infra/slack/manifest.json @@ -132,11 +126,10 @@ test: setup: up @echo "インフラ起動を待機中..." @sleep 5 + $(MAKE) init-sqs $(MAKE) migrate @echo "" @echo "セットアップ完了。次のステップ:" @echo " 1. make dev-api (別ターミナル)" @echo " 2. make dev-worker (別ターミナル)" @echo " 3. make dev-web (別ターミナル)" - @echo " 4. ngrok http 8080" - @echo " 5. make slack-setup URL=" diff --git a/apps/api/cmd/server/main.go b/apps/api/cmd/server/main.go index 8c19a79..f46fcad 100644 --- a/apps/api/cmd/server/main.go +++ b/apps/api/cmd/server/main.go @@ -36,6 +36,10 @@ func main() { log.Fatalf("sqs init: %v", err) } + // Start Slack Socket Mode in a background goroutine. + // Connects to Slack via WebSocket — no public URL required. + go slackhandler.RunSocketMode(cfg, sqlDB, sqsClient) + if !cfg.IsDev() { gin.SetMode(gin.ReleaseMode) } @@ -46,10 +50,6 @@ func main() { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) - // Slack webhook routes - r.POST("/slack/commands", slackhandler.HandleSlashCommand(cfg, sqlDB)) - r.POST("/slack/interactions", slackhandler.HandleInteraction(cfg, sqlDB, sqsClient)) - // REST API routes for the dashboard api := r.Group("/api") { diff --git a/apps/api/go.mod b/apps/api/go.mod index 14f967a..d621dff 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -1,8 +1,6 @@ module github.com/Asheze1127/HackHub/apps/api -go 1.24.0 - -toolchain go1.24.5 +go 1.25 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 @@ -39,6 +37,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -46,6 +45,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/slack-go/slack v0.21.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect diff --git a/apps/api/go.sum b/apps/api/go.sum index c2da3bb..853f378 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -62,6 +62,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -86,6 +88,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/slack-go/slack v0.21.0 h1:TAGnZYFp79LAG/oqFzYhFJ9LwEwXJ93heCkPvwjxc7o= +github.com/slack-go/slack v0.21.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE= github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/apps/api/internal/api/handler.go b/apps/api/internal/api/handler.go index 49ad09f..3b858c2 100644 --- a/apps/api/internal/api/handler.go +++ b/apps/api/internal/api/handler.go @@ -34,7 +34,11 @@ func HandleListQuestions(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, questions) + resp := make([]QuestionResponse, len(questions)) + for i, question := range questions { + resp[i] = toQuestionResponse(question) + } + c.JSON(http.StatusOK, resp) } } @@ -56,7 +60,7 @@ func HandleGetQuestion(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, question) + c.JSON(http.StatusOK, toQuestionResponse(question)) } } @@ -87,7 +91,11 @@ func HandleListProgress(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, logs) + resp := make([]ProgressLogResponse, len(logs)) + for i, log := range logs { + resp[i] = toProgressLogResponse(log) + } + c.JSON(http.StatusOK, resp) } } @@ -100,7 +108,11 @@ func HandleListTeams(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, teams) + resp := make([]TeamResponse, len(teams)) + for i, team := range teams { + resp[i] = toTeamResponse(team) + } + c.JSON(http.StatusOK, resp) } } diff --git a/apps/api/internal/api/response.go b/apps/api/internal/api/response.go new file mode 100644 index 0000000..77deb09 --- /dev/null +++ b/apps/api/internal/api/response.go @@ -0,0 +1,144 @@ +package api + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" + + "github.com/Asheze1127/HackHub/apps/api/db/sqlcgen" +) + +type QuestionResponse struct { + ID uuid.UUID `json:"id"` + TeamID *string `json:"team_id"` + SlackUserID string `json:"slack_user_id"` + SlackThreadTs *string `json:"slack_thread_ts"` + SlackChannelID string `json:"slack_channel_id"` + Title string `json:"title"` + Body *string `json:"body"` + ErrorMessage *string `json:"error_message"` + Tried *string `json:"tried"` + TriageResult *json.RawMessage `json:"triage_result"` + Category *string `json:"category"` + Confidence *float64 `json:"confidence"` + Status string `json:"status"` + EscalatedToMentor bool `json:"escalated_to_mentor"` + MentorChannelTs *string `json:"mentor_channel_ts"` + SlackEventID *string `json:"slack_event_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ProgressLogResponse struct { + ID uuid.UUID `json:"id"` + TeamID *string `json:"team_id"` + SlackUserID string `json:"slack_user_id"` + Phase string `json:"phase"` + StatusText *string `json:"status_text"` + Blockers *string `json:"blockers"` + IsSos bool `json:"is_sos"` + SlackMessageTs *string `json:"slack_message_ts"` + SlackEventID *string `json:"slack_event_id"` + CreatedAt time.Time `json:"created_at"` +} + +type TeamResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + SlackChannelID *string `json:"slack_channel_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func toQuestionResponse(q sqlcgen.Question) QuestionResponse { + r := QuestionResponse{ + ID: q.ID, + SlackUserID: q.SlackUserID, + SlackChannelID: q.SlackChannelID, + Title: q.Title, + Status: q.Status, + EscalatedToMentor: q.EscalatedToMentor, + CreatedAt: q.CreatedAt, + UpdatedAt: q.UpdatedAt, + } + if q.TeamID.Valid { + s := q.TeamID.UUID.String() + r.TeamID = &s + } + if q.SlackThreadTs.Valid { + r.SlackThreadTs = &q.SlackThreadTs.String + } + if q.Body.Valid { + r.Body = &q.Body.String + } + if q.ErrorMessage.Valid { + r.ErrorMessage = &q.ErrorMessage.String + } + if q.Tried.Valid { + r.Tried = &q.Tried.String + } + if q.TriageResult.Valid { + raw := json.RawMessage(q.TriageResult.RawMessage) + r.TriageResult = &raw + } + if q.Category.Valid { + r.Category = &q.Category.String + } + if q.Confidence.Valid { + r.Confidence = &q.Confidence.Float64 + } + if q.MentorChannelTs.Valid { + r.MentorChannelTs = &q.MentorChannelTs.String + } + if q.SlackEventID.Valid { + r.SlackEventID = &q.SlackEventID.String + } + return r +} + +func toProgressLogResponse(p sqlcgen.ProgressLog) ProgressLogResponse { + r := ProgressLogResponse{ + ID: p.ID, + SlackUserID: p.SlackUserID, + Phase: p.Phase, + IsSos: p.IsSos, + CreatedAt: p.CreatedAt, + } + if p.TeamID.Valid { + s := p.TeamID.UUID.String() + r.TeamID = &s + } + if p.StatusText.Valid { + r.StatusText = &p.StatusText.String + } + if p.Blockers.Valid { + r.Blockers = &p.Blockers.String + } + if p.SlackMessageTs.Valid { + r.SlackMessageTs = &p.SlackMessageTs.String + } + if p.SlackEventID.Valid { + r.SlackEventID = &p.SlackEventID.String + } + return r +} + +func toTeamResponse(t sqlcgen.Team) TeamResponse { + r := TeamResponse{ + ID: t.ID, + Name: t.Name, + TechStack: t.TechStack, + Phase: t.Phase, + IsSos: t.IsSos, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } + if t.SlackChannelID.Valid { + r.SlackChannelID = &t.SlackChannelID.String + } + return r +} diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index 93209ad..63dbec2 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -6,18 +6,19 @@ import ( ) type Config struct { - Port string - Env string - DatabaseURL string - SlackBotToken string - SlackSigningSecret string - SlackMentorChannel string + Port string + Env string + DatabaseURL string + SlackBotToken string + SlackAppToken string + SlackSigningSecret string + SlackMentorChannel string SlackProgressChannel string - AWSRegion string - AWSEndpointURL string - SQSQuestionNewURL string - SQSFollowupURL string - SQSProgressURL string + AWSRegion string + AWSEndpointURL string + SQSQuestionNewURL string + SQSFollowupURL string + SQSProgressURL string } func Load() (*Config, error) { @@ -26,10 +27,11 @@ func Load() (*Config, error) { Env: getEnv("ENV", "development"), DatabaseURL: os.Getenv("DATABASE_URL"), SlackBotToken: os.Getenv("SLACK_BOT_TOKEN"), + SlackAppToken: os.Getenv("SLACK_APP_TOKEN"), SlackSigningSecret: os.Getenv("SLACK_SIGNING_SECRET"), SlackMentorChannel: os.Getenv("SLACK_MENTOR_CHANNEL_ID"), SlackProgressChannel: os.Getenv("SLACK_PROGRESS_CHANNEL_ID"), - AWSRegion: getEnv("AWS_REGION", "ap-northeast-1"), + AWSRegion: getEnv("AWS_REGION", "us-east-1"), AWSEndpointURL: os.Getenv("AWS_ENDPOINT_URL"), SQSQuestionNewURL: os.Getenv("SQS_QUESTION_NEW_URL"), SQSFollowupURL: os.Getenv("SQS_QUESTION_FOLLOWUP_URL"), @@ -38,8 +40,11 @@ func Load() (*Config, error) { if cfg.DatabaseURL == "" { return nil, fmt.Errorf("DATABASE_URL is required") } - if cfg.SlackSigningSecret == "" { - return nil, fmt.Errorf("SLACK_SIGNING_SECRET is required") + if cfg.SlackBotToken == "" { + return nil, fmt.Errorf("SLACK_BOT_TOKEN is required") + } + if cfg.SlackAppToken == "" { + return nil, fmt.Errorf("SLACK_APP_TOKEN is required (xapp-... Socket Mode token)") } return cfg, nil } @@ -54,4 +59,3 @@ func getEnv(key, fallback string) string { } return fallback } - diff --git a/apps/api/internal/slack/modal.go b/apps/api/internal/slack/modal.go index 6b4b01c..fd04ca7 100644 --- a/apps/api/internal/slack/modal.go +++ b/apps/api/internal/slack/modal.go @@ -85,6 +85,23 @@ func OpenProgressModal(botToken, triggerID, channelID string) error { return callViewsOpen(botToken, triggerID, modal) } +// OpenMentorReplyModal opens the mentor reply modal via views.open. +// metadata is stored in private_metadata (format: channelId|threadTs|userId). +func OpenMentorReplyModal(botToken, triggerID, metadata string) error { + modal := map[string]any{ + "type": "modal", + "callback_id": "mentor_reply_modal", + "private_metadata": metadata, + "title": plainText("回答を入力"), + "submit": plainText("送信(匿名)"), + "close": plainText("キャンセル"), + "blocks": []map[string]any{ + textareaBlock("reply_block", "reply", "回答内容", "参加者への回答を入力してください", false), + }, + } + return callViewsOpen(botToken, triggerID, modal) +} + func callViewsOpen(botToken, triggerID string, view map[string]any) error { payload := map[string]any{ "trigger_id": triggerID, diff --git a/apps/api/internal/slack/socketmode.go b/apps/api/internal/slack/socketmode.go new file mode 100644 index 0000000..62e5b33 --- /dev/null +++ b/apps/api/internal/slack/socketmode.go @@ -0,0 +1,477 @@ +package slack + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + + goslack "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" + + "github.com/Asheze1127/HackHub/apps/api/internal/config" + "github.com/Asheze1127/HackHub/apps/api/internal/sqs" +) + +// RunSocketMode connects to Slack via WebSocket and handles slash commands +// and view submissions. It blocks until the connection is closed. +func RunSocketMode(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender) { + api := goslack.New( + cfg.SlackBotToken, + goslack.OptionAppLevelToken(cfg.SlackAppToken), + ) + client := socketmode.New(api, + socketmode.OptionLog(log.New(log.Writer(), "[socketmode] ", log.LstdFlags)), + ) + + go func() { + for evt := range client.Events { + switch evt.Type { + case socketmode.EventTypeSlashCommand: + cmd, ok := evt.Data.(goslack.SlashCommand) + if !ok { + client.Ack(*evt.Request) + continue + } + onSlashCommand(cfg, client, &evt, &cmd) + + case socketmode.EventTypeInteractive: + callback, ok := evt.Data.(goslack.InteractionCallback) + if !ok { + client.Ack(*evt.Request) + continue + } + onInteraction(cfg, db, sqsClient, client, &evt, &callback) + + case socketmode.EventTypeEventsAPI: + eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) + client.Ack(*evt.Request) + if !ok { + continue + } + onEventsAPI(cfg, db, sqsClient, &eventsAPIEvent) + + case socketmode.EventTypeConnecting: + log.Println("[socketmode] connecting to Slack...") + case socketmode.EventTypeConnected: + log.Println("[socketmode] connected") + } + } + }() + + if err := client.Run(); err != nil { + log.Printf("[socketmode] disconnected: %v", err) + } +} + +// onSlashCommand opens the appropriate modal. Must complete within 3 seconds. +func onSlashCommand(cfg *config.Config, client *socketmode.Client, evt *socketmode.Event, cmd *goslack.SlashCommand) { + var err error + switch cmd.Command { + case "/question": + err = OpenQuestionModal(cfg.SlackBotToken, cmd.TriggerID, cmd.ChannelID) + case "/progress": + err = OpenProgressModal(cfg.SlackBotToken, cmd.TriggerID, cmd.ChannelID) + default: + client.Ack(*evt.Request) + return + } + + if err != nil { + log.Printf("[socketmode] open modal failed (%s): %v", cmd.Command, err) + client.Ack(*evt.Request, map[string]interface{}{ + "response_type": "ephemeral", + "text": "モーダルを開けませんでした。しばらく待ってから再試行してください。", + }) + return + } + client.Ack(*evt.Request) +} + +// onInteraction handles interactive callbacks (block_actions and view_submission). +func onInteraction(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender, client *socketmode.Client, evt *socketmode.Event, cb *goslack.InteractionCallback) { + // Handle block_actions (button clicks) + if cb.Type == goslack.InteractionTypeBlockActions { + for _, action := range cb.ActionCallback.BlockActions { + switch action.ActionID { + case "mentor_reply": + if err := OpenMentorReplyModal(cfg.SlackBotToken, cb.TriggerID, action.Value); err != nil { + log.Printf("[socketmode] open mentor reply modal failed: %v", err) + } + case "question_resolved": + onQuestionResolved(cfg, db, action.Value) + case "question_not_resolved": + onQuestionNotResolved(cfg, db, action.Value) + } + } + client.Ack(*evt.Request) + return + } + + if cb.Type != goslack.InteractionTypeViewSubmission { + client.Ack(*evt.Request) + return + } + + // Handle mentor_reply_modal submission + if cb.View.CallbackID == "mentor_reply_modal" { + parts := strings.SplitN(cb.View.PrivateMetadata, "|", 3) + if len(parts) != 3 { + log.Printf("[socketmode] invalid mentor_reply_modal metadata: %s", cb.View.PrivateMetadata) + client.Ack(*evt.Request) + return + } + channelID, threadTs, userID := parts[0], parts[1], parts[2] + reply := blockVal(cb.View.State.Values, "reply_block", "reply") + text := fmt.Sprintf("<@%s> メンターより:\n%s", userID, reply) + if err := callChatPostMessage(cfg.SlackBotToken, channelID, threadTs, text); err != nil { + log.Printf("[socketmode] mentor reply post failed: %v", err) + } + client.Ack(*evt.Request, map[string]interface{}{"response_action": "clear"}) + return + } + + // Idempotency: view.id is unique per modal open. + inserted, err := insertEventIfNew(context.Background(), db, cb.View.ID, "view_submission") + if err != nil { + log.Printf("[socketmode] idempotency db error: %v", err) + client.Ack(*evt.Request) + return + } + if !inserted { + client.Ack(*evt.Request) + return + } + + var enqueueErr error + switch cb.View.CallbackID { + case "question_modal": + enqueueErr = enqueueQuestion(sqsClient, cb) + case "progress_modal": + enqueueErr = enqueueProgress(sqsClient, cb) + default: + log.Printf("[socketmode] unknown callback_id: %s", cb.View.CallbackID) + client.Ack(*evt.Request) + return + } + + if enqueueErr != nil { + log.Printf("[socketmode] enqueue failed for %s: %v", cb.View.CallbackID, enqueueErr) + client.Ack(*evt.Request, map[string]interface{}{ + "response_action": "errors", + "errors": map[string]string{ + "title_block": "送信に失敗しました。しばらく待ってから再試行してください。", + }, + }) + return + } + + client.Ack(*evt.Request, map[string]interface{}{"response_action": "clear"}) +} + +// onQuestionResolved handles the "✅ 解決した" button. +// value format: "questionId|channelId|threadTs|userId" +func onQuestionResolved(cfg *config.Config, db *sql.DB, value string) { + parts := strings.SplitN(value, "|", 4) + if len(parts) != 4 { + log.Printf("[socketmode] invalid question_resolved value: %s", value) + return + } + questionID, channelID, threadTs, userID := parts[0], parts[1], parts[2], parts[3] + + if _, err := db.ExecContext(context.Background(), + `UPDATE questions SET status = 'resolved', updated_at = NOW() WHERE id = $1`, + questionID, + ); err != nil { + log.Printf("[socketmode] mark resolved failed: %v", err) + } + + text := fmt.Sprintf("<@%s> ✅ 解決済みとしてマークしました。お役に立てて良かったです!", userID) + if err := callChatPostMessage(cfg.SlackBotToken, channelID, threadTs, text); err != nil { + log.Printf("[socketmode] post resolved message failed: %v", err) + } +} + +// onQuestionNotResolved handles the "🆘 まだ解決していない" button. +// value format: "questionId|channelId|threadTs|userId" +func onQuestionNotResolved(cfg *config.Config, db *sql.DB, value string) { + parts := strings.SplitN(value, "|", 4) + if len(parts) != 4 { + log.Printf("[socketmode] invalid question_not_resolved value: %s", value) + return + } + questionID, channelID, threadTs, userID := parts[0], parts[1], parts[2], parts[3] + + // Fetch question title for escalation message. + var title string + if err := db.QueryRowContext(context.Background(), + `SELECT title FROM questions WHERE id = $1`, questionID, + ).Scan(&title); err != nil { + log.Printf("[socketmode] fetch question title failed: %v", err) + title = "(タイトル不明)" + } + + // Update status to escalated. + if _, err := db.ExecContext(context.Background(), + `UPDATE questions SET status = 'escalated', escalated_to_mentor = TRUE, updated_at = NOW() WHERE id = $1`, + questionID, + ); err != nil { + log.Printf("[socketmode] mark escalated failed: %v", err) + } + + // Notify user in thread. + userText := fmt.Sprintf("<@%s> メンターに転送しました。しばらくお待ちください 🙏", userID) + if err := callChatPostMessage(cfg.SlackBotToken, channelID, threadTs, userText); err != nil { + log.Printf("[socketmode] post not-resolved message failed: %v", err) + } + + // Post to mentor channel with reply button. + mentorChannel := cfg.SlackMentorChannel + if mentorChannel == "" { + log.Printf("[socketmode] SLACK_MENTOR_CHANNEL_ID not set; skipping mentor escalation") + return + } + threadURL := fmt.Sprintf("https://slack.com/archives/%s/p%s", channelID, strings.ReplaceAll(threadTs, ".", "")) + mentorText := fmt.Sprintf("[ユーザー未解決] %s", title) + mentorBlocks := buildMentorEscalationBlocks(title, channelID, threadTs, userID, threadURL) + if err := callChatPostMessageWithBlocks(cfg.SlackBotToken, mentorChannel, "", mentorText, mentorBlocks); err != nil { + log.Printf("[socketmode] post mentor escalation failed: %v", err) + } +} + +// buildMentorEscalationBlocks builds the Block Kit payload for mentor channel escalation. +func buildMentorEscalationBlocks(title, channelID, threadTs, userID, threadURL string) []map[string]any { + return []map[string]any{ + { + "type": "section", + "text": map[string]any{ + "type": "mrkdwn", + "text": fmt.Sprintf("*[ユーザー未解決] %s*\n元スレッド: %s", title, threadURL), + }, + }, + { + "type": "actions", + "elements": []map[string]any{ + { + "type": "button", + "text": map[string]any{"type": "plain_text", "text": "回答する", "emoji": true}, + "action_id": "mentor_reply", + "value": fmt.Sprintf("%s|%s|%s", channelID, threadTs, userID), + }, + }, + }, + } +} + +// callChatPostMessageWithBlocks posts a Block Kit message to a Slack channel. +func callChatPostMessageWithBlocks(botToken, channelID, threadTs, text string, blocks []map[string]any) error { + payload := map[string]any{ + "channel": channelID, + "text": text, + "blocks": blocks, + } + if threadTs != "" { + payload["thread_ts"] = threadTs + } + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal chat.postMessage payload: %w", err) + } + req, err := http.NewRequest(http.MethodPost, slackAPIBase+"/chat.postMessage", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create chat.postMessage request: %w", err) + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+botToken) + + resp, err := slackHTTPClient.Do(req) + if err != nil { + return fmt.Errorf("chat.postMessage http call: %w", err) + } + defer resp.Body.Close() + + var result struct { + OK bool `json:"ok"` + Error string `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("decode chat.postMessage response: %w", err) + } + if !result.OK { + return fmt.Errorf("chat.postMessage error: %s", result.Error) + } + return nil +} + +// callChatPostMessage posts a message to a Slack channel/thread via the Web API. +func callChatPostMessage(botToken, channelID, threadTs, text string) error { + payload := map[string]any{ + "channel": channelID, + "thread_ts": threadTs, + "text": text, + } + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal chat.postMessage payload: %w", err) + } + req, err := http.NewRequest(http.MethodPost, slackAPIBase+"/chat.postMessage", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create chat.postMessage request: %w", err) + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+botToken) + + resp, err := slackHTTPClient.Do(req) + if err != nil { + return fmt.Errorf("chat.postMessage http call: %w", err) + } + defer resp.Body.Close() + + var result struct { + OK bool `json:"ok"` + Error string `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("decode chat.postMessage response: %w", err) + } + if !result.OK { + return fmt.Errorf("chat.postMessage error: %s", result.Error) + } + return nil +} + +// onEventsAPI handles Events API callbacks (e.g. message events). +func onEventsAPI(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender, event *slackevents.EventsAPIEvent) { + if event.InnerEvent.Type != "message" { + return + } + msg, ok := event.InnerEvent.Data.(*slackevents.MessageEvent) + if !ok { + return + } + // Skip bot messages, edited messages, etc. + if msg.BotID != "" || msg.SubType != "" { + return + } + // Only process thread replies (not top-level messages) + if msg.ThreadTimeStamp == "" { + return + } + // Skip the thread-parent itself + if msg.ThreadTimeStamp == msg.TimeStamp { + return + } + + questionID, userID, title, found, err := findQuestionByThread(context.Background(), db, msg.Channel, msg.ThreadTimeStamp) + if err != nil { + log.Printf("[socketmode] findQuestionByThread error: %v", err) + return + } + if !found { + return + } + + followup := questionFollowupMessage{ + QuestionID: questionID, + ChannelID: msg.Channel, + ThreadTs: msg.ThreadTimeStamp, + UserID: userID, + ReplyText: msg.Text, + OriginalTitle: title, + } + body, err := json.Marshal(followup) + if err != nil { + log.Printf("[socketmode] marshal followup message: %v", err) + return + } + if err := sqsClient.Enqueue(context.Background(), sqsClient.QuestionFollowupURL(), string(body)); err != nil { + log.Printf("[socketmode] enqueue followup failed: %v", err) + } +} + +// findQuestionByThread looks up a question by channel + thread_ts. +func findQuestionByThread(ctx context.Context, db *sql.DB, channelID, threadTs string) (questionID, userID, title string, found bool, err error) { + row := db.QueryRowContext(ctx, + `SELECT id, slack_user_id, title FROM questions WHERE slack_channel_id = $1 AND slack_thread_ts = $2 LIMIT 1`, + channelID, threadTs, + ) + if err = row.Scan(&questionID, &userID, &title); err != nil { + if err == sql.ErrNoRows { + return "", "", "", false, nil + } + return "", "", "", false, err + } + return questionID, userID, title, true, nil +} + +// questionFollowupMessage is the SQS payload for question followups. +type questionFollowupMessage struct { + QuestionID string `json:"question_id"` + ChannelID string `json:"channel_id"` + ThreadTs string `json:"thread_ts"` + UserID string `json:"user_id"` + ReplyText string `json:"reply_text"` + OriginalTitle string `json:"original_title"` +} + +func enqueueQuestion(sqsClient sqs.Sender, cb *goslack.InteractionCallback) error { + vals := cb.View.State.Values + msg := questionNewMessage{ + UserID: cb.User.ID, + TeamID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Title: blockVal(vals, "title_block", "title"), + Body: blockVal(vals, "body_block", "body"), + ErrorMessage: blockVal(vals, "error_block", "error_message"), + Tried: blockVal(vals, "tried_block", "tried"), + } + body, err := json.Marshal(msg) + if err != nil { + return err + } + return sqsClient.Enqueue(context.Background(), sqsClient.QuestionNewURL(), string(body)) +} + +func enqueueProgress(sqsClient sqs.Sender, cb *goslack.InteractionCallback) error { + vals := cb.View.State.Values + sosChecked := len(vals["sos_block"]["sos_flag"].SelectedOptions) > 0 + msg := progressMessage{ + UserID: cb.User.ID, + TeamID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Phase: blockSelectedVal(vals, "phase_block", "phase"), + CurrentStatus: blockVal(vals, "status_block", "current_status"), + Blockers: blockVal(vals, "blockers_block", "blockers"), + SOSFlag: sosChecked, + } + body, err := json.Marshal(msg) + if err != nil { + return err + } + return sqsClient.Enqueue(context.Background(), sqsClient.ProgressURL(), string(body)) +} + +// blockVal extracts a plain_text_input value from slack-go's ViewState. +func blockVal(vals map[string]map[string]goslack.BlockAction, blockID, actionID string) string { + if b, ok := vals[blockID]; ok { + if a, ok := b[actionID]; ok { + return a.Value + } + } + return "" +} + +// blockSelectedVal extracts a static_select value from slack-go's ViewState. +func blockSelectedVal(vals map[string]map[string]goslack.BlockAction, blockID, actionID string) string { + if b, ok := vals[blockID]; ok { + if a, ok := b[actionID]; ok && a.SelectedOption.Value != "" { + return a.SelectedOption.Value + } + } + return "" +} diff --git a/apps/worker/src/ai/triage.ts b/apps/worker/src/ai/triage.ts index a008af2..2965e61 100644 --- a/apps/worker/src/ai/triage.ts +++ b/apps/worker/src/ai/triage.ts @@ -22,11 +22,52 @@ interface KnowledgeContext { const TRIAGE_PROMPT_TEMPLATE = `あなたは KCL ハッカソンの質問受付担当です。 参加者の質問を受け取り、指定された JSON 形式で分析結果を返してください。 -【重要な制約】 -- 断定的な技術回答をしないこと。あなたは「受付係」です -- 不確かな場合は needs_more_info を true にして情報を求めること -- short_answer_for_user は 500 文字以内、日本語で -- AI による誤答の可能性がある旨を必要に応じて伝えること +【役割】 +- あなたは「問題切り分けを補助する受付係」です +- 解決に必要な情報が十分なら、まず短く一次回答してください +- 情報が不足しているなら、診断に本当に必要な情報だけを求めてください + +【needs_more_info の判断基準 — 最重要】 +needs_more_info は「次の回答や切り分けに進むために不可欠な事実が足りない場合のみ」true にすること。 +以下の場合は needs_more_info を false にして回答すること: +- 概念・仕組みの説明系の質問(例:「Next.js と React の違いは?」「JWT とは何か?」) +- ベストプラクティス・設計方針・責務分担の質問 +- 一般的な how-to、比較、用語説明 +- 与えられた情報だけで一次回答できる質問 +needs_more_info を true にするのは、たとえば以下のような場合だけです: +- 実際のエラーメッセージがない +- 実行したコマンドや再現手順がない +- 問題が起きているファイル、設定、コード箇所が分からない +- バージョンや実行環境が不明で切り分けできない + +【missing_info の書き方 — 最重要】 +- missing_info は「ユーザーが次の 1 通で返せる具体的な事実・資料名」だけを書くこと +- missing_info に書くのは「何を送ってほしいか」であり、「何を説明してほしいか」ではない +- 抽象的な説明要求は禁止 +- 各項目は短い名詞句にすること +- 1項目につき1情報だけにすること +- 最大 3 件までにすること +- すでに質問文に含まれている情報は要求しないこと +- needs_more_info が false のとき missing_info は必ず空配列 [] にすること + +良い missing_info の例: +- 実際のエラーメッセージ全文 +- 実行したコマンド +- 問題が起きているコードの該当関数 +- package.json の scripts 部分 +- 使用している Node.js のバージョン + +悪い missing_info の例: +- DDD の Utility 設計に関する詳細な説明 +- クリーンアーキテクチャにおける Utility の役割 +- もう少し詳しく +- 関連情報 +- 背景知識の説明 + +【short_answer_for_user の書き方】 +あなたは「受付の一次回答」として、知っている範囲で回答を提供してからフォローアップを聞く形にすること。 +例:「〜です。(知識を簡潔に回答) 追加で〜を教えてもらえると、さらに詳しいアドバイスができます。」 +500 文字以内、日本語で。AI による誤答の可能性がある旨を必要に応じて伝えること。 【チーム情報】 チーム名: {team_name} @@ -80,12 +121,13 @@ async function triageWithProvider( apiKey: string, model: string, ): Promise { - const provider = createOpenAI({ baseURL, apiKey }) + const provider = createOpenAI({ baseURL, apiKey, compatibility: "compatible" }) const { object } = await generateObject({ model: provider(model), schema: QuestionTriageResultSchema, prompt, maxTokens: 1024, + mode: "json", }) return object } diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 1ea531a..3e60935 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -1,7 +1,11 @@ -import "dotenv/config" +import dotenv from "dotenv" +import path from "path" +// モノレポルートの .env を優先して読む(apps/worker/.env があればそちらも読む) +dotenv.config({ path: path.resolve(__dirname, "../../../.env") }) +dotenv.config() import { checkConnection, closePool } from "./db/client" import { startConsumer } from "./queue/consumer" -import { handleQuestionNew, type QuestionNewMessage } from "./workers/question" +import { handleQuestionNew, handleQuestionFollowup, type QuestionNewMessage, type QuestionFollowupMessage } from "./workers/question" import { handleProgress, type ProgressMessage } from "./workers/progress" function requireEnv(name: string): string { @@ -36,8 +40,8 @@ async function main(): Promise { }, signal), startConsumer(QUEUES.questionFollowup, async (body) => { - console.log("[worker] question:followup", JSON.stringify(body)) - // TODO: gh-19 Phase 3 — FollowupWorker + console.log("[worker] question:followup received") + await handleQuestionFollowup(body as QuestionFollowupMessage) }, signal), startConsumer(QUEUES.progress, async (body) => { diff --git a/apps/worker/src/slack/client.ts b/apps/worker/src/slack/client.ts index e62bb2f..21e6b87 100644 --- a/apps/worker/src/slack/client.ts +++ b/apps/worker/src/slack/client.ts @@ -46,6 +46,22 @@ export async function postMessage(channelId: string, text: string): Promise { + const data = await callSlack("chat.postMessage", { + channel: channelId, + text, + blocks, + }) + if (!data.ts) throw new Error("chat.postMessage (blocks) returned no ts") + return data.ts +} + // postThreadReply sends a threaded reply to an existing message. export async function postThreadReply( channelId: string, @@ -60,3 +76,20 @@ export async function postThreadReply( if (!data.ts) throw new Error("chat.postMessage (thread) returned no ts") return data.ts } + +// postThreadReplyWithBlocks sends a threaded reply with Block Kit blocks. +export async function postThreadReplyWithBlocks( + channelId: string, + threadTs: string, + text: string, + blocks: unknown[], +): Promise { + const data = await callSlack("chat.postMessage", { + channel: channelId, + thread_ts: threadTs, + text, + blocks, + }) + if (!data.ts) throw new Error("chat.postMessage (thread blocks) returned no ts") + return data.ts +} diff --git a/apps/worker/src/workers/question.ts b/apps/worker/src/workers/question.ts index a204653..cf0956f 100644 --- a/apps/worker/src/workers/question.ts +++ b/apps/worker/src/workers/question.ts @@ -1,6 +1,6 @@ import { triageQuestion, type QuestionInput } from "../ai/triage" import { insertQuestion, updateQuestionTriage, markEscalated } from "../db/questions" -import { postMessage, postThreadReply } from "../slack/client" +import { postMessage, postMessageWithBlocks, postThreadReply, postThreadReplyWithBlocks } from "../slack/client" export interface QuestionNewMessage { user_id: string @@ -59,19 +59,17 @@ export async function handleQuestionNew(msg: QuestionNewMessage): Promise status, }) - // Post Slack thread reply. - if (triage.needs_more_info) { - await postThreadReply(msg.channel_id, threadTs, buildAskMoreInfoReply(triage.missing_info)) - } else { - await postThreadReply(msg.channel_id, threadTs, buildAnswerReply(triage)) - } + // Post Slack thread reply with resolution buttons. + const replyText = buildReply(triage) + const replyBlocks = buildReplyBlocks(replyText, row.id, msg.channel_id, threadTs, msg.user_id) + await postThreadReplyWithBlocks(msg.channel_id, threadTs, replyText, replyBlocks) // Escalate to mentor channel if flagged. if (triage.should_escalate_to_mentor) { const mentorChannel = getMentorChannel() if (mentorChannel) { - const mentorText = buildMentorEscalationText(msg, triage, msg.channel_id, threadTs) - const mentorTs = await postMessage(mentorChannel, mentorText) + const { text, blocks } = buildMentorEscalationBlocks(msg, triage, msg.channel_id, threadTs, msg.user_id) + const mentorTs = await postMessageWithBlocks(mentorChannel, text, blocks) await markEscalated(row.id, mentorTs) } else { // Mentor channel not configured — log and skip escalation. @@ -81,6 +79,37 @@ export async function handleQuestionNew(msg: QuestionNewMessage): Promise } } +export interface QuestionFollowupMessage { + question_id: string + channel_id: string + thread_ts: string + user_id: string + reply_text: string + original_title: string +} + +// handleQuestionFollowup processes a follow-up reply in a question thread. +export async function handleQuestionFollowup(msg: QuestionFollowupMessage): Promise { + const input: QuestionInput = { + title: msg.original_title, + body: msg.reply_text, + } + const triage = await triageQuestion(input) + + const status = triage.should_escalate_to_mentor ? "escalated" : "answered" + await updateQuestionTriage({ + id: msg.question_id, + triage_result: triage, + category: triage.category, + confidence: triage.confidence, + status, + }) + + const replyText = buildReply(triage) + const replyBlocks = buildReplyBlocks(replyText, msg.question_id, msg.channel_id, msg.thread_ts, msg.user_id) + await postThreadReplyWithBlocks(msg.channel_id, msg.thread_ts, replyText, replyBlocks) +} + // buildQuestionText formats the initial channel message. function buildQuestionText(msg: QuestionNewMessage): string { const lines = [`*[質問] ${msg.title}*`, `<@${msg.user_id}> からの質問です。`] @@ -90,41 +119,85 @@ function buildQuestionText(msg: QuestionNewMessage): string { return lines.join("\n") } -// buildAskMoreInfoReply formats the "please provide more information" reply. -function buildAskMoreInfoReply(missingInfo: string[]): string { - const items = missingInfo.length > 0 - ? missingInfo.map((i) => `• ${i}`).join("\n") - : "• もう少し詳しい情報" - return [ - "ありがとうございます!質問を受け取りました 🔍", - "", - "確認させてください:", - items, - "", - "上記を教えていただけると、より正確にサポートできます。", - "このスレッドに返信してください!", - ].join("\n") -} - -// buildAnswerReply formats the AI first-response reply. -function buildAnswerReply(triage: Awaited>): string { +// buildReply formats the AI reply — 常に回答を先に出し、追加情報が必要なら末尾に添える。 +function buildReply(triage: Awaited>): string { const note = triage.confidence >= 0.8 ? "" : "\n\n_※ AIによる一次回答です。内容に誤りがある可能性があります。_" - return `${triage.short_answer_for_user}${note}` + const followup = triage.needs_more_info && triage.missing_info.length > 0 + ? "\n\n追加で教えてもらえると助かります:\n" + triage.missing_info.map((i) => `• ${i}`).join("\n") + : "" + return `${triage.short_answer_for_user}${note}${followup}` } -// buildMentorEscalationText formats the mentor channel escalation message. -function buildMentorEscalationText( +// buildReplyBlocks wraps the AI reply text with resolution buttons. +// button value: "questionId|channelId|threadTs|userId" +function buildReplyBlocks( + text: string, + questionId: string, + channelId: string, + threadTs: string, + userId: string, +): unknown[] { + const value = `${questionId}|${channelId}|${threadTs}|${userId}` + return [ + { type: "section", text: { type: "mrkdwn", text } }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "✅ 解決した", emoji: true }, + style: "primary", + action_id: "question_resolved", + value, + }, + { + type: "button", + text: { type: "plain_text", text: "🆘 まだ解決していない", emoji: true }, + style: "danger", + action_id: "question_not_resolved", + value, + }, + ], + }, + ] +} + +// buildMentorEscalationBlocks formats the mentor channel escalation message as Block Kit. +function buildMentorEscalationBlocks( msg: QuestionNewMessage, triage: Awaited>, channelId: string, threadTs: string, -): string { - return [ - `*[メンター転送] ${msg.title}*`, - `カテゴリ: \`${triage.category}\``, - `AI 判定理由: ${triage.escalation_reason ?? "信頼度が低いため"}`, - `元スレッド: https://slack.com/archives/${channelId}/p${threadTs.replace(".", "")}`, - ].join("\n") + userId: string, +): { text: string; blocks: unknown[] } { + const threadUrl = `https://slack.com/archives/${channelId}/p${threadTs.replace(".", "")}` + const text = `[メンター転送] ${msg.title}` + const blocks: unknown[] = [ + { + type: "section", + text: { + type: "mrkdwn", + text: [ + `*[メンター転送] ${msg.title}*`, + `カテゴリ: \`${triage.category}\``, + `AI 判定理由: ${triage.escalation_reason ?? "信頼度が低いため"}`, + `元スレッド: ${threadUrl}`, + ].join("\n"), + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "回答する", emoji: true }, + action_id: "mentor_reply", + value: `${channelId}|${threadTs}|${userId}`, + }, + ], + }, + ] + return { text, blocks } } diff --git a/docs-host/.vitepress/config.mts b/docs-host/.vitepress/config.mts deleted file mode 100644 index 628feb9..0000000 --- a/docs-host/.vitepress/config.mts +++ /dev/null @@ -1,107 +0,0 @@ -import { defineConfig } from "vitepress" -import { withMermaid } from "vitepress-plugin-mermaid" - -export default withMermaid( - defineConfig({ - srcDir: "../docs", - title: "KCL Support Hub", - description: "ハッカソン参加者向け AI サポートシステム", - lang: "ja", - - themeConfig: { - logo: "/logo.svg", - siteTitle: "KCL Support Hub", - - nav: [ - { text: "はじめに", link: "/specification" }, - { text: "セットアップ", link: "/setup/" }, - { text: "詳細設計", link: "/detail/01_product-definition" }, - ], - - sidebar: { - "/setup/": [ - { - text: "セットアップ", - items: [ - { text: "概要", link: "/setup/" }, - { text: "Slack App", link: "/setup/slack" }, - { text: "ローカル開発", link: "/setup/local" }, - { text: "自前サーバー (VPS)", link: "/setup/own" }, - { text: "クラウド (AWS)", link: "/setup/remote" }, - ], - }, - ], - "/detail/": [ - { - text: "詳細設計", - items: [ - { text: "01 プロダクト定義", link: "/detail/01_product-definition" }, - { text: "02 アーキテクチャ", link: "/detail/02_architecture" }, - { text: "03 AI フロー", link: "/detail/03_ai-flow" }, - { text: "04 データモデル", link: "/detail/04_data-models" }, - { text: "05 実装フェーズ", link: "/detail/05_implementation-phases" }, - { text: "06 ディレクトリ構成", link: "/detail/06_directory" }, - { text: "07 環境変数", link: "/detail/07_env-vars" }, - { text: "08 実装タスク", link: "/detail/08_implementation-tasks" }, - { text: "09 最初の PR", link: "/detail/09_first-pr" }, - ], - }, - ], - "/": [ - { - text: "概要", - items: [ - { text: "設計仕様", link: "/specification" }, - { text: "AI セットアップ", link: "/ai-setup" }, - { text: "Slack セットアップ", link: "/slack-setup" }, - ], - }, - { - text: "セットアップ", - items: [ - { text: "概要", link: "/setup/" }, - { text: "Slack App", link: "/setup/slack" }, - { text: "ローカル開発", link: "/setup/local" }, - { text: "自前サーバー (VPS)", link: "/setup/own" }, - { text: "クラウド (AWS)", link: "/setup/remote" }, - ], - }, - { - text: "詳細設計", - items: [ - { text: "01 プロダクト定義", link: "/detail/01_product-definition" }, - { text: "02 アーキテクチャ", link: "/detail/02_architecture" }, - { text: "03 AI フロー", link: "/detail/03_ai-flow" }, - { text: "04 データモデル", link: "/detail/04_data-models" }, - { text: "05 実装フェーズ", link: "/detail/05_implementation-phases" }, - { text: "06 ディレクトリ構成", link: "/detail/06_directory" }, - { text: "07 環境変数", link: "/detail/07_env-vars" }, - { text: "08 実装タスク", link: "/detail/08_implementation-tasks" }, - { text: "09 最初の PR", link: "/detail/09_first-pr" }, - ], - }, - ], - }, - - socialLinks: [ - { icon: "github", link: "https://github.com/Asheze1127/HackHub" }, - ], - - search: { - provider: "local", - }, - - footer: { - message: "KCL Support Hub", - }, - }, - - markdown: { - lineNumbers: true, - }, - - mermaid: { - theme: "default", - }, - }) -) diff --git a/docs-host/index.html b/docs-host/index.html new file mode 100644 index 0000000..411d76e --- /dev/null +++ b/docs-host/index.html @@ -0,0 +1,63 @@ + + + + + + KCL Support Hub Docs + + + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/docs-host/package.json b/docs-host/package.json index d6a4c06..b6b92db 100644 --- a/docs-host/package.json +++ b/docs-host/package.json @@ -2,13 +2,6 @@ "name": "@kcl/docs", "private": true, "scripts": { - "dev": "vitepress dev --port 4000", - "build": "vitepress build", - "preview": "vitepress preview --port 4000" - }, - "dependencies": { - "mermaid": "^11.4.1", - "vitepress": "^1.6.3", - "vitepress-plugin-mermaid": "^2.0.17" + "dev": "node server.js" } } diff --git a/docs-host/server.js b/docs-host/server.js new file mode 100644 index 0000000..c271554 --- /dev/null +++ b/docs-host/server.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +// docs-host/server.js — KCL Support Hub ドキュメントサーバー +// プロジェクトルートを静的ファイルとして配信します +// docsify は /docs/*.md を AJAX で取得するため、ルートからの配信が必要です + +const http = require("http") +const fs = require("fs") +const path = require("path") + +const PORT = process.env.PORT || 4000 +const PROJECT_ROOT = path.join(__dirname, "..") + +const MIME = { + ".html": "text/html; charset=utf-8", + ".md": "text/plain; charset=utf-8", + ".js": "application/javascript", + ".css": "text/css", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", +} + +const server = http.createServer((req, res) => { + let urlPath = req.url.split("?")[0] + + // / → docs-host/index.html + if (urlPath === "/" || urlPath === "") { + urlPath = "/docs-host/index.html" + } + + const filePath = path.join(PROJECT_ROOT, urlPath) + + // パストラバーサル防止 + if (!filePath.startsWith(PROJECT_ROOT)) { + res.writeHead(403) + res.end("Forbidden") + return + } + + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404) + res.end(`Not found: ${urlPath}`) + return + } + + const ext = path.extname(filePath) + res.writeHead(200, { + "Content-Type": MIME[ext] || "application/octet-stream", + "Cache-Control": "no-cache", + }) + res.end(data) + }) +}) + +server.listen(PORT, () => { + console.log("") + console.log(" KCL Support Hub — ドキュメントサーバー") + console.log("") + console.log(` http://localhost:${PORT}`) + console.log("") + console.log(" Ctrl+C で停止") +}) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..baf7f46 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,56 @@ +# KCL Support Hub + +> ハッカソン参加者向け AI サポートシステム + +Slack 中心・AI 一次対応・進捗可視化システムのドキュメントです。 + +--- + +## はじめに + +| ドキュメント | 説明 | +|---|---| +| [設計仕様](/specification) | システム全体の仕様と要件定義 | +| [AI セットアップ](/ai-setup) | Bonsai / Ollama の設定方法 | + +## セットアップ + +| ガイド | 対象 | +|---|---| +| [Slack App](/setup/slack) | Slack App の作成・設定(全パターン共通) | +| [ローカル開発](/setup/local) | 開発・動作確認 | +| [自前サーバー (VPS)](/setup/own) | ハッカソン当日・社内 VPS | +| [クラウド (AWS)](/setup/remote) | 本番・長期運用 | + +## クイックスタート + +```bash +# 1. .env を設定 +cp .env.example .env && nano .env + +# 2. インフラ + DB +make setup + +# 3. 各サービス起動 +make dev-api # ターミナル 1 +make dev-worker # ターミナル 2 +make dev-web # ターミナル 3 + +# 4. Slack に公開 +ngrok http 8080 +make slack-setup URL=https://xxxx.ngrok-free.app +``` + +## システム構成 + +```mermaid +graph TB + User["参加者 Slack"] -->|/question /progress| API["API サーバー (Go)"] + API -->|ACK| User + API --> SQS["SQS Queues"] + SQS --> Worker["Worker (Node.js)"] + Worker --> DB[("PostgreSQL")] + Worker -->|AI トリアージ| AI["Bonsai / Ollama"] + Worker -->|スレッド返信| User + Dashboard["Web ダッシュボード"] -->|REST| API +``` diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 0000000..a666a7a --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,22 @@ +- **概要** + - [トップ](/) + - [設計仕様](/specification) + - [AI セットアップ](/ai-setup) + +- **セットアップ** + - [概要](/setup/) + - [Slack App](/setup/slack) + - [ローカル開発](/setup/local) + - [自前サーバー (VPS)](/setup/own) + - [クラウド (AWS)](/setup/remote) + +- **詳細設計** + - [01 プロダクト定義](/detail/01_product-definition) + - [02 アーキテクチャ](/detail/02_architecture) + - [03 AI フロー](/detail/03_ai-flow) + - [04 データモデル](/detail/04_data-models) + - [05 実装フェーズ](/detail/05_implementation-phases) + - [06 ディレクトリ構成](/detail/06_directory) + - [07 環境変数](/detail/07_env-vars) + - [08 実装タスク](/detail/08_implementation-tasks) + - [09 最初の PR](/detail/09_first-pr) diff --git a/docs/detail/06_directory.md b/docs/detail/06_directory.md index 2c12360..202c40c 100644 --- a/docs/detail/06_directory.md +++ b/docs/detail/06_directory.md @@ -192,7 +192,7 @@ services: # 1️⃣ 全体構成(Monorepo想定) -```id="o8d9si" +``` root/ ├── apps/ # 実行可能アプリ │ ├── web/ @@ -213,7 +213,7 @@ root/ # 2️⃣ フロントエンド構成テンプレ -```id="tq93md" +``` apps/web/ ├── src/ │ ├── app/ # ルーティング層 @@ -231,7 +231,7 @@ apps/web/ ## Featureベース構成(推奨) -```id="t6lm52" +``` features/ ├── auth/ │ ├── components/ @@ -249,7 +249,7 @@ features/ # 3️⃣ バックエンド構成テンプレ(Clean Architecture) -```id="9wh13c" +``` apps/api/ ├── cmd/ # エントリポイント ├── internal/ @@ -267,7 +267,7 @@ apps/api/ # 4️⃣ DDDベース構成テンプレ -```id="kl2m91" +``` src/ ├── modules/ │ ├── user/ @@ -283,7 +283,7 @@ src/ # 5️⃣ マイクロサービス構成 -```id="0b5zvx" +``` services/ ├── auth-service/ ├── core-service/ @@ -295,7 +295,7 @@ services/ # 6️⃣ インフラ構成 -```id="v9k0mz" +``` infra/ ├── terraform/ │ ├── modules/ @@ -311,7 +311,7 @@ infra/ # 7️⃣ ドキュメント構成 -```id="az1k93" +``` docs/ ├── 01_feature-list.md ├── 02_db-design.md @@ -325,7 +325,7 @@ docs/ # 8️⃣ テスト構成テンプレ -```id="c32po1" +``` tests/ ├── unit/ ├── integration/ @@ -337,7 +337,7 @@ tests/ # 9️⃣ ベクトルDB / AI機能がある場合 -```id="q91dte" +``` packages/ ├── embeddings/ │ ├── generator.ts @@ -352,7 +352,7 @@ packages/ # 🔟 状態管理分離パターン(FE) -```id="8t1k4d" +``` stores/ ├── auth.store.ts ├── entity.store.ts @@ -363,7 +363,7 @@ stores/ # 11️⃣ API設計分離パターン -```id="nb29df" +``` api/ ├── client.ts ├── endpoints/ diff --git a/docs/detail/07_env-vars.md b/docs/detail/07_env-vars.md index ff981c9..a0795af 100644 --- a/docs/detail/07_env-vars.md +++ b/docs/detail/07_env-vars.md @@ -15,7 +15,7 @@ SLACK_PROGRESS_CHANNEL_ID=C0XXXXXXXX # #progress-board チャンネル DATABASE_URL=postgres://postgres:postgres@localhost:5432/kcl_support_hub # AWS SQS -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 SQS_QUESTION_NEW_URL=https://sqs.ap-northeast-1.amazonaws.com/xxx/question-new SQS_QUESTION_FOLLOWUP_URL=https://sqs.ap-northeast-1.amazonaws.com/xxx/question-followup SQS_PROGRESS_URL=https://sqs.ap-northeast-1.amazonaws.com/xxx/progress @@ -56,7 +56,7 @@ ONYX_API_URL=http://localhost:3000 # Onyx サーバーの URL(な ONYX_API_KEY=xxxx # optional # AWS SQS(ポーリング用) -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 SQS_QUESTION_NEW_URL=https://sqs... SQS_QUESTION_FOLLOWUP_URL=https://sqs... SQS_PROGRESS_URL=https://sqs... @@ -114,7 +114,7 @@ ONYX_API_URL= ONYX_API_KEY= # === AWS === -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 SQS_QUESTION_NEW_URL= SQS_QUESTION_FOLLOWUP_URL= SQS_PROGRESS_URL= diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index f429065..0000000 --- a/docs/index.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -layout: home - -hero: - name: "KCL Support Hub" - text: "ハッカソン参加者向け AI サポートシステム" - tagline: Slack 中心・AI 一次対応・進捗可視化 - actions: - - theme: brand - text: セットアップ - link: /setup/ - - theme: alt - text: 設計仕様 - link: /specification - -features: - - icon: 🤖 - title: AI トリアージ - details: 参加者の質問を Bonsai (Ollama) が自動分類し、スレッドに回答または追加質問を返します。 - - icon: 📊 - title: 進捗可視化 - details: /progress コマンドで投稿された進捗を #progress-board に自動集約。SOS はメンターに即エスカレーション。 - - icon: ⚡ - title: Slack 3 秒制約対応 - details: 受信 → SQS 投入 → ACK を 3 秒以内に完了。重い処理はすべて非同期 Worker で処理します。 ---- diff --git a/docs/setup/local.md b/docs/setup/local.md index 5e131a2..80ffb6e 100644 --- a/docs/setup/local.md +++ b/docs/setup/local.md @@ -127,11 +127,23 @@ BONSAI_MODEL=qwen2.5:7b docker compose -f infra/docker/compose.yml ps # healthy になっているか確認 ``` -**Worker が SQS を受け取らない** -→ LocalStack のキューが作成されているか確認。 +**Worker が `QueueDoesNotExist` エラーを出す** + +原因: LocalStack v3 はコンテナ再作成時に init スクリプトを自動実行するが、macOS の Docker bind mount は execute ビットを落とすため Permission denied で失敗する。 + +```bash +# キューが存在するか確認 +docker exec docker-localstack-1 awslocal sqs list-queues + +# キューがなければ手動作成 +make init-sqs +``` + +もし `make init-sqs` 後もエラーが続く場合は `.env` の `AWS_REGION` を確認する。 +LocalStack は常に `us-east-1` でキューを作成するため、`us-east-1` でなければならない。 ```bash -aws --endpoint-url http://localhost:4566 sqs list-queues +grep AWS_REGION .env # → AWS_REGION=us-east-1 であること ``` **ngrok URL が毎回変わって不便** diff --git a/docs/setup/slack.md b/docs/setup/slack.md index 130faf7..223acf7 100644 --- a/docs/setup/slack.md +++ b/docs/setup/slack.md @@ -2,6 +2,8 @@ local / own / remote のどのパターンでも最初に一度だけ実施します。 +Socket Mode を使うため **ngrok や公開 URL は不要**です。 + --- ## 1. App 作成(manifest を使う・推奨) @@ -15,13 +17,24 @@ local / own / remote のどのパターンでも最初に一度だけ実施し --- -## 2. 値を `.env` に設定 +## 2. App-Level Token を発行 + +Socket Mode には `xapp-...` トークンが必要です。 + +1. **Basic Information → App-Level Tokens** → **Generate Token and Scopes** +2. Token Name: `socket-mode`(任意) +3. Scope: `connections:write` を追加 +4. **Generate** → 表示された `xapp-...` をコピー + +--- + +## 3. 値を `.env` に設定 | 取得場所 | 変数名 | |---|---| | Basic Information → **App ID** | `SLACK_APP_ID` | | OAuth & Permissions → **Bot User OAuth Token** (`xoxb-...`) | `SLACK_BOT_TOKEN` | -| Basic Information → App Credentials → **Signing Secret** | `SLACK_SIGNING_SECRET` | +| Basic Information → App-Level Tokens (`xapp-...`) | `SLACK_APP_TOKEN` | | メンター通知チャンネル → 詳細の `C0XXXXXXXX` | `SLACK_MENTOR_CHANNEL_ID` | | 進捗投稿チャンネル → 詳細の `C0XXXXXXXX` | `SLACK_PROGRESS_CHANNEL_ID` | @@ -29,29 +42,15 @@ local / own / remote のどのパターンでも最初に一度だけ実施し --- -## 3. Request URL の設定 +## 4. 動作確認 -サーバーが起動してから以下を実行(`` は ngrok URL や本番ドメイン): +API サーバーを起動して Slack で `/question` を実行 → モーダルが開けば OK。 ```bash -make slack-setup URL= -# 例: make slack-setup URL=https://xxxx.ngrok-free.app +make dev-api +# → [socketmode] connected と表示されれば接続成功 ``` -スクリプトが以下を自動更新します: - -| 設定箇所 | URL | -|---|---| -| Slash Commands `/question` | `/slack/commands` | -| Slash Commands `/progress` | `/slack/commands` | -| Interactivity & Shortcuts | `/slack/interactions` | - ---- - -## 4. 動作確認 - -Slack で `/question` を実行 → モーダルが開けば OK。 - --- ## 手動設定 @@ -64,6 +63,6 @@ manifest を使わない場合: 3. **Slash Commands** → Create New Command: - `/question`: Description `質問を投稿する` - `/progress`: Description `進捗を共有する` -4. **Interactivity & Shortcuts** → 有効化 -5. **Install App** -6. Request URL は `make slack-setup URL=...` で後から一括設定 +4. **Interactivity & Shortcuts** → 有効化(URL 不要) +5. **Settings → Socket Mode** → Enable Socket Mode +6. **Install App** diff --git a/infra/docker/compose.yml b/infra/docker/compose.yml index b759a39..418bd96 100644 --- a/infra/docker/compose.yml +++ b/infra/docker/compose.yml @@ -19,7 +19,6 @@ services: image: localstack/localstack:3 environment: SERVICES: sqs - DEFAULT_REGION: ap-northeast-1 LOCALSTACK_HOST: localstack ports: - "4566:4566" diff --git a/infra/docker/init-localstack.sh b/infra/docker/init-localstack.sh old mode 100644 new mode 100755 diff --git a/infra/slack/manifest.json b/infra/slack/manifest.json index 156c8ae..4257584 100644 --- a/infra/slack/manifest.json +++ b/infra/slack/manifest.json @@ -34,17 +34,21 @@ "bot": [ "commands", "chat:write", - "channels:read" + "channels:read", + "channels:history", + "groups:history" ] } }, "settings": { "interactivity": { - "is_enabled": true, - "request_url": "https://your-domain.example.com/slack/interactions" + "is_enabled": true + }, + "event_subscriptions": { + "bot_events": ["message.channels", "message.groups"] }, "org_deploy_enabled": false, - "socket_mode_enabled": false, + "socket_mode_enabled": true, "token_rotation_enabled": false } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1b6e158..b8e2f70 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,4 +2,3 @@ packages: - "apps/*" - "packages/*" - "tools/*" - - "docs-host" From a663583eca0fea1f540d7e1e00f08c159d1f806d Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 02:47:59 +0900 Subject: [PATCH 02/36] feat: add subagent orchestration kit --- .agents/plugins/marketplace.json | 20 +++++ .../.codex-plugin/plugin.json | 45 +++++++++++ plugins/subagent-orchestration-kit/README.md | 59 +++++++++++++++ .../agents/openai.yaml | 6 ++ .../assets/subagent-kit.svg | 10 +++ .../contexts/hackhub-product-overview.md | 74 +++++++++++++++++++ .../contexts/project-overview-template.md | 40 ++++++++++ .../skills/coding-worker/SKILL.md | 38 ++++++++++ .../skills/error-checker/SKILL.md | 67 +++++++++++++++++ .../skills/idea-consultant/SKILL.md | 47 ++++++++++++ .../skills/implementation-evaluator/SKILL.md | 36 +++++++++ .../skills/orchestrator-cycle/SKILL.md | 64 ++++++++++++++++ .../skills/reviewer/SKILL.md | 40 ++++++++++ 13 files changed, 546 insertions(+) create mode 100644 .agents/plugins/marketplace.json create mode 100644 plugins/subagent-orchestration-kit/.codex-plugin/plugin.json create mode 100644 plugins/subagent-orchestration-kit/README.md create mode 100644 plugins/subagent-orchestration-kit/agents/openai.yaml create mode 100644 plugins/subagent-orchestration-kit/assets/subagent-kit.svg create mode 100644 plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md create mode 100644 plugins/subagent-orchestration-kit/contexts/project-overview-template.md create mode 100644 plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md create mode 100644 plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md create mode 100644 plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md create mode 100644 plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md create mode 100644 plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md create mode 100644 plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..db83f7b --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "changehack-local-plugins", + "interface": { + "displayName": "changeHack Local Plugins" + }, + "plugins": [ + { + "name": "subagent-orchestration-kit", + "source": { + "source": "local", + "path": "./plugins/subagent-orchestration-kit" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/plugins/subagent-orchestration-kit/.codex-plugin/plugin.json b/plugins/subagent-orchestration-kit/.codex-plugin/plugin.json new file mode 100644 index 0000000..7709ef7 --- /dev/null +++ b/plugins/subagent-orchestration-kit/.codex-plugin/plugin.json @@ -0,0 +1,45 @@ +{ + "name": "subagent-orchestration-kit", + "version": "0.1.0", + "description": "Reusable role prompts and orchestration rules for idea generation, implementation, evaluation, and error checking.", + "author": { + "name": "Akira Kakiuchi", + "email": "support@example.com", + "url": "https://github.com/Asheze1127" + }, + "homepage": "https://github.com/Asheze1127/HackHub", + "repository": "https://github.com/Asheze1127/HackHub", + "license": "UNLICENSED", + "keywords": [ + "subagent", + "orchestration", + "evaluation", + "review", + "planning" + ], + "skills": "./skills/", + "interface": { + "displayName": "Subagent Orchestration Kit", + "shortDescription": "Reusable sub-agent playbooks for product work and implementation loops", + "longDescription": "Packages reusable role definitions, project-context handling, and an implementation loop for idea consulting, coding, evaluation, error checking, and orchestration across repos.", + "developerName": "Akira Kakiuchi", + "category": "Productivity", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "websiteURL": "https://github.com/Asheze1127/HackHub", + "privacyPolicyURL": "https://github.com/Asheze1127/HackHub", + "termsOfServiceURL": "https://github.com/Asheze1127/HackHub", + "defaultPrompt": [ + "Use the orchestrator cycle to plan and ship the next slice.", + "Use idea-consultant to propose feature options from project docs.", + "Use error-checker to compare implementation against specs." + ], + "brandColor": "#0F766E", + "composerIcon": "./assets/subagent-kit.svg", + "logo": "./assets/subagent-kit.svg", + "screenshots": [] + } +} diff --git a/plugins/subagent-orchestration-kit/README.md b/plugins/subagent-orchestration-kit/README.md new file mode 100644 index 0000000..ed0050d --- /dev/null +++ b/plugins/subagent-orchestration-kit/README.md @@ -0,0 +1,59 @@ +# Subagent Orchestration Kit + +This plugin packages a reusable improvement loop for product work and implementation work. + +It does **not** add new first-class agent types to Codex. Instead, it gives you a portable set of: + +- role definitions +- project-context files +- orchestration rules +- usage guidance you can reuse in other repositories + +## Included roles + +- `idea-consultant` + - proposes feature options and product slices from source docs +- `coding-worker` + - implements one bounded slice with clear file ownership +- `implementation-evaluator` + - verifies acceptance criteria and test coverage for the slice +- `error-checker` + - compares code against specs, product direction, and architectural constraints +- `reviewer` + - reviews the concrete diff for regressions, maintainability, and missed tests +- `orchestrator-cycle` + - coordinates the loop and keeps scope under control + +## Current repo context + +This copy includes a project context file for the current HackHub direction: + +- [contexts/hackhub-product-overview.md](./contexts/hackhub-product-overview.md) + +That context merges: + +- `changeHack` +- `progress-checker` +- `hack-evaluater` +- the user override that the product must support multiple hackathons and serve participants, sponsors, and organizers + +## How to use in this repo + +1. Start with `orchestrator-cycle`. +2. Use `idea-consultant` to decide the smallest valuable slice. +3. Give `coding-worker` exclusive ownership of the write scope. +4. Run `error-checker` against docs and code. +5. Run `implementation-evaluator` against the agreed acceptance criteria. +6. Repeat in small slices. + +## How to reuse in another repo + +1. Copy `plugins/subagent-orchestration-kit` into the target repo. +2. Copy or recreate `.agents/plugins/marketplace.json`, or merge the plugin entry into the target repo marketplace. +3. Replace `contexts/hackhub-product-overview.md` with a project-specific overview based on [contexts/project-overview-template.md](./contexts/project-overview-template.md). +4. Update the source-of-truth file list inside the skill docs if the target repo uses different spec paths. +5. Keep the same role split unless the target repo needs fewer roles. + +## Portability note + +If the plugin is not installed in a target environment, the files still work as prompt packs and operating docs. You can open the relevant `SKILL.md` files and use them directly when spawning helper agents. diff --git a/plugins/subagent-orchestration-kit/agents/openai.yaml b/plugins/subagent-orchestration-kit/agents/openai.yaml new file mode 100644 index 0000000..fca76d7 --- /dev/null +++ b/plugins/subagent-orchestration-kit/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Subagent Orchestration Kit" + short_description: "Reusable role prompts for idea consulting, coding, evaluation, and implementation review" + icon_small: "./assets/subagent-kit.svg" + icon_large: "./assets/subagent-kit.svg" + default_prompt: "Use the subagent orchestration kit to plan the next slice, implement it with a bounded worker, and verify it with evaluator and error-checker roles." diff --git a/plugins/subagent-orchestration-kit/assets/subagent-kit.svg b/plugins/subagent-orchestration-kit/assets/subagent-kit.svg new file mode 100644 index 0000000..c0b0cd7 --- /dev/null +++ b/plugins/subagent-orchestration-kit/assets/subagent-kit.svg @@ -0,0 +1,10 @@ + + Subagent Orchestration Kit + Three coordinated blocks around a central hub. + + + + + + + diff --git a/plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md b/plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md new file mode 100644 index 0000000..1b66dc0 --- /dev/null +++ b/plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md @@ -0,0 +1,74 @@ +# HackHub Product Overview + +## Source of truth order + +Use this order when the repository is inconsistent: + +1. Direct user instructions in the current thread +2. This file +3. `changeHack/docs/` +4. `progress-checker/docs/` +5. `hack-evaluater/docs/` +6. Current implementation + +## Product direction + +The intended product is not just the current `changeHack` MVP. + +It is a combined product that should merge: + +- the Slack-driven progress and question workflow from `progress-checker` +- the multi-hackathon and role-aware product model from `hack-evaluater` + +The product must support multiple hackathons and serve: + +- participants / hackers +- mentors / judges +- organizers +- corporate sponsors +- tenant or platform admins when needed + +## Problem the product solves + +- Participants need a low-friction way to ask questions and share progress. +- Mentors need structured, triaged questions instead of unformatted Slack messages. +- Organizers need a web view of progress, blockers, and unanswered questions across hackathons. +- Sponsors need scoped access to the hackathons and teams they are allowed to see. + +## Combined product shape + +### From progress-checker + +- `/progress` modal and progress board flow +- `/question` modal and AI first response flow +- async queue architecture for Slack's 3-second constraint +- web dashboard for progress and questions +- future Slack discussion to GitHub issue flow + +### From hack-evaluater + +- multi-hackathon structure +- role separation and scoped permissions +- web architecture patterns for feature/container based UI +- backend layering and clearer domain boundaries +- sponsor and organizer facing workflows + +## Known current drift + +The current `changeHack` implementation is still closer to a KCL-specific support hub MVP: + +- single-event assumptions remain in docs and code +- sponsor and organizer flows are not implemented +- multi-hackathon modeling is not implemented +- mentor-only operational views are incomplete +- Slack discussion to Issue flow is still missing + +Treat these as implementation gaps, not final product decisions. + +## Planning constraints + +- Preserve Slack-first entry points. +- Keep heavy work asynchronous. +- Do not break the existing `changeHack` MVP while expanding scope. +- Favor small vertical slices over broad rewrites. +- When docs conflict, note the conflict explicitly before choosing a path. diff --git a/plugins/subagent-orchestration-kit/contexts/project-overview-template.md b/plugins/subagent-orchestration-kit/contexts/project-overview-template.md new file mode 100644 index 0000000..9d8ac4a --- /dev/null +++ b/plugins/subagent-orchestration-kit/contexts/project-overview-template.md @@ -0,0 +1,40 @@ +# Project Overview Template + +## Source of truth order + +1. Current user instructions +2. This file +3. Primary product specs +4. Architecture and permission docs +5. Current implementation + +## Product direction + +- What the product is +- Which older repos or docs it merges +- Which user roles matter most +- Which scope assumptions are critical + +## Target users + +- user group 1 +- user group 2 +- user group 3 + +## Core workflows + +- workflow 1 +- workflow 2 +- workflow 3 + +## Non-negotiable constraints + +- architecture constraint +- security constraint +- product constraint +- delivery constraint + +## Known drift + +- docs vs implementation mismatch 1 +- docs vs implementation mismatch 2 diff --git a/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md b/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md new file mode 100644 index 0000000..278e28c --- /dev/null +++ b/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md @@ -0,0 +1,38 @@ +--- +name: coding-worker +description: Implement one bounded slice with clear ownership, tests, and minimal scope creep. +--- + +# Coding Worker + +## Overview + +Use this skill for implementation once the orchestrator has already chosen a slice. + +This role owns one bounded write scope and should avoid expanding the task. + +## Required inputs + +- exact acceptance criteria +- the files or modules this role owns +- known constraints from the project overview +- any test requirements for the slice + +## Required behavior + +- Implement only the assigned slice. +- Preserve existing behavior outside the slice. +- Add or update tests for the touched area. +- Surface blockers instead of silently changing scope. + +## Deliverables + +- code changes +- tests or verification updates +- short note on any unresolved risk + +## Guardrails + +- Do not redefine the product while implementing. +- Do not take ownership of files assigned to another worker. +- Prefer small diffs over broad refactors unless the slice explicitly requires one. diff --git a/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md b/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md new file mode 100644 index 0000000..8b6c75c --- /dev/null +++ b/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md @@ -0,0 +1,67 @@ +--- +name: error-checker +description: Compare the repository's code and plans against the actual product direction, specs, and architectural constraints. +--- + +# Error Checker + +## Overview + +Use this skill to find implementation drift. + +This role is not only a bug checker. It checks whether the repository is moving in the wrong direction. + +## Required inputs + +- the project overview under `../../contexts/` +- relevant specs and architecture docs +- the code or plan under review + +## What to look for + +### Product drift + +- current implementation assumes a smaller product than the docs require +- single-event assumptions where multi-hackathon support is required +- mentor-only assumptions where sponsor or organizer workflows are required + +### Architectural drift + +- synchronous handling where async design is required +- permission shortcuts that violate the intended role model +- hard-coded assumptions that block future role or tenant expansion + +### Implementation gaps + +- documented flows with no code path +- partial flows with no persistence or no UI +- UI routes that exist without backend support +- backend support that exists without user-facing flow + +### Quality problems + +- missing tests in modified areas +- broken or stale docs after implementation +- hidden coupling and poor ownership boundaries + +## Output format + +List findings in severity order: + +1. `critical` +2. `high` +3. `medium` +4. `low` + +Each finding should include: + +- what is wrong +- why it matters +- where it shows up +- the narrowest fix that would reduce the problem + +## Guardrails + +- Do not treat every future feature as a bug. +- Focus on real mismatches between product direction, docs, and implementation. +- Flag contradictions explicitly when the source docs disagree. diff --git a/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md b/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md new file mode 100644 index 0000000..3a7f705 --- /dev/null +++ b/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md @@ -0,0 +1,47 @@ +--- +name: idea-consultant +description: Propose the next useful product or implementation slice without losing sight of the actual product direction. +--- + +# Idea Consultant + +## Overview + +Use this skill to decide **what to build next**. + +This role turns product docs, architecture docs, and implementation gaps into a concrete next slice. + +## Required inputs + +- the project overview under `../../contexts/` +- the main product spec +- current gap analysis or known missing features +- any direct user overrides from the current thread + +## Required behavior + +- Start from the real product, not just the current codebase. +- Prefer the smallest slice that unlocks the next important workflow. +- Call out when docs and implementation disagree. +- Separate: + - product requirement + - implementation constraint + - optional enhancement + +## Good output + +Return a short decision memo with: + +1. `next slice` +2. `user value` +3. `why now` +4. `minimal implementation` +5. `acceptance criteria` +6. `follow-up slices` + +## Guardrails + +- Do not invent features that contradict the project overview. +- Do not prioritize polish over broken or missing core flows. +- When the product is multi-tenant or multi-hackathon, explicitly check whether the proposed slice preserves that direction. +- Treat role and permission mismatches as product problems, not just technical debt. diff --git a/plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md b/plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md new file mode 100644 index 0000000..7476c28 --- /dev/null +++ b/plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md @@ -0,0 +1,36 @@ +--- +name: implementation-evaluator +description: Verify that an implemented slice meets acceptance criteria, tests, and basic quality expectations. +--- + +# Implementation Evaluator + +## Overview + +Use this skill after implementation to decide whether the slice is actually done. + +This role does not redesign the feature. It checks whether the agreed slice now works. + +## Evaluation checklist + +- Does the change satisfy the written acceptance criteria? +- Are the tests appropriate for the changed behavior? +- Is the implementation smaller and more direct than alternatives? +- Did the change accidentally widen scope? +- Are there obvious regressions or missing edge cases? + +## Output format + +Return: + +1. `pass` or `fail` +2. acceptance criteria status +3. test status +4. remaining risks +5. exact follow-up if it fails + +## Guardrails + +- Keep the judgment tied to the agreed slice. +- Distinguish hard failures from future improvements. +- If the slice passes but the larger product is still incomplete, say that clearly. diff --git a/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md b/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md new file mode 100644 index 0000000..712fcea --- /dev/null +++ b/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md @@ -0,0 +1,64 @@ +--- +name: orchestrator-cycle +description: Coordinate a reusable sub-agent improvement loop across ideation, implementation, evaluation, and drift checking. +--- + +# Orchestrator Cycle + +## Overview + +Use this skill when a repository needs a repeatable improvement loop instead of a single isolated edit. + +This role is the coordinator. It does not own every edit itself. It owns: + +- choosing the next slice +- assigning clear write ownership +- keeping the loop small +- deciding when a slice is ready to merge + +## Required startup + +Before proposing work: + +1. Read the project overview under `../../contexts/`. +2. Read the product and architecture docs that the overview points to. +3. Identify the smallest vertical slice that reduces the most important gap. +4. Write down acceptance criteria before implementation starts. + +## Recommended role split + +- `idea-consultant` + - decides what should be built next and why +- `coding-worker` + - implements one bounded slice +- `error-checker` + - finds drift against docs, product direction, and architecture +- `implementation-evaluator` + - verifies the slice against acceptance criteria and tests +- `reviewer` + - reviews the concrete diff for regressions and maintainability + +## Coordination rules + +- Give each write task a single owner. +- Run read-only exploration in parallel whenever possible. +- Do not let `idea-consultant` implement code. +- Do not let `coding-worker` redefine product direction while coding. +- Run `error-checker` after each meaningful implementation step. +- Run `implementation-evaluator` before merge or PR creation. +- Run `reviewer` on the final diff before merge or PR creation. + +## Output format + +For each loop, produce: + +1. the chosen slice +2. why it is the next slice +3. acceptance criteria +4. write owner +5. verification owner +6. remaining risks + +## Reuse guidance + +When reused in another repo, replace the project overview file first. The orchestration rules can stay mostly unchanged. diff --git a/plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md b/plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md new file mode 100644 index 0000000..02e6667 --- /dev/null +++ b/plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md @@ -0,0 +1,40 @@ +--- +name: reviewer +description: Review a concrete change for regressions, maintainability issues, and missing tests after the slice is implemented. +--- + +# Reviewer + +## Overview + +Use this skill after a concrete diff exists. + +This role focuses on the implemented change itself, not broad product ideation. + +## What to review + +- behavioral regressions +- missing edge cases +- poor naming or unclear control flow +- hidden coupling +- missing or weak tests +- security or validation gaps in the touched area + +## Output format + +Return findings first, ordered by severity. + +Each finding should include: + +- path +- why it is a problem +- the user-visible or maintenance impact +- the narrowest practical fix + +If there are no findings, say so explicitly and mention residual risks or test gaps. + +## Guardrails + +- Do not restate the whole implementation unless needed. +- Prefer concrete findings over generic advice. +- Keep focus on the actual diff, not the entire repo. From cc498b1a7c31f92de7d7b89e9144fdad5b1610c1 Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 03:01:24 +0900 Subject: [PATCH 03/36] fix: clarify plugin context resolution --- plugins/subagent-orchestration-kit/README.md | 1 + .../contexts/hackhub-product-overview.md | 19 +++++++++++++++---- .../skills/coding-worker/SKILL.md | 1 + .../skills/error-checker/SKILL.md | 4 +++- .../skills/idea-consultant/SKILL.md | 4 +++- .../skills/implementation-evaluator/SKILL.md | 1 + .../skills/orchestrator-cycle/SKILL.md | 2 ++ .../skills/reviewer/SKILL.md | 1 + 8 files changed, 27 insertions(+), 6 deletions(-) diff --git a/plugins/subagent-orchestration-kit/README.md b/plugins/subagent-orchestration-kit/README.md index ed0050d..e5d6b34 100644 --- a/plugins/subagent-orchestration-kit/README.md +++ b/plugins/subagent-orchestration-kit/README.md @@ -40,6 +40,7 @@ That context merges: ## How to use in this repo 1. Start with `orchestrator-cycle`. + Use [contexts/hackhub-product-overview.md](./contexts/hackhub-product-overview.md) as the project overview file. 2. Use `idea-consultant` to decide the smallest valuable slice. 3. Give `coding-worker` exclusive ownership of the write scope. 4. Run `error-checker` against docs and code. diff --git a/plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md b/plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md index 1b66dc0..32d59bb 100644 --- a/plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md +++ b/plugins/subagent-orchestration-kit/contexts/hackhub-product-overview.md @@ -6,10 +6,21 @@ Use this order when the repository is inconsistent: 1. Direct user instructions in the current thread 2. This file -3. `changeHack/docs/` -4. `progress-checker/docs/` -5. `hack-evaluater/docs/` -6. Current implementation +3. The current repo product and architecture docs +4. Any sibling-repo specs that this product explicitly inherits from +5. Current implementation + +## Current workspace path mapping + +This context file is written for the current HackHub workspace layout. + +From the `changeHack/` repo root, the intended doc locations are: + +- `docs/` +- `../progress-checker/docs/` +- `../hack-evaluater/docs/` + +If those sibling paths do not exist in another repo, do not follow them. Replace this file with a repo-specific overview derived from [project-overview-template.md](./project-overview-template.md) first. ## Product direction diff --git a/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md b/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md index 278e28c..d51a930 100644 --- a/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md @@ -16,6 +16,7 @@ This role owns one bounded write scope and should avoid expanding the task. - exact acceptance criteria - the files or modules this role owns - known constraints from the project overview + - in this repo: `../../contexts/hackhub-product-overview.md` - any test requirements for the slice ## Required behavior diff --git a/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md b/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md index 8b6c75c..74fbcaf 100644 --- a/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md @@ -13,7 +13,9 @@ This role is not only a bug checker. It checks whether the repository is moving ## Required inputs -- the project overview under `../../contexts/` +- the project overview file + - in this repo: `../../contexts/hackhub-product-overview.md` + - in another repo: the project-specific overview that replaced it - relevant specs and architecture docs - the code or plan under review diff --git a/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md b/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md index 3a7f705..856df6a 100644 --- a/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md @@ -13,7 +13,9 @@ This role turns product docs, architecture docs, and implementation gaps into a ## Required inputs -- the project overview under `../../contexts/` +- the project overview file + - in this repo: `../../contexts/hackhub-product-overview.md` + - in another repo: the project-specific overview that replaced it - the main product spec - current gap analysis or known missing features - any direct user overrides from the current thread diff --git a/plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md b/plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md index 7476c28..791c2f4 100644 --- a/plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/implementation-evaluator/SKILL.md @@ -13,6 +13,7 @@ This role does not redesign the feature. It checks whether the agreed slice now ## Evaluation checklist +- Does the implementation still match the active project overview? - Does the change satisfy the written acceptance criteria? - Are the tests appropriate for the changed behavior? - Is the implementation smaller and more direct than alternatives? diff --git a/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md b/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md index 712fcea..7147d10 100644 --- a/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md @@ -21,6 +21,8 @@ This role is the coordinator. It does not own every edit itself. It owns: Before proposing work: 1. Read the project overview under `../../contexts/`. + In this repo, that file is `../../contexts/hackhub-product-overview.md`. + In another repo, use the project-specific overview that replaced it. 2. Read the product and architecture docs that the overview points to. 3. Identify the smallest vertical slice that reduces the most important gap. 4. Write down acceptance criteria before implementation starts. diff --git a/plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md b/plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md index 02e6667..4d9d1b5 100644 --- a/plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/reviewer/SKILL.md @@ -13,6 +13,7 @@ This role focuses on the implemented change itself, not broad product ideation. ## What to review +- consistency with the active project overview - behavioral regressions - missing edge cases - poor naming or unclear control flow From 3c3b25502c7da2f57f3b8d7b7c4ab50ff6db9e1a Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 03:17:42 +0900 Subject: [PATCH 04/36] feat: start console opportunity cycle --- apps/console/.gitignore | 19 + apps/console/app/globals.css | 39 + apps/console/app/layout.tsx | 60 + apps/console/app/login/page.tsx | 14 + apps/console/app/mentor/progress/page.tsx | 66 + .../mentor/progress/teams/[teamId]/page.tsx | 54 + apps/console/app/mentor/questions/page.tsx | 50 + apps/console/app/organizer/page.tsx | 14 + apps/console/app/page.tsx | 87 + apps/console/app/platform/page.tsx | 14 + apps/console/app/sponsor/page.tsx | 14 + apps/console/app/tenant/page.tsx | 14 + apps/console/eslint.config.mjs | 16 + apps/console/next.config.ts | 5 + apps/console/package.json | 27 + apps/console/postcss.config.mjs | 7 + apps/console/shared/lib/api.ts | 81 + apps/console/tsconfig.json | 34 + .../10_combined-product-gap-analysis.md | 204 ++ docs/detail/11_opportunity-cycle-1-plan.md | 105 + docs/specification.md | 2 + pnpm-lock.yaml | 2646 ++--------------- 22 files changed, 1096 insertions(+), 2476 deletions(-) create mode 100644 apps/console/.gitignore create mode 100644 apps/console/app/globals.css create mode 100644 apps/console/app/layout.tsx create mode 100644 apps/console/app/login/page.tsx create mode 100644 apps/console/app/mentor/progress/page.tsx create mode 100644 apps/console/app/mentor/progress/teams/[teamId]/page.tsx create mode 100644 apps/console/app/mentor/questions/page.tsx create mode 100644 apps/console/app/organizer/page.tsx create mode 100644 apps/console/app/page.tsx create mode 100644 apps/console/app/platform/page.tsx create mode 100644 apps/console/app/sponsor/page.tsx create mode 100644 apps/console/app/tenant/page.tsx create mode 100644 apps/console/eslint.config.mjs create mode 100644 apps/console/next.config.ts create mode 100644 apps/console/package.json create mode 100644 apps/console/postcss.config.mjs create mode 100644 apps/console/shared/lib/api.ts create mode 100644 apps/console/tsconfig.json create mode 100644 docs/detail/10_combined-product-gap-analysis.md create mode 100644 docs/detail/11_opportunity-cycle-1-plan.md diff --git a/apps/console/.gitignore b/apps/console/.gitignore new file mode 100644 index 0000000..353d5e2 --- /dev/null +++ b/apps/console/.gitignore @@ -0,0 +1,19 @@ +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store + +# env files +.env* + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/console/app/globals.css b/apps/console/app/globals.css new file mode 100644 index 0000000..0313e2c --- /dev/null +++ b/apps/console/app/globals.css @@ -0,0 +1,39 @@ +@import "tailwindcss"; + +:root { + --background: #f5f0e8; + --foreground: #1e293b; + --panel: rgba(255, 255, 255, 0.84); + --panel-strong: rgba(255, 255, 255, 0.94); + --border: rgba(30, 41, 59, 0.12); + --accent: #0f766e; + --accent-strong: #155e75; + --alert: #b91c1c; + --font-body: "Hiragino Sans", "Yu Gothic", "BIZ UDPGothic", sans-serif; + --font-heading: "Hiragino Mincho ProN", "Yu Mincho", serif; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-body); + --font-mono: var(--font-body); +} + +html { + background: + radial-gradient(circle at top left, rgba(15, 118, 110, 0.16), transparent 28%), + radial-gradient(circle at bottom right, rgba(21, 94, 117, 0.16), transparent 34%), + linear-gradient(180deg, #f8f4ec 0%, #efe5d4 100%); +} + +body { + min-height: 100vh; + color: var(--foreground); + font-family: var(--font-body), sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/apps/console/app/layout.tsx b/apps/console/app/layout.tsx new file mode 100644 index 0000000..496ef2a --- /dev/null +++ b/apps/console/app/layout.tsx @@ -0,0 +1,60 @@ +import type { Metadata } from "next" +import Link from "next/link" +import "./globals.css" + +export const metadata: Metadata = { + title: "HackHub Console", + description: "multi-hackathon control plane for mentors, sponsors, and organizers", +} + +const navItems = [ + { href: "/", label: "Overview" }, + { href: "/mentor/questions", label: "Mentor Queue" }, + { href: "/mentor/progress", label: "Mentor Progress" }, + { href: "/platform", label: "Platform" }, + { href: "/tenant", label: "Tenant" }, + { href: "/organizer", label: "Organizer" }, + { href: "/sponsor", label: "Sponsor" }, +] + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+
+
+
+

Opportunity Branch

+ + HackHub Console + +

+ `progress-checker` の運用導線と `hack-evaluater` の多ハッカソン前提をつなぐ新しい control-plane。 +

+
+ + Login Shell + +
+ +
+
{children}
+
+ + + ) +} diff --git a/apps/console/app/login/page.tsx b/apps/console/app/login/page.tsx new file mode 100644 index 0000000..231681a --- /dev/null +++ b/apps/console/app/login/page.tsx @@ -0,0 +1,14 @@ +export default function LoginShellPage() { + return ( +
+

Login shell

+

+ Auth is intentionally deferred, but the route boundary exists now. +

+

+ `progress-checker` では Mentor ログインが必要で、`hack-evaluater` では role / scope 切替が前提です。 + Cycle 1 では認証実装をまだ入れず、この route を後続 slice の固定入口として先に確保しています。 +

+
+ ) +} diff --git a/apps/console/app/mentor/progress/page.tsx b/apps/console/app/mentor/progress/page.tsx new file mode 100644 index 0000000..2ed0a05 --- /dev/null +++ b/apps/console/app/mentor/progress/page.tsx @@ -0,0 +1,66 @@ +import Link from "next/link" +import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" + +export default async function MentorProgressPage() { + const [logs, teams] = await Promise.all([ + fetchProgressLogs({ limit: 30 }).catch(() => []), + fetchTeams().catch(() => []), + ]) + + const teamMap = new Map(teams.map((team) => [team.id, team])) + const sosCount = logs.filter((log) => log.is_sos).length + + return ( +
+
+
+

Mentor

+

+ Progress overview +

+

+ `progress-checker` で必要だった team progress detail への導線を新しい console 側に作っています。 +

+
+
+

SOS

+

{sosCount}

+

最新 progress logs のうち SOS フラグ付きの件数

+
+
+ +
+ {logs.length === 0 ? ( +
+ progress データがまだありません。API 未接続またはデータ未投入の可能性があります。 +
+ ) : ( + logs.map((log) => { + const team = log.team_id ? teamMap.get(log.team_id) : null + return ( +
+
+ {log.phase} + {log.is_sos ? ( + SOS + ) : null} +
+

{team?.name ?? "Unassigned team"}

+ {log.status_text ?

現状: {log.status_text}

: null} + {log.blockers ?

詰まり: {log.blockers}

: null} +
+ {new Date(log.created_at).toLocaleString("ja-JP")} + {log.team_id ? ( + + Team detail + + ) : null} +
+
+ ) + }) + )} +
+
+ ) +} diff --git a/apps/console/app/mentor/progress/teams/[teamId]/page.tsx b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx new file mode 100644 index 0000000..dc61f34 --- /dev/null +++ b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx @@ -0,0 +1,54 @@ +import Link from "next/link" +import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" + +type TeamProgressDetailPageProps = { + params: Promise<{ teamId: string }> +} + +export default async function TeamProgressDetailPage({ params }: TeamProgressDetailPageProps) { + const { teamId } = await params + const [teams, logs] = await Promise.all([ + fetchTeams().catch(() => []), + fetchProgressLogs({ team_id: teamId, limit: 100 }).catch(() => []), + ]) + + const team = teams.find((candidate) => candidate.id === teamId) + + return ( +
+
+ + Back to progress overview + +

+ {team?.name ?? "Unknown team"} +

+

+ `progress-checker` docs にあった team detail route を先に切り出しました。後続 slice で hackathon scope と role 判定を重ねます。 +

+
+ +
+ {logs.length === 0 ? ( +
+ この team の progress log はまだありません。 +
+ ) : ( + logs.map((log) => ( +
+
+ {log.phase} + {log.is_sos ? ( + SOS + ) : null} +
+ {log.status_text ?

現状: {log.status_text}

: null} + {log.blockers ?

詰まり: {log.blockers}

: null} +

{new Date(log.created_at).toLocaleString("ja-JP")}

+
+ )) + )} +
+
+ ) +} diff --git a/apps/console/app/mentor/questions/page.tsx b/apps/console/app/mentor/questions/page.tsx new file mode 100644 index 0000000..9a5879f --- /dev/null +++ b/apps/console/app/mentor/questions/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link" +import { fetchQuestions } from "@/shared/lib/api" + +export default async function MentorQuestionsPage() { + const questions = await fetchQuestions({ limit: 20 }).catch(() => []) + + return ( +
+
+

Mentor

+

+ Question queue +

+

+ 既存 API の質問一覧を新しい console で再利用しています。後続 slice で hackathon scope と web-auth をここに載せます。 +

+
+ +
+ {questions.length === 0 ? ( +
+ 質問データがまだありません。API 未接続またはデータ未投入の可能性があります。 +
+ ) : ( + questions.map((question) => ( +
+
+ {question.status} + {question.category ? ( + {question.category} + ) : null} + {question.escalated_to_mentor ? ( + escalated + ) : null} +
+

{question.title}

+ {question.body ?

{question.body}

: null} +
+ {new Date(question.created_at).toLocaleString("ja-JP")} + + API detail + +
+
+ )) + )} +
+
+ ) +} diff --git a/apps/console/app/organizer/page.tsx b/apps/console/app/organizer/page.tsx new file mode 100644 index 0000000..fb2e548 --- /dev/null +++ b/apps/console/app/organizer/page.tsx @@ -0,0 +1,14 @@ +export default function OrganizerPage() { + return ( +
+

HackathonOrganizer

+

+ Per-hackathon operations shell +

+

+ 招待管理、Hackathon 単位のスカウト ON/OFF、Judge 送信権限はここで扱います。 + `progress-checker` の mentor 運用画面とは別責務に分け、後続で hackathon scope を明示的に入れます。 +

+
+ ) +} diff --git a/apps/console/app/page.tsx b/apps/console/app/page.tsx new file mode 100644 index 0000000..4ee4d87 --- /dev/null +++ b/apps/console/app/page.tsx @@ -0,0 +1,87 @@ +import Link from "next/link" + +const roleCards = [ + { + href: "/mentor/questions", + eyebrow: "Mentor", + title: "Question queue and progress overview", + body: "既存の `changeHack` API を読みながら、`progress-checker` の mentor 運用導線を新しい console に切り出します。", + }, + { + href: "/platform", + eyebrow: "PlatformAdmin", + title: "Tenant bootstrap shell", + body: "複数ハッカソン前提の control-plane をここから始めます。Tenant 作成と初期割り当ては次の slice で実装します。", + }, + { + href: "/tenant", + eyebrow: "TenantAdmin", + title: "Hackathon and sponsor management shell", + body: "Sponsor 可視範囲、Hackathon 作成、Organizer 割り当ての受け皿になる route shell を先に用意します。", + }, + { + href: "/organizer", + eyebrow: "HackathonOrganizer", + title: "Per-hackathon operation shell", + body: "招待管理、スカウト設定、Judge 権限設定を hackathon 単位で扱うための導線です。", + }, +] + +const cycleSteps = [ + "Step 1: docs と code の差分を固定し、単一イベント前提の drift を明文化する", + "Step 2: 別 app の control-plane を作り、既存 apps/web と責務を分離する", + "Step 3: mentor 導線を実データ接続しながら multi-hackathon 化の受け皿を作る", +] + +export default function HomePage() { + return ( +
+
+

Cycle 1

+

+ Start the hackathon-scoped live board before more single-event assumptions harden. +

+

+ 現在の `apps/web` は KCL 単一イベント向けの support dashboard です。ここではそれを壊さずに、 + hackathon scope を導入できる新しい board を `apps/console` 側で育てます。 +

+
+ {roleCards.map((card) => ( + +

{card.eyebrow}

+

{card.title}

+

{card.body}

+ + ))} +
+
+ + +
+ ) +} diff --git a/apps/console/app/platform/page.tsx b/apps/console/app/platform/page.tsx new file mode 100644 index 0000000..fec900b --- /dev/null +++ b/apps/console/app/platform/page.tsx @@ -0,0 +1,14 @@ +export default function PlatformPage() { + return ( +
+

PlatformAdmin

+

+ Tenant bootstrap shell +

+

+ 次の slice で Tenant 一覧、Tenant 作成、初期 TenantAdmin 割り当てをここに実装します。 + 現時点では multi-hackathon product を `apps/web` から分離するための route shell です。 +

+
+ ) +} diff --git a/apps/console/app/sponsor/page.tsx b/apps/console/app/sponsor/page.tsx new file mode 100644 index 0000000..437935a --- /dev/null +++ b/apps/console/app/sponsor/page.tsx @@ -0,0 +1,14 @@ +export default function SponsorPage() { + return ( +
+

Sponsor

+

+ Sponsor-facing shell +

+

+ 閲覧可能 Hackathon 範囲を前提に、将来的にはスカウト作成や送信履歴をここへ寄せます。 + Cycle 1 では sponsor flow の受け皿を先に切り出します。 +

+
+ ) +} diff --git a/apps/console/app/tenant/page.tsx b/apps/console/app/tenant/page.tsx new file mode 100644 index 0000000..dccf464 --- /dev/null +++ b/apps/console/app/tenant/page.tsx @@ -0,0 +1,14 @@ +export default function TenantPage() { + return ( +
+

TenantAdmin

+

+ Hackathon and sponsor management shell +

+

+ Sponsor 招待、可視範囲設定、Hackathon 作成、Organizer 割り当てはこの route に集約します。 + まだ backend がないため、Cycle 1 では責務の切り出しだけを先に確定させます。 +

+
+ ) +} diff --git a/apps/console/eslint.config.mjs b/apps/console/eslint.config.mjs new file mode 100644 index 0000000..4ddbed1 --- /dev/null +++ b/apps/console/eslint.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig, globalIgnores } from "eslint/config" +import nextVitals from "eslint-config-next/core-web-vitals" +import nextTs from "eslint-config-next/typescript" + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([ + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]) + +export default eslintConfig diff --git a/apps/console/next.config.ts b/apps/console/next.config.ts new file mode 100644 index 0000000..f62ca42 --- /dev/null +++ b/apps/console/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next" + +const nextConfig: NextConfig = {} + +export default nextConfig diff --git a/apps/console/package.json b/apps/console/package.json new file mode 100644 index 0000000..72d38c2 --- /dev/null +++ b/apps/console/package.json @@ -0,0 +1,27 @@ +{ + "name": "@hackhub/console", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build --webpack", + "start": "next start", + "lint": "eslint", + "typecheck": "next typegen && tsc --noEmit" + }, + "dependencies": { + "next": "16.2.2", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.2", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/apps/console/postcss.config.mjs b/apps/console/postcss.config.mjs new file mode 100644 index 0000000..2f8795a --- /dev/null +++ b/apps/console/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config diff --git a/apps/console/shared/lib/api.ts b/apps/console/shared/lib/api.ts new file mode 100644 index 0000000..e9c94bb --- /dev/null +++ b/apps/console/shared/lib/api.ts @@ -0,0 +1,81 @@ +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080" + +export interface Question { + id: string + team_id: string | null + slack_user_id: string + slack_thread_ts: string | null + slack_channel_id: string + title: string + body: string | null + category: string | null + confidence: number | null + status: string + escalated_to_mentor: boolean + created_at: string +} + +export interface ProgressLog { + id: string + team_id: string | null + slack_user_id: string + phase: string + status_text: string | null + blockers: string | null + is_sos: boolean + created_at: string +} + +export interface Team { + id: string + name: string + slack_channel_id: string | null + tech_stack: string[] + phase: string + is_sos: boolean +} + +async function apiFetch(path: string): Promise { + const response = await fetch(`${API_BASE}${path}`, { + cache: "no-store", + headers: { + "Content-Type": "application/json", + }, + }) + + if (!response.ok) { + throw new Error(`API error ${response.status} for ${path}`) + } + + return response.json() as Promise +} + +export async function fetchQuestions(params?: { + status?: string + limit?: number + offset?: number +}): Promise { + const query = new URLSearchParams() + if (params?.status) query.set("status", params.status) + if (params?.limit != null) query.set("limit", String(params.limit)) + if (params?.offset != null) query.set("offset", String(params.offset)) + const queryString = query.toString() + return apiFetch(`/api/questions${queryString ? `?${queryString}` : ""}`) +} + +export async function fetchProgressLogs(params?: { + team_id?: string + limit?: number + offset?: number +}): Promise { + const query = new URLSearchParams() + if (params?.team_id) query.set("team_id", params.team_id) + if (params?.limit != null) query.set("limit", String(params.limit)) + if (params?.offset != null) query.set("offset", String(params.offset)) + const queryString = query.toString() + return apiFetch(`/api/progress${queryString ? `?${queryString}` : ""}`) +} + +export async function fetchTeams(): Promise { + return apiFetch("/api/teams") +} diff --git a/apps/console/tsconfig.json b/apps/console/tsconfig.json new file mode 100644 index 0000000..3a13f90 --- /dev/null +++ b/apps/console/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/docs/detail/10_combined-product-gap-analysis.md b/docs/detail/10_combined-product-gap-analysis.md new file mode 100644 index 0000000..6f339cd --- /dev/null +++ b/docs/detail/10_combined-product-gap-analysis.md @@ -0,0 +1,204 @@ +# 10. Combined Product Gap Analysis + +--- + +## 1. 前提 + +このリポジトリは、単純に `changeHack` を進めるだけではなく、以下を統合したプロダクトとして扱う。 + +- `progress-checker` の Slack 起点の質問受付・進捗可視化・Issue 化構想 +- `hack-evaluater` の multi-hackathon / role-aware なプロダクトモデル + +加えて、現時点のユーザー指示として以下を優先前提にする。 + +- 複数ハッカソンに対応する +- 対象ユーザーは参加者、企業スポンサー、主催者を含む +- 既存 `apps/web` とは別に新しい web-based control-plane を持つ + +--- + +## 2. すでに実装されているもの + +### `changeHack` で実装済み + +- Slack `/question` の modal 起動 +- Slack `/progress` の modal 起動 +- SQS 経由の `question:new` / `question:followup` / `progress` 非同期処理 +- AI 一次回答 +- 追加質問のスレッド返信 +- メンターチャンネルへのエスカレーション +- 質問一覧 / 質問詳細 / 進捗一覧 / チーム一覧の read API +- 既存 `apps/web` の簡易ダッシュボード + +### ここまでで足りている観点 + +- Slack の 3 秒制約への基本対応 +- 質問と進捗を Postgres に保存する基盤 +- mentor 向けの最低限の監視 UI + +--- + +## 3. `progress-checker` 由来で未実装または不足しているもの + +### High + +- Slack 議論の Issue 化フロー + - `issue` queue + - Issue Worker + - thread history 取得 + - GitHub Issue 作成 + - Slack thread への Issue URL 返却 + +- team progress detail + - `progress-checker` では `/progress/teams/:teamId` が必要 + - `changeHack` の既存 `apps/web` には未実装 + +- Mentor 用 web 運用導線 + - `progress-checker` は login, question queue, question detail, progress overview を前提 + - `changeHack` の既存 `apps/web` は read-only dashboard で、auth / role 前提がない + +### Medium + +- `question_sessions` 的な継続回答の状態管理 + - 現状は thread reply で follow-up を再処理できる + - ただし `awaiting_user` などの明示状態ストアがない + +- `issue` / `question` / `progress` の DLQ と運用パス + - docs にはあるが、実装はまだ最小構成 + +- Web からの mentor 操作 + - 状態更新 + - 解決マーク + - コメント + +### Low + +- Onyx / local knowledge の運用 UI +- 高度な可視化 + +--- + +## 4. `hack-evaluater` 由来で不足しているもの + +### Critical + +- multi-hackathon domain model + - Tenant + - Hackathon + - role binding + - sponsor visibility scope + +- role-aware access model + - `PlatformAdmin` + - `TenantAdmin` + - `HackathonOrganizer` + - `Sponsor` + - `Judge` + - `Hacker` + +- 招待 / 参加 / scope 切替 + - Hackathon 招待 + - Sponsor 招待 + - role / scope switch + +### High + +- sponsor / organizer / platform 向け web 画面 +- Hackathon 単位設定 +- Sponsor の閲覧可能 Hackathon 制御 + +### Medium + +- scout workflow +- email verification +- outbound email notifications + +--- + +## 5. サブエージェントによる追補 findings + +### Critical + +- 現在の `changeHack` は単一イベント前提で、`hackathons`・membership・role binding・sponsor visibility が存在しない +- このまま feature を積むと、後から multi-hackathon へ広げる際の再設計コストが大きい + +### High + +- `/api/questions`・`/api/progress`・`/api/teams` は認証や scope 制約なしで全件読める +- sponsor / organizer 分離前提とは整合しない + +- Slack payload の `team_id` は Slack workspace ID だが、product team と誤解しやすい形で流れている +- しかも worker で persistence 時に落としているため、現在でも domain scope を再構成できない + +### Medium + +- docs は webhook-first / layered frontend を説明しているが、現実装は Socket Mode + app 直置きに寄っている +- question status machine も docs と実装でズレがある + +--- + +## 6. 現行実装の drift + +### Product drift + +- 現実装は KCL 単一イベント向け support hub に寄っている +- sponsor / organizer / platform の業務フローが存在しない +- hackathon scope がないため、後から multi-hackathon を載せると再配線が大きい + +### Architecture drift + +- docs では webhook 受信前提、現実装は Socket Mode を採用 +- これは直ちにバグではないが、インフラ docs と運用 docs にズレがある + +### UI drift + +- `apps/web` は mentor 運用の read-only dashboard に近い +- control-plane と participant-facing / sponsor-facing app が分離されていない + +--- + +## 7. 優先順位 + +### 先にやるべきこと + +1. hackathon を top-level scope にする +2. Slack ingress が product scope を落とさないようにする +3. read path に hackathon filter と role boundary を入れる +4. その上で新しい control-plane を `apps/web` と分離する + +### まだ後ろでよいこと + +1. full sponsor flow の実装 +2. scout send flow +3. Slack discussion to Issue automation +4. auth / invite の本実装 + +理由: + +- いま Issue 化や sponsor flow を足しても、単一イベント前提の UI と domain に強く結びつく +- 先に control-plane を切り出した方が後続の role-aware 実装を載せやすい + +--- + +## 8. Cycle 1 の結論 + +Cycle 1 では、`hackathon-scoped live board` を最初の slice とする。 + +その中でこの branch では、先行して: + +- 新しい control-plane app `apps/console` を追加 +- mentor の question/progress 導線を新 app に移植 +- 後続の hackathon scope 実装を載せる受け皿を作る + +次に実装すべき本体は: + +- `hackathons` 導入 +- `teams.hackathon_id` +- Slack channel / workspace と hackathon の mapping +- hackathon filter 付き read path + +これにより: + +- `progress-checker` の mentor 導線を前進させる +- `hack-evaluater` の multi-role / multi-hackathon 前提を受け入れる器を先に作れる +- 既存 `apps/web` を壊さずに前進できる diff --git a/docs/detail/11_opportunity-cycle-1-plan.md b/docs/detail/11_opportunity-cycle-1-plan.md new file mode 100644 index 0000000..807ca5b --- /dev/null +++ b/docs/detail/11_opportunity-cycle-1-plan.md @@ -0,0 +1,105 @@ +# 11. Opportunity Cycle 1 Plan + +--- + +## 1. cycle の目的 + +`progress-checker` の mentor 運用導線を前進させつつ、`hack-evaluater` の multi-hackathon / role-aware product へ移行できる新しい app 境界を作る。 + +--- + +## 2. cycle で扱う slice + +### Slice name + +hackathon-scoped live board + +### User value + +- 参加者の質問と進捗が複数 hackathon 間で混ざらなくなる +- organizer は hackathon 単位で queue / SOS / board を見られる +- sponsor は許可された hackathon の sponsor-safe board だけを見られる + +### Why now + +- 現在の schema と Slack ingestion は single-event 前提で、これ以上 feature を積むと rework が増える +- hackathon scope を先に入れないと sponsor / organizer / platform の分離が進められない + +--- + +## 3. Cycle 1 の実装範囲 + +### In scope + +- `hackathons` を top-level scope として導入する設計 +- `teams.hackathon_id` 導入方針 +- Slack から入る question / progress に scope を持たせる設計 +- hackathon filter 付き board の方向性整理 +- 新しい Next.js app `apps/console` +- top-level overview page +- login shell page +- platform / tenant / organizer / sponsor の route shell +- mentor question queue page +- mentor progress overview page +- mentor team progress detail page +- combined-product gap analysis doc +- cycle plan doc + +### Out of scope + +- full auth の実装 +- sponsor 可視範囲の backend +- issue queue / Issue Worker +- scout workflow +- invite flow + +--- + +## 4. acceptance criteria + +1. `apps/console` が `apps/web` と独立した別 app として存在する +2. `apps/console` に以下の route が存在する + - `/` + - `/login` + - `/platform` + - `/tenant` + - `/organizer` + - `/sponsor` + - `/mentor/questions` + - `/mentor/progress` + - `/mentor/progress/teams/:teamId` +3. mentor の question/progress 画面は既存 API を使って read-only 表示できる +4. combined product の差分と priority が docs に明文化される +5. hackathon scope を導入するための schema / ingress / read-path の次 slice が明確になる +6. 変更は `opportunity` branch に留まり、`dev` へは merge しない + +--- + +## 5. 次の slice + +### Slice 2 + +multi-hackathon domain schema + +- tenants +- hackathons +- role_bindings +- memberships +- sponsor visibility scopes + +### Slice 3 + +auth and role-aware routing + +- login +- role / scope switch +- protected routes + +### Slice 4 + +Slack payload scoping and issue flow + +- question / progress の hackathon 紐付け +- issue queue +- Issue Worker +- GitHub issue creation diff --git a/docs/specification.md b/docs/specification.md index 1065188..6b2b562 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -27,6 +27,8 @@ Slack を主入口とした AI 支援・進捗管理・メンターエスカレ | [07_env-vars.md](./detail/07_env-vars.md) | 必要な環境変数一覧 | | [08_implementation-tasks.md](./detail/08_implementation-tasks.md) | 具体的な実装タスク一覧 | | [09_first-pr.md](./detail/09_first-pr.md) | 着手すべき最初の PR 案 | +| [10_combined-product-gap-analysis.md](./detail/10_combined-product-gap-analysis.md) | `progress-checker` / `hack-evaluater` / `changeHack` の差分整理 | +| [11_opportunity-cycle-1-plan.md](./detail/11_opportunity-cycle-1-plan.md) | opportunity branch で進める Cycle 1 の実装計画 | --- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec71fc6..a9b263f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,43 @@ importers: specifier: ^5.4.0 version: 5.9.3 + apps/console: + dependencies: + next: + specifier: 16.2.2 + version: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 19.2.4 + version: 19.2.4 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.2.2 + '@types/node': + specifier: ^20 + version: 20.19.39 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + eslint: + specifier: ^9 + version: 9.39.4(jiti@2.6.1) + eslint-config-next: + specifier: 16.2.2 + version: 16.2.2(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.2.2 + typescript: + specifier: ^5 + version: 5.9.3 + apps/web: dependencies: class-variance-authority: @@ -53,7 +90,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 16.2.2 - version: 16.2.2(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 16.2.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) tailwindcss: specifier: ^4 version: 4.2.2 @@ -101,18 +138,6 @@ importers: specifier: ^5.4.0 version: 5.9.3 - docs-host: - dependencies: - mermaid: - specifier: ^11.4.1 - version: 11.14.0 - vitepress: - specifier: ^1.6.3 - version: 1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3) - vitepress-plugin-mermaid: - specifier: ^2.0.17 - version: 2.0.17(mermaid@11.14.0)(vitepress@1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3)) - packages/schemas: dependencies: zod: @@ -217,89 +242,10 @@ packages: vue: optional: true - '@algolia/abtesting@1.16.1': - resolution: {integrity: sha512-Xxk4l00pYI+jE0PNw8y0MvsQWh5278WRtZQav8/BMMi3HKi2xmeuqe11WJ3y8/6nuBHdv39w76OpJb09TMfAVQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/autocomplete-core@1.17.7': - resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} - - '@algolia/autocomplete-plugin-algolia-insights@1.17.7': - resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} - peerDependencies: - search-insights: '>= 1 < 3' - - '@algolia/autocomplete-preset-algolia@1.17.7': - resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - - '@algolia/autocomplete-shared@1.17.7': - resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - - '@algolia/client-abtesting@5.50.1': - resolution: {integrity: sha512-4peZlPXMwTOey9q1rQKMdCnwZb/E95/1e+7KujXpLLSh0FawJzg//U2NM+r4AiJy4+naT2MTBhj0K30yshnVTA==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-analytics@5.50.1': - resolution: {integrity: sha512-i+aWHHG8NZvGFHtPeMZkxL2Loc6Fm7iaRo15lYSMx8gFL+at9vgdWxhka7mD1fqxkrxXsQstUBCIsSY8FvkEOw==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-common@5.50.1': - resolution: {integrity: sha512-Hw52Fwapyk/7hMSV/fI4+s3H9MGZEUcRh4VphyXLAk2oLYdndVUkc6KBi0zwHSzwPAr+ZBwFPe2x6naUt9mZGw==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-insights@5.50.1': - resolution: {integrity: sha512-Bn/wtwhJ7p1OD/6pY+Zzn+zlu2N/SJnH46md/PAbvqIzmjVuwjNwD4y0vV5Ov8naeukXdd7UU9v550+v8+mtlg==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-personalization@5.50.1': - resolution: {integrity: sha512-0V4Tu0RWR8YxkgI9EPVOZHGE4K5pEIhkLNN0CTkP/rnPsqaaSQpNMYW3/mGWdiKOWbX0iVmwLB9QESk3H0jS5g==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-query-suggestions@5.50.1': - resolution: {integrity: sha512-jofcWNYMXJDDr87Z2eivlWY6o71Zn7F7aOvQCXSDAo9QTlyf7BhXEsZymLUvF0O1yU9Q9wvrjAWn8uVHYnAvgw==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-search@5.50.1': - resolution: {integrity: sha512-OteRb8WubcmEvU0YlMJwCXs3Q6xrdkb0v50/qZBJP1TF0CvujFZQM++9BjEkTER/Jr9wbPHvjSFKnbMta0b4dQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/ingestion@1.50.1': - resolution: {integrity: sha512-0GmfSgDQK6oiIVXnJvGxtNFOfosBspRTR7csCOYCTL1P8QtxX2vDCIKwTM7xdSAEbJaZ43QlWg25q0Qdsndz8Q==} - engines: {node: '>= 14.0.0'} - - '@algolia/monitoring@1.50.1': - resolution: {integrity: sha512-ySuigKEe4YjYV3si8NVk9BHQpFj/1B+ON7DhhvTvbrZJseHQQloxzq0yHwKmznSdlO6C956fx4pcfOKkZClsyg==} - engines: {node: '>= 14.0.0'} - - '@algolia/recommend@5.50.1': - resolution: {integrity: sha512-Cp8T/B0gVmjFlzzp6eP47hwKh5FGyeqQp1N48/ANDdvdiQkPqLyFHQVDwLBH0LddfIPQE+yqmZIgmKc82haF4A==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-browser-xhr@5.50.1': - resolution: {integrity: sha512-XKdGGLikfrlK66ZSXh/vWcXZZ8Vg3byDFbJD8pwEvN1FoBRGxhxya476IY2ohoTymLa4qB5LBRlIa+2TLHx3Uw==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-fetch@5.50.1': - resolution: {integrity: sha512-mBAU6WyVsDwhHyGM+nodt1/oebHxgvuLlOAoMGbj/1i6LygDHZWDgL1t5JEs37x9Aywv7ZGhqbM1GsfZ54sU6g==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-node-http@5.50.1': - resolution: {integrity: sha512-qmo1LXrNKLHvJE6mdQbLnsZAoZvj7VyF2ft4xmbSGWI2WWm87fx/CjUX4kEExt4y0a6T6nEts6ofpUfH5TEE1A==} - engines: {node: '>= 14.0.0'} - '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@antfu/install-pkg@1.1.0': - resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -484,54 +430,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@braintree/sanitize-url@6.0.4': - resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} - - '@braintree/sanitize-url@7.1.2': - resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - - '@chevrotain/cst-dts-gen@12.0.0': - resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} - - '@chevrotain/gast@12.0.0': - resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} - - '@chevrotain/regexp-to-ast@12.0.0': - resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} - - '@chevrotain/types@12.0.0': - resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} - - '@chevrotain/utils@12.0.0': - resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@docsearch/css@3.8.2': - resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} - - '@docsearch/js@3.8.2': - resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} - - '@docsearch/react@3.8.2': - resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} - peerDependencies: - '@types/react': '>= 16.8.0 < 19.0.0' - react: '>= 16.8.0 < 19.0.0' - react-dom: '>= 16.8.0 < 19.0.0' - search-insights: '>= 1 < 3' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - react-dom: - optional: true - search-insights: - optional: true - '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} @@ -541,144 +443,6 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -733,15 +497,6 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iconify-json/simple-icons@1.2.77': - resolution: {integrity: sha512-oaENvo6C3BkAEWMlcQA3XemxU9v2SFOTlApSUCODAkIu1haeLCjzrmH3HgmGqjRnJjM+LevO8sA+MgdMHBFBDA==} - - '@iconify/types@2.0.0': - resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - - '@iconify/utils@3.1.0': - resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -914,12 +669,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@mermaid-js/mermaid-mindmap@9.3.0': - resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} - - '@mermaid-js/parser@1.1.0': - resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} - '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1001,171 +750,9 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} - cpu: [x64] - os: [win32] - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@shikijs/core@2.5.0': - resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} - - '@shikijs/engine-javascript@2.5.0': - resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} - - '@shikijs/engine-oniguruma@2.5.0': - resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} - - '@shikijs/langs@2.5.0': - resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} - - '@shikijs/themes@2.5.0': - resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} - - '@shikijs/transformers@2.5.0': - resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} - - '@shikijs/types@2.5.0': - resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@smithy/config-resolver@4.4.14': resolution: {integrity: sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==} engines: {node: '>=18.0.0'} @@ -1453,129 +1040,18 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - - '@types/d3-axis@3.0.6': - resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} - - '@types/d3-brush@3.0.6': - resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} - - '@types/d3-chord@3.0.6': - resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-contour@3.0.6': - resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} - - '@types/d3-delaunay@6.0.4': - resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - - '@types/d3-dispatch@3.0.7': - resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} - - '@types/d3-drag@3.0.7': - resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - - '@types/d3-dsv@3.0.7': - resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-fetch@3.0.7': - resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} - - '@types/d3-force@3.0.10': - resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} - - '@types/d3-format@3.0.4': - resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - - '@types/d3-hierarchy@3.1.7': - resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-polygon@3.0.2': - resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - - '@types/d3-quadtree@3.0.6': - resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - - '@types/d3-random@3.0.3': - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - - '@types/d3-scale-chromatic@3.1.0': - resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-selection@3.0.11': - resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} - - '@types/d3-shape@3.1.8': - resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - - '@types/d3-time-format@4.0.3': - resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - - '@types/d3-transition@3.0.9': - resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} - - '@types/d3-zoom@3.0.8': - resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - - '@types/d3@7.4.3': - resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/diff-match-patch@1.0.36': resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/geojson@7946.0.16': - resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} @@ -1593,12 +1069,6 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@types/web-bluetooth@0.0.21': - resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} - '@typescript-eslint/eslint-plugin@8.58.0': resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1658,9 +1128,6 @@ packages: resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -1764,16 +1231,6 @@ packages: cpu: [x64] os: [win32] - '@upsetjs/venn.js@2.0.0': - resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - - '@vitejs/plugin-vue@5.2.4': - resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - vite: ^5.0.0 || ^6.0.0 - vue: ^3.2.25 - '@vue/compiler-core@3.5.32': resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} @@ -1786,15 +1243,6 @@ packages: '@vue/compiler-ssr@3.5.32': resolution: {integrity: sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==} - '@vue/devtools-api@7.7.9': - resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} - - '@vue/devtools-kit@7.7.9': - resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} - - '@vue/devtools-shared@7.7.9': - resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} - '@vue/reactivity@3.5.32': resolution: {integrity: sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==} @@ -1812,56 +1260,6 @@ packages: '@vue/shared@3.5.32': resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==} - '@vueuse/core@12.8.2': - resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} - - '@vueuse/integrations@12.8.2': - resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} - peerDependencies: - async-validator: ^4 - axios: ^1 - change-case: ^5 - drauu: ^0.4 - focus-trap: ^7 - fuse.js: ^7 - idb-keyval: ^6 - jwt-decode: ^4 - nprogress: ^0.2 - qrcode: ^1.5 - sortablejs: ^1 - universal-cookie: ^7 - peerDependenciesMeta: - async-validator: - optional: true - axios: - optional: true - change-case: - optional: true - drauu: - optional: true - focus-trap: - optional: true - fuse.js: - optional: true - idb-keyval: - optional: true - jwt-decode: - optional: true - nprogress: - optional: true - qrcode: - optional: true - sortablejs: - optional: true - universal-cookie: - optional: true - - '@vueuse/metadata@12.8.2': - resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} - - '@vueuse/shared@12.8.2': - resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1900,10 +1298,6 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - algoliasearch@5.50.1: - resolution: {integrity: sha512-/bwdue1/8LWELn/DBalGRfuLsXBLXULJo/yOeavJtDu8rBwxIzC6/Rz9Jg19S21VkJvRuZO1k8CZXBMS73mYbA==} - engines: {node: '>= 14.0.0'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1985,9 +1379,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - birpc@2.9.0: - resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} - bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -2003,270 +1394,72 @@ packages: engines: {node: '>=8'} browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001786: - resolution: {integrity: sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - chevrotain-allstar@0.4.1: - resolution: {integrity: sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==} - peerDependencies: - chevrotain: ^12.0.0 - - chevrotain@12.0.0: - resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} - engines: {node: '>=22.0.0'} - - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - copy-anything@4.0.5: - resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} - engines: {node: '>=18'} - - cose-base@1.0.3: - resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} - - cose-base@2.2.0: - resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - cytoscape-cose-bilkent@4.1.0: - resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape-fcose@2.2.0: - resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape@3.33.2: - resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==} - engines: {node: '>=0.10'} - - d3-array@2.12.1: - resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} - - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-axis@3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - - d3-brush@3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} - - d3-chord@3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-contour@4.0.2: - resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} - engines: {node: '>=12'} - - d3-delaunay@6.0.4: - resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} - engines: {node: '>=12'} - - d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - - d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} - - d3-dsv@3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-fetch@3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} - - d3-force@3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-geo@3.1.1: - resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} - engines: {node: '>=12'} - - d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true - d3-path@1.0.9: - resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} - d3-polygon@3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} - d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} - d3-random@3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} + caniuse-lite@1.0.30001786: + resolution: {integrity: sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==} - d3-sankey@0.12.3: - resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} - d3-scale-chromatic@3.1.0: - resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} - engines: {node: '>=12'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - d3-shape@1.3.7: - resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - d3-transition@3.0.1: - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - d3@7.9.0: - resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} - engines: {node: '>=12'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} - dagre-d3-es@7.0.14: - resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2283,9 +1476,6 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2314,9 +1504,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - delaunator@5.1.0: - resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2328,9 +1515,6 @@ packages: devalue@5.6.4: resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff-match-patch@1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} @@ -2345,9 +1529,6 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} - dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2359,9 +1540,6 @@ packages: electron-to-chromium@1.5.331: resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} - emoji-regex-xs@1.0.0: - resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} - emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -2405,11 +1583,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2602,18 +1775,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - focus-trap@7.8.0: - resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2674,9 +1839,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hachure-fill@0.5.2: - resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2704,28 +1866,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2746,13 +1892,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - internmap@1.0.1: - resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2859,10 +1998,6 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is-what@5.5.0: - resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} - engines: {node: '>=18'} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2919,20 +2054,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - katex@0.16.45: - resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} - hasBin: true - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - khroma@2.1.0: - resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - - langium@4.2.2: - resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -2940,12 +2064,6 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} - layout-base@1.0.2: - resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} - - layout-base@2.0.1: - resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3031,9 +2149,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.18.1: - resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3055,43 +2170,14 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - mark.js@8.11.1: - resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} - - marked@16.4.2: - resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} - engines: {node: '>= 20'} - hasBin: true - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdast-util-to-hast@13.2.1: - resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.14.0: - resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3106,15 +2192,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minisearch@7.2.0: - resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} - - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -3171,9 +2248,6 @@ packages: node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} - non-layered-tidy-tree-layout@2.0.2: - resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3206,9 +2280,6 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - oniguruma-to-es@3.1.1: - resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3225,16 +2296,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - path-data-parser@0.1.0: - resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3250,12 +2315,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -3301,15 +2360,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - points-on-curve@0.2.0: - resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} - - points-on-path@0.2.1: - resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3338,9 +2388,6 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - preact@10.29.1: - resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3348,9 +2395,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3381,15 +2425,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -3414,26 +2449,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - - robust-predicates@3.0.3: - resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - roughjs@4.6.6: - resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -3446,15 +2464,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - search-insights@2.17.3: - resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} - secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -3491,9 +2503,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@2.5.0: - resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} - side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -3514,13 +2523,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - speakingurl@14.0.1: - resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} - engines: {node: '>=0.10.0'} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -3564,9 +2566,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -3591,13 +2590,6 @@ packages: babel-plugin-macros: optional: true - stylis@4.3.6: - resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - - superjson@2.2.6: - resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} - engines: {node: '>=16'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3623,9 +2615,6 @@ packages: peerDependencies: vue: '>=3.2.26 < 4' - tabbable@6.4.0: - resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} - tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -3640,10 +2629,6 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} - tinyexec@1.0.4: - resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} - engines: {node: '>=18'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -3652,19 +2637,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' - ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -3717,9 +2695,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3727,21 +2702,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unist-util-is@6.0.1: - resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.2: - resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - - unist-util-visit@5.1.0: - resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -3759,88 +2719,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - vfile-message@4.0.3: - resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vitepress-plugin-mermaid@2.0.17: - resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==} - peerDependencies: - mermaid: 10 || 11 - vitepress: ^1.0.0 || ^1.0.0-alpha - - vitepress@1.6.4: - resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} - hasBin: true - peerDependencies: - markdown-it-mathjax3: ^4 - postcss: ^8 - peerDependenciesMeta: - markdown-it-mathjax3: - optional: true - postcss: - optional: true - - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue@3.5.32: resolution: {integrity: sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==} peerDependencies: @@ -3906,9 +2787,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - snapshots: '@ai-sdk/anthropic@0.0.56(zod@3.25.76)': @@ -3952,169 +2830,52 @@ snapshots: '@ai-sdk/react@0.0.70(react@19.2.4)(zod@3.25.76)': dependencies: '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - swr: 2.4.1(react@19.2.4) - throttleit: 2.1.0 - optionalDependencies: - react: 19.2.4 - zod: 3.25.76 - - '@ai-sdk/solid@0.0.54(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - transitivePeerDependencies: - - zod - - '@ai-sdk/svelte@0.0.57(svelte@5.55.1)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - sswr: 2.2.0(svelte@5.55.1) - optionalDependencies: - svelte: 5.55.1 - transitivePeerDependencies: - - zod - - '@ai-sdk/ui-utils@0.0.50(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.26 - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - json-schema: 0.4.0 - secure-json-parse: 2.7.0 - zod-to-json-schema: 3.25.2(zod@3.25.76) - optionalDependencies: - zod: 3.25.76 - - '@ai-sdk/vue@0.0.59(vue@3.5.32(typescript@5.9.3))(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - swrv: 1.2.0(vue@3.5.32(typescript@5.9.3)) - optionalDependencies: - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - zod - - '@algolia/abtesting@1.16.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3)': - dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3) - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - - search-insights - - '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3)': - dependencies: - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - search-insights: 2.17.3 - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - - '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)': - dependencies: - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - '@algolia/client-search': 5.50.1 - algoliasearch: 5.50.1 - - '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)': - dependencies: - '@algolia/client-search': 5.50.1 - algoliasearch: 5.50.1 - - '@algolia/client-abtesting@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-analytics@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-common@5.50.1': {} - - '@algolia/client-insights@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-personalization@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-query-suggestions@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-search@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/ingestion@1.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/monitoring@1.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + swr: 2.4.1(react@19.2.4) + throttleit: 2.1.0 + optionalDependencies: + react: 19.2.4 + zod: 3.25.76 - '@algolia/recommend@5.50.1': + '@ai-sdk/solid@0.0.54(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + transitivePeerDependencies: + - zod - '@algolia/requester-browser-xhr@5.50.1': + '@ai-sdk/svelte@0.0.57(svelte@5.55.1)(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + sswr: 2.2.0(svelte@5.55.1) + optionalDependencies: + svelte: 5.55.1 + transitivePeerDependencies: + - zod - '@algolia/requester-fetch@5.50.1': + '@ai-sdk/ui-utils@0.0.50(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 + '@ai-sdk/provider': 0.0.26 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + json-schema: 0.4.0 + secure-json-parse: 2.7.0 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + zod: 3.25.76 - '@algolia/requester-node-http@5.50.1': + '@ai-sdk/vue@0.0.59(vue@3.5.32(typescript@5.9.3))(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + swrv: 1.2.0(vue@3.5.32(typescript@5.9.3)) + optionalDependencies: + vue: 3.5.32(typescript@5.9.3) + transitivePeerDependencies: + - zod '@alloc/quick-lru@5.2.0': {} - '@antfu/install-pkg@1.1.0': - dependencies: - package-manager-detector: 1.6.0 - tinyexec: 1.0.4 - '@aws-crypto/sha256-browser@5.2.0': dependencies: '@aws-crypto/sha256-js': 5.2.0 @@ -4552,54 +3313,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@braintree/sanitize-url@6.0.4': - optional: true - - '@braintree/sanitize-url@7.1.2': {} - - '@chevrotain/cst-dts-gen@12.0.0': - dependencies: - '@chevrotain/gast': 12.0.0 - '@chevrotain/types': 12.0.0 - - '@chevrotain/gast@12.0.0': - dependencies: - '@chevrotain/types': 12.0.0 - - '@chevrotain/regexp-to-ast@12.0.0': {} - - '@chevrotain/types@12.0.0': {} - - '@chevrotain/utils@12.0.0': {} - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@docsearch/css@3.8.2': {} - - '@docsearch/js@3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3)': - dependencies: - '@docsearch/react': 3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3) - preact: 10.29.1 - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/react' - - react - - react-dom - - search-insights - - '@docsearch/react@3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3)': - dependencies: - '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3) - '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - '@docsearch/css': 3.8.2 - algoliasearch: 5.50.1 - optionalDependencies: - search-insights: 2.17.3 - transitivePeerDependencies: - - '@algolia/client-search' - '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -4616,75 +3333,6 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -4742,18 +3390,6 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/simple-icons@1.2.77': - dependencies: - '@iconify/types': 2.0.0 - - '@iconify/types@2.0.0': {} - - '@iconify/utils@3.1.0': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@iconify/types': 2.0.0 - mlly: 1.8.2 - '@img/colour@1.1.0': optional: true @@ -4875,21 +3511,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mermaid-js/mermaid-mindmap@9.3.0': - dependencies: - '@braintree/sanitize-url': 6.0.4 - cytoscape: 3.33.2 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) - cytoscape-fcose: 2.2.0(cytoscape@3.33.2) - d3: 7.9.0 - khroma: 2.1.0 - non-layered-tidy-tree-layout: 2.0.2 - optional: true - - '@mermaid-js/parser@1.1.0': - dependencies: - langium: 4.2.2 - '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.2 @@ -4943,123 +3564,8 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@rollup/rollup-android-arm-eabi@4.60.1': - optional: true - - '@rollup/rollup-android-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-x64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.1': - optional: true - '@rtsao/scc@1.1.0': {} - '@shikijs/core@2.5.0': - dependencies: - '@shikijs/engine-javascript': 2.5.0 - '@shikijs/engine-oniguruma': 2.5.0 - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 3.1.1 - - '@shikijs/engine-oniguruma@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - - '@shikijs/themes@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - - '@shikijs/transformers@2.5.0': - dependencies: - '@shikijs/core': 2.5.0 - '@shikijs/types': 2.5.0 - - '@shikijs/types@2.5.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - '@smithy/config-resolver@4.4.14': dependencies: '@smithy/node-config-provider': 4.3.13 @@ -5426,150 +3932,14 @@ snapshots: tslib: 2.8.1 optional: true - '@types/d3-array@3.2.2': {} - - '@types/d3-axis@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-brush@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-chord@3.0.6': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-contour@3.0.6': - dependencies: - '@types/d3-array': 3.2.2 - '@types/geojson': 7946.0.16 - - '@types/d3-delaunay@6.0.4': {} - - '@types/d3-dispatch@3.0.7': {} - - '@types/d3-drag@3.0.7': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-dsv@3.0.7': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-fetch@3.0.7': - dependencies: - '@types/d3-dsv': 3.0.7 - - '@types/d3-force@3.0.10': {} - - '@types/d3-format@3.0.4': {} - - '@types/d3-geo@3.1.0': - dependencies: - '@types/geojson': 7946.0.16 - - '@types/d3-hierarchy@3.1.7': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-polygon@3.0.2': {} - - '@types/d3-quadtree@3.0.6': {} - - '@types/d3-random@3.0.3': {} - - '@types/d3-scale-chromatic@3.1.0': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-selection@3.0.11': {} - - '@types/d3-shape@3.1.8': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time-format@4.0.3': {} - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} - - '@types/d3-transition@3.0.9': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-zoom@3.0.8': - dependencies: - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - - '@types/d3@7.4.3': - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-axis': 3.0.6 - '@types/d3-brush': 3.0.6 - '@types/d3-chord': 3.0.6 - '@types/d3-color': 3.1.3 - '@types/d3-contour': 3.0.6 - '@types/d3-delaunay': 6.0.4 - '@types/d3-dispatch': 3.0.7 - '@types/d3-drag': 3.0.7 - '@types/d3-dsv': 3.0.7 - '@types/d3-ease': 3.0.2 - '@types/d3-fetch': 3.0.7 - '@types/d3-force': 3.0.10 - '@types/d3-format': 3.0.4 - '@types/d3-geo': 3.1.0 - '@types/d3-hierarchy': 3.1.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-path': 3.1.1 - '@types/d3-polygon': 3.0.2 - '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 - '@types/d3-scale': 4.0.9 - '@types/d3-scale-chromatic': 3.1.0 - '@types/d3-selection': 3.0.11 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-time-format': 4.0.3 - '@types/d3-timer': 3.0.2 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - '@types/diff-match-patch@1.0.36': {} '@types/estree@1.0.8': {} - '@types/geojson@7946.0.16': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} - '@types/linkify-it@5.0.0': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdurl@2.0.0': {} - '@types/node@20.19.39': dependencies: undici-types: 6.21.0 @@ -5590,10 +3960,6 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@types/unist@3.0.3': {} - - '@types/web-bluetooth@0.0.21': {} - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5685,8 +4051,6 @@ snapshots: '@typescript-eslint/types': 8.58.0 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.0': {} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -5746,16 +4110,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@upsetjs/venn.js@2.0.0': - optionalDependencies: - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))(vue@3.5.32(typescript@5.9.3))': - dependencies: - vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0) - vue: 3.5.32(typescript@5.9.3) - '@vue/compiler-core@3.5.32': dependencies: '@babel/parser': 7.29.2 @@ -5786,24 +4140,6 @@ snapshots: '@vue/compiler-dom': 3.5.32 '@vue/shared': 3.5.32 - '@vue/devtools-api@7.7.9': - dependencies: - '@vue/devtools-kit': 7.7.9 - - '@vue/devtools-kit@7.7.9': - dependencies: - '@vue/devtools-shared': 7.7.9 - birpc: 2.9.0 - hookable: 5.5.3 - mitt: 3.0.1 - perfect-debounce: 1.0.0 - speakingurl: 14.0.1 - superjson: 2.2.6 - - '@vue/devtools-shared@7.7.9': - dependencies: - rfdc: 1.4.1 - '@vue/reactivity@3.5.32': dependencies: '@vue/shared': 3.5.32 @@ -5826,35 +4162,8 @@ snapshots: '@vue/shared': 3.5.32 vue: 3.5.32(typescript@5.9.3) - '@vue/shared@3.5.32': {} - - '@vueuse/core@12.8.2(typescript@5.9.3)': - dependencies: - '@types/web-bluetooth': 0.0.21 - '@vueuse/metadata': 12.8.2 - '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - typescript - - '@vueuse/integrations@12.8.2(focus-trap@7.8.0)(typescript@5.9.3)': - dependencies: - '@vueuse/core': 12.8.2(typescript@5.9.3) - '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - focus-trap: 7.8.0 - transitivePeerDependencies: - - typescript - - '@vueuse/metadata@12.8.2': {} - - '@vueuse/shared@12.8.2(typescript@5.9.3)': - dependencies: - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - typescript - + '@vue/shared@3.5.32': {} + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -5896,23 +4205,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - algoliasearch@5.50.1: - dependencies: - '@algolia/abtesting': 1.16.1 - '@algolia/client-abtesting': 5.50.1 - '@algolia/client-analytics': 5.50.1 - '@algolia/client-common': 5.50.1 - '@algolia/client-insights': 5.50.1 - '@algolia/client-personalization': 5.50.1 - '@algolia/client-query-suggestions': 5.50.1 - '@algolia/client-search': 5.50.1 - '@algolia/ingestion': 1.50.1 - '@algolia/monitoring': 1.50.1 - '@algolia/recommend': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -6010,8 +4302,6 @@ snapshots: baseline-browser-mapping@2.10.16: {} - birpc@2.9.0: {} - bowser@2.14.1: {} brace-expansion@1.1.13: @@ -6056,8 +4346,6 @@ snapshots: caniuse-lite@1.0.30001786: {} - ccount@2.0.1: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6065,23 +4353,6 @@ snapshots: chalk@5.6.2: {} - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - chevrotain-allstar@0.4.1(chevrotain@12.0.0): - dependencies: - chevrotain: 12.0.0 - lodash-es: 4.18.1 - - chevrotain@12.0.0: - dependencies: - '@chevrotain/cst-dts-gen': 12.0.0 - '@chevrotain/gast': 12.0.0 - '@chevrotain/regexp-to-ast': 12.0.0 - '@chevrotain/types': 12.0.0 - '@chevrotain/utils': 12.0.0 - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -6096,32 +4367,12 @@ snapshots: color-name@1.1.4: {} - comma-separated-tokens@2.0.3: {} - commander@2.20.3: {} - commander@7.2.0: {} - - commander@8.3.0: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} - convert-source-map@2.0.0: {} - copy-anything@4.0.5: - dependencies: - is-what: 5.5.0 - - cose-base@1.0.3: - dependencies: - layout-base: 1.0.2 - - cose-base@2.2.0: - dependencies: - layout-base: 2.0.1 - create-require@1.1.1: {} cross-spawn@7.0.6: @@ -6132,190 +4383,6 @@ snapshots: csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2): - dependencies: - cose-base: 1.0.3 - cytoscape: 3.33.2 - - cytoscape-fcose@2.2.0(cytoscape@3.33.2): - dependencies: - cose-base: 2.2.0 - cytoscape: 3.33.2 - - cytoscape@3.33.2: {} - - d3-array@2.12.1: - dependencies: - internmap: 1.0.1 - - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-axis@3.0.0: {} - - d3-brush@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3-chord@3.0.1: - dependencies: - d3-path: 3.1.0 - - d3-color@3.1.0: {} - - d3-contour@4.0.2: - dependencies: - d3-array: 3.2.4 - - d3-delaunay@6.0.4: - dependencies: - delaunator: 5.1.0 - - d3-dispatch@3.0.1: {} - - d3-drag@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - - d3-dsv@3.0.1: - dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - - d3-ease@3.0.1: {} - - d3-fetch@3.0.1: - dependencies: - d3-dsv: 3.0.1 - - d3-force@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - - d3-format@3.1.2: {} - - d3-geo@3.1.1: - dependencies: - d3-array: 3.2.4 - - d3-hierarchy@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@1.0.9: {} - - d3-path@3.1.0: {} - - d3-polygon@3.0.1: {} - - d3-quadtree@3.0.1: {} - - d3-random@3.0.1: {} - - d3-sankey@0.12.3: - dependencies: - d3-array: 2.12.1 - d3-shape: 1.3.7 - - d3-scale-chromatic@3.1.0: - dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-selection@3.0.0: {} - - d3-shape@1.3.7: - dependencies: - d3-path: 1.0.9 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - - d3-transition@3.0.1(d3-selection@3.0.0): - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - - d3-zoom@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3@7.9.0: - dependencies: - d3-array: 3.2.4 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.2 - d3-delaunay: 6.0.4 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.2 - d3-geo: 3.1.1 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.1.0 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1(d3-selection@3.0.0) - d3-zoom: 3.0.0 - - dagre-d3-es@7.0.14: - dependencies: - d3: 7.9.0 - lodash-es: 4.18.1 - damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -6336,8 +4403,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - dayjs@1.11.20: {} - debug@3.2.7: dependencies: ms: 2.1.3 @@ -6360,20 +4425,12 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - delaunator@5.1.0: - dependencies: - robust-predicates: 3.0.3 - dequal@2.0.3: {} detect-libc@2.1.2: {} devalue@5.6.4: {} - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - diff-match-patch@1.0.5: {} diff@4.0.4: {} @@ -6384,10 +4441,6 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.3.3: - optionalDependencies: - '@types/trusted-types': 2.0.7 - dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -6398,8 +4451,6 @@ snapshots: electron-to-chromium@1.5.331: {} - emoji-regex-xs@1.0.0: {} - emoji-regex@9.2.2: {} enhanced-resolve@5.20.1: @@ -6511,32 +4562,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -6561,6 +4586,26 @@ snapshots: - eslint-plugin-import-x - supports-color + eslint-config-next@16.2.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.2.2 + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 @@ -6624,6 +4669,33 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -6809,17 +4881,10 @@ snapshots: flatted@3.4.2: {} - focus-trap@7.8.0: - dependencies: - tabbable: 6.4.0 - for-each@0.3.5: dependencies: is-callable: 1.2.7 - fsevents@2.3.3: - optional: true - function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -6886,8 +4951,6 @@ snapshots: graceful-fs@4.2.11: {} - hachure-fill@0.5.2: {} - has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -6910,38 +4973,12 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 - hookable@5.5.3: {} - - html-void-elements@3.0.0: {} - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - ignore@5.3.2: {} ignore@7.0.5: {} @@ -6959,10 +4996,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - internmap@1.0.1: {} - - internmap@2.0.3: {} - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -7079,8 +5112,6 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-what@5.5.0: {} - isarray@2.0.5: {} isexe@2.0.0: {} @@ -7131,35 +5162,16 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - katex@0.16.45: - dependencies: - commander: 8.3.0 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 - khroma@2.1.0: {} - - langium@4.2.2: - dependencies: - '@chevrotain/regexp-to-ast': 12.0.0 - chevrotain: 12.0.0 - chevrotain-allstar: 0.4.1(chevrotain@12.0.0) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - language-subtag-registry@0.3.23: {} language-tags@1.0.9: dependencies: language-subtag-registry: 0.3.23 - layout-base@1.0.2: {} - - layout-base@2.0.1: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -7220,8 +5232,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.18.1: {} - lodash.merge@4.6.2: {} loose-envify@1.4.0: @@ -7242,67 +5252,10 @@ snapshots: make-error@1.3.6: {} - mark.js@8.11.1: {} - - marked@16.4.2: {} - math-intrinsics@1.1.0: {} - mdast-util-to-hast@13.2.1: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - merge2@1.4.1: {} - mermaid@11.14.0: - dependencies: - '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.1.0 - '@types/d3': 7.4.3 - '@upsetjs/venn.js': 2.0.0 - cytoscape: 3.33.2 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) - cytoscape-fcose: 2.2.0(cytoscape@3.33.2) - d3: 7.9.0 - d3-sankey: 0.12.3 - dagre-d3-es: 7.0.14 - dayjs: 1.11.20 - dompurify: 3.3.3 - katex: 0.16.45 - khroma: 2.1.0 - lodash-es: 4.18.1 - marked: 16.4.2 - roughjs: 4.6.6 - stylis: 4.3.6 - ts-dedent: 2.2.0 - uuid: 11.1.0 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-encode@2.0.1: {} - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -7318,17 +5271,6 @@ snapshots: minimist@1.2.8: {} - minisearch@7.2.0: {} - - mitt@3.0.1: {} - - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - moo@0.5.3: {} ms@2.1.3: {} @@ -7382,9 +5324,6 @@ snapshots: node-releases@2.0.37: {} - non-layered-tidy-tree-layout@2.0.2: - optional: true - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -7427,12 +5366,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - oniguruma-to-es@3.1.1: - dependencies: - emoji-regex-xs: 1.0.0 - regex: 6.1.0 - regex-recursion: 6.0.2 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7456,14 +5389,10 @@ snapshots: dependencies: p-limit: 3.1.0 - package-manager-detector@1.6.0: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 - path-data-parser@0.1.0: {} - path-exists@4.0.0: {} path-expression-matcher@1.2.1: {} @@ -7472,10 +5401,6 @@ snapshots: path-parse@1.0.7: {} - pathe@2.0.3: {} - - perfect-debounce@1.0.0: {} - pg-cloudflare@1.3.0: optional: true @@ -7517,19 +5442,6 @@ snapshots: picomatch@4.0.4: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - points-on-curve@0.2.0: {} - - points-on-path@0.2.1: - dependencies: - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -7554,8 +5466,6 @@ snapshots: dependencies: xtend: 4.0.2 - preact@10.29.1: {} - prelude-ls@1.2.1: {} prop-types@15.8.1: @@ -7564,8 +5474,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - property-information@7.1.0: {} - punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -7597,16 +5505,6 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -7633,54 +5531,10 @@ snapshots: reusify@1.1.0: {} - rfdc@1.4.1: {} - - robust-predicates@3.0.3: {} - - rollup@4.60.1: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 - fsevents: 2.3.3 - - roughjs@4.6.6: - dependencies: - hachure-fill: 0.5.2 - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - points-on-path: 0.2.1 - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - rw@1.3.3: {} - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -7700,12 +5554,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - safer-buffer@2.1.2: {} - scheduler@0.27.0: {} - search-insights@2.17.3: {} - secure-json-parse@2.7.0: {} semver@6.3.1: {} @@ -7772,17 +5622,6 @@ snapshots: shebang-regex@3.0.0: {} - shiki@2.5.0: - dependencies: - '@shikijs/core': 2.5.0 - '@shikijs/engine-javascript': 2.5.0 - '@shikijs/engine-oniguruma': 2.5.0 - '@shikijs/langs': 2.5.0 - '@shikijs/themes': 2.5.0 - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -7813,10 +5652,6 @@ snapshots: source-map-js@1.2.1: {} - space-separated-tokens@2.0.2: {} - - speakingurl@14.0.1: {} - split2@4.2.0: {} sql-formatter@15.7.3: @@ -7886,11 +5721,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} @@ -7904,12 +5734,6 @@ snapshots: optionalDependencies: '@babel/core': 7.29.0 - stylis@4.3.6: {} - - superjson@2.2.6: - dependencies: - copy-anything: 4.0.5 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -7947,8 +5771,6 @@ snapshots: dependencies: vue: 3.5.32(typescript@5.9.3) - tabbable@6.4.0: {} - tailwind-merge@3.5.0: {} tailwindcss@4.2.2: {} @@ -7957,8 +5779,6 @@ snapshots: throttleit@2.1.0: {} - tinyexec@1.0.4: {} - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -7968,14 +5788,10 @@ snapshots: dependencies: is-number: 7.0.0 - trim-lines@3.0.1: {} - ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 - ts-dedent@2.2.0: {} - ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -8053,8 +5869,6 @@ snapshots: typescript@5.9.3: {} - ufo@1.6.3: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -8064,29 +5878,6 @@ snapshots: undici-types@6.21.0: {} - unist-util-is@6.0.1: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-visit@5.1.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -8125,103 +5916,8 @@ snapshots: dependencies: react: 19.2.4 - uuid@11.1.0: {} - v8-compile-cache-lib@3.0.1: {} - vfile-message@4.0.3: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.3 - - vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.8 - rollup: 4.60.1 - optionalDependencies: - '@types/node': 20.19.39 - fsevents: 2.3.3 - lightningcss: 1.32.0 - - vitepress-plugin-mermaid@2.0.17(mermaid@11.14.0)(vitepress@1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3)): - dependencies: - mermaid: 11.14.0 - vitepress: 1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3) - optionalDependencies: - '@mermaid-js/mermaid-mindmap': 9.3.0 - - vitepress@1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3): - dependencies: - '@docsearch/css': 3.8.2 - '@docsearch/js': 3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3) - '@iconify-json/simple-icons': 1.2.77 - '@shikijs/core': 2.5.0 - '@shikijs/transformers': 2.5.0 - '@shikijs/types': 2.5.0 - '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))(vue@3.5.32(typescript@5.9.3)) - '@vue/devtools-api': 7.7.9 - '@vue/shared': 3.5.32 - '@vueuse/core': 12.8.2(typescript@5.9.3) - '@vueuse/integrations': 12.8.2(focus-trap@7.8.0)(typescript@5.9.3) - focus-trap: 7.8.0 - mark.js: 8.11.1 - minisearch: 7.2.0 - shiki: 2.5.0 - vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0) - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - postcss: 8.5.8 - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/node' - - '@types/react' - - async-validator - - axios - - change-case - - drauu - - fuse.js - - idb-keyval - - jwt-decode - - less - - lightningcss - - nprogress - - qrcode - - react - - react-dom - - sass - - sass-embedded - - search-insights - - sortablejs - - stylus - - sugarss - - terser - - typescript - - universal-cookie - - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - - vscode-uri@3.1.0: {} - vue@3.5.32(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.32 @@ -8298,5 +5994,3 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} - - zwitch@2.0.4: {} From f28d0bc148784d82c9ea5b18b312c0ddf6714258 Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 03:28:50 +0900 Subject: [PATCH 05/36] fix: improve plugin portability --- .agents/plugins/marketplace.json | 4 ++-- .../.codex-plugin/plugin.json | 17 +++++---------- plugins/subagent-orchestration-kit/LICENSE | 21 +++++++++++++++++++ plugins/subagent-orchestration-kit/README.md | 15 ++++++++----- .../contexts/project-overview.md | 12 +++++++++++ .../skills/coding-worker/SKILL.md | 2 +- .../skills/error-checker/SKILL.md | 4 ++-- .../skills/idea-consultant/SKILL.md | 4 ++-- .../skills/orchestrator-cycle/SKILL.md | 7 ++++--- 9 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 plugins/subagent-orchestration-kit/LICENSE create mode 100644 plugins/subagent-orchestration-kit/contexts/project-overview.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index db83f7b..9000b74 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -1,7 +1,7 @@ { - "name": "changehack-local-plugins", + "name": "local-plugins", "interface": { - "displayName": "changeHack Local Plugins" + "displayName": "Local Plugins" }, "plugins": [ { diff --git a/plugins/subagent-orchestration-kit/.codex-plugin/plugin.json b/plugins/subagent-orchestration-kit/.codex-plugin/plugin.json index 7709ef7..f0c4001 100644 --- a/plugins/subagent-orchestration-kit/.codex-plugin/plugin.json +++ b/plugins/subagent-orchestration-kit/.codex-plugin/plugin.json @@ -3,13 +3,9 @@ "version": "0.1.0", "description": "Reusable role prompts and orchestration rules for idea generation, implementation, evaluation, and error checking.", "author": { - "name": "Akira Kakiuchi", - "email": "support@example.com", - "url": "https://github.com/Asheze1127" + "name": "Project maintainer" }, - "homepage": "https://github.com/Asheze1127/HackHub", - "repository": "https://github.com/Asheze1127/HackHub", - "license": "UNLICENSED", + "license": "MIT", "keywords": [ "subagent", "orchestration", @@ -22,20 +18,17 @@ "displayName": "Subagent Orchestration Kit", "shortDescription": "Reusable sub-agent playbooks for product work and implementation loops", "longDescription": "Packages reusable role definitions, project-context handling, and an implementation loop for idea consulting, coding, evaluation, error checking, and orchestration across repos.", - "developerName": "Akira Kakiuchi", + "developerName": "Project maintainer", "category": "Productivity", "capabilities": [ "Interactive", "Read", "Write" ], - "websiteURL": "https://github.com/Asheze1127/HackHub", - "privacyPolicyURL": "https://github.com/Asheze1127/HackHub", - "termsOfServiceURL": "https://github.com/Asheze1127/HackHub", "defaultPrompt": [ "Use the orchestrator cycle to plan and ship the next slice.", - "Use idea-consultant to propose feature options from project docs.", - "Use error-checker to compare implementation against specs." + "Use idea-consultant to propose feature options from project docs after reading contexts/project-overview.md.", + "Use error-checker to compare implementation against specs, and replace contexts/project-overview.md when reusing the plugin in another repo." ], "brandColor": "#0F766E", "composerIcon": "./assets/subagent-kit.svg", diff --git a/plugins/subagent-orchestration-kit/LICENSE b/plugins/subagent-orchestration-kit/LICENSE new file mode 100644 index 0000000..14fac91 --- /dev/null +++ b/plugins/subagent-orchestration-kit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/subagent-orchestration-kit/README.md b/plugins/subagent-orchestration-kit/README.md index e5d6b34..f4fae62 100644 --- a/plugins/subagent-orchestration-kit/README.md +++ b/plugins/subagent-orchestration-kit/README.md @@ -26,10 +26,15 @@ It does **not** add new first-class agent types to Codex. Instead, it gives you ## Current repo context -This copy includes a project context file for the current HackHub direction: +This copy includes two context files: +- [contexts/project-overview.md](./contexts/project-overview.md) - [contexts/hackhub-product-overview.md](./contexts/hackhub-product-overview.md) +Use `contexts/project-overview.md` as the canonical file that the skills read. +In this repo it currently mirrors the HackHub direction. In another repo, replace +that file before you use the skills. + That context merges: - `changeHack` @@ -40,7 +45,7 @@ That context merges: ## How to use in this repo 1. Start with `orchestrator-cycle`. - Use [contexts/hackhub-product-overview.md](./contexts/hackhub-product-overview.md) as the project overview file. + Use [contexts/project-overview.md](./contexts/project-overview.md) as the project overview file. 2. Use `idea-consultant` to decide the smallest valuable slice. 3. Give `coding-worker` exclusive ownership of the write scope. 4. Run `error-checker` against docs and code. @@ -50,9 +55,9 @@ That context merges: ## How to reuse in another repo 1. Copy `plugins/subagent-orchestration-kit` into the target repo. -2. Copy or recreate `.agents/plugins/marketplace.json`, or merge the plugin entry into the target repo marketplace. -3. Replace `contexts/hackhub-product-overview.md` with a project-specific overview based on [contexts/project-overview-template.md](./contexts/project-overview-template.md). -4. Update the source-of-truth file list inside the skill docs if the target repo uses different spec paths. +2. Merge the plugin entry into the target repo's `.agents/plugins/marketplace.json`. +3. Replace `contexts/project-overview.md` with a project-specific overview based on [contexts/project-overview-template.md](./contexts/project-overview-template.md). +4. Update `contexts/project-overview.md` so it points to the target repo's real source-of-truth specs and architecture docs. 5. Keep the same role split unless the target repo needs fewer roles. ## Portability note diff --git a/plugins/subagent-orchestration-kit/contexts/project-overview.md b/plugins/subagent-orchestration-kit/contexts/project-overview.md new file mode 100644 index 0000000..9876f56 --- /dev/null +++ b/plugins/subagent-orchestration-kit/contexts/project-overview.md @@ -0,0 +1,12 @@ +# Project Overview + +This file is the canonical project context that the bundled skills read. + +Replace it before reusing this plugin in another repository. + +In the current HackHub workspace, the active project context is: + +- [hackhub-product-overview.md](./hackhub-product-overview.md) + +Use that file as the current repo-specific source of truth until this file is +replaced with a repo-specific version. diff --git a/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md b/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md index d51a930..7c92f6d 100644 --- a/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/coding-worker/SKILL.md @@ -16,7 +16,7 @@ This role owns one bounded write scope and should avoid expanding the task. - exact acceptance criteria - the files or modules this role owns - known constraints from the project overview - - in this repo: `../../contexts/hackhub-product-overview.md` + - canonical path: `../../contexts/project-overview.md` - any test requirements for the slice ## Required behavior diff --git a/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md b/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md index 74fbcaf..6b52fbc 100644 --- a/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/error-checker/SKILL.md @@ -14,8 +14,8 @@ This role is not only a bug checker. It checks whether the repository is moving ## Required inputs - the project overview file - - in this repo: `../../contexts/hackhub-product-overview.md` - - in another repo: the project-specific overview that replaced it + - canonical path: `../../contexts/project-overview.md` + - replace that file when reusing the plugin in another repo - relevant specs and architecture docs - the code or plan under review diff --git a/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md b/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md index 856df6a..b3c02de 100644 --- a/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/idea-consultant/SKILL.md @@ -14,8 +14,8 @@ This role turns product docs, architecture docs, and implementation gaps into a ## Required inputs - the project overview file - - in this repo: `../../contexts/hackhub-product-overview.md` - - in another repo: the project-specific overview that replaced it + - canonical path: `../../contexts/project-overview.md` + - replace that file when reusing the plugin in another repo - the main product spec - current gap analysis or known missing features - any direct user overrides from the current thread diff --git a/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md b/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md index 7147d10..812729f 100644 --- a/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md +++ b/plugins/subagent-orchestration-kit/skills/orchestrator-cycle/SKILL.md @@ -21,8 +21,8 @@ This role is the coordinator. It does not own every edit itself. It owns: Before proposing work: 1. Read the project overview under `../../contexts/`. - In this repo, that file is `../../contexts/hackhub-product-overview.md`. - In another repo, use the project-specific overview that replaced it. + The canonical file is `../../contexts/project-overview.md`. + Replace that file before reusing the plugin in another repo. 2. Read the product and architecture docs that the overview points to. 3. Identify the smallest vertical slice that reduces the most important gap. 4. Write down acceptance criteria before implementation starts. @@ -63,4 +63,5 @@ For each loop, produce: ## Reuse guidance -When reused in another repo, replace the project overview file first. The orchestration rules can stay mostly unchanged. +When reused in another repo, replace `../../contexts/project-overview.md` first. +The orchestration rules can stay mostly unchanged. From 28a3519cb9c82812ee665013bab1c0820333a8ab Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 03:46:04 +0900 Subject: [PATCH 06/36] feat: add hackathon scoped mentor board --- apps/api/cmd/server/main.go | 1 + .../db/migrations/007_create_hackathons.sql | 39 +++++++ apps/api/db/queries/hackathons.sql | 8 ++ apps/api/db/queries/progress.sql | 15 ++- apps/api/db/queries/questions.sql | 15 ++- apps/api/db/queries/teams.sql | 17 ++- apps/api/db/sqlcgen/hackathons.sql.go | 49 +++++++++ apps/api/db/sqlcgen/models.go | 26 +++-- apps/api/db/sqlcgen/progress.sql.go | 74 ++++++++++--- apps/api/db/sqlcgen/questions.sql.go | 103 +++++++++++++----- apps/api/db/sqlcgen/teams.sql.go | 74 ++++++++++--- apps/api/internal/api/handler.go | 77 ++++++++++--- apps/api/internal/api/response.go | 54 ++++++--- apps/api/internal/slack/handler.go | 64 +++++------ apps/api/internal/slack/socketmode.go | 28 ++--- apps/console/app/mentor/progress/page.tsx | 50 ++++++--- .../mentor/progress/teams/[teamId]/page.tsx | 51 +++++++-- apps/console/app/mentor/questions/page.tsx | 36 +++++- apps/console/app/page.tsx | 10 +- .../shared/components/hackathon-switcher.tsx | 36 ++++++ apps/console/shared/lib/api.ts | 25 ++++- apps/console/shared/lib/hackathon-scope.ts | 34 ++++++ apps/worker/src/db/progress.ts | 6 +- apps/worker/src/db/questions.ts | 6 +- apps/worker/src/db/teams.ts | 29 +++++ apps/worker/src/workers/progress.ts | 9 +- apps/worker/src/workers/question.ts | 9 +- .../10_combined-product-gap-analysis.md | 13 ++- docs/detail/11_opportunity-cycle-1-plan.md | 35 +++--- docs/specification.md | 9 +- 30 files changed, 775 insertions(+), 227 deletions(-) create mode 100644 apps/api/db/migrations/007_create_hackathons.sql create mode 100644 apps/api/db/queries/hackathons.sql create mode 100644 apps/api/db/sqlcgen/hackathons.sql.go create mode 100644 apps/console/shared/components/hackathon-switcher.tsx create mode 100644 apps/console/shared/lib/hackathon-scope.ts create mode 100644 apps/worker/src/db/teams.ts diff --git a/apps/api/cmd/server/main.go b/apps/api/cmd/server/main.go index f46fcad..705d100 100644 --- a/apps/api/cmd/server/main.go +++ b/apps/api/cmd/server/main.go @@ -53,6 +53,7 @@ func main() { // REST API routes for the dashboard api := r.Group("/api") { + api.GET("/hackathons", apihandler.HandleListHackathons(sqlDB)) api.GET("/questions", apihandler.HandleListQuestions(sqlDB)) api.GET("/questions/:id", apihandler.HandleGetQuestion(sqlDB)) api.GET("/progress", apihandler.HandleListProgress(sqlDB)) diff --git a/apps/api/db/migrations/007_create_hackathons.sql b/apps/api/db/migrations/007_create_hackathons.sql new file mode 100644 index 0000000..30cf0c3 --- /dev/null +++ b/apps/api/db/migrations/007_create_hackathons.sql @@ -0,0 +1,39 @@ +-- +goose Up +CREATE TABLE hackathons ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +ALTER TABLE teams + ADD COLUMN hackathon_id UUID REFERENCES hackathons (id), + ADD COLUMN slack_workspace_id TEXT; + +INSERT INTO hackathons (id, slug, name) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'default-hackathon', + 'Default Hackathon' +); + +UPDATE teams +SET hackathon_id = '00000000-0000-0000-0000-000000000001' +WHERE hackathon_id IS NULL; + +ALTER TABLE teams + ALTER COLUMN hackathon_id SET NOT NULL; + +CREATE INDEX idx_teams_hackathon_id ON teams (hackathon_id); +CREATE INDEX idx_teams_slack_scope ON teams (slack_workspace_id, slack_channel_id); + +-- +goose Down +DROP INDEX idx_teams_slack_scope; +DROP INDEX idx_teams_hackathon_id; + +ALTER TABLE teams + DROP COLUMN slack_workspace_id, + DROP COLUMN hackathon_id; + +DROP TABLE hackathons; diff --git a/apps/api/db/queries/hackathons.sql b/apps/api/db/queries/hackathons.sql new file mode 100644 index 0000000..5799132 --- /dev/null +++ b/apps/api/db/queries/hackathons.sql @@ -0,0 +1,8 @@ +-- name: ListHackathons :many +SELECT + * +FROM + hackathons +ORDER BY + created_at ASC, + name ASC; diff --git a/apps/api/db/queries/progress.sql b/apps/api/db/queries/progress.sql index 80a13b1..21fcc26 100644 --- a/apps/api/db/queries/progress.sql +++ b/apps/api/db/queries/progress.sql @@ -16,20 +16,25 @@ RETURNING -- name: ListProgressLogs :many SELECT - * + progress_logs.* FROM progress_logs + LEFT JOIN teams ON teams.id = progress_logs.team_id WHERE ( sqlc.narg (team_id)::uuid IS NULL - OR team_id = sqlc.narg (team_id)::uuid + OR progress_logs.team_id = sqlc.narg (team_id)::uuid + ) + AND ( + sqlc.narg (hackathon_id)::uuid IS NULL + OR teams.hackathon_id = sqlc.narg (hackathon_id)::uuid ) ORDER BY - created_at DESC + progress_logs.created_at DESC LIMIT - $2 + sqlc.arg(page_limit)::int OFFSET - $3; + sqlc.arg(page_offset)::int; -- name: LatestProgressPerTeam :many SELECT DISTINCT diff --git a/apps/api/db/queries/questions.sql b/apps/api/db/queries/questions.sql index 76820df..a56048d 100644 --- a/apps/api/db/queries/questions.sql +++ b/apps/api/db/queries/questions.sql @@ -35,20 +35,25 @@ WHERE -- name: ListQuestions :many SELECT - * + questions.* FROM questions + LEFT JOIN teams ON teams.id = questions.team_id WHERE ( sqlc.narg (status)::text IS NULL - OR status = sqlc.narg (status) + OR questions.status = sqlc.narg (status)::text + ) + AND ( + sqlc.narg (hackathon_id)::uuid IS NULL + OR teams.hackathon_id = sqlc.narg (hackathon_id)::uuid ) ORDER BY - created_at DESC + questions.created_at DESC LIMIT - $2 + sqlc.arg(page_limit)::int OFFSET - $3; + sqlc.arg(page_offset)::int; -- name: UpdateQuestionStatus :exec UPDATE questions diff --git a/apps/api/db/queries/teams.sql b/apps/api/db/queries/teams.sql index 7e6902b..f4b2bcf 100644 --- a/apps/api/db/queries/teams.sql +++ b/apps/api/db/queries/teams.sql @@ -11,14 +11,27 @@ SELECT * FROM teams +WHERE + ( + sqlc.narg (hackathon_id)::uuid IS NULL + OR hackathon_id = sqlc.narg (hackathon_id)::uuid + ) ORDER BY name; -- name: InsertTeam :one INSERT INTO - teams (name, slack_channel_id, tech_stack, phase, is_sos) + teams ( + name, + slack_channel_id, + tech_stack, + phase, + is_sos, + hackathon_id, + slack_workspace_id + ) VALUES - ($1, $2, $3, $4, $5) + ($1, $2, $3, $4, $5, $6, $7) RETURNING *; diff --git a/apps/api/db/sqlcgen/hackathons.sql.go b/apps/api/db/sqlcgen/hackathons.sql.go new file mode 100644 index 0000000..d971b92 --- /dev/null +++ b/apps/api/db/sqlcgen/hackathons.sql.go @@ -0,0 +1,49 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: hackathons.sql + +package sqlcgen + +import ( + "context" +) + +const listHackathons = `-- name: ListHackathons :many +SELECT + id, slug, name, created_at, updated_at +FROM + hackathons +ORDER BY + created_at ASC, + name ASC +` + +func (q *Queries) ListHackathons(ctx context.Context) ([]Hackathon, error) { + rows, err := q.db.QueryContext(ctx, listHackathons) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Hackathon{} + for rows.Next() { + var i Hackathon + if err := rows.Scan( + &i.ID, + &i.Slug, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/apps/api/db/sqlcgen/models.go b/apps/api/db/sqlcgen/models.go index f9665ec..388d591 100644 --- a/apps/api/db/sqlcgen/models.go +++ b/apps/api/db/sqlcgen/models.go @@ -12,6 +12,14 @@ import ( "github.com/sqlc-dev/pqtype" ) +type Hackathon struct { + ID uuid.UUID `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type KnowledgeItem struct { ID uuid.UUID `json:"id"` Category string `json:"category"` @@ -75,12 +83,14 @@ type SlackEvent struct { } type Team struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - SlackChannelID sql.NullString `json:"slack_channel_id"` - TechStack []string `json:"tech_stack"` - Phase string `json:"phase"` - IsSos bool `json:"is_sos"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + SlackChannelID sql.NullString `json:"slack_channel_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + HackathonID uuid.UUID `json:"hackathon_id"` + SlackWorkspaceID sql.NullString `json:"slack_workspace_id"` } diff --git a/apps/api/db/sqlcgen/progress.sql.go b/apps/api/db/sqlcgen/progress.sql.go index 8ab6329..24bb794 100644 --- a/apps/api/db/sqlcgen/progress.sql.go +++ b/apps/api/db/sqlcgen/progress.sql.go @@ -13,12 +13,20 @@ import ( ) const insertProgressLog = `-- name: InsertProgressLog :one -INSERT INTO progress_logs ( - team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_event_id -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -) -RETURNING id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at +INSERT INTO + progress_logs ( + team_id, + slack_user_id, + phase, + status_text, + blockers, + is_sos, + slack_event_id + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) +RETURNING + id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at ` type InsertProgressLogParams struct { @@ -58,9 +66,13 @@ func (q *Queries) InsertProgressLog(ctx context.Context, arg InsertProgressLogPa } const latestProgressPerTeam = `-- name: LatestProgressPerTeam :many -SELECT DISTINCT ON (team_id) id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at -FROM progress_logs -ORDER BY team_id, created_at DESC +SELECT DISTINCT + ON (team_id) id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at +FROM + progress_logs +ORDER BY + team_id, + created_at DESC ` func (q *Queries) LatestProgressPerTeam(ctx context.Context) ([]ProgressLog, error) { @@ -98,20 +110,42 @@ func (q *Queries) LatestProgressPerTeam(ctx context.Context) ([]ProgressLog, err } const listProgressLogs = `-- name: ListProgressLogs :many -SELECT id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at FROM progress_logs -WHERE ($1::uuid IS NULL OR team_id = $1) -ORDER BY created_at DESC -LIMIT $2 OFFSET $3 +SELECT + progress_logs.id, progress_logs.team_id, progress_logs.slack_user_id, progress_logs.phase, progress_logs.status_text, progress_logs.blockers, progress_logs.is_sos, progress_logs.slack_message_ts, progress_logs.slack_event_id, progress_logs.created_at +FROM + progress_logs + LEFT JOIN teams ON teams.id = progress_logs.team_id +WHERE + ( + $1::uuid IS NULL + OR progress_logs.team_id = $1::uuid + ) + AND ( + $2::uuid IS NULL + OR teams.hackathon_id = $2::uuid + ) +ORDER BY + progress_logs.created_at DESC +LIMIT + $4::int +OFFSET + $3::int ` type ListProgressLogsParams struct { - TeamID uuid.NullUUID `json:"team_id"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + TeamID uuid.NullUUID `json:"team_id"` + HackathonID uuid.NullUUID `json:"hackathon_id"` + PageOffset int32 `json:"page_offset"` + PageLimit int32 `json:"page_limit"` } func (q *Queries) ListProgressLogs(ctx context.Context, arg ListProgressLogsParams) ([]ProgressLog, error) { - rows, err := q.db.QueryContext(ctx, listProgressLogs, arg.TeamID, arg.Limit, arg.Offset) + rows, err := q.db.QueryContext(ctx, listProgressLogs, + arg.TeamID, + arg.HackathonID, + arg.PageOffset, + arg.PageLimit, + ) if err != nil { return nil, err } @@ -145,7 +179,11 @@ func (q *Queries) ListProgressLogs(ctx context.Context, arg ListProgressLogsPara } const updateProgressSlackTS = `-- name: UpdateProgressSlackTS :exec -UPDATE progress_logs SET slack_message_ts = $2 WHERE id = $1 +UPDATE progress_logs +SET + slack_message_ts = $2 +WHERE + id = $1 ` type UpdateProgressSlackTSParams struct { diff --git a/apps/api/db/sqlcgen/questions.sql.go b/apps/api/db/sqlcgen/questions.sql.go index 3540ad0..869d14a 100644 --- a/apps/api/db/sqlcgen/questions.sql.go +++ b/apps/api/db/sqlcgen/questions.sql.go @@ -14,7 +14,12 @@ import ( ) const getQuestion = `-- name: GetQuestion :one -SELECT id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at FROM questions WHERE id = $1 +SELECT + id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at +FROM + questions +WHERE + id = $1 ` func (q *Queries) GetQuestion(ctx context.Context, id uuid.UUID) (Question, error) { @@ -44,7 +49,12 @@ func (q *Queries) GetQuestion(ctx context.Context, id uuid.UUID) (Question, erro } const getQuestionByEventID = `-- name: GetQuestionByEventID :one -SELECT id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at FROM questions WHERE slack_event_id = $1 +SELECT + id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at +FROM + questions +WHERE + slack_event_id = $1 ` func (q *Queries) GetQuestionByEventID(ctx context.Context, slackEventID sql.NullString) (Question, error) { @@ -74,13 +84,23 @@ func (q *Queries) GetQuestionByEventID(ctx context.Context, slackEventID sql.Nul } const insertQuestion = `-- name: InsertQuestion :one -INSERT INTO questions ( - team_id, slack_user_id, slack_thread_ts, slack_channel_id, - title, body, error_message, tried, status, slack_event_id -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, 'pending', $9 -) -RETURNING id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at +INSERT INTO + questions ( + team_id, + slack_user_id, + slack_thread_ts, + slack_channel_id, + title, + body, + error_message, + tried, + status, + slack_event_id + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, 'pending', $9) +RETURNING + id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at ` type InsertQuestionParams struct { @@ -132,20 +152,42 @@ func (q *Queries) InsertQuestion(ctx context.Context, arg InsertQuestionParams) } const listQuestions = `-- name: ListQuestions :many -SELECT id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at FROM questions -WHERE ($1::text IS NULL OR status = $1) -ORDER BY created_at DESC -LIMIT $2 OFFSET $3 +SELECT + questions.id, questions.team_id, questions.slack_user_id, questions.slack_thread_ts, questions.slack_channel_id, questions.title, questions.body, questions.error_message, questions.tried, questions.triage_result, questions.category, questions.confidence, questions.status, questions.escalated_to_mentor, questions.mentor_channel_ts, questions.slack_event_id, questions.created_at, questions.updated_at +FROM + questions + LEFT JOIN teams ON teams.id = questions.team_id +WHERE + ( + $1::text IS NULL + OR questions.status = $1::text + ) + AND ( + $2::uuid IS NULL + OR teams.hackathon_id = $2::uuid + ) +ORDER BY + questions.created_at DESC +LIMIT + $4::int +OFFSET + $3::int ` type ListQuestionsParams struct { - Status sql.NullString `json:"status"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Status sql.NullString `json:"status"` + HackathonID uuid.NullUUID `json:"hackathon_id"` + PageOffset int32 `json:"page_offset"` + PageLimit int32 `json:"page_limit"` } func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]Question, error) { - rows, err := q.db.QueryContext(ctx, listQuestions, arg.Status, arg.Limit, arg.Offset) + rows, err := q.db.QueryContext(ctx, listQuestions, + arg.Status, + arg.HackathonID, + arg.PageOffset, + arg.PageLimit, + ) if err != nil { return nil, err } @@ -190,10 +232,11 @@ const updateQuestionEscalated = `-- name: UpdateQuestionEscalated :exec UPDATE questions SET escalated_to_mentor = TRUE, - mentor_channel_ts = $2, - status = 'escalated', - updated_at = NOW() -WHERE id = $1 + mentor_channel_ts = $2, + status = 'escalated', + updated_at = NOW() +WHERE + id = $1 ` type UpdateQuestionEscalatedParams struct { @@ -208,8 +251,11 @@ func (q *Queries) UpdateQuestionEscalated(ctx context.Context, arg UpdateQuestio const updateQuestionStatus = `-- name: UpdateQuestionStatus :exec UPDATE questions -SET status = $2, updated_at = NOW() -WHERE id = $1 +SET + status = $2, + updated_at = NOW() +WHERE + id = $1 ` type UpdateQuestionStatusParams struct { @@ -226,11 +272,12 @@ const updateQuestionTriage = `-- name: UpdateQuestionTriage :exec UPDATE questions SET triage_result = $2, - category = $3, - confidence = $4, - status = $5, - updated_at = NOW() -WHERE id = $1 + category = $3, + confidence = $4, + status = $5, + updated_at = NOW() +WHERE + id = $1 ` type UpdateQuestionTriageParams struct { diff --git a/apps/api/db/sqlcgen/teams.sql.go b/apps/api/db/sqlcgen/teams.sql.go index 5874bf4..7f58b48 100644 --- a/apps/api/db/sqlcgen/teams.sql.go +++ b/apps/api/db/sqlcgen/teams.sql.go @@ -14,7 +14,12 @@ import ( ) const getTeam = `-- name: GetTeam :one -SELECT id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at FROM teams WHERE id = $1 +SELECT + id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at, hackathon_id, slack_workspace_id +FROM + teams +WHERE + id = $1 ` func (q *Queries) GetTeam(ctx context.Context, id uuid.UUID) (Team, error) { @@ -29,22 +34,37 @@ func (q *Queries) GetTeam(ctx context.Context, id uuid.UUID) (Team, error) { &i.IsSos, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, + &i.SlackWorkspaceID, ) return i, err } const insertTeam = `-- name: InsertTeam :one -INSERT INTO teams (name, slack_channel_id, tech_stack, phase, is_sos) -VALUES ($1, $2, $3, $4, $5) -RETURNING id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at +INSERT INTO + teams ( + name, + slack_channel_id, + tech_stack, + phase, + is_sos, + hackathon_id, + slack_workspace_id + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) +RETURNING + id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at, hackathon_id, slack_workspace_id ` type InsertTeamParams struct { - Name string `json:"name"` - SlackChannelID sql.NullString `json:"slack_channel_id"` - TechStack []string `json:"tech_stack"` - Phase string `json:"phase"` - IsSos bool `json:"is_sos"` + Name string `json:"name"` + SlackChannelID sql.NullString `json:"slack_channel_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + HackathonID uuid.UUID `json:"hackathon_id"` + SlackWorkspaceID sql.NullString `json:"slack_workspace_id"` } func (q *Queries) InsertTeam(ctx context.Context, arg InsertTeamParams) (Team, error) { @@ -54,6 +74,8 @@ func (q *Queries) InsertTeam(ctx context.Context, arg InsertTeamParams) (Team, e pq.Array(arg.TechStack), arg.Phase, arg.IsSos, + arg.HackathonID, + arg.SlackWorkspaceID, ) var i Team err := row.Scan( @@ -65,16 +87,28 @@ func (q *Queries) InsertTeam(ctx context.Context, arg InsertTeamParams) (Team, e &i.IsSos, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, + &i.SlackWorkspaceID, ) return i, err } const listTeams = `-- name: ListTeams :many -SELECT id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at FROM teams ORDER BY name +SELECT + id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at, hackathon_id, slack_workspace_id +FROM + teams +WHERE + ( + $1::uuid IS NULL + OR hackathon_id = $1::uuid + ) +ORDER BY + name ` -func (q *Queries) ListTeams(ctx context.Context) ([]Team, error) { - rows, err := q.db.QueryContext(ctx, listTeams) +func (q *Queries) ListTeams(ctx context.Context, hackathonID uuid.NullUUID) ([]Team, error) { + rows, err := q.db.QueryContext(ctx, listTeams, hackathonID) if err != nil { return nil, err } @@ -91,6 +125,8 @@ func (q *Queries) ListTeams(ctx context.Context) ([]Team, error) { &i.IsSos, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, + &i.SlackWorkspaceID, ); err != nil { return nil, err } @@ -106,7 +142,12 @@ func (q *Queries) ListTeams(ctx context.Context) ([]Team, error) { } const updateTeamPhase = `-- name: UpdateTeamPhase :exec -UPDATE teams SET phase = $2, updated_at = NOW() WHERE id = $1 +UPDATE teams +SET + phase = $2, + updated_at = NOW() +WHERE + id = $1 ` type UpdateTeamPhaseParams struct { @@ -120,7 +161,12 @@ func (q *Queries) UpdateTeamPhase(ctx context.Context, arg UpdateTeamPhaseParams } const updateTeamSOS = `-- name: UpdateTeamSOS :exec -UPDATE teams SET is_sos = $2, updated_at = NOW() WHERE id = $1 +UPDATE teams +SET + is_sos = $2, + updated_at = NOW() +WHERE + id = $1 ` type UpdateTeamSOSParams struct { diff --git a/apps/api/internal/api/handler.go b/apps/api/internal/api/handler.go index 3b858c2..6ecdee3 100644 --- a/apps/api/internal/api/handler.go +++ b/apps/api/internal/api/handler.go @@ -16,19 +16,54 @@ const ( maxLimit = 100 ) +func parseUUIDQuery(c *gin.Context, key string) (uuid.NullUUID, bool) { + raw := c.Query(key) + if raw == "" { + return uuid.NullUUID{}, true + } + parsed, err := uuid.Parse(raw) + if err != nil { + return uuid.NullUUID{}, false + } + return uuid.NullUUID{UUID: parsed, Valid: true}, true +} + +// HandleListHackathons handles GET /api/hackathons. +func HandleListHackathons(db *sql.DB) gin.HandlerFunc { + q := sqlcgen.New(db) + return func(c *gin.Context) { + hackathons, err := q.ListHackathons(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) + return + } + resp := make([]HackathonResponse, len(hackathons)) + for i, hackathon := range hackathons { + resp[i] = toHackathonResponse(hackathon) + } + c.JSON(http.StatusOK, resp) + } +} + // HandleListQuestions handles GET /api/questions. -// Optional query params: status (string), limit (int, default 20), offset (int, default 0). +// Optional query params: status (string), hackathon_id (uuid), limit (int, default 20), offset (int, default 0). func HandleListQuestions(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) return func(c *gin.Context) { status := c.Query("status") + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return + } limit := parseIntQuery(c, "limit", defaultLimit, maxLimit) offset := parseIntQuery(c, "offset", 0, -1) questions, err := q.ListQuestions(c.Request.Context(), sqlcgen.ListQuestionsParams{ - Status: sql.NullString{String: status, Valid: status != ""}, - Limit: int32(limit), - Offset: int32(offset), + Status: sql.NullString{String: status, Valid: status != ""}, + HackathonID: hackathonID, + PageLimit: int32(limit), + PageOffset: int32(offset), }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) @@ -65,27 +100,29 @@ func HandleGetQuestion(db *sql.DB) gin.HandlerFunc { } // HandleListProgress handles GET /api/progress. -// Optional query params: team_id (uuid), limit, offset. +// Optional query params: team_id (uuid), hackathon_id (uuid), limit, offset. func HandleListProgress(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) return func(c *gin.Context) { limit := parseIntQuery(c, "limit", defaultLimit, maxLimit) offset := parseIntQuery(c, "offset", 0, -1) - var teamID uuid.NullUUID - if raw := c.Query("team_id"); raw != "" { - parsed, err := uuid.Parse(raw) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team_id"}) - return - } - teamID = uuid.NullUUID{UUID: parsed, Valid: true} + teamID, ok := parseUUIDQuery(c, "team_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team_id"}) + return + } + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return } logs, err := q.ListProgressLogs(c.Request.Context(), sqlcgen.ListProgressLogsParams{ - TeamID: teamID, - Limit: int32(limit), - Offset: int32(offset), + TeamID: teamID, + HackathonID: hackathonID, + PageLimit: int32(limit), + PageOffset: int32(offset), }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) @@ -100,10 +137,16 @@ func HandleListProgress(db *sql.DB) gin.HandlerFunc { } // HandleListTeams handles GET /api/teams. +// Optional query params: hackathon_id (uuid). func HandleListTeams(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) return func(c *gin.Context) { - teams, err := q.ListTeams(c.Request.Context()) + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return + } + teams, err := q.ListTeams(c.Request.Context(), hackathonID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return diff --git a/apps/api/internal/api/response.go b/apps/api/internal/api/response.go index 77deb09..8f9f4aa 100644 --- a/apps/api/internal/api/response.go +++ b/apps/api/internal/api/response.go @@ -30,6 +30,14 @@ type QuestionResponse struct { UpdatedAt time.Time `json:"updated_at"` } +type HackathonResponse struct { + ID uuid.UUID `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type ProgressLogResponse struct { ID uuid.UUID `json:"id"` TeamID *string `json:"team_id"` @@ -44,14 +52,26 @@ type ProgressLogResponse struct { } type TeamResponse struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - SlackChannelID *string `json:"slack_channel_id"` - TechStack []string `json:"tech_stack"` - Phase string `json:"phase"` - IsSos bool `json:"is_sos"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + SlackChannelID *string `json:"slack_channel_id"` + SlackWorkspaceID *string `json:"slack_workspace_id"` + HackathonID string `json:"hackathon_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func toHackathonResponse(h sqlcgen.Hackathon) HackathonResponse { + return HackathonResponse{ + ID: h.ID, + Slug: h.Slug, + Name: h.Name, + CreatedAt: h.CreatedAt, + UpdatedAt: h.UpdatedAt, + } } func toQuestionResponse(q sqlcgen.Question) QuestionResponse { @@ -129,16 +149,20 @@ func toProgressLogResponse(p sqlcgen.ProgressLog) ProgressLogResponse { func toTeamResponse(t sqlcgen.Team) TeamResponse { r := TeamResponse{ - ID: t.ID, - Name: t.Name, - TechStack: t.TechStack, - Phase: t.Phase, - IsSos: t.IsSos, - CreatedAt: t.CreatedAt, - UpdatedAt: t.UpdatedAt, + ID: t.ID, + Name: t.Name, + HackathonID: t.HackathonID.String(), + TechStack: t.TechStack, + Phase: t.Phase, + IsSos: t.IsSos, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, } if t.SlackChannelID.Valid { r.SlackChannelID = &t.SlackChannelID.String } + if t.SlackWorkspaceID.Valid { + r.SlackWorkspaceID = &t.SlackWorkspaceID.String + } return r } diff --git a/apps/api/internal/slack/handler.go b/apps/api/internal/slack/handler.go index e97cfab..0e917f3 100644 --- a/apps/api/internal/slack/handler.go +++ b/apps/api/internal/slack/handler.go @@ -18,7 +18,7 @@ import ( // HandleSlashCommand handles POST /slack/commands. // Slack sends application/x-www-form-urlencoded with fields: -// command, trigger_id, user_id, team_id, channel_id, text. +// command, trigger_id, user_id, team_id (workspace id), channel_id, text. func HandleSlashCommand(cfg *config.Config, db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { // Read raw body first so we can verify the signature. @@ -166,13 +166,13 @@ func HandleInteraction(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender) gin func handleQuestionSubmission(ctx context.Context, sqsClient sqs.Sender, p *interactionPayload) error { vals := p.View.State.Values msg := questionNewMessage{ - UserID: p.User.ID, - TeamID: p.Team.ID, - ChannelID: p.View.PrivateMetadata, - Title: strVal(vals, "title_block", "title"), - Body: strVal(vals, "body_block", "body"), - ErrorMessage: strVal(vals, "error_block", "error_message"), - Tried: strVal(vals, "tried_block", "tried"), + UserID: p.User.ID, + SlackWorkspaceID: p.Team.ID, + ChannelID: p.View.PrivateMetadata, + Title: strVal(vals, "title_block", "title"), + Body: strVal(vals, "body_block", "body"), + ErrorMessage: strVal(vals, "error_block", "error_message"), + Tried: strVal(vals, "tried_block", "tried"), } body, err := json.Marshal(msg) if err != nil { @@ -186,13 +186,13 @@ func handleProgressSubmission(ctx context.Context, sqsClient sqs.Sender, p *inte vals := p.View.State.Values sosChecked := len(vals["sos_block"]["sos_flag"].SelectedOptions) > 0 msg := progressMessage{ - UserID: p.User.ID, - TeamID: p.Team.ID, - ChannelID: p.View.PrivateMetadata, - Phase: selectedVal(vals, "phase_block", "phase"), - CurrentStatus: strVal(vals, "status_block", "current_status"), - Blockers: strVal(vals, "blockers_block", "blockers"), - SOSFlag: sosChecked, + UserID: p.User.ID, + SlackWorkspaceID: p.Team.ID, + ChannelID: p.View.PrivateMetadata, + Phase: selectedVal(vals, "phase_block", "phase"), + CurrentStatus: strVal(vals, "status_block", "current_status"), + Blockers: strVal(vals, "blockers_block", "blockers"), + SOSFlag: sosChecked, } body, err := json.Marshal(msg) if err != nil { @@ -270,9 +270,9 @@ type viewState struct { type stateValues map[string]map[string]elementState type elementState struct { - Type string `json:"type"` - Value string `json:"value"` - SelectedOption *selectOption `json:"selected_option"` + Type string `json:"type"` + Value string `json:"value"` + SelectedOption *selectOption `json:"selected_option"` SelectedOptions []selectOption `json:"selected_options"` } @@ -286,21 +286,21 @@ type selectOption struct { // --- SQS message types --- type questionNewMessage struct { - UserID string `json:"user_id"` - TeamID string `json:"team_id"` - ChannelID string `json:"channel_id"` - Title string `json:"title"` - Body string `json:"body,omitempty"` - ErrorMessage string `json:"error_message,omitempty"` - Tried string `json:"tried,omitempty"` + UserID string `json:"user_id"` + SlackWorkspaceID string `json:"slack_workspace_id"` + ChannelID string `json:"channel_id"` + Title string `json:"title"` + Body string `json:"body,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + Tried string `json:"tried,omitempty"` } type progressMessage struct { - UserID string `json:"user_id"` - TeamID string `json:"team_id"` - ChannelID string `json:"channel_id"` - Phase string `json:"phase"` - CurrentStatus string `json:"current_status,omitempty"` - Blockers string `json:"blockers,omitempty"` - SOSFlag bool `json:"sos_flag"` + UserID string `json:"user_id"` + SlackWorkspaceID string `json:"slack_workspace_id"` + ChannelID string `json:"channel_id"` + Phase string `json:"phase"` + CurrentStatus string `json:"current_status,omitempty"` + Blockers string `json:"blockers,omitempty"` + SOSFlag bool `json:"sos_flag"` } diff --git a/apps/api/internal/slack/socketmode.go b/apps/api/internal/slack/socketmode.go index 62e5b33..d0fe43f 100644 --- a/apps/api/internal/slack/socketmode.go +++ b/apps/api/internal/slack/socketmode.go @@ -422,13 +422,13 @@ type questionFollowupMessage struct { func enqueueQuestion(sqsClient sqs.Sender, cb *goslack.InteractionCallback) error { vals := cb.View.State.Values msg := questionNewMessage{ - UserID: cb.User.ID, - TeamID: cb.Team.ID, - ChannelID: cb.View.PrivateMetadata, - Title: blockVal(vals, "title_block", "title"), - Body: blockVal(vals, "body_block", "body"), - ErrorMessage: blockVal(vals, "error_block", "error_message"), - Tried: blockVal(vals, "tried_block", "tried"), + UserID: cb.User.ID, + SlackWorkspaceID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Title: blockVal(vals, "title_block", "title"), + Body: blockVal(vals, "body_block", "body"), + ErrorMessage: blockVal(vals, "error_block", "error_message"), + Tried: blockVal(vals, "tried_block", "tried"), } body, err := json.Marshal(msg) if err != nil { @@ -441,13 +441,13 @@ func enqueueProgress(sqsClient sqs.Sender, cb *goslack.InteractionCallback) erro vals := cb.View.State.Values sosChecked := len(vals["sos_block"]["sos_flag"].SelectedOptions) > 0 msg := progressMessage{ - UserID: cb.User.ID, - TeamID: cb.Team.ID, - ChannelID: cb.View.PrivateMetadata, - Phase: blockSelectedVal(vals, "phase_block", "phase"), - CurrentStatus: blockVal(vals, "status_block", "current_status"), - Blockers: blockVal(vals, "blockers_block", "blockers"), - SOSFlag: sosChecked, + UserID: cb.User.ID, + SlackWorkspaceID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Phase: blockSelectedVal(vals, "phase_block", "phase"), + CurrentStatus: blockVal(vals, "status_block", "current_status"), + Blockers: blockVal(vals, "blockers_block", "blockers"), + SOSFlag: sosChecked, } body, err := json.Marshal(msg) if err != nil { diff --git a/apps/console/app/mentor/progress/page.tsx b/apps/console/app/mentor/progress/page.tsx index 2ed0a05..9a95b96 100644 --- a/apps/console/app/mentor/progress/page.tsx +++ b/apps/console/app/mentor/progress/page.tsx @@ -1,14 +1,25 @@ import Link from "next/link" -import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" +import { fetchHackathons, fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { resolveActiveHackathon, type HackathonSearchParams, withHackathonScope } from "@/shared/lib/hackathon-scope" -export default async function MentorProgressPage() { - const [logs, teams] = await Promise.all([ - fetchProgressLogs({ limit: 30 }).catch(() => []), - fetchTeams().catch(() => []), - ]) +type MentorProgressPageProps = { + searchParams: Promise +} + +export default async function MentorProgressPage({ searchParams }: MentorProgressPageProps) { + const params = await searchParams + const hackathons = await fetchHackathons().catch(() => []) + const activeHackathon = resolveActiveHackathon(params, hackathons) + const [logs, teams] = activeHackathon + ? await Promise.all([ + fetchProgressLogs({ hackathon_id: activeHackathon.id, limit: 30 }).catch(() => null), + fetchTeams({ hackathon_id: activeHackathon.id }).catch(() => null), + ]) + : [[], []] - const teamMap = new Map(teams.map((team) => [team.id, team])) - const sosCount = logs.filter((log) => log.is_sos).length + const teamMap = new Map((teams ?? []).map((team) => [team.id, team])) + const sosCount = (logs ?? []).filter((log) => log.is_sos).length return (
@@ -19,20 +30,33 @@ export default async function MentorProgressPage() { Progress overview

- `progress-checker` で必要だった team progress detail への導線を新しい console 側に作っています。 + `progress-checker` で必要だった team progress detail への導線を、hackathon ごとに分けて新しい console 側に置いています。

+

SOS

{sosCount}

-

最新 progress logs のうち SOS フラグ付きの件数

+

選択中の hackathon の最新 progress logs に含まれる SOS 件数

- {logs.length === 0 ? ( + {hackathons.length === 0 ? ( +
+ hackathon がまだ設定されていません。migration と team backfill を先に適用してください。 +
+ ) : logs === null || teams === null ? ( +
+ progress board を読み込めませんでした。API 側の `hackathon_id` filter を確認してください。 +
+ ) : logs.length === 0 ? (
- progress データがまだありません。API 未接続またはデータ未投入の可能性があります。 + {activeHackathon?.name ?? "選択中の hackathon"} に progress データがまだありません。
) : ( logs.map((log) => { @@ -51,7 +75,7 @@ export default async function MentorProgressPage() {
{new Date(log.created_at).toLocaleString("ja-JP")} {log.team_id ? ( - + Team detail ) : null} diff --git a/apps/console/app/mentor/progress/teams/[teamId]/page.tsx b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx index dc61f34..a355005 100644 --- a/apps/console/app/mentor/progress/teams/[teamId]/page.tsx +++ b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx @@ -1,35 +1,66 @@ import Link from "next/link" -import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" +import { fetchHackathons, fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { isUuidLike, resolveActiveHackathon, type HackathonSearchParams, withHackathonScope } from "@/shared/lib/hackathon-scope" type TeamProgressDetailPageProps = { params: Promise<{ teamId: string }> + searchParams: Promise } -export default async function TeamProgressDetailPage({ params }: TeamProgressDetailPageProps) { +export default async function TeamProgressDetailPage({ params, searchParams }: TeamProgressDetailPageProps) { const { teamId } = await params - const [teams, logs] = await Promise.all([ - fetchTeams().catch(() => []), - fetchProgressLogs({ team_id: teamId, limit: 100 }).catch(() => []), - ]) + const resolvedSearchParams = await searchParams + const hackathons = await fetchHackathons().catch(() => []) + const activeHackathon = resolveActiveHackathon(resolvedSearchParams, hackathons) + const isValidTeamId = isUuidLike(teamId) + const [teams, logs] = activeHackathon && isValidTeamId + ? await Promise.all([ + fetchTeams({ hackathon_id: activeHackathon.id }).catch(() => null), + fetchProgressLogs({ hackathon_id: activeHackathon.id, team_id: teamId, limit: 100 }).catch(() => null), + ]) + : [[], []] - const team = teams.find((candidate) => candidate.id === teamId) + const team = (teams ?? []).find((candidate) => candidate.id === teamId) + const backHref = withHackathonScope("/mentor/progress", activeHackathon?.id ?? null) return (
- + Back to progress overview

{team?.name ?? "Unknown team"}

- `progress-checker` docs にあった team detail route を先に切り出しました。後続 slice で hackathon scope と role 判定を重ねます。 + `progress-checker` docs にあった team detail route に hackathon scope を重ねています。空状態と境界エラーを分けて表示します。

+
- {logs.length === 0 ? ( + {hackathons.length === 0 ? ( +
+ hackathon がまだ設定されていません。migration と team backfill を先に適用してください。 +
+ ) : !isValidTeamId ? ( +
+ URL の teamId が不正です。progress overview から遷移し直してください。 +
+ ) : logs === null || teams === null ? ( +
+ team detail を読み込めませんでした。API 側の `hackathon_id` filter を確認してください。 +
+ ) : !team ? ( +
+ この team は選択中の hackathon に属していないか、まだ scope backfill されていません。 +
+ ) : logs.length === 0 ? (
この team の progress log はまだありません。
diff --git a/apps/console/app/mentor/questions/page.tsx b/apps/console/app/mentor/questions/page.tsx index 9a5879f..61484e5 100644 --- a/apps/console/app/mentor/questions/page.tsx +++ b/apps/console/app/mentor/questions/page.tsx @@ -1,8 +1,19 @@ import Link from "next/link" -import { fetchQuestions } from "@/shared/lib/api" +import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" +import { fetchHackathons, fetchQuestions } from "@/shared/lib/api" +import { resolveActiveHackathon, type HackathonSearchParams } from "@/shared/lib/hackathon-scope" -export default async function MentorQuestionsPage() { - const questions = await fetchQuestions({ limit: 20 }).catch(() => []) +type MentorQuestionsPageProps = { + searchParams: Promise +} + +export default async function MentorQuestionsPage({ searchParams }: MentorQuestionsPageProps) { + const params = await searchParams + const hackathons = await fetchHackathons().catch(() => []) + const activeHackathon = resolveActiveHackathon(params, hackathons) + const questions = activeHackathon + ? await fetchQuestions({ hackathon_id: activeHackathon.id, limit: 20 }).catch(() => null) + : [] return (
@@ -12,14 +23,27 @@ export default async function MentorQuestionsPage() { Question queue

- 既存 API の質問一覧を新しい console で再利用しています。後続 slice で hackathon scope と web-auth をここに載せます。 + 既存 API の質問一覧を hackathon 単位で読み分けます。auth はまだ deferred ですが、board の event 混在はここで止めます。

+
- {questions.length === 0 ? ( + {hackathons.length === 0 ? ( +
+ hackathon がまだ設定されていません。migration と team backfill を先に適用してください。 +
+ ) : questions === null ? ( +
+ 質問一覧を読み込めませんでした。API 側で `hackathon_id` filter が反映されているか確認してください。 +
+ ) : questions.length === 0 ? (
- 質問データがまだありません。API 未接続またはデータ未投入の可能性があります。 + {activeHackathon?.name ?? "選択中の hackathon"} に質問データがまだありません。
) : ( questions.map((question) => ( diff --git a/apps/console/app/page.tsx b/apps/console/app/page.tsx index 4ee4d87..aea66a9 100644 --- a/apps/console/app/page.tsx +++ b/apps/console/app/page.tsx @@ -4,8 +4,8 @@ const roleCards = [ { href: "/mentor/questions", eyebrow: "Mentor", - title: "Question queue and progress overview", - body: "既存の `changeHack` API を読みながら、`progress-checker` の mentor 運用導線を新しい console に切り出します。", + title: "Hackathon-scoped mentor live board", + body: "既存の `changeHack` API を hackathon 単位で読み、`progress-checker` の mentor 導線を新しい console に移します。", }, { href: "/platform", @@ -30,7 +30,7 @@ const roleCards = [ const cycleSteps = [ "Step 1: docs と code の差分を固定し、単一イベント前提の drift を明文化する", "Step 2: 別 app の control-plane を作り、既存 apps/web と責務を分離する", - "Step 3: mentor 導線を実データ接続しながら multi-hackathon 化の受け皿を作る", + "Step 3: hackathon scope を read path に入れ、mentor 導線を event 混在なしで見られるようにする", ] export default function HomePage() { @@ -39,11 +39,11 @@ export default function HomePage() {

Cycle 1

- Start the hackathon-scoped live board before more single-event assumptions harden. + Start the hackathon-scoped mentor board before more single-event assumptions harden.

現在の `apps/web` は KCL 単一イベント向けの support dashboard です。ここではそれを壊さずに、 - hackathon scope を導入できる新しい board を `apps/console` 側で育てます。 + まず mentor の question / progress 導線に hackathon scope を入れられる新しい board を `apps/console` 側で育てます。

{roleCards.map((card) => ( diff --git a/apps/console/shared/components/hackathon-switcher.tsx b/apps/console/shared/components/hackathon-switcher.tsx new file mode 100644 index 0000000..db17bfc --- /dev/null +++ b/apps/console/shared/components/hackathon-switcher.tsx @@ -0,0 +1,36 @@ +import Link from "next/link" +import type { Hackathon } from "@/shared/lib/api" +import { withHackathonScope } from "@/shared/lib/hackathon-scope" + +type HackathonSwitcherProps = { + basePath: string + hackathons: Hackathon[] + activeHackathonId: string | null +} + +export function HackathonSwitcher({ basePath, hackathons, activeHackathonId }: HackathonSwitcherProps) { + if (hackathons.length === 0) { + return null + } + + return ( +
+ {hackathons.map((hackathon) => { + const isActive = hackathon.id === activeHackathonId + return ( + + {hackathon.name} + + ) + })} +
+ ) +} diff --git a/apps/console/shared/lib/api.ts b/apps/console/shared/lib/api.ts index e9c94bb..6b8d4fc 100644 --- a/apps/console/shared/lib/api.ts +++ b/apps/console/shared/lib/api.ts @@ -1,5 +1,13 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080" +export interface Hackathon { + id: string + slug: string + name: string + created_at: string + updated_at: string +} + export interface Question { id: string team_id: string | null @@ -30,6 +38,8 @@ export interface Team { id: string name: string slack_channel_id: string | null + slack_workspace_id: string | null + hackathon_id: string tech_stack: string[] phase: string is_sos: boolean @@ -52,11 +62,13 @@ async function apiFetch(path: string): Promise { export async function fetchQuestions(params?: { status?: string + hackathon_id?: string limit?: number offset?: number }): Promise { const query = new URLSearchParams() if (params?.status) query.set("status", params.status) + if (params?.hackathon_id) query.set("hackathon_id", params.hackathon_id) if (params?.limit != null) query.set("limit", String(params.limit)) if (params?.offset != null) query.set("offset", String(params.offset)) const queryString = query.toString() @@ -65,17 +77,26 @@ export async function fetchQuestions(params?: { export async function fetchProgressLogs(params?: { team_id?: string + hackathon_id?: string limit?: number offset?: number }): Promise { const query = new URLSearchParams() if (params?.team_id) query.set("team_id", params.team_id) + if (params?.hackathon_id) query.set("hackathon_id", params.hackathon_id) if (params?.limit != null) query.set("limit", String(params.limit)) if (params?.offset != null) query.set("offset", String(params.offset)) const queryString = query.toString() return apiFetch(`/api/progress${queryString ? `?${queryString}` : ""}`) } -export async function fetchTeams(): Promise { - return apiFetch("/api/teams") +export async function fetchTeams(params?: { hackathon_id?: string }): Promise { + const query = new URLSearchParams() + if (params?.hackathon_id) query.set("hackathon_id", params.hackathon_id) + const queryString = query.toString() + return apiFetch(`/api/teams${queryString ? `?${queryString}` : ""}`) +} + +export async function fetchHackathons(): Promise { + return apiFetch("/api/hackathons") } diff --git a/apps/console/shared/lib/hackathon-scope.ts b/apps/console/shared/lib/hackathon-scope.ts new file mode 100644 index 0000000..5b63b1e --- /dev/null +++ b/apps/console/shared/lib/hackathon-scope.ts @@ -0,0 +1,34 @@ +import type { Hackathon } from "./api" + +export type HackathonSearchParams = { + hackathon_id?: string | string[] +} + +export function readSingleSearchParam(value: string | string[] | undefined): string | null { + if (Array.isArray(value)) { + return value[0] ?? null + } + return value ?? null +} + +export function resolveActiveHackathon( + searchParams: HackathonSearchParams, + hackathons: Hackathon[], +): Hackathon | null { + const requestedId = readSingleSearchParam(searchParams.hackathon_id) + if (!requestedId) { + return hackathons[0] ?? null + } + return hackathons.find((hackathon) => hackathon.id === requestedId) ?? hackathons[0] ?? null +} + +export function withHackathonScope(path: string, hackathonId: string | null): string { + if (!hackathonId) { + return path + } + return `${path}?hackathon_id=${encodeURIComponent(hackathonId)}` +} + +export function isUuidLike(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) +} diff --git a/apps/worker/src/db/progress.ts b/apps/worker/src/db/progress.ts index 2f75e65..011eae1 100644 --- a/apps/worker/src/db/progress.ts +++ b/apps/worker/src/db/progress.ts @@ -1,6 +1,7 @@ import { getPool } from "./client" export interface InsertProgressParams { + team_id?: string slack_user_id: string phase: string current_status?: string @@ -23,10 +24,11 @@ export interface ProgressRow { export async function insertProgressLog(params: InsertProgressParams): Promise { const { rows } = await getPool().query( `INSERT INTO progress_logs - (slack_user_id, phase, status_text, blockers, is_sos) - VALUES ($1, $2, $3, $4, $5) + (team_id, slack_user_id, phase, status_text, blockers, is_sos) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [ + params.team_id ?? null, params.slack_user_id, params.phase, params.current_status ?? null, diff --git a/apps/worker/src/db/questions.ts b/apps/worker/src/db/questions.ts index 43c9ae1..5b7a7d5 100644 --- a/apps/worker/src/db/questions.ts +++ b/apps/worker/src/db/questions.ts @@ -1,6 +1,7 @@ import { getPool } from "./client" export interface InsertQuestionParams { + team_id?: string slack_user_id: string slack_channel_id: string slack_thread_ts: string @@ -32,10 +33,11 @@ export interface QuestionRow { export async function insertQuestion(params: InsertQuestionParams): Promise { const { rows } = await getPool().query( `INSERT INTO questions - (slack_user_id, slack_channel_id, slack_thread_ts, title, body, error_message, tried, status) - VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending') + (team_id, slack_user_id, slack_channel_id, slack_thread_ts, title, body, error_message, tried, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending') RETURNING *`, [ + params.team_id ?? null, params.slack_user_id, params.slack_channel_id, params.slack_thread_ts, diff --git a/apps/worker/src/db/teams.ts b/apps/worker/src/db/teams.ts new file mode 100644 index 0000000..0348943 --- /dev/null +++ b/apps/worker/src/db/teams.ts @@ -0,0 +1,29 @@ +import { getPool } from "./client" + +export interface TeamScopeRow { + id: string + name: string + hackathon_id: string +} + +export async function resolveTeamScopeBySlackChannel(params: { + slack_channel_id: string + slack_workspace_id?: string +}): Promise { + const workspaceId = params.slack_workspace_id?.trim() ?? "" + const { rows } = await getPool().query( + `SELECT id, name, hackathon_id + FROM teams + WHERE slack_channel_id = $1 + AND ( + slack_workspace_id = $2 + OR slack_workspace_id IS NULL + OR $2 = '' + ) + ORDER BY CASE WHEN slack_workspace_id = $2 THEN 0 ELSE 1 END, created_at ASC + LIMIT 1`, + [params.slack_channel_id, workspaceId], + ) + + return rows[0] ?? null +} diff --git a/apps/worker/src/workers/progress.ts b/apps/worker/src/workers/progress.ts index 46ca3e2..23a77b6 100644 --- a/apps/worker/src/workers/progress.ts +++ b/apps/worker/src/workers/progress.ts @@ -1,9 +1,10 @@ import { insertProgressLog, updateProgressSlackTs } from "../db/progress" +import { resolveTeamScopeBySlackChannel } from "../db/teams" import { postMessage } from "../slack/client" export interface ProgressMessage { user_id: string - team_id: string + slack_workspace_id: string channel_id: string phase: string current_status?: string @@ -19,7 +20,13 @@ function getProgressChannel(): string { // 1. Save progress_log to DB // 2. Post to #progress-board channel (SOS messages use a prominent format) export async function handleProgress(msg: ProgressMessage): Promise { + const resolvedTeam = await resolveTeamScopeBySlackChannel({ + slack_channel_id: msg.channel_id, + slack_workspace_id: msg.slack_workspace_id, + }) + const row = await insertProgressLog({ + team_id: resolvedTeam?.id, slack_user_id: msg.user_id, phase: msg.phase, current_status: msg.current_status, diff --git a/apps/worker/src/workers/question.ts b/apps/worker/src/workers/question.ts index cf0956f..25bf752 100644 --- a/apps/worker/src/workers/question.ts +++ b/apps/worker/src/workers/question.ts @@ -1,10 +1,11 @@ import { triageQuestion, type QuestionInput } from "../ai/triage" import { insertQuestion, updateQuestionTriage, markEscalated } from "../db/questions" +import { resolveTeamScopeBySlackChannel } from "../db/teams" import { postMessage, postMessageWithBlocks, postThreadReply, postThreadReplyWithBlocks } from "../slack/client" export interface QuestionNewMessage { user_id: string - team_id: string + slack_workspace_id: string channel_id: string title: string body?: string @@ -25,12 +26,18 @@ function getMentorChannel(): string { // 5. Reply in thread (ask for more info OR send short answer) // 6. Escalate to mentor channel if needed export async function handleQuestionNew(msg: QuestionNewMessage): Promise { + const resolvedTeam = await resolveTeamScopeBySlackChannel({ + slack_channel_id: msg.channel_id, + slack_workspace_id: msg.slack_workspace_id, + }) + // Post the initial question to the channel so we have a thread to reply to. const questionText = buildQuestionText(msg) const threadTs = await postMessage(msg.channel_id, questionText) // Persist the question in pending state. const row = await insertQuestion({ + team_id: resolvedTeam?.id, slack_user_id: msg.user_id, slack_channel_id: msg.channel_id, slack_thread_ts: threadTs, diff --git a/docs/detail/10_combined-product-gap-analysis.md b/docs/detail/10_combined-product-gap-analysis.md index 6f339cd..ef74730 100644 --- a/docs/detail/10_combined-product-gap-analysis.md +++ b/docs/detail/10_combined-product-gap-analysis.md @@ -182,23 +182,24 @@ ## 8. Cycle 1 の結論 -Cycle 1 では、`hackathon-scoped live board` を最初の slice とする。 +Cycle 1 では、`hackathon-scoped mentor live board` を最初の slice とする。 -その中でこの branch では、先行して: +この branch では先行して: - 新しい control-plane app `apps/console` を追加 - mentor の question/progress 導線を新 app に移植 - 後続の hackathon scope 実装を載せる受け皿を作る -次に実装すべき本体は: +その上で次の reviewable step として: - `hackathons` 導入 - `teams.hackathon_id` -- Slack channel / workspace と hackathon の mapping -- hackathon filter 付き read path +- default hackathon への既存 team backfill +- Slack channel / workspace から real `team_id` を引く seam +- hackathon filter 付き mentor read path これにより: -- `progress-checker` の mentor 導線を前進させる +- `progress-checker` の mentor 導線を event 混在なしで前進させる - `hack-evaluater` の multi-role / multi-hackathon 前提を受け入れる器を先に作れる - 既存 `apps/web` を壊さずに前進できる diff --git a/docs/detail/11_opportunity-cycle-1-plan.md b/docs/detail/11_opportunity-cycle-1-plan.md index 807ca5b..9eda825 100644 --- a/docs/detail/11_opportunity-cycle-1-plan.md +++ b/docs/detail/11_opportunity-cycle-1-plan.md @@ -12,7 +12,7 @@ ### Slice name -hackathon-scoped live board +hackathon-scoped mentor live board ### User value @@ -31,10 +31,12 @@ hackathon-scoped live board ### In scope -- `hackathons` を top-level scope として導入する設計 -- `teams.hackathon_id` 導入方針 -- Slack から入る question / progress に scope を持たせる設計 -- hackathon filter 付き board の方向性整理 +- `hackathons` table の導入 +- `teams.hackathon_id` と default hackathon backfill +- Slack submission で real `team_id` を channel から解決する seam +- `GET /api/hackathons` +- `GET /api/questions`, `GET /api/progress`, `GET /api/teams` の `hackathon_id` filter +- hackathon filter 付き mentor board - 新しい Next.js app `apps/console` - top-level overview page - login shell page @@ -68,9 +70,9 @@ hackathon-scoped live board - `/mentor/questions` - `/mentor/progress` - `/mentor/progress/teams/:teamId` -3. mentor の question/progress 画面は既存 API を使って read-only 表示できる +3. mentor の question/progress 画面は hackathon ごとに read-only 表示できる 4. combined product の差分と priority が docs に明文化される -5. hackathon scope を導入するための schema / ingress / read-path の次 slice が明確になる +5. channel mapping 済みの Slack `/question` と `/progress` は real `team_id` を保存できる 6. 変更は `opportunity` branch に留まり、`dev` へは merge しない --- @@ -79,27 +81,24 @@ hackathon-scoped live board ### Slice 2 -multi-hackathon domain schema +role and scope routing shell -- tenants -- hackathons -- role_bindings -- memberships -- sponsor visibility scopes +- login +- active context +- protected mentor / organizer / tenant routes ### Slice 3 -auth and role-aware routing +tenant and sponsor visibility model -- login -- role / scope switch -- protected routes +- tenants +- role_bindings +- sponsor visibility scopes ### Slice 4 Slack payload scoping and issue flow -- question / progress の hackathon 紐付け - issue queue - Issue Worker - GitHub issue creation diff --git a/docs/specification.md b/docs/specification.md index 6b2b562..528aa00 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -9,6 +9,9 @@ KCL Support Hub は、ハッカソン参加者が「質問しやすく」「詰まりにくく」なるための、 Slack を主入口とした AI 支援・進捗管理・メンターエスカレーションシステムです。 +現行 `changeHack` の MVP は KCL 向け support hub ですが、opportunity branch では +`progress-checker` と `hack-evaluater` を統合した multi-hackathon / role-aware product へ拡張する前提で進めます。 + 既存の `progress-checker` の設計思想を継承しつつ、 **Bonsai-8B をローカル推論エンジンとして活用し**、運営コストを最小化しながら初心者支援を実現します。 @@ -51,13 +54,13 @@ Slack を主入口とした AI 支援・進捗管理・メンターエスカレ | 継承する | 継承しない | |---|---| -| ロール分離設計(Organizer / Mentor / Hacker) | マルチテナント・スポンサー・スカウト機能 | +| ロール分離設計(PlatformAdmin / TenantAdmin / HackathonOrganizer / Sponsor / Judge / Hacker) | scout workflow の本実装 | | `features/container` 構造のフロントエンド | 複雑な権限スコープ管理 | -| `handler / service / repository` の3層バックエンド | 企業向け招待フロー | +| `handler / service / repository` の3層バックエンド | 企業向け招待フローの本実装 | | Zod + React Hook Form の組み合わせ | | --- ## 新プロダクトの一言要約 -> Slack から質問するだけで、Bonsai が一次整理し、運営・メンターが俯瞰できる、KCL 専用の「質問受付係 + 進捗ボード」 +> Slack から質問と進捗を集め、まず mentor 導線を hackathon 単位で整理し、将来的に organizer / sponsor へ広げる「質問受付係 + 進捗ボード」 From 910e82248a352f732e2398bbe7ac18fdcc6aba58 Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 04:02:12 +0900 Subject: [PATCH 07/36] feat: add mentor question detail workflow --- Makefile | 9 +- README.md | 8 + apps/api/cmd/server/main.go | 1 + apps/api/internal/api/handler.go | 61 +++++++ .../mentor/questions/[questionId]/page.tsx | 159 ++++++++++++++++++ apps/console/app/mentor/questions/page.tsx | 13 +- .../components/question-status-panel.tsx | 89 ++++++++++ apps/console/shared/lib/api.ts | 33 ++++ apps/worker/eslint.config.mjs | 21 +++ apps/worker/package.json | 4 + apps/worker/src/workers/question.ts | 2 +- docs/README.md | 4 +- .../10_combined-product-gap-analysis.md | 10 +- docs/detail/11_opportunity-cycle-1-plan.md | 2 + docs/setup/README.md | 3 +- docs/setup/local.md | 8 +- pnpm-lock.yaml | 70 ++------ 17 files changed, 425 insertions(+), 72 deletions(-) create mode 100644 apps/console/app/mentor/questions/[questionId]/page.tsx create mode 100644 apps/console/shared/components/question-status-panel.tsx create mode 100644 apps/worker/eslint.config.mjs diff --git a/Makefile b/Makefile index e788b27..6aca0fa 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ export up down logs \ init-sqs \ migrate migrate-down migrate-status \ - dev-api dev-worker dev-web \ + dev-api dev-worker dev-web dev-console \ docs \ build \ slack-manifest \ @@ -34,6 +34,7 @@ help: @echo " make dev-api Go API サーバー起動 (port 8080)" @echo " make dev-worker TypeScript Worker 起動" @echo " make dev-web Next.js ダッシュボード起動 (port 3000)" + @echo " make dev-console Next.js control-plane 起動 (port 3001)" @echo " make docs ドキュメントサーバー起動 (port 4000)" @echo "" @echo " Slack" @@ -88,6 +89,9 @@ dev-worker: dev-web: cd apps/web && PORT=3000 pnpm dev +dev-console: + cd apps/console && PORT=3001 pnpm dev + # ----------------------------------------------- # ドキュメント # ----------------------------------------------- @@ -107,6 +111,7 @@ build: $(MAKE) -C apps/api build cd apps/worker && pnpm build cd apps/web && pnpm build + cd apps/console && pnpm build # ----------------------------------------------- # リント / テスト @@ -115,6 +120,7 @@ lint: $(MAKE) -C apps/api lint cd apps/worker && pnpm lint cd apps/web && pnpm lint + cd apps/console && pnpm lint test: $(MAKE) -C apps/api test @@ -133,3 +139,4 @@ setup: up @echo " 1. make dev-api (別ターミナル)" @echo " 2. make dev-worker (別ターミナル)" @echo " 3. make dev-web (別ターミナル)" + @echo " 4. make dev-console (別ターミナル)" diff --git a/README.md b/README.md index 3b1a0d6..24a56e2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ KCL(初心者向けハッカソン)の質問受付・進捗可視化・AI 一次対応システム。 Slack を主入口として、Bonsai-8B(ローカル LLM)が質問をトリアージし、メンターへのエスカレーションを支援します。 +現在の `dev-codex` 系統では、`progress-checker` と `hack-evaluater` を統合する方向で +`apps/console` を新しい control-plane として拡張しています。 ## ドキュメント @@ -10,6 +12,7 @@ Slack を主入口として、Bonsai-8B(ローカル LLM)が質問をトリ - [アーキテクチャ](./docs/detail/02_architecture.md) - [AI フロー](./docs/detail/03_ai-flow.md) - [実装フェーズ](./docs/detail/05_implementation-phases.md) +- [統合差分と opportunity cycle](./docs/detail/10_combined-product-gap-analysis.md) - [AI コーディング環境セットアップ](./docs/ai-setup.md) ## セットアップ @@ -22,3 +25,8 @@ docker compose -f infra/docker/compose.yml up -d ``` 詳細は [`docs/detail/07_env-vars.md`](./docs/detail/07_env-vars.md) を参照。 + +## 開発用アプリ + +- `apps/web` : 既存の read-only dashboard +- `apps/console` : multi-hackathon / role-aware 方向へ育てる新しい control-plane diff --git a/apps/api/cmd/server/main.go b/apps/api/cmd/server/main.go index 705d100..3b945e1 100644 --- a/apps/api/cmd/server/main.go +++ b/apps/api/cmd/server/main.go @@ -56,6 +56,7 @@ func main() { api.GET("/hackathons", apihandler.HandleListHackathons(sqlDB)) api.GET("/questions", apihandler.HandleListQuestions(sqlDB)) api.GET("/questions/:id", apihandler.HandleGetQuestion(sqlDB)) + api.PATCH("/questions/:id/status", apihandler.HandleUpdateQuestionStatus(sqlDB)) api.GET("/progress", apihandler.HandleListProgress(sqlDB)) api.GET("/teams", apihandler.HandleListTeams(sqlDB)) } diff --git a/apps/api/internal/api/handler.go b/apps/api/internal/api/handler.go index 6ecdee3..2045178 100644 --- a/apps/api/internal/api/handler.go +++ b/apps/api/internal/api/handler.go @@ -2,6 +2,7 @@ package api import ( "database/sql" + "errors" "net/http" "strconv" @@ -28,6 +29,15 @@ func parseUUIDQuery(c *gin.Context, key string) (uuid.NullUUID, bool) { return uuid.NullUUID{UUID: parsed, Valid: true}, true } +func validateQuestionStatus(status string) error { + switch status { + case "pending", "answered", "in_progress", "escalated", "resolved", "closed": + return nil + default: + return errors.New("invalid question status") + } +} + // HandleListHackathons handles GET /api/hackathons. func HandleListHackathons(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) @@ -99,6 +109,57 @@ func HandleGetQuestion(db *sql.DB) gin.HandlerFunc { } } +type updateQuestionStatusRequest struct { + Status string `json:"status"` +} + +// HandleUpdateQuestionStatus handles PATCH /api/questions/:id/status. +func HandleUpdateQuestionStatus(db *sql.DB) gin.HandlerFunc { + q := sqlcgen.New(db) + return func(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + + var req updateQuestionStatusRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + if err := validateQuestionStatus(req.Status); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + existing, err := q.GetQuestion(c.Request.Context(), id) + if err == sql.ErrNoRows { + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + return + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) + return + } + + if err := q.UpdateQuestionStatus(c.Request.Context(), sqlcgen.UpdateQuestionStatusParams{ + ID: existing.ID, + Status: req.Status, + }); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) + return + } + + updated, err := q.GetQuestion(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) + return + } + c.JSON(http.StatusOK, toQuestionResponse(updated)) + } +} + // HandleListProgress handles GET /api/progress. // Optional query params: team_id (uuid), hackathon_id (uuid), limit, offset. func HandleListProgress(db *sql.DB) gin.HandlerFunc { diff --git a/apps/console/app/mentor/questions/[questionId]/page.tsx b/apps/console/app/mentor/questions/[questionId]/page.tsx new file mode 100644 index 0000000..c00147e --- /dev/null +++ b/apps/console/app/mentor/questions/[questionId]/page.tsx @@ -0,0 +1,159 @@ +import Link from "next/link" +import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" +import { QuestionStatusPanel } from "@/shared/components/question-status-panel" +import { fetchHackathons, fetchQuestion, fetchTeams } from "@/shared/lib/api" +import { isUuidLike, resolveActiveHackathon, type HackathonSearchParams, withHackathonScope } from "@/shared/lib/hackathon-scope" + +type MentorQuestionDetailPageProps = { + params: Promise<{ questionId: string }> + searchParams: Promise +} + +function renderJson(value: unknown): string { + if (!value) { + return "No triage payload" + } + try { + return JSON.stringify(value, null, 2) + } catch { + return "Unable to render triage payload" + } +} + +export default async function MentorQuestionDetailPage({ params, searchParams }: MentorQuestionDetailPageProps) { + const { questionId } = await params + const resolvedSearchParams = await searchParams + const hackathons = await fetchHackathons().catch(() => []) + const activeHackathon = resolveActiveHackathon(resolvedSearchParams, hackathons) + const isValidQuestionId = isUuidLike(questionId) + + const question = isValidQuestionId ? await fetchQuestion(questionId).catch(() => null) : null + const teams = activeHackathon + ? await fetchTeams({ hackathon_id: activeHackathon.id }).catch(() => null) + : [] + + const team = question?.team_id ? (teams ?? []).find((candidate) => candidate.id === question.team_id) : null + const inActiveScope = !question?.team_id || !!team + const backHref = withHackathonScope("/mentor/questions", activeHackathon?.id ?? null) + + return ( +
+
+ + Back to question queue + +

+ Mentor question detail +

+

+ `progress-checker` の P0 画面だった question detail を `apps/console` に移し、mentor が Web から状態更新できるようにします。 +

+ +
+ + {!isValidQuestionId ? ( +
+ URL の questionId が不正です。question queue から遷移し直してください。 +
+ ) : question === null ? ( +
+ question detail を読み込めませんでした。API 接続または対象データを確認してください。 +
+ ) : !inActiveScope ? ( +
+ この質問は選択中の hackathon scope に属していません。hackathon を切り替えて確認してください。 +
+ ) : ( +
+
+
+ {question.status} + {question.category ? ( + {question.category} + ) : null} + {question.escalated_to_mentor ? ( + escalated + ) : null} +
+ +

+ {question.title} +

+ +
+
+
Hackathon
+
{activeHackathon?.name ?? "Unknown hackathon"}
+
+
+
Team
+
{team?.name ?? "Unassigned team"}
+
+
+
Slack user
+
{question.slack_user_id}
+
+
+
Created at
+
{new Date(question.created_at).toLocaleString("ja-JP")}
+
+
+ + {question.body ? ( +
+

Body

+

{question.body}

+
+ ) : null} + + {question.error_message ? ( +
+

Error message

+
+                  {question.error_message}
+                
+
+ ) : null} + + {question.tried ? ( +
+

Tried

+

{question.tried}

+
+ ) : null} + +
+

Triage payload

+
+                {renderJson(question.triage_result)}
+              
+
+
+ +
+ + +
+

API

+

+ Raw endpoints +

+
+ + GET /api/questions/{question.id} + +
+
+
+
+ )} +
+ ) +} diff --git a/apps/console/app/mentor/questions/page.tsx b/apps/console/app/mentor/questions/page.tsx index 61484e5..da96081 100644 --- a/apps/console/app/mentor/questions/page.tsx +++ b/apps/console/app/mentor/questions/page.tsx @@ -1,7 +1,7 @@ import Link from "next/link" import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" import { fetchHackathons, fetchQuestions } from "@/shared/lib/api" -import { resolveActiveHackathon, type HackathonSearchParams } from "@/shared/lib/hackathon-scope" +import { resolveActiveHackathon, type HackathonSearchParams, withHackathonScope } from "@/shared/lib/hackathon-scope" type MentorQuestionsPageProps = { searchParams: Promise @@ -61,9 +61,14 @@ export default async function MentorQuestionsPage({ searchParams }: MentorQuesti {question.body ?

{question.body}

: null}
{new Date(question.created_at).toLocaleString("ja-JP")} - - API detail - +
+ + Detail + + + API detail + +
)) diff --git a/apps/console/shared/components/question-status-panel.tsx b/apps/console/shared/components/question-status-panel.tsx new file mode 100644 index 0000000..b9f5a1e --- /dev/null +++ b/apps/console/shared/components/question-status-panel.tsx @@ -0,0 +1,89 @@ +"use client" + +import { startTransition, useState } from "react" +import { useRouter } from "next/navigation" +import { updateQuestionStatus } from "@/shared/lib/api" + +const STATUS_OPTIONS = [ + { value: "in_progress", label: "In Progress" }, + { value: "resolved", label: "Resolved" }, + { value: "escalated", label: "Escalated" }, + { value: "closed", label: "Closed" }, +] as const + +type QuestionStatusPanelProps = { + questionId: string + currentStatus: string +} + +export function QuestionStatusPanel({ questionId, currentStatus }: QuestionStatusPanelProps) { + const router = useRouter() + const [selectedStatus, setSelectedStatus] = useState(currentStatus) + const [isPending, setIsPending] = useState(false) + const [error, setError] = useState(null) + const [message, setMessage] = useState(null) + + async function submitStatusUpdate() { + setIsPending(true) + setError(null) + setMessage(null) + try { + const updated = await updateQuestionStatus(questionId, selectedStatus) + setMessage(`ステータスを ${updated.status} に更新しました。`) + startTransition(() => { + router.refresh() + }) + } catch (updateError) { + setError(updateError instanceof Error ? updateError.message : "status update failed") + } finally { + setIsPending(false) + } + } + + return ( +
+

Mentor action

+

+ Update question status +

+

+ Web 側の mentor 運用で最低限必要な状態更新をここから行います。 +

+ +
+ {STATUS_OPTIONS.map((option) => { + const isSelected = option.value === selectedStatus + return ( + + ) + })} +
+ +
+ + Current: {currentStatus} +
+ + {message ?

{message}

: null} + {error ?

{error}

: null} +
+ ) +} diff --git a/apps/console/shared/lib/api.ts b/apps/console/shared/lib/api.ts index 6b8d4fc..2ab693e 100644 --- a/apps/console/shared/lib/api.ts +++ b/apps/console/shared/lib/api.ts @@ -16,11 +16,16 @@ export interface Question { slack_channel_id: string title: string body: string | null + error_message?: string | null + tried?: string | null + triage_result?: unknown category: string | null confidence: number | null status: string escalated_to_mentor: boolean + mentor_channel_ts?: string | null created_at: string + updated_at?: string } export interface ProgressLog { @@ -60,6 +65,23 @@ async function apiFetch(path: string): Promise { return response.json() as Promise } +async function apiFetchWithInit(path: string, init: RequestInit): Promise { + const response = await fetch(`${API_BASE}${path}`, { + cache: "no-store", + headers: { + "Content-Type": "application/json", + ...(init.headers ?? {}), + }, + ...init, + }) + + if (!response.ok) { + throw new Error(`API error ${response.status} for ${path}`) + } + + return response.json() as Promise +} + export async function fetchQuestions(params?: { status?: string hackathon_id?: string @@ -75,6 +97,10 @@ export async function fetchQuestions(params?: { return apiFetch(`/api/questions${queryString ? `?${queryString}` : ""}`) } +export async function fetchQuestion(id: string): Promise { + return apiFetch(`/api/questions/${id}`) +} + export async function fetchProgressLogs(params?: { team_id?: string hackathon_id?: string @@ -100,3 +126,10 @@ export async function fetchTeams(params?: { hackathon_id?: string }): Promise { return apiFetch("/api/hackathons") } + +export async function updateQuestionStatus(id: string, status: string): Promise { + return apiFetchWithInit(`/api/questions/${id}/status`, { + method: "PATCH", + body: JSON.stringify({ status }), + }) +} diff --git a/apps/worker/eslint.config.mjs b/apps/worker/eslint.config.mjs new file mode 100644 index 0000000..198a331 --- /dev/null +++ b/apps/worker/eslint.config.mjs @@ -0,0 +1,21 @@ +import js from "@eslint/js" +import globals from "globals" +import tseslint from "typescript-eslint" + +export default tseslint.config( + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.ts"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.node, + }, + }, + rules: { + "@typescript-eslint/no-explicit-any": "off", + }, + }, +) diff --git a/apps/worker/package.json b/apps/worker/package.json index 2ffb1c1..6c4c270 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -20,9 +20,13 @@ "zod": "^3.23.0" }, "devDependencies": { + "@eslint/js": "^9.38.0", "@types/node": "^20.0.0", "@types/pg": "^8.11.0", + "eslint": "^9.38.0", + "globals": "^16.4.0", "ts-node": "^10.9.2", + "typescript-eslint": "^8.46.0", "typescript": "^5.4.0" } } diff --git a/apps/worker/src/workers/question.ts b/apps/worker/src/workers/question.ts index 25bf752..0c2dc32 100644 --- a/apps/worker/src/workers/question.ts +++ b/apps/worker/src/workers/question.ts @@ -1,7 +1,7 @@ import { triageQuestion, type QuestionInput } from "../ai/triage" import { insertQuestion, updateQuestionTriage, markEscalated } from "../db/questions" import { resolveTeamScopeBySlackChannel } from "../db/teams" -import { postMessage, postMessageWithBlocks, postThreadReply, postThreadReplyWithBlocks } from "../slack/client" +import { postMessage, postMessageWithBlocks, postThreadReplyWithBlocks } from "../slack/client" export interface QuestionNewMessage { user_id: string diff --git a/docs/README.md b/docs/README.md index baf7f46..4f8a01d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,7 @@ make setup make dev-api # ターミナル 1 make dev-worker # ターミナル 2 make dev-web # ターミナル 3 +make dev-console # ターミナル 4 # 4. Slack に公開 ngrok http 8080 @@ -52,5 +53,6 @@ graph TB Worker --> DB[("PostgreSQL")] Worker -->|AI トリアージ| AI["Bonsai / Ollama"] Worker -->|スレッド返信| User - Dashboard["Web ダッシュボード"] -->|REST| API + Dashboard["apps/web"] -->|REST| API + Console["apps/console"] -->|REST| API ``` diff --git a/docs/detail/10_combined-product-gap-analysis.md b/docs/detail/10_combined-product-gap-analysis.md index ef74730..4c4aa8b 100644 --- a/docs/detail/10_combined-product-gap-analysis.md +++ b/docs/detail/10_combined-product-gap-analysis.md @@ -29,6 +29,8 @@ - メンターチャンネルへのエスカレーション - 質問一覧 / 質問詳細 / 進捗一覧 / チーム一覧の read API - 既存 `apps/web` の簡易ダッシュボード +- `apps/console` の mentor question queue / question detail / progress overview / team detail +- Web からの question status 更新 API ### ここまでで足りている観点 @@ -49,13 +51,9 @@ - GitHub Issue 作成 - Slack thread への Issue URL 返却 -- team progress detail - - `progress-checker` では `/progress/teams/:teamId` が必要 - - `changeHack` の既存 `apps/web` には未実装 - - Mentor 用 web 運用導線 - `progress-checker` は login, question queue, question detail, progress overview を前提 - - `changeHack` の既存 `apps/web` は read-only dashboard で、auth / role 前提がない + - `apps/console` で mentor 導線は揃い始めたが、auth / role 前提はまだ未実装 ### Medium @@ -67,8 +65,6 @@ - docs にはあるが、実装はまだ最小構成 - Web からの mentor 操作 - - 状態更新 - - 解決マーク - コメント ### Low diff --git a/docs/detail/11_opportunity-cycle-1-plan.md b/docs/detail/11_opportunity-cycle-1-plan.md index 9eda825..b842be1 100644 --- a/docs/detail/11_opportunity-cycle-1-plan.md +++ b/docs/detail/11_opportunity-cycle-1-plan.md @@ -42,8 +42,10 @@ hackathon-scoped mentor live board - login shell page - platform / tenant / organizer / sponsor の route shell - mentor question queue page +- mentor question detail page - mentor progress overview page - mentor team progress detail page +- question status update API and console action panel - combined-product gap analysis doc - cycle plan doc diff --git a/docs/setup/README.md b/docs/setup/README.md index 0c91b50..95bd456 100644 --- a/docs/setup/README.md +++ b/docs/setup/README.md @@ -19,10 +19,11 @@ cp .env.example .env && nano .env # 3. インフラ起動 + マイグレーション make setup -# 4. 各サービス起動(別ターミナル×3) +# 4. 各サービス起動(別ターミナル×4) make dev-api make dev-worker make dev-web +make dev-console # 5. ngrok + Slack URL 一括更新 ngrok http 8080 # 別ターミナル diff --git a/docs/setup/local.md b/docs/setup/local.md index 80ffb6e..4534225 100644 --- a/docs/setup/local.md +++ b/docs/setup/local.md @@ -66,12 +66,13 @@ make down # インフラ停止 ### 4. 各サービス起動 -ターミナルを 3 つ開いてそれぞれ実行: +ターミナルを 4 つ開いてそれぞれ実行: ```bash make dev-api # Go API → http://localhost:8080 make dev-worker # TypeScript Worker → SQS ポーリング開始 -make dev-web # Next.js → http://localhost:3000 +make dev-web # 既存 dashboard → http://localhost:3000 +make dev-console # 新しい control-plane → http://localhost:3001 ``` ### 5. Slack に公開 + URL 設定 @@ -113,7 +114,8 @@ BONSAI_MODEL=qwen2.5:7b |---|---| | `/question` フロー | Slack で `/question` → モーダル送信 → スレッドに返信 | | `/progress` フロー | Slack で `/progress` → モーダル送信 → `#progress-board` に投稿 | -| Web ダッシュボード | ブラウザで `http://localhost:3000` | +| 既存ダッシュボード | ブラウザで `http://localhost:3000` | +| 新しい control-plane | ブラウザで `http://localhost:3001` | | API 疎通 | `curl http://localhost:8080/api/teams` | --- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9b263f..5156d87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,7 +41,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 16.2.2 - version: 16.2.2(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 16.2.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) tailwindcss: specifier: ^4 version: 4.2.2 @@ -125,18 +125,30 @@ importers: specifier: ^3.23.0 version: 3.25.76 devDependencies: + '@eslint/js': + specifier: ^9.38.0 + version: 9.39.4 '@types/node': specifier: ^20.0.0 version: 20.19.39 '@types/pg': specifier: ^8.11.0 version: 8.20.0 + eslint: + specifier: ^9.38.0 + version: 9.39.4(jiti@2.6.1) + globals: + specifier: ^16.4.0 + version: 16.4.0 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.19.39)(typescript@5.9.3) typescript: specifier: ^5.4.0 version: 5.9.3 + typescript-eslint: + specifier: ^8.46.0 + version: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) packages/schemas: dependencies: @@ -4566,26 +4578,6 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.2.2(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@next/eslint-plugin-next': 16.2.2 - eslint: 9.39.4(jiti@2.6.1) - eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) - globals: 16.4.0 - typescript-eslint: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-next@16.2.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.2.2 @@ -4625,50 +4617,20 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.39.4(jiti@2.6.1) - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -4680,7 +4642,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From 9856217a6f7d5bd64038f6dc6fe1535a72dfba48 Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 04:11:18 +0900 Subject: [PATCH 08/36] feat: add console role scope session shell --- README.md | 1 + apps/api/internal/api/handler.go | 45 +++++++ apps/console/app/auth/login/route.ts | 41 ++++++ apps/console/app/auth/logout/route.ts | 14 ++ apps/console/app/forbidden/page.tsx | 25 ++++ apps/console/app/layout.tsx | 52 ++++++-- apps/console/app/login/page.tsx | 124 ++++++++++++++++-- apps/console/app/mentor/progress/page.tsx | 4 +- .../mentor/progress/teams/[teamId]/page.tsx | 4 +- .../mentor/questions/[questionId]/page.tsx | 14 +- apps/console/app/mentor/questions/page.tsx | 4 +- apps/console/proxy.ts | 44 +++++++ .../components/question-status-panel.tsx | 5 +- apps/console/shared/lib/api.ts | 14 +- apps/console/shared/lib/hackathon-scope.ts | 4 + apps/console/shared/lib/session-schema.ts | 102 ++++++++++++++ apps/console/shared/lib/session.ts | 7 + docs/README.md | 2 +- .../10_combined-product-gap-analysis.md | 4 +- docs/detail/11_opportunity-cycle-1-plan.md | 11 +- docs/setup/README.md | 3 +- docs/setup/local.md | 2 +- 22 files changed, 487 insertions(+), 39 deletions(-) create mode 100644 apps/console/app/auth/login/route.ts create mode 100644 apps/console/app/auth/logout/route.ts create mode 100644 apps/console/app/forbidden/page.tsx create mode 100644 apps/console/proxy.ts create mode 100644 apps/console/shared/lib/session-schema.ts create mode 100644 apps/console/shared/lib/session.ts diff --git a/README.md b/README.md index 24a56e2..7805c5e 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,4 @@ docker compose -f infra/docker/compose.yml up -d - `apps/web` : 既存の read-only dashboard - `apps/console` : multi-hackathon / role-aware 方向へ育てる新しい control-plane + - `dev-codex` では `/login` から role / hackathon scope session を作って protected route を切り替える diff --git a/apps/api/internal/api/handler.go b/apps/api/internal/api/handler.go index 2045178..7a528bb 100644 --- a/apps/api/internal/api/handler.go +++ b/apps/api/internal/api/handler.go @@ -38,6 +38,23 @@ func validateQuestionStatus(status string) error { } } +func questionInHackathon(q *sqlcgen.Queries, c *gin.Context, question sqlcgen.Question, hackathonID uuid.NullUUID) (bool, error) { + if !hackathonID.Valid { + return true, nil + } + if !question.TeamID.Valid { + return false, nil + } + team, err := q.GetTeam(c.Request.Context(), question.TeamID.UUID) + if err == sql.ErrNoRows { + return false, nil + } + if err != nil { + return false, err + } + return team.HackathonID == hackathonID.UUID, nil +} + // HandleListHackathons handles GET /api/hackathons. func HandleListHackathons(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) @@ -105,6 +122,20 @@ func HandleGetQuestion(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return + } + inScope, err := questionInHackathon(q, c, question, hackathonID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) + return + } + if !inScope { + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + return + } c.JSON(http.StatusOK, toQuestionResponse(question)) } } @@ -132,6 +163,11 @@ func HandleUpdateQuestionStatus(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return + } existing, err := q.GetQuestion(c.Request.Context(), id) if err == sql.ErrNoRows { @@ -142,6 +178,15 @@ func HandleUpdateQuestionStatus(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } + inScope, err := questionInHackathon(q, c, existing, hackathonID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) + return + } + if !inScope { + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + return + } if err := q.UpdateQuestionStatus(c.Request.Context(), sqlcgen.UpdateQuestionStatusParams{ ID: existing.ID, diff --git a/apps/console/app/auth/login/route.ts b/apps/console/app/auth/login/route.ts new file mode 100644 index 0000000..2b4b111 --- /dev/null +++ b/apps/console/app/auth/login/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server" +import { + defaultPathForSession, + isConsoleRole, + roleRequiresHackathon, + serializeConsoleSession, + SESSION_COOKIE_NAME, +} from "@/shared/lib/session-schema" + +function sanitizeRedirectPath(value: string): string { + if (!value.startsWith("/") || value.startsWith("//")) { + return "" + } + return value +} + +export async function POST(request: Request) { + const formData = await request.formData() + const role = String(formData.get("role") ?? "") + const hackathonId = String(formData.get("hackathon_id") ?? "").trim() || null + const redirectTo = sanitizeRedirectPath(String(formData.get("redirect_to") ?? "").trim()) + + if (!isConsoleRole(role)) { + return NextResponse.redirect(new URL("/login?reason=missing_role", request.url)) + } + + if (roleRequiresHackathon(role) && !hackathonId) { + return NextResponse.redirect(new URL("/login?reason=hackathon_required", request.url)) + } + + const session = { role, hackathonId } + const response = NextResponse.redirect(new URL(redirectTo || defaultPathForSession(session), request.url)) + response.cookies.set(SESSION_COOKIE_NAME, serializeConsoleSession(session), { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 60 * 60 * 12, + }) + return response +} diff --git a/apps/console/app/auth/logout/route.ts b/apps/console/app/auth/logout/route.ts new file mode 100644 index 0000000..f05128f --- /dev/null +++ b/apps/console/app/auth/logout/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server" +import { SESSION_COOKIE_NAME } from "@/shared/lib/session-schema" + +export async function POST(request: Request) { + const response = NextResponse.redirect(new URL("/login", request.url)) + response.cookies.set(SESSION_COOKIE_NAME, "", { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 0, + }) + return response +} diff --git a/apps/console/app/forbidden/page.tsx b/apps/console/app/forbidden/page.tsx new file mode 100644 index 0000000..87aaf0f --- /dev/null +++ b/apps/console/app/forbidden/page.tsx @@ -0,0 +1,25 @@ +import Link from "next/link" + +export default async function ForbiddenPage({ + searchParams, +}: { + searchParams: Promise<{ from?: string | string[] }> +}) { + const params = await searchParams + const from = Array.isArray(params.from) ? params.from[0] : params.from + + return ( +
+

Forbidden

+

+ Current role cannot access this route. +

+

+ {from ? `アクセス元: ${from}` : "role / scope session を切り替えてから再試行してください。"} +

+ + Back to login + +
+ ) +} diff --git a/apps/console/app/layout.tsx b/apps/console/app/layout.tsx index 496ef2a..511cea8 100644 --- a/apps/console/app/layout.tsx +++ b/apps/console/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next" import Link from "next/link" +import { getConsoleSession } from "@/shared/lib/session" +import { canAccessPath, defaultPathForSession, roleLabel } from "@/shared/lib/session-schema" import "./globals.css" export const metadata: Metadata = { @@ -7,7 +9,7 @@ export const metadata: Metadata = { description: "multi-hackathon control plane for mentors, sponsors, and organizers", } -const navItems = [ +const baseNavItems = [ { href: "/", label: "Overview" }, { href: "/mentor/questions", label: "Mentor Queue" }, { href: "/mentor/progress", label: "Mentor Progress" }, @@ -17,7 +19,10 @@ const navItems = [ { href: "/sponsor", label: "Sponsor" }, ] -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const session = await getConsoleSession() + const navItems = baseNavItems.filter((item) => !session || canAccessPath(session.role, item.href)) + return ( @@ -25,20 +30,49 @@ export default function RootLayout({ children }: { children: React.ReactNode })
-

Opportunity Branch

+

dev-codex

HackHub Console

`progress-checker` の運用導線と `hack-evaluater` の多ハッカソン前提をつなぐ新しい control-plane。

+ {session ? ( +
+ {roleLabel(session.role)} + {session.hackathonId ? ( + + hackathon {session.hackathonId} + + ) : null} +
+ ) : null}
- - Login Shell - + {session ? ( +
+ + Active landing + +
+ +
+
+ ) : ( + + Login + + )}