From 95b22c595d24a30c1f951af628b2a51fbfbfe7cf Mon Sep 17 00:00:00 2001 From: hzhang Date: Sun, 15 Feb 2026 02:28:28 +0000 Subject: [PATCH] init --- .idea/Marshmallow.iml | 9 + AbstractWizard/Dockerfile | 21 ++ AbstractWizard/README.md | 287 ++++++++++++++++++ AbstractWizard/audit/logger.go | 46 +++ AbstractWizard/config/atomic.go | 55 ++++ AbstractWizard/config/backup.go | 156 ++++++++++ AbstractWizard/config/parser.go | 83 +++++ AbstractWizard/config/validation.go | 49 +++ AbstractWizard/docker-compose.yaml | 14 + AbstractWizard/go.mod | 5 + AbstractWizard/go.sum | 4 + AbstractWizard/main.go | 51 ++++ AbstractWizard/server/handlers.go | 265 ++++++++++++++++ AbstractWizard/server/middleware.go | 27 ++ AbstractWizard/server/server.go | 137 +++++++++ .../.idea/DependencyExtractor.iml | 10 + Dialectic/docker-compose.yaml | 57 ++++ Dialectic/init.sql | 47 +++ DungeonSolver.Workspace/CLAUDE.md | 115 +++++++ DungeonSolver.Workspace/DungeonSolver | 1 + .../DungeonSolver.Analyzer | 1 + DungeonSolver.Workspace/DungeonSolver.Core | 1 + .../DungeonSolver.Core.Analyzer | 1 + .../DungeonSolver.Core.Generators | 1 + .../DungeonSolver.Generator.Core | 1 + .../DungeonSolver.Generators | 1 + .../.idea/workspace.xml | 132 ++++++++ ...nSolver.Core.Analyzer.sln.DotSettings.user | 2 + .../.idea/workspace.xml | 120 ++++++++ .../.idea/workspace.xml | 87 ++++++ DungeonSolver.Workspace/Skeleton | 1 + DungeonSolver.Workspace/statistic.py | 20 ++ .../SkeletonTest/.idea/SkeletonTest.iml | 10 + JupyterProject/.idea/JupyterProject.iml | 10 + JupyterProject/s.ipynb | 47 +++ .../.idea/structured-output-parser-mcp.iml | 10 + 36 files changed, 1884 insertions(+) create mode 100644 .idea/Marshmallow.iml create mode 100644 AbstractWizard/Dockerfile create mode 100644 AbstractWizard/README.md create mode 100644 AbstractWizard/audit/logger.go create mode 100644 AbstractWizard/config/atomic.go create mode 100644 AbstractWizard/config/backup.go create mode 100644 AbstractWizard/config/parser.go create mode 100644 AbstractWizard/config/validation.go create mode 100644 AbstractWizard/docker-compose.yaml create mode 100644 AbstractWizard/go.mod create mode 100644 AbstractWizard/go.sum create mode 100644 AbstractWizard/main.go create mode 100644 AbstractWizard/server/handlers.go create mode 100644 AbstractWizard/server/middleware.go create mode 100644 AbstractWizard/server/server.go create mode 100644 DependencyExtractor/.idea/DependencyExtractor.iml create mode 100644 Dialectic/docker-compose.yaml create mode 100644 Dialectic/init.sql create mode 100644 DungeonSolver.Workspace/CLAUDE.md create mode 160000 DungeonSolver.Workspace/DungeonSolver create mode 160000 DungeonSolver.Workspace/DungeonSolver.Analyzer create mode 160000 DungeonSolver.Workspace/DungeonSolver.Core create mode 160000 DungeonSolver.Workspace/DungeonSolver.Core.Analyzer create mode 160000 DungeonSolver.Workspace/DungeonSolver.Core.Generators create mode 160000 DungeonSolver.Workspace/DungeonSolver.Generator.Core create mode 160000 DungeonSolver.Workspace/DungeonSolver.Generators create mode 100644 DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/.idea/.idea.DungeonSolver.Global.Analyzer/.idea/workspace.xml create mode 100644 DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/DungeonSolver.Core.Analyzer.sln.DotSettings.user create mode 100644 DungeonSolver.Workspace/DungeonSolver.Global.Generators/.idea/.idea.DungeonSolver.Global.Generators/.idea/workspace.xml create mode 100644 DungeonSolver.Workspace/DungeonSolver.Global/.idea/.idea.DungeonSolver.Global/.idea/workspace.xml create mode 160000 DungeonSolver.Workspace/Skeleton create mode 100644 DungeonSolver.Workspace/statistic.py create mode 100644 Hangman-Lab/SkeletonTest/.idea/SkeletonTest.iml create mode 100644 JupyterProject/.idea/JupyterProject.iml create mode 100644 JupyterProject/s.ipynb create mode 100644 structured-output-parser-mcp/.idea/structured-output-parser-mcp.iml diff --git a/.idea/Marshmallow.iml b/.idea/Marshmallow.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/Marshmallow.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/AbstractWizard/Dockerfile b/AbstractWizard/Dockerfile new file mode 100644 index 0000000..a96637d --- /dev/null +++ b/AbstractWizard/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.24-alpine AS build + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /abstract-wizard . + +FROM gcr.io/distroless/static-debian12 + +COPY --from=build /abstract-wizard /abstract-wizard + +USER nonroot:nonroot + +ENV CONFIG_DIR=/config +ENV LISTEN_ADDR=0.0.0.0:8080 +EXPOSE 8080 + +VOLUME /config + +ENTRYPOINT ["/abstract-wizard"] diff --git a/AbstractWizard/README.md b/AbstractWizard/README.md new file mode 100644 index 0000000..bf00a6e --- /dev/null +++ b/AbstractWizard/README.md @@ -0,0 +1,287 @@ +# Abstract Wizard + +[English](#english) | [中文](#中文) + +--- + +## English + +Secure configuration file management service. Read, modify, and version-control JSON/YAML config files via REST API. Listens on localhost only — access remotely via SSH tunnel. + +### Quick Start + +#### Docker Compose (Recommended) + +```bash +docker compose up -d +``` + +The service listens on `127.0.0.1:18080`. + +#### Run Locally + +```bash +go build -o abstract-wizard . +CONFIG_DIR=./configs ./abstract-wizard +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `CONFIG_DIR` | `/config` | Base directory for config files | +| `LISTEN_ADDR` | `127.0.0.1:8080` | HTTP listen address | +| `MAX_BACKUPS` | `10` | Max backup versions per file | + +### API + +#### Health Check + +```bash +curl http://127.0.0.1:18080/health +``` + +#### Read Config + +```bash +curl http://127.0.0.1:18080/api/v1/config/app.json +``` + +#### Full Replace + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/config/app.json \ + -d '{"database": {"host": "localhost", "port": 5432}}' +``` + +A backup is created automatically before each write. The response includes `backup_version`. + +#### Partial Update (Deep Merge) + +```bash +curl -X PATCH http://127.0.0.1:18080/api/v1/config/app.json \ + -d '{"database": {"port": 3306}}' +``` + +Only the specified fields are updated; nested objects are merged recursively. + +#### List Backups + +```bash +curl http://127.0.0.1:18080/api/v1/backups/app.json +``` + +#### Rollback + +```bash +# Get the version from the backup list, then: +curl -X POST http://127.0.0.1:18080/api/v1/rollback/app.json \ + -d '{"version": "20260215T120000Z"}' +``` + +#### Mode Switching + +Get current mode: + +```bash +curl http://127.0.0.1:18080/api/v1/mode +``` + +Switch to readonly (rejects all writes): + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/mode \ + -d '{"mode": "readonly"}' +``` + +Switch back to init (read/write): + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/mode \ + -d '{"mode": "init"}' +``` + +### YAML Support + +All endpoints support both JSON and YAML. The format is detected by file extension: + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/config/app.yaml \ + -H "Content-Type: text/yaml" \ + -d ' +database: + host: localhost + port: 5432 +' +``` + +### Security Model + +The service binds to `127.0.0.1` by default and is not exposed externally. Use an SSH tunnel for remote access: + +```bash +ssh -L 18080:127.0.0.1:18080 user@server +``` + +Then access via `http://127.0.0.1:18080` locally. + +### Project Structure + +``` +├── main.go # Entry point, env config, graceful shutdown +├── config/ +│ ├── validation.go # Path validation, traversal prevention +│ ├── parser.go # JSON/YAML parse, serialize, deep merge +│ ├── atomic.go # Atomic write (temp → fsync → rename) +│ └── backup.go # Timestamped backups, pruning, rollback +├── audit/ +│ └── logger.go # Structured JSON audit log +├── server/ +│ ├── server.go # HTTP server, routing, mode state machine +│ ├── middleware.go # Request logging middleware +│ └── handlers.go # API handlers +├── Dockerfile # Multi-stage build +└── docker-compose.yaml # Example deployment +``` + +--- + +## 中文 + +安全的配置文件管理服务。通过 REST API 对 JSON/YAML 配置文件进行读取、修改和版本管理,仅监听 localhost,通过 SSH 隧道访问。 + +### 快速开始 + +#### Docker Compose(推荐) + +```bash +docker compose up -d +``` + +服务启动后监听 `127.0.0.1:18080`。 + +#### 本地运行 + +```bash +go build -o abstract-wizard . +CONFIG_DIR=./configs ./abstract-wizard +``` + +### 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `CONFIG_DIR` | `/config` | 配置文件存放目录 | +| `LISTEN_ADDR` | `127.0.0.1:8080` | 监听地址 | +| `MAX_BACKUPS` | `10` | 每个文件保留的最大备份数 | + +### API + +#### 健康检查 + +```bash +curl http://127.0.0.1:18080/health +``` + +#### 读取配置 + +```bash +curl http://127.0.0.1:18080/api/v1/config/app.json +``` + +#### 完整替换配置 + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/config/app.json \ + -d '{"database": {"host": "localhost", "port": 5432}}' +``` + +写入前会自动创建备份,响应中包含 `backup_version`。 + +#### 局部更新(深度合并) + +```bash +curl -X PATCH http://127.0.0.1:18080/api/v1/config/app.json \ + -d '{"database": {"port": 3306}}' +``` + +仅更新指定字段,嵌套对象递归合并。 + +#### 查看备份列表 + +```bash +curl http://127.0.0.1:18080/api/v1/backups/app.json +``` + +#### 回滚到指定版本 + +```bash +# 先从备份列表获取版本号,然后: +curl -X POST http://127.0.0.1:18080/api/v1/rollback/app.json \ + -d '{"version": "20260215T120000Z"}' +``` + +#### 模式切换 + +查看当前模式: + +```bash +curl http://127.0.0.1:18080/api/v1/mode +``` + +切换为只读模式(拒绝所有写操作): + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/mode \ + -d '{"mode": "readonly"}' +``` + +切换回初始化模式(允许读写): + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/mode \ + -d '{"mode": "init"}' +``` + +### YAML 支持 + +所有端点同时支持 JSON 和 YAML,格式由文件扩展名自动判断: + +```bash +curl -X PUT http://127.0.0.1:18080/api/v1/config/app.yaml \ + -H "Content-Type: text/yaml" \ + -d ' +database: + host: localhost + port: 5432 +' +``` + +### 安全模型 + +服务默认仅监听 `127.0.0.1`,不暴露到外部网络。远程访问通过 SSH 隧道实现: + +```bash +ssh -L 18080:127.0.0.1:18080 user@server +``` + +之后本地即可通过 `http://127.0.0.1:18080` 访问。 + +### 项目结构 + +``` +├── main.go # 入口:环境变量、初始化、优雅关闭 +├── config/ +│ ├── validation.go # 路径校验,防止目录穿越 +│ ├── parser.go # JSON/YAML 解析、序列化、深度合并 +│ ├── atomic.go # 原子写入(temp → fsync → rename) +│ └── backup.go # 时间戳备份、清理、回滚 +├── audit/ +│ └── logger.go # 结构化 JSON 审计日志 +├── server/ +│ ├── server.go # HTTP 服务、路由、模式状态机 +│ ├── middleware.go # 请求日志中间件 +│ └── handlers.go # API 处理函数 +├── Dockerfile # 多阶段构建 +└── docker-compose.yaml # 示例部署配置 +``` diff --git a/AbstractWizard/audit/logger.go b/AbstractWizard/audit/logger.go new file mode 100644 index 0000000..376c741 --- /dev/null +++ b/AbstractWizard/audit/logger.go @@ -0,0 +1,46 @@ +package audit + +import ( + "encoding/json" + "log" + "os" + "time" +) + +// Entry represents a single audit log record. +type Entry struct { + Timestamp string `json:"timestamp"` + RemoteIP string `json:"remote_ip"` + Action string `json:"action"` + Path string `json:"path"` + Summary string `json:"summary"` +} + +// Logger writes structured audit entries as JSON to stdout. +type Logger struct { + out *log.Logger +} + +// NewLogger creates a new audit logger writing to stdout. +func NewLogger() *Logger { + return &Logger{ + out: log.New(os.Stdout, "", 0), + } +} + +// Log writes a structured audit entry. +func (l *Logger) Log(remoteIP, action, path, summary string) { + entry := Entry{ + Timestamp: time.Now().UTC().Format(time.RFC3339), + RemoteIP: remoteIP, + Action: action, + Path: path, + Summary: summary, + } + data, err := json.Marshal(entry) + if err != nil { + l.out.Printf(`{"error":"failed to marshal audit entry: %v"}`, err) + return + } + l.out.Print(string(data)) +} diff --git a/AbstractWizard/config/atomic.go b/AbstractWizard/config/atomic.go new file mode 100644 index 0000000..d041525 --- /dev/null +++ b/AbstractWizard/config/atomic.go @@ -0,0 +1,55 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" +) + +// AtomicWrite writes data to the target path atomically by writing to a temp file +// in the same directory, fsyncing, then renaming over the target. +func AtomicWrite(path string, data []byte, perm os.FileMode) error { + dir := filepath.Dir(path) + + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create directory: %w", err) + } + + tmp, err := os.CreateTemp(dir, ".tmp-*") + if err != nil { + return fmt.Errorf("create temp file: %w", err) + } + tmpName := tmp.Name() + + cleanup := func() { + tmp.Close() + os.Remove(tmpName) + } + + if _, err := tmp.Write(data); err != nil { + cleanup() + return fmt.Errorf("write temp file: %w", err) + } + + if err := tmp.Sync(); err != nil { + cleanup() + return fmt.Errorf("fsync temp file: %w", err) + } + + if err := tmp.Close(); err != nil { + os.Remove(tmpName) + return fmt.Errorf("close temp file: %w", err) + } + + if err := os.Chmod(tmpName, perm); err != nil { + os.Remove(tmpName) + return fmt.Errorf("chmod temp file: %w", err) + } + + if err := os.Rename(tmpName, path); err != nil { + os.Remove(tmpName) + return fmt.Errorf("rename temp to target: %w", err) + } + + return nil +} diff --git a/AbstractWizard/config/backup.go b/AbstractWizard/config/backup.go new file mode 100644 index 0000000..d7f33f5 --- /dev/null +++ b/AbstractWizard/config/backup.go @@ -0,0 +1,156 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +const backupDirName = ".backups" + +// BackupInfo holds metadata about a single backup. +type BackupInfo struct { + Version string `json:"version"` + Timestamp string `json:"timestamp"` + Path string `json:"path"` +} + +// CreateBackup copies the current file into a timestamped backup before modification. +// Returns the backup version string. If the source file doesn't exist, returns "" with no error. +func CreateBackup(baseDir, relPath string, maxBackups int) (string, error) { + srcPath, err := FullPath(baseDir, relPath) + if err != nil { + return "", err + } + + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + return "", nil + } + + data, err := os.ReadFile(srcPath) + if err != nil { + return "", fmt.Errorf("read source for backup: %w", err) + } + + backupDir := filepath.Join(baseDir, backupDirName, filepath.Dir(filepath.Clean(relPath))) + if err := os.MkdirAll(backupDir, 0o755); err != nil { + return "", fmt.Errorf("create backup dir: %w", err) + } + + version := time.Now().UTC().Format("20060102T150405Z") + base := filepath.Base(relPath) + ext := filepath.Ext(base) + name := strings.TrimSuffix(base, ext) + backupName := fmt.Sprintf("%s.%s%s", name, version, ext) + backupPath := filepath.Join(backupDir, backupName) + + if err := AtomicWrite(backupPath, data, 0o644); err != nil { + return "", fmt.Errorf("write backup: %w", err) + } + + if err := pruneBackups(backupDir, name, ext, maxBackups); err != nil { + return "", fmt.Errorf("prune backups: %w", err) + } + + return version, nil +} + +// ListBackups returns all backup versions for a given config file, newest first. +func ListBackups(baseDir, relPath string) ([]BackupInfo, error) { + if err := ValidatePath(relPath); err != nil { + return nil, err + } + + backupDir := filepath.Join(baseDir, backupDirName, filepath.Dir(filepath.Clean(relPath))) + base := filepath.Base(relPath) + ext := filepath.Ext(base) + name := strings.TrimSuffix(base, ext) + prefix := name + "." + + entries, err := os.ReadDir(backupDir) + if os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("read backup dir: %w", err) + } + + var backups []BackupInfo + for _, e := range entries { + fname := e.Name() + if !strings.HasPrefix(fname, prefix) || !strings.HasSuffix(fname, ext) { + continue + } + version := strings.TrimSuffix(strings.TrimPrefix(fname, prefix), ext) + backups = append(backups, BackupInfo{ + Version: version, + Timestamp: version, + Path: filepath.Join(backupDir, fname), + }) + } + + sort.Slice(backups, func(i, j int) bool { + return backups[i].Version > backups[j].Version + }) + + return backups, nil +} + +// Rollback restores a config file to the given backup version. +func Rollback(baseDir, relPath, version string) error { + if err := ValidatePath(relPath); err != nil { + return err + } + + backupDir := filepath.Join(baseDir, backupDirName, filepath.Dir(filepath.Clean(relPath))) + base := filepath.Base(relPath) + ext := filepath.Ext(base) + name := strings.TrimSuffix(base, ext) + backupName := fmt.Sprintf("%s.%s%s", name, version, ext) + backupPath := filepath.Join(backupDir, backupName) + + data, err := os.ReadFile(backupPath) + if err != nil { + return fmt.Errorf("read backup %q: %w", version, err) + } + + targetPath, err := FullPath(baseDir, relPath) + if err != nil { + return err + } + + return AtomicWrite(targetPath, data, 0o644) +} + +// pruneBackups removes oldest backups exceeding maxBackups. +func pruneBackups(backupDir, name, ext string, maxBackups int) error { + if maxBackups <= 0 { + return nil + } + + prefix := name + "." + entries, err := os.ReadDir(backupDir) + if err != nil { + return err + } + + var matching []string + for _, e := range entries { + if strings.HasPrefix(e.Name(), prefix) && strings.HasSuffix(e.Name(), ext) { + matching = append(matching, e.Name()) + } + } + + sort.Strings(matching) + + if len(matching) > maxBackups { + for _, old := range matching[:len(matching)-maxBackups] { + os.Remove(filepath.Join(backupDir, old)) + } + } + + return nil +} diff --git a/AbstractWizard/config/parser.go b/AbstractWizard/config/parser.go new file mode 100644 index 0000000..6a81a05 --- /dev/null +++ b/AbstractWizard/config/parser.go @@ -0,0 +1,83 @@ +package config + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +// Format represents a config file format. +type Format int + +const ( + FormatJSON Format = iota + FormatYAML +) + +// DetectFormat returns the format based on file extension. +func DetectFormat(path string) (Format, error) { + ext := strings.ToLower(filepath.Ext(path)) + switch ext { + case ".json": + return FormatJSON, nil + case ".yaml", ".yml": + return FormatYAML, nil + default: + return 0, fmt.Errorf("unsupported format: %s", ext) + } +} + +// Parse deserializes raw bytes into a map based on the given format. +func Parse(data []byte, format Format) (map[string]any, error) { + var result map[string]any + switch format { + case FormatJSON: + if err := json.Unmarshal(data, &result); err != nil { + return nil, fmt.Errorf("invalid JSON: %w", err) + } + case FormatYAML: + if err := yaml.Unmarshal(data, &result); err != nil { + return nil, fmt.Errorf("invalid YAML: %w", err) + } + default: + return nil, fmt.Errorf("unknown format") + } + if result == nil { + result = make(map[string]any) + } + return result, nil +} + +// Serialize converts a map back to bytes in the given format. +func Serialize(data map[string]any, format Format) ([]byte, error) { + switch format { + case FormatJSON: + return json.MarshalIndent(data, "", " ") + case FormatYAML: + return yaml.Marshal(data) + default: + return nil, fmt.Errorf("unknown format") + } +} + +// DeepMerge merges src into dst recursively. Values in src override those in dst. +// Nested maps are merged; all other types are replaced. +func DeepMerge(dst, src map[string]any) map[string]any { + out := make(map[string]any, len(dst)) + for k, v := range dst { + out[k] = v + } + for k, v := range src { + if srcMap, ok := v.(map[string]any); ok { + if dstMap, ok := out[k].(map[string]any); ok { + out[k] = DeepMerge(dstMap, srcMap) + continue + } + } + out[k] = v + } + return out +} diff --git a/AbstractWizard/config/validation.go b/AbstractWizard/config/validation.go new file mode 100644 index 0000000..b4dae91 --- /dev/null +++ b/AbstractWizard/config/validation.go @@ -0,0 +1,49 @@ +package config + +import ( + "fmt" + "path/filepath" + "strings" +) + +var allowedExtensions = map[string]bool{ + ".json": true, + ".yaml": true, + ".yml": true, +} + +// ValidatePath checks that the given relative path is safe and points to a supported file type. +// It prevents directory traversal and rejects unsupported extensions. +func ValidatePath(relPath string) error { + if relPath == "" { + return fmt.Errorf("empty path") + } + + cleaned := filepath.Clean(relPath) + + if filepath.IsAbs(cleaned) { + return fmt.Errorf("absolute paths not allowed") + } + + for _, part := range strings.Split(cleaned, string(filepath.Separator)) { + if part == ".." { + return fmt.Errorf("path traversal not allowed") + } + } + + ext := strings.ToLower(filepath.Ext(cleaned)) + if !allowedExtensions[ext] { + return fmt.Errorf("unsupported file extension %q; allowed: .json, .yaml, .yml", ext) + } + + return nil +} + +// FullPath joins the base config directory with the validated relative path. +func FullPath(baseDir, relPath string) (string, error) { + if err := ValidatePath(relPath); err != nil { + return "", err + } + full := filepath.Join(baseDir, filepath.Clean(relPath)) + return full, nil +} diff --git a/AbstractWizard/docker-compose.yaml b/AbstractWizard/docker-compose.yaml new file mode 100644 index 0000000..ac6b32e --- /dev/null +++ b/AbstractWizard/docker-compose.yaml @@ -0,0 +1,14 @@ +services: + abstract-wizard: + build: . + ports: + - "127.0.0.1:18080:8080" + volumes: + - app_config:/config + environment: + CONFIG_DIR: /config + LISTEN_ADDR: "0.0.0.0:8080" + MAX_BACKUPS: "10" + +volumes: + app_config: diff --git a/AbstractWizard/go.mod b/AbstractWizard/go.mod new file mode 100644 index 0000000..2240697 --- /dev/null +++ b/AbstractWizard/go.mod @@ -0,0 +1,5 @@ +module AbstractWizard + +go 1.24 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/AbstractWizard/go.sum b/AbstractWizard/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/AbstractWizard/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/AbstractWizard/main.go b/AbstractWizard/main.go new file mode 100644 index 0000000..068535b --- /dev/null +++ b/AbstractWizard/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "log" + "os" + "strconv" + + "AbstractWizard/audit" + "AbstractWizard/server" +) + +func main() { + cfg := server.AppConfig{ + ConfigDir: envOrDefault("CONFIG_DIR", "/config"), + ListenAddr: envOrDefault("LISTEN_ADDR", "127.0.0.1:8080"), + MaxBackups: envOrDefaultInt("MAX_BACKUPS", 10), + } + + if err := os.MkdirAll(cfg.ConfigDir, 0o755); err != nil { + log.Fatalf("failed to create config directory %s: %v", cfg.ConfigDir, err) + } + + auditLog := audit.NewLogger() + srv := server.New(cfg, auditLog) + + log.Printf("config_dir=%s listen_addr=%s max_backups=%d", cfg.ConfigDir, cfg.ListenAddr, cfg.MaxBackups) + + if err := srv.ListenAndServe(); err != nil { + log.Fatalf("server error: %v", err) + } +} + +func envOrDefault(key, defaultVal string) string { + if v := os.Getenv(key); v != "" { + return v + } + return defaultVal +} + +func envOrDefaultInt(key string, defaultVal int) int { + v := os.Getenv(key) + if v == "" { + return defaultVal + } + n, err := strconv.Atoi(v) + if err != nil { + log.Printf("warning: invalid %s=%q, using default %d", key, v, defaultVal) + return defaultVal + } + return n +} diff --git a/AbstractWizard/server/handlers.go b/AbstractWizard/server/handlers.go new file mode 100644 index 0000000..cc1e365 --- /dev/null +++ b/AbstractWizard/server/handlers.go @@ -0,0 +1,265 @@ +package server + +import ( + "encoding/json" + "io" + "net/http" + "os" + + "AbstractWizard/config" +) + +func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { + relPath := r.PathValue("path") + + fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + + data, err := os.ReadFile(fullPath) + if os.IsNotExist(err) { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"}) + return + } + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read file"}) + return + } + + format, err := config.DetectFormat(fullPath) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + + parsed, err := config.Parse(data, format) + if err != nil { + writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) + return + } + + s.audit.Log(r.RemoteAddr, "read", relPath, "config read") + writeJSON(w, http.StatusOK, parsed) +} + +func (s *Server) handlePutConfig(w http.ResponseWriter, r *http.Request) { + if s.GetMode() == ModeReadOnly { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"}) + return + } + + relPath := r.PathValue("path") + + fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + + body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) // 10 MB limit + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read request body"}) + return + } + + format, err := config.DetectFormat(fullPath) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + + // Validate that the body is valid config + if _, err := config.Parse(body, format); err != nil { + writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) + return + } + + version, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "backup failed: " + err.Error()}) + return + } + + if err := config.AtomicWrite(fullPath, body, 0o644); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "write failed: " + err.Error()}) + return + } + + s.audit.Log(r.RemoteAddr, "replace", relPath, "full replace") + resp := map[string]string{"status": "ok"} + if version != "" { + resp["backup_version"] = version + } + writeJSON(w, http.StatusOK, resp) +} + +func (s *Server) handlePatchConfig(w http.ResponseWriter, r *http.Request) { + if s.GetMode() == ModeReadOnly { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"}) + return + } + + relPath := r.PathValue("path") + + fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + + format, err := config.DetectFormat(fullPath) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + + // Read existing config + existing := make(map[string]any) + data, err := os.ReadFile(fullPath) + if err != nil && !os.IsNotExist(err) { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read existing file"}) + return + } + if err == nil { + existing, err = config.Parse(data, format) + if err != nil { + writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "existing file is invalid: " + err.Error()}) + return + } + } + + // Parse patch body + body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read request body"}) + return + } + + patch, err := config.Parse(body, format) + if err != nil { + writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) + return + } + + merged := config.DeepMerge(existing, patch) + + out, err := config.Serialize(merged, format) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "serialization failed"}) + return + } + + version, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "backup failed: " + err.Error()}) + return + } + + if err := config.AtomicWrite(fullPath, out, 0o644); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "write failed"}) + return + } + + s.audit.Log(r.RemoteAddr, "patch", relPath, "deep merge update") + resp := map[string]string{"status": "ok"} + if version != "" { + resp["backup_version"] = version + } + writeJSON(w, http.StatusOK, resp) +} + +func (s *Server) handleListBackups(w http.ResponseWriter, r *http.Request) { + relPath := r.PathValue("path") + + backups, err := config.ListBackups(s.cfg.ConfigDir, relPath) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + + type backupResponse struct { + Version string `json:"version"` + Timestamp string `json:"timestamp"` + } + var resp []backupResponse + for _, b := range backups { + resp = append(resp, backupResponse{Version: b.Version, Timestamp: b.Timestamp}) + } + if resp == nil { + resp = []backupResponse{} + } + + s.audit.Log(r.RemoteAddr, "list_backups", relPath, "listed backups") + writeJSON(w, http.StatusOK, resp) +} + +func (s *Server) handleRollback(w http.ResponseWriter, r *http.Request) { + if s.GetMode() == ModeReadOnly { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"}) + return + } + + relPath := r.PathValue("path") + + var body struct { + Version string `json:"version"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + if body.Version == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "version is required"}) + return + } + + // Backup current state before rollback + if _, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "pre-rollback backup failed: " + err.Error()}) + return + } + + if err := config.Rollback(s.cfg.ConfigDir, relPath, body.Version); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "rollback failed: " + err.Error()}) + return + } + + s.audit.Log(r.RemoteAddr, "rollback", relPath, "rolled back to "+body.Version) + writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "restored_version": body.Version}) +} + +func (s *Server) handleGetMode(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{"mode": s.GetMode().String()}) +} + +func (s *Server) handleSetMode(w http.ResponseWriter, r *http.Request) { + var body struct { + Mode string `json:"mode"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + + mode, ok := ParseMode(body.Mode) + if !ok { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid mode; must be 'init' or 'readonly'"}) + return + } + + s.SetMode(mode) + s.audit.Log(r.RemoteAddr, "set_mode", "", "mode changed to "+body.Mode) + writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "mode": mode.String()}) +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(v) +} diff --git a/AbstractWizard/server/middleware.go b/AbstractWizard/server/middleware.go new file mode 100644 index 0000000..df7909a --- /dev/null +++ b/AbstractWizard/server/middleware.go @@ -0,0 +1,27 @@ +package server + +import ( + "log" + "net/http" + "time" +) + +type responseRecorder struct { + http.ResponseWriter + statusCode int +} + +func (r *responseRecorder) WriteHeader(code int) { + r.statusCode = code + r.ResponseWriter.WriteHeader(code) +} + +// LoggingMiddleware logs each incoming request with method, path, status, and duration. +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rec := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK} + next.ServeHTTP(rec, r) + log.Printf("%s %s %d %s", r.Method, r.URL.Path, rec.statusCode, time.Since(start).Round(time.Millisecond)) + }) +} diff --git a/AbstractWizard/server/server.go b/AbstractWizard/server/server.go new file mode 100644 index 0000000..27cd468 --- /dev/null +++ b/AbstractWizard/server/server.go @@ -0,0 +1,137 @@ +package server + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "AbstractWizard/audit" +) + +// Mode represents the server operating mode. +type Mode int + +const ( + ModeInit Mode = iota // init: read/write allowed + ModeReadOnly // readonly: only reads allowed +) + +func (m Mode) String() string { + switch m { + case ModeInit: + return "init" + case ModeReadOnly: + return "readonly" + default: + return "unknown" + } +} + +// ParseMode converts a string to a Mode. +func ParseMode(s string) (Mode, bool) { + switch s { + case "init": + return ModeInit, true + case "readonly": + return ModeReadOnly, true + default: + return 0, false + } +} + +// AppConfig holds environment-based configuration. +type AppConfig struct { + ConfigDir string + ListenAddr string + MaxBackups int +} + +// Server is the main HTTP server. +type Server struct { + cfg AppConfig + audit *audit.Logger + mode Mode + modeMu sync.RWMutex + srv *http.Server +} + +// New creates a new Server. +func New(cfg AppConfig, auditLog *audit.Logger) *Server { + s := &Server{ + cfg: cfg, + audit: auditLog, + mode: ModeInit, + } + + mux := http.NewServeMux() + s.registerRoutes(mux) + + s.srv = &http.Server{ + Addr: cfg.ListenAddr, + Handler: LoggingMiddleware(mux), + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + return s +} + +// registerRoutes sets up all API routes. +func (s *Server) registerRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /health", s.handleHealth) + mux.HandleFunc("GET /api/v1/config/{path...}", s.handleGetConfig) + mux.HandleFunc("PUT /api/v1/config/{path...}", s.handlePutConfig) + mux.HandleFunc("PATCH /api/v1/config/{path...}", s.handlePatchConfig) + mux.HandleFunc("GET /api/v1/backups/{path...}", s.handleListBackups) + mux.HandleFunc("POST /api/v1/rollback/{path...}", s.handleRollback) + mux.HandleFunc("GET /api/v1/mode", s.handleGetMode) + mux.HandleFunc("PUT /api/v1/mode", s.handleSetMode) +} + +// GetMode returns the current server mode. +func (s *Server) GetMode() Mode { + s.modeMu.RLock() + defer s.modeMu.RUnlock() + return s.mode +} + +// SetMode changes the server mode. +func (s *Server) SetMode(m Mode) { + s.modeMu.Lock() + defer s.modeMu.Unlock() + s.mode = m +} + +// ListenAndServe starts the HTTP server and blocks until shutdown. +func (s *Server) ListenAndServe() error { + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + errCh := make(chan error, 1) + go func() { + log.Printf("listening on %s", s.cfg.ListenAddr) + if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errCh <- err + } + close(errCh) + }() + + select { + case sig := <-stop: + log.Printf("received signal %v, shutting down", sig) + case err := <-errCh: + if err != nil { + return err + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + return s.srv.Shutdown(ctx) +} diff --git a/DependencyExtractor/.idea/DependencyExtractor.iml b/DependencyExtractor/.idea/DependencyExtractor.iml new file mode 100644 index 0000000..949f550 --- /dev/null +++ b/DependencyExtractor/.idea/DependencyExtractor.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Dialectic/docker-compose.yaml b/Dialectic/docker-compose.yaml new file mode 100644 index 0000000..c5b27a2 --- /dev/null +++ b/Dialectic/docker-compose.yaml @@ -0,0 +1,57 @@ +services: + frontend: + build: + context: ./Dialectic.Frontend + dockerfile: Dockerfile + ports: + - "${DIALECTIC_FRONTEND_PORT:-3000}:3000" + environment: + - REACT_APP_DIALECTIC_BACKEND_HOST=${DIALECTIC_BACKEND_URL:-http://localhost:8000} + networks: + - app-network + + backend: + build: + context: ./Dialectic.Backend + dockerfile: Dockerfile + ports: + - "${DIALECTIC_BACKEND_PORT:-8000}:8000" + - "127.0.0.1:${DIALECTIC_CONFIG_PORT:-9090}:9090" + volumes: + - dialectic_config:/app/config + environment: + - ENCRYPTION_KEY=${ENCRYPTION_KEY:-BgS9_9K2UMYxiYnP-BE63UEdi7a6PHaaZ6rQZQnSx54=} + - CONFIG_PATH=/app/config/dialectic.yaml + - CONFIG_PORT=${DIALECTIC_CONFIG_PORT:-9090} + depends_on: + db: + condition: service_healthy + networks: + - app-network + + db: + image: mysql:8.0 + restart: always + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword} + MYSQL_DATABASE: ${MYSQL_DATABASE:-dialectic} + MYSQL_USER: ${MYSQL_USER:-dialectic} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-dialectic} + ports: + - "${DB_PORT:-3306}:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + networks: + - app-network +volumes: + mysql_data: + dialectic_config: + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/Dialectic/init.sql b/Dialectic/init.sql new file mode 100644 index 0000000..b32d503 --- /dev/null +++ b/Dialectic/init.sql @@ -0,0 +1,47 @@ +-- MySQL initialization script for Dialectic + +CREATE DATABASE IF NOT EXISTS dialectic; +USE dialectic; + +-- Table for storing API keys for different providers +CREATE TABLE IF NOT EXISTS api_keys ( + id INT AUTO_INCREMENT PRIMARY KEY, + provider VARCHAR(50) NOT NULL UNIQUE, + api_key_encrypted TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Table for storing model configurations +CREATE TABLE IF NOT EXISTS model_configs ( + id INT AUTO_INCREMENT PRIMARY KEY, + provider VARCHAR(50) NOT NULL, + model_name VARCHAR(100) NOT NULL, + display_name VARCHAR(100), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_provider_model (provider, model_name) +); + +-- Insert default API key records (encrypted field will be empty initially) +INSERT IGNORE INTO api_keys (provider, api_key_encrypted) VALUES +('openai', ''), +('claude', ''), +('qwen', ''), +('deepseek', ''); + +-- Insert default model configurations +INSERT IGNORE INTO model_configs (provider, model_name, display_name) VALUES +('openai', 'gpt-4', 'GPT-4'), +('openai', 'gpt-3.5-turbo', 'GPT-3.5 Turbo'), +('claude', 'claude-3-opus', 'Claude 3 Opus'), +('claude', 'claude-3-sonnet', 'Claude 3 Sonnet'), +('claude', 'claude-3-haiku', 'Claude 3 Haiku'), +('qwen', 'qwen-max', 'Qwen Max'), +('qwen', 'qwen-plus', 'Qwen Plus'), +('deepseek', 'deepseek-chat', 'DeepSeek Chat'), +('deepseek', 'deepseek-coder', 'DeepSeek Coder'); + +-- Add evidence_library column to debate_sessions (migration for existing databases) +ALTER TABLE debate_sessions ADD COLUMN IF NOT EXISTS evidence_library TEXT NULL; \ No newline at end of file diff --git a/DungeonSolver.Workspace/CLAUDE.md b/DungeonSolver.Workspace/CLAUDE.md new file mode 100644 index 0000000..7acf72e --- /dev/null +++ b/DungeonSolver.Workspace/CLAUDE.md @@ -0,0 +1,115 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Workspace Architecture + +**DungeonSolver.Workspace** is a multi-project C# solution (.NET 8.0) organized as a modular game development workspace. Each project serves a specific purpose and can be built independently. + +### Project Structure + +- **DungeonSolver/**: Main Godot 4.5 game project with Python integration (see `DungeonSolver/CLAUDE.md` for detailed game-specific guidance) +- **DungeonSolver.Global/**: Core game systems and global providers, outputs to `DungeonSolver/Data/` for game consumption +- **DungeonSolver.PythonBridge/**: Python integration layer using IronPython/pythonnet +- **DungeonSolver.Game/**: Game-specific logic and systems +- **DungeonSolver.Generators/**: Roslyn source generators for automated code generation +- **DungeonSolver.Global.Generators/**: Source generators specifically for the Global project +- **DungeonSolver.Tasks/**: Build tasks and utilities + +### Key Architectural Patterns + +**Modular Project Design**: Each component is isolated in its own project with specific responsibilities: +- Global project compiles to `DungeonSolver/Data/` for runtime access +- Source generators use `OutputItemType="Analyzer" ReferenceOutputAssembly="false"` pattern +- Python bridge allows C# game systems to be accessed from Python via `clr.AddReference()` + +**Source Generator Integration**: Projects use Roslyn source generators for code generation: +- Generators project contains sample incremental source generators +- Target classes use `[Generators.ReportAttribute]` for report generation +- Build generators to see generated code in IDE + +**Python-C# Interop**: +- **Python Package Generator**: Automatically generates Python wrapper classes for C# types marked with `[PythonSharedType]` attributes + - Generates type-safe Python interfaces from C# classes, interfaces, and enums + - Handles complex generic types and nested class hierarchies + - Supports inheritance, member access, and method calls with proper type hints + - Generated Python packages are output to `user://dungeon_solver/types/` +- **Type Registration System**: Uses `PythonSharedTypeRegister` to map C# types to Python equivalents +- **Shared Memory Communication**: Advanced IPC system for real-time C#-Python communication (see `Documents/SharedMemoryCommunicationProtocol.md`) +- **Runtime Access**: Python scripts access C# via `DungeonSolver.Global.dll` using IronPython/pythonnet +- **Bridge Pattern**: Example implementation in `DungeonSolver/Data/Solver.py` + +## Build and Development Commands + +**Building Individual Projects**: +```bash +# Build specific project +dotnet build DungeonSolver.Global/ +dotnet build DungeonSolver.Generators/ +dotnet build DungeonSolver.PythonBridge/ + +# Build main game project (includes all dependencies) +cd DungeonSolver && dotnet build +``` + +**Building the Entire Workspace**: +```bash +# Build all projects from workspace root +dotnet build DungeonSolver/DungeonSolver.sln +``` + +**Source Generator Development**: +```bash +# Build generators to see generated code +dotnet build DungeonSolver.Generators/ +dotnet build DungeonSolver.Global.Generators/ +``` + +**Testing**: +```bash +# Run generator unit tests +cd DungeonSolver.Generators && dotnet test +cd DungeonSolver.Global.Generators && dotnet test +``` + +## Code Style Guidelines + +**C# Coding Standards**: +- **No `var` keyword**: Always use explicit types instead of `var` + ```csharp + string name = "example"; // Correct + List numbers = new List(); // Correct + // var name = "example"; // Wrong + ``` +- **No comments**: Do not write any comments in code (XML documentation, inline comments, or block comments) +- Write self-documenting code with clear variable and method names + +## Development Workflow Notes + +**Project Dependencies**: The main solution (`DungeonSolver/DungeonSolver.sln`) references all other projects, creating a unified build process while maintaining modular development. + +**Global System Access**: Python scripts can access the Global project's compiled output via the established bridge pattern in `Data/Solver.py`. + +**Source Generator Workflow**: +1. Modify generator code in `*.Generators` projects +2. Build generator project to compile changes +3. Target projects automatically regenerate code during their build +4. Use Rossynt plugin for syntax tree analysis during development + +**Cross-Project Development**: Each project can be developed and tested independently, with the main game project serving as the integration point. + +**Python Type System Development**: +1. Mark C# types with appropriate `[PythonSharedType]` attributes (`[PythonSharedClass]`, `[PythonSharedInterface]`, `[PythonSharedEnum]`) +2. Add `[PythonSharedMember]` and `[PythonSharedMethod]` attributes to expose specific members +3. Build `DungeonSolver.Global` project to register types in `PythonSharedTypeRegister` +4. Generated Python types automatically handle complex scenarios like: + - Generic type parameters and constraints + - Nested class inheritance hierarchies + - Interface implementation and multiple inheritance + - Enum value mapping and type safety + +**Important Development Notes**: +- Always build `DungeonSolver.Global` before the main game project to ensure Python bindings are up-to-date +- When modifying Python-shared types, rebuild is required to regenerate Python wrapper classes +- Use `PythonPackageGenerator.Generate()` to manually trigger Python code generation during development +- Nested classes inherit sharing attributes from parent classes automatically via `NestedClassWrapper` pattern \ No newline at end of file diff --git a/DungeonSolver.Workspace/DungeonSolver b/DungeonSolver.Workspace/DungeonSolver new file mode 160000 index 0000000..0d81d98 --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver @@ -0,0 +1 @@ +Subproject commit 0d81d98a3581232f9e2ecb6c144370c7bff9b3b7 diff --git a/DungeonSolver.Workspace/DungeonSolver.Analyzer b/DungeonSolver.Workspace/DungeonSolver.Analyzer new file mode 160000 index 0000000..b82ca16 --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Analyzer @@ -0,0 +1 @@ +Subproject commit b82ca1657e88b3f1c7dd10052bf06f7e73c63e65 diff --git a/DungeonSolver.Workspace/DungeonSolver.Core b/DungeonSolver.Workspace/DungeonSolver.Core new file mode 160000 index 0000000..9de186f --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Core @@ -0,0 +1 @@ +Subproject commit 9de186f41ab4babd7b52e10d064f1f47bdf1626d diff --git a/DungeonSolver.Workspace/DungeonSolver.Core.Analyzer b/DungeonSolver.Workspace/DungeonSolver.Core.Analyzer new file mode 160000 index 0000000..d7d39d2 --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Core.Analyzer @@ -0,0 +1 @@ +Subproject commit d7d39d2332849abdb9f405402b1c08cd45537e9b diff --git a/DungeonSolver.Workspace/DungeonSolver.Core.Generators b/DungeonSolver.Workspace/DungeonSolver.Core.Generators new file mode 160000 index 0000000..056e924 --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Core.Generators @@ -0,0 +1 @@ +Subproject commit 056e9249438b46bccac0b385af3e3a602f9049be diff --git a/DungeonSolver.Workspace/DungeonSolver.Generator.Core b/DungeonSolver.Workspace/DungeonSolver.Generator.Core new file mode 160000 index 0000000..8c66583 --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Generator.Core @@ -0,0 +1 @@ +Subproject commit 8c66583d2319e8502e66421d3feef8f77a8831e5 diff --git a/DungeonSolver.Workspace/DungeonSolver.Generators b/DungeonSolver.Workspace/DungeonSolver.Generators new file mode 160000 index 0000000..57afa2c --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Generators @@ -0,0 +1 @@ +Subproject commit 57afa2cc50cdb1e80e675b48747805f1ead9d23f diff --git a/DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/.idea/.idea.DungeonSolver.Global.Analyzer/.idea/workspace.xml b/DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/.idea/.idea.DungeonSolver.Global.Analyzer/.idea/workspace.xml new file mode 100644 index 0000000..84850eb --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/.idea/.idea.DungeonSolver.Global.Analyzer/.idea/workspace.xml @@ -0,0 +1,132 @@ + + + + DungeonSolver.Global.Analyzer/DungeonSolver.Global.Analyzer.csproj + + + + + + + + + + { + "associatedIndex": 6 +} + + + + { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.git.unshallow": "true", + "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultAutoModeForALLUsers.v1": "true", + "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true", + "git-widget-placeholder": "master", + "junie.onboarding.icon.badge.shown": "true", + "node.js.detected.package.eslint": "true", + "node.js.detected.package.tslint": "true", + "node.js.selected.package.eslint": "(autodetect)", + "node.js.selected.package.tslint": "(autodetect)", + "nodejs_package_manager_path": "npm", + "to.speed.mode.migration.done": "true", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + 1761816676447 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/DungeonSolver.Core.Analyzer.sln.DotSettings.user b/DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/DungeonSolver.Core.Analyzer.sln.DotSettings.user new file mode 100644 index 0000000..11b6e06 --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Global.Analyzer/DungeonSolver.Core.Analyzer.sln.DotSettings.user @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/DungeonSolver.Workspace/DungeonSolver.Global.Generators/.idea/.idea.DungeonSolver.Global.Generators/.idea/workspace.xml b/DungeonSolver.Workspace/DungeonSolver.Global.Generators/.idea/.idea.DungeonSolver.Global.Generators/.idea/workspace.xml new file mode 100644 index 0000000..be2e341 --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Global.Generators/.idea/.idea.DungeonSolver.Global.Generators/.idea/workspace.xml @@ -0,0 +1,120 @@ + + + + ../DungeonSolver.Core.Generators/DungeonSolver.Core.Generators.csproj + DungeonSolver.Global.Generators/DungeonSolver.Global.Generators/DungeonSolver.Global.Generators.csproj + + + + + + + + + + + { + "associatedIndex": 7 +} + + + + + + + + + + + + + + + 1731403531514 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DungeonSolver.Workspace/DungeonSolver.Global/.idea/.idea.DungeonSolver.Global/.idea/workspace.xml b/DungeonSolver.Workspace/DungeonSolver.Global/.idea/.idea.DungeonSolver.Global/.idea/workspace.xml new file mode 100644 index 0000000..3a6cb9b --- /dev/null +++ b/DungeonSolver.Workspace/DungeonSolver.Global/.idea/.idea.DungeonSolver.Global/.idea/workspace.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + { + "associatedIndex": 7 +} + + + + + + + + + 1727693834377 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DungeonSolver.Workspace/Skeleton b/DungeonSolver.Workspace/Skeleton new file mode 160000 index 0000000..7299a2d --- /dev/null +++ b/DungeonSolver.Workspace/Skeleton @@ -0,0 +1 @@ +Subproject commit 7299a2dba3c72e3f13d960b90e275b7bbbb94348 diff --git a/DungeonSolver.Workspace/statistic.py b/DungeonSolver.Workspace/statistic.py new file mode 100644 index 0000000..64bdcbe --- /dev/null +++ b/DungeonSolver.Workspace/statistic.py @@ -0,0 +1,20 @@ +import os + +def count_cs_lines(root_dir: str): + total = 0 + for dirpath, _, filenames in os.walk(root_dir): + for filename in filenames: + if filename.endswith(".cs"): + filepath = os.path.join(dirpath, filename) + with open(filepath, "r", encoding="utf-8", errors="ignore") as f: + lines = [ + line for line in f + if line.strip() and not line.strip().startswith("//") + ] + total += len(lines) + print(f"Total non-empty lines in .cs files: {total}") + +if __name__ == "__main__": + os.chdir(os.path.dirname(os.path.abspath(__file__))) + count_cs_lines(".") + diff --git a/Hangman-Lab/SkeletonTest/.idea/SkeletonTest.iml b/Hangman-Lab/SkeletonTest/.idea/SkeletonTest.iml new file mode 100644 index 0000000..dd533ea --- /dev/null +++ b/Hangman-Lab/SkeletonTest/.idea/SkeletonTest.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/JupyterProject/.idea/JupyterProject.iml b/JupyterProject/.idea/JupyterProject.iml new file mode 100644 index 0000000..5bc2aea --- /dev/null +++ b/JupyterProject/.idea/JupyterProject.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/JupyterProject/s.ipynb b/JupyterProject/s.ipynb new file mode 100644 index 0000000..14c491f --- /dev/null +++ b/JupyterProject/s.ipynb @@ -0,0 +1,47 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-09-29T14:08:48.168337Z", + "start_time": "2025-09-29T14:08:48.157155Z" + } + }, + "source": "import sys", + "outputs": [], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "652c2222ded0ab4f" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/structured-output-parser-mcp/.idea/structured-output-parser-mcp.iml b/structured-output-parser-mcp/.idea/structured-output-parser-mcp.iml new file mode 100644 index 0000000..9905361 --- /dev/null +++ b/structured-output-parser-mcp/.idea/structured-output-parser-mcp.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file