@dewtech/dare-cli 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -39
- package/dist/bin/dare.js +1 -3
- package/dist/bin/dare.js.map +1 -1
- package/dist/commands/__tests__/init.integration.spec.d.ts +2 -0
- package/dist/commands/__tests__/init.integration.spec.d.ts.map +1 -0
- package/dist/commands/__tests__/init.integration.spec.js +134 -0
- package/dist/commands/__tests__/init.integration.spec.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +84 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +2 -1
- package/dist/commands/new.js.map +1 -1
- package/dist/mcp-server/bin/server.js +0 -0
- package/dist/stacks/__tests__/dna-emitter.spec.d.ts +2 -0
- package/dist/stacks/__tests__/dna-emitter.spec.d.ts.map +1 -0
- package/dist/stacks/__tests__/dna-emitter.spec.js +207 -0
- package/dist/stacks/__tests__/dna-emitter.spec.js.map +1 -0
- package/dist/stacks/__tests__/dna.spec.d.ts +2 -0
- package/dist/stacks/__tests__/dna.spec.d.ts.map +1 -0
- package/dist/stacks/__tests__/dna.spec.js +211 -0
- package/dist/stacks/__tests__/dna.spec.js.map +1 -0
- package/dist/stacks/__tests__/parity-rails.fixture.json +228 -0
- package/dist/stacks/__tests__/parity-rails.spec.d.ts +2 -0
- package/dist/stacks/__tests__/parity-rails.spec.d.ts.map +1 -0
- package/dist/stacks/__tests__/parity-rails.spec.js +99 -0
- package/dist/stacks/__tests__/parity-rails.spec.js.map +1 -0
- package/dist/stacks/__tests__/registry.spec.d.ts +2 -0
- package/dist/stacks/__tests__/registry.spec.d.ts.map +1 -0
- package/dist/stacks/__tests__/registry.spec.js +101 -0
- package/dist/stacks/__tests__/registry.spec.js.map +1 -0
- package/dist/stacks/__tests__/template-engine.spec.d.ts +2 -0
- package/dist/stacks/__tests__/template-engine.spec.d.ts.map +1 -0
- package/dist/stacks/__tests__/template-engine.spec.js +149 -0
- package/dist/stacks/__tests__/template-engine.spec.js.map +1 -0
- package/dist/stacks/dna-emitter.d.ts +45 -0
- package/dist/stacks/dna-emitter.d.ts.map +1 -0
- package/dist/stacks/dna-emitter.js +267 -0
- package/dist/stacks/dna-emitter.js.map +1 -0
- package/dist/stacks/go-gin/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/go-gin/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/go-gin/__tests__/scaffold.spec.js +221 -0
- package/dist/stacks/go-gin/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/go-gin/scaffold.d.ts +3 -0
- package/dist/stacks/go-gin/scaffold.d.ts.map +1 -0
- package/dist/stacks/go-gin/scaffold.js +105 -0
- package/dist/stacks/go-gin/scaffold.js.map +1 -0
- package/dist/stacks/go-stdlib/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/go-stdlib/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/go-stdlib/__tests__/scaffold.spec.js +215 -0
- package/dist/stacks/go-stdlib/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/go-stdlib/scaffold.d.ts +3 -0
- package/dist/stacks/go-stdlib/scaffold.d.ts.map +1 -0
- package/dist/stacks/go-stdlib/scaffold.js +106 -0
- package/dist/stacks/go-stdlib/scaffold.js.map +1 -0
- package/dist/stacks/mcp-go/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/mcp-go/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/mcp-go/__tests__/scaffold.spec.js +203 -0
- package/dist/stacks/mcp-go/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/mcp-go/scaffold.d.ts +3 -0
- package/dist/stacks/mcp-go/scaffold.d.ts.map +1 -0
- package/dist/stacks/mcp-go/scaffold.js +94 -0
- package/dist/stacks/mcp-go/scaffold.js.map +1 -0
- package/dist/stacks/mcp-node-ts/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/mcp-node-ts/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/mcp-node-ts/__tests__/scaffold.spec.js +236 -0
- package/dist/stacks/mcp-node-ts/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/mcp-node-ts/scaffold.d.ts +3 -0
- package/dist/stacks/mcp-node-ts/scaffold.d.ts.map +1 -0
- package/dist/stacks/mcp-node-ts/scaffold.js +95 -0
- package/dist/stacks/mcp-node-ts/scaffold.js.map +1 -0
- package/dist/stacks/mcp-python/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/mcp-python/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/mcp-python/__tests__/scaffold.spec.js +228 -0
- package/dist/stacks/mcp-python/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/mcp-python/scaffold.d.ts +3 -0
- package/dist/stacks/mcp-python/scaffold.d.ts.map +1 -0
- package/dist/stacks/mcp-python/scaffold.js +98 -0
- package/dist/stacks/mcp-python/scaffold.js.map +1 -0
- package/dist/stacks/mcp-rust/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/mcp-rust/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/mcp-rust/__tests__/scaffold.spec.js +213 -0
- package/dist/stacks/mcp-rust/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/mcp-rust/scaffold.d.ts +3 -0
- package/dist/stacks/mcp-rust/scaffold.d.ts.map +1 -0
- package/dist/stacks/mcp-rust/scaffold.js +98 -0
- package/dist/stacks/mcp-rust/scaffold.js.map +1 -0
- package/dist/stacks/node-nestjs/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/node-nestjs/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/node-nestjs/__tests__/scaffold.spec.js +172 -0
- package/dist/stacks/node-nestjs/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/node-nestjs/scaffold.d.ts +3 -0
- package/dist/stacks/node-nestjs/scaffold.d.ts.map +1 -0
- package/dist/stacks/node-nestjs/scaffold.js +117 -0
- package/dist/stacks/node-nestjs/scaffold.js.map +1 -0
- package/dist/stacks/php-laravel/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/php-laravel/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/php-laravel/__tests__/scaffold.spec.js +205 -0
- package/dist/stacks/php-laravel/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/php-laravel/scaffold.d.ts +3 -0
- package/dist/stacks/php-laravel/scaffold.d.ts.map +1 -0
- package/dist/stacks/php-laravel/scaffold.js +109 -0
- package/dist/stacks/php-laravel/scaffold.js.map +1 -0
- package/dist/stacks/python-fastapi/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/python-fastapi/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/python-fastapi/__tests__/scaffold.spec.js +168 -0
- package/dist/stacks/python-fastapi/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/python-fastapi/scaffold.d.ts +3 -0
- package/dist/stacks/python-fastapi/scaffold.d.ts.map +1 -0
- package/dist/stacks/python-fastapi/scaffold.js +108 -0
- package/dist/stacks/python-fastapi/scaffold.js.map +1 -0
- package/dist/stacks/registry.d.ts +38 -0
- package/dist/stacks/registry.d.ts.map +1 -0
- package/dist/stacks/registry.js +153 -0
- package/dist/stacks/registry.js.map +1 -0
- package/dist/stacks/ruby-rails-8/__tests__/scaffold.spec.d.ts +6 -0
- package/dist/stacks/ruby-rails-8/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/ruby-rails-8/__tests__/scaffold.spec.js +604 -0
- package/dist/stacks/ruby-rails-8/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/ruby-rails-8/scaffold.d.ts +91 -0
- package/dist/stacks/ruby-rails-8/scaffold.d.ts.map +1 -0
- package/dist/stacks/ruby-rails-8/scaffold.js +410 -0
- package/dist/stacks/ruby-rails-8/scaffold.js.map +1 -0
- package/dist/stacks/rust-axum/__tests__/scaffold.spec.d.ts +2 -0
- package/dist/stacks/rust-axum/__tests__/scaffold.spec.d.ts.map +1 -0
- package/dist/stacks/rust-axum/__tests__/scaffold.spec.js +203 -0
- package/dist/stacks/rust-axum/__tests__/scaffold.spec.js.map +1 -0
- package/dist/stacks/rust-axum/scaffold.d.ts +3 -0
- package/dist/stacks/rust-axum/scaffold.d.ts.map +1 -0
- package/dist/stacks/rust-axum/scaffold.js +105 -0
- package/dist/stacks/rust-axum/scaffold.js.map +1 -0
- package/dist/stacks/template-engine.d.ts +38 -0
- package/dist/stacks/template-engine.d.ts.map +1 -0
- package/dist/stacks/template-engine.js +134 -0
- package/dist/stacks/template-engine.js.map +1 -0
- package/dist/stacks/types.d.ts +69 -0
- package/dist/stacks/types.d.ts.map +1 -0
- package/dist/stacks/types.js +29 -0
- package/dist/stacks/types.js.map +1 -0
- package/dist/utils/project-generator.d.ts +1 -1
- package/dist/utils/project-generator.d.ts.map +1 -1
- package/dist/utils/project-generator.js +16 -7
- package/dist/utils/project-generator.js.map +1 -1
- package/dist/utils/stack-bootstrap.d.ts +3 -2
- package/dist/utils/stack-bootstrap.d.ts.map +1 -1
- package/dist/utils/stack-bootstrap.js +46 -16
- package/dist/utils/stack-bootstrap.js.map +1 -1
- package/package.json +91 -87
- package/templates/stacks/go-gin/.dare/skills.yml +11 -0
- package/templates/stacks/go-gin/.env.example +24 -0
- package/templates/stacks/go-gin/.github/workflows/dare-ci.yml +42 -0
- package/templates/stacks/go-gin/README.md.tpl +38 -0
- package/templates/stacks/go-gin/cmd/server/main.go.tpl +78 -0
- package/templates/stacks/go-gin/db/migrations/0001_create_users.down.sql +2 -0
- package/templates/stacks/go-gin/db/migrations/0001_create_users.up.sql +12 -0
- package/templates/stacks/go-gin/db/queries/users.sql +23 -0
- package/templates/stacks/go-gin/gitignore +7 -0
- package/templates/stacks/go-gin/go.mod.tpl +17 -0
- package/templates/stacks/go-gin/internal/config/config.go +41 -0
- package/templates/stacks/go-gin/internal/db/postgres.go.tpl +25 -0
- package/templates/stacks/go-gin/internal/handler/auth_handler.go.tpl +72 -0
- package/templates/stacks/go-gin/internal/handler/users_handler.go.tpl +72 -0
- package/templates/stacks/go-gin/internal/handler/ws_handler.go +37 -0
- package/templates/stacks/go-gin/internal/llm/dummy.go +14 -0
- package/templates/stacks/go-gin/internal/llm/provider.go +8 -0
- package/templates/stacks/go-gin/internal/middleware/jwt.go.tpl +58 -0
- package/templates/stacks/go-gin/internal/middleware/rate_limit.go +55 -0
- package/templates/stacks/go-gin/internal/model/user.go +17 -0
- package/templates/stacks/go-gin/internal/repository/users_repository.go.tpl +79 -0
- package/templates/stacks/go-gin/internal/service/auth_service.go.tpl +55 -0
- package/templates/stacks/go-gin/internal/service/users_service.go.tpl +53 -0
- package/templates/stacks/go-gin/llms.txt.tpl +54 -0
- package/templates/stacks/go-gin/openapi.json.tpl +46 -0
- package/templates/stacks/go-gin/sqlc.yaml +14 -0
- package/templates/stacks/go-gin/tests/smoke_test.go.tpl +22 -0
- package/templates/stacks/go-stdlib/.dare/skills.yml +11 -0
- package/templates/stacks/go-stdlib/.env.example +24 -0
- package/templates/stacks/go-stdlib/.github/workflows/dare-ci.yml +42 -0
- package/templates/stacks/go-stdlib/README.md.tpl +41 -0
- package/templates/stacks/go-stdlib/cmd/server/main.go.tpl +82 -0
- package/templates/stacks/go-stdlib/db/migrations/0001_create_users.down.sql +2 -0
- package/templates/stacks/go-stdlib/db/migrations/0001_create_users.up.sql +12 -0
- package/templates/stacks/go-stdlib/db/queries/users.sql +23 -0
- package/templates/stacks/go-stdlib/gitignore +6 -0
- package/templates/stacks/go-stdlib/go.mod.tpl +15 -0
- package/templates/stacks/go-stdlib/internal/config/config.go +41 -0
- package/templates/stacks/go-stdlib/internal/db/postgres.go.tpl +24 -0
- package/templates/stacks/go-stdlib/internal/handler/auth_handler.go.tpl +71 -0
- package/templates/stacks/go-stdlib/internal/handler/users_handler.go.tpl +84 -0
- package/templates/stacks/go-stdlib/internal/handler/ws_handler.go +36 -0
- package/templates/stacks/go-stdlib/internal/httpx/json.go +32 -0
- package/templates/stacks/go-stdlib/internal/llm/dummy.go +14 -0
- package/templates/stacks/go-stdlib/internal/llm/provider.go +8 -0
- package/templates/stacks/go-stdlib/internal/middleware/chain.go +21 -0
- package/templates/stacks/go-stdlib/internal/middleware/cors.go +27 -0
- package/templates/stacks/go-stdlib/internal/middleware/jwt.go.tpl +51 -0
- package/templates/stacks/go-stdlib/internal/middleware/rate_limit.go +81 -0
- package/templates/stacks/go-stdlib/internal/model/user.go +17 -0
- package/templates/stacks/go-stdlib/internal/repository/users_repository.go.tpl +75 -0
- package/templates/stacks/go-stdlib/internal/service/auth_service.go.tpl +55 -0
- package/templates/stacks/go-stdlib/internal/service/users_service.go.tpl +53 -0
- package/templates/stacks/go-stdlib/llms.txt.tpl +60 -0
- package/templates/stacks/go-stdlib/openapi.json.tpl +46 -0
- package/templates/stacks/go-stdlib/sqlc.yaml +14 -0
- package/templates/stacks/go-stdlib/tests/smoke_test.go.tpl +45 -0
- package/templates/stacks/mcp-go/.dare/skills.yml +8 -0
- package/templates/stacks/mcp-go/.env.example +14 -0
- package/templates/stacks/mcp-go/.github/workflows/dare-ci.yml +42 -0
- package/templates/stacks/mcp-go/README.md.tpl +50 -0
- package/templates/stacks/mcp-go/cmd/server/main.go.tpl +62 -0
- package/templates/stacks/mcp-go/gitignore +6 -0
- package/templates/stacks/mcp-go/go.mod.tpl +9 -0
- package/templates/stacks/mcp-go/internal/prompts/summarize.go +9 -0
- package/templates/stacks/mcp-go/internal/server/server.go.tpl +80 -0
- package/templates/stacks/mcp-go/internal/tools/echo.go +15 -0
- package/templates/stacks/mcp-go/internal/transports/http.go.tpl +21 -0
- package/templates/stacks/mcp-go/internal/transports/sse.go.tpl +17 -0
- package/templates/stacks/mcp-go/internal/transports/stdio.go.tpl +14 -0
- package/templates/stacks/mcp-go/llms.txt.tpl +60 -0
- package/templates/stacks/mcp-go/openapi.json.tpl +31 -0
- package/templates/stacks/mcp-go/tests/echo_test.go.tpl +37 -0
- package/templates/stacks/mcp-node-ts/.dare/skills.yml +8 -0
- package/templates/stacks/mcp-node-ts/.env.example +16 -0
- package/templates/stacks/mcp-node-ts/.github/workflows/dare-ci.yml +54 -0
- package/templates/stacks/mcp-node-ts/README.md.hbs +49 -0
- package/templates/stacks/mcp-node-ts/gitignore +7 -0
- package/templates/stacks/mcp-node-ts/llms.txt.hbs +61 -0
- package/templates/stacks/mcp-node-ts/openapi.json.hbs +39 -0
- package/templates/stacks/mcp-node-ts/package.json.hbs +35 -0
- package/templates/stacks/mcp-node-ts/src/cli.ts.hbs +71 -0
- package/templates/stacks/mcp-node-ts/src/prompts/index.ts +36 -0
- package/templates/stacks/mcp-node-ts/src/server.ts.hbs +45 -0
- package/templates/stacks/mcp-node-ts/src/tools/echo.ts +23 -0
- package/templates/stacks/mcp-node-ts/src/tools/index.ts +18 -0
- package/templates/stacks/mcp-node-ts/src/transports/http.ts +68 -0
- package/templates/stacks/mcp-node-ts/src/transports/sse.ts +58 -0
- package/templates/stacks/mcp-node-ts/src/transports/stdio.ts +5 -0
- package/templates/stacks/mcp-node-ts/tests/echo.test.ts +50 -0
- package/templates/stacks/mcp-node-ts/tsconfig.json +17 -0
- package/templates/stacks/mcp-python/.dare/skills.yml +8 -0
- package/templates/stacks/mcp-python/.env.example +14 -0
- package/templates/stacks/mcp-python/.github/workflows/dare-ci.yml +42 -0
- package/templates/stacks/mcp-python/README.md.j2 +49 -0
- package/templates/stacks/mcp-python/gitignore +12 -0
- package/templates/stacks/mcp-python/llms.txt.j2 +56 -0
- package/templates/stacks/mcp-python/openapi.json.j2 +33 -0
- package/templates/stacks/mcp-python/pyproject.toml.j2 +37 -0
- package/templates/stacks/mcp-python/src/__init__.py +0 -0
- package/templates/stacks/mcp-python/src/cli.py.j2 +68 -0
- package/templates/stacks/mcp-python/src/prompts/__init__.py +0 -0
- package/templates/stacks/mcp-python/src/prompts/summarize.py +10 -0
- package/templates/stacks/mcp-python/src/server.py.j2 +28 -0
- package/templates/stacks/mcp-python/src/tools/__init__.py +0 -0
- package/templates/stacks/mcp-python/src/tools/echo.py +12 -0
- package/templates/stacks/mcp-python/src/transports/__init__.py +0 -0
- package/templates/stacks/mcp-python/src/transports/http.py +12 -0
- package/templates/stacks/mcp-python/src/transports/sse.py +13 -0
- package/templates/stacks/mcp-python/src/transports/stdio.py +6 -0
- package/templates/stacks/mcp-python/tests/__init__.py +0 -0
- package/templates/stacks/mcp-python/tests/test_echo.py +28 -0
- package/templates/stacks/mcp-rust/.dare/skills.yml +8 -0
- package/templates/stacks/mcp-rust/.env.example +14 -0
- package/templates/stacks/mcp-rust/.github/workflows/dare-ci.yml +38 -0
- package/templates/stacks/mcp-rust/Cargo.toml.tera +35 -0
- package/templates/stacks/mcp-rust/README.md.tera +50 -0
- package/templates/stacks/mcp-rust/gitignore +5 -0
- package/templates/stacks/mcp-rust/llms.txt.tera +60 -0
- package/templates/stacks/mcp-rust/openapi.json.tera +31 -0
- package/templates/stacks/mcp-rust/src/cli.rs.tera +33 -0
- package/templates/stacks/mcp-rust/src/lib.rs +6 -0
- package/templates/stacks/mcp-rust/src/main.rs.tera +30 -0
- package/templates/stacks/mcp-rust/src/prompts/mod.rs +1 -0
- package/templates/stacks/mcp-rust/src/prompts/summarize.rs +5 -0
- package/templates/stacks/mcp-rust/src/server.rs.tera +38 -0
- package/templates/stacks/mcp-rust/src/tools/echo.rs +18 -0
- package/templates/stacks/mcp-rust/src/tools/mod.rs +22 -0
- package/templates/stacks/mcp-rust/src/transports/http.rs +27 -0
- package/templates/stacks/mcp-rust/src/transports/mod.rs +3 -0
- package/templates/stacks/mcp-rust/src/transports/sse.rs +33 -0
- package/templates/stacks/mcp-rust/src/transports/stdio.rs +14 -0
- package/templates/stacks/mcp-rust/tests/echo_test.rs.tera +27 -0
- package/templates/stacks/node-nestjs/.dare/skills.yml +11 -0
- package/templates/stacks/node-nestjs/.env.example +21 -0
- package/templates/stacks/node-nestjs/.github/workflows/dare-ci.yml +54 -0
- package/templates/stacks/node-nestjs/README.md.hbs +35 -0
- package/templates/stacks/node-nestjs/gitignore +7 -0
- package/templates/stacks/node-nestjs/llms.txt.hbs +47 -0
- package/templates/stacks/node-nestjs/nest-cli.json +16 -0
- package/templates/stacks/node-nestjs/openapi.json.hbs +75 -0
- package/templates/stacks/node-nestjs/package.json.hbs +57 -0
- package/templates/stacks/node-nestjs/prisma/schema.prisma +25 -0
- package/templates/stacks/node-nestjs/prisma/seed.ts.hbs +25 -0
- package/templates/stacks/node-nestjs/src/app.module.ts +39 -0
- package/templates/stacks/node-nestjs/src/auth/auth.controller.ts +29 -0
- package/templates/stacks/node-nestjs/src/auth/auth.module.ts +25 -0
- package/templates/stacks/node-nestjs/src/auth/auth.service.ts +36 -0
- package/templates/stacks/node-nestjs/src/auth/dto/login-response.dto.ts +9 -0
- package/templates/stacks/node-nestjs/src/auth/dto/login.dto.ts +17 -0
- package/templates/stacks/node-nestjs/src/auth/jwt.strategy.ts +25 -0
- package/templates/stacks/node-nestjs/src/common/filters/problem-details.filter.ts +38 -0
- package/templates/stacks/node-nestjs/src/common/interceptors/json-response.interceptor.ts +13 -0
- package/templates/stacks/node-nestjs/src/main.ts.hbs +44 -0
- package/templates/stacks/node-nestjs/src/prisma/prisma.module.ts +9 -0
- package/templates/stacks/node-nestjs/src/prisma/prisma.service.ts +9 -0
- package/templates/stacks/node-nestjs/src/users/dto/create-user.dto.ts +22 -0
- package/templates/stacks/node-nestjs/src/users/dto/user.dto.ts +15 -0
- package/templates/stacks/node-nestjs/src/users/users.controller.ts +41 -0
- package/templates/stacks/node-nestjs/src/users/users.module.ts +11 -0
- package/templates/stacks/node-nestjs/src/users/users.repository.ts +38 -0
- package/templates/stacks/node-nestjs/src/users/users.service.ts +38 -0
- package/templates/stacks/node-nestjs/tsconfig.build.json +4 -0
- package/templates/stacks/node-nestjs/tsconfig.json +28 -0
- package/templates/stacks/php-laravel/.dare/skills.yml +11 -0
- package/templates/stacks/php-laravel/.env.example +41 -0
- package/templates/stacks/php-laravel/.github/workflows/dare-ci.yml +43 -0
- package/templates/stacks/php-laravel/README.md.hbs +36 -0
- package/templates/stacks/php-laravel/app/Http/Controllers/Api/AuthController.php +36 -0
- package/templates/stacks/php-laravel/app/Http/Controllers/Api/UsersController.php +33 -0
- package/templates/stacks/php-laravel/app/Http/Requests/CreateUserRequest.php +26 -0
- package/templates/stacks/php-laravel/app/Http/Requests/LoginRequest.php +34 -0
- package/templates/stacks/php-laravel/app/Llm/Contracts/LlmProvider.php +12 -0
- package/templates/stacks/php-laravel/app/Llm/Providers/DummyProvider.php +13 -0
- package/templates/stacks/php-laravel/app/Llm/Providers/OpenAiProvider.php +33 -0
- package/templates/stacks/php-laravel/app/Models/User.php +44 -0
- package/templates/stacks/php-laravel/app/Repositories/UsersRepository.php +32 -0
- package/templates/stacks/php-laravel/app/Services/AuthService.php +37 -0
- package/templates/stacks/php-laravel/app/Services/UsersService.php +57 -0
- package/templates/stacks/php-laravel/artisan +12 -0
- package/templates/stacks/php-laravel/bootstrap/app.php +29 -0
- package/templates/stacks/php-laravel/bootstrap/providers.php +5 -0
- package/templates/stacks/php-laravel/composer.json.hbs +58 -0
- package/templates/stacks/php-laravel/config/l5-swagger.php +41 -0
- package/templates/stacks/php-laravel/config/reverb.php +34 -0
- package/templates/stacks/php-laravel/config/sanctum.php +15 -0
- package/templates/stacks/php-laravel/database/migrations/2026_06_01_000001_create_users_table.php +27 -0
- package/templates/stacks/php-laravel/database/seeders/DatabaseSeeder.php +21 -0
- package/templates/stacks/php-laravel/gitignore +23 -0
- package/templates/stacks/php-laravel/llms.txt.hbs +53 -0
- package/templates/stacks/php-laravel/openapi.json.hbs +43 -0
- package/templates/stacks/php-laravel/phpstan.neon +9 -0
- package/templates/stacks/php-laravel/routes/api.php +13 -0
- package/templates/stacks/php-laravel/routes/channels.php +7 -0
- package/templates/stacks/php-laravel/tests/Feature/AuthTest.php +35 -0
- package/templates/stacks/php-laravel/tests/Feature/UsersTest.php +30 -0
- package/templates/stacks/php-laravel/tests/Pest.php +5 -0
- package/templates/stacks/python-fastapi/.dare/skills.yml +11 -0
- package/templates/stacks/python-fastapi/.env.example +21 -0
- package/templates/stacks/python-fastapi/.github/workflows/dare-ci.yml +43 -0
- package/templates/stacks/python-fastapi/README.md.j2 +35 -0
- package/templates/stacks/python-fastapi/alembic/env.py +46 -0
- package/templates/stacks/python-fastapi/alembic/script.py.mako +26 -0
- package/templates/stacks/python-fastapi/alembic/versions/0001_create_users.py.j2 +37 -0
- package/templates/stacks/python-fastapi/alembic.ini.j2 +39 -0
- package/templates/stacks/python-fastapi/app/__init__.py +0 -0
- package/templates/stacks/python-fastapi/app/core/__init__.py +0 -0
- package/templates/stacks/python-fastapi/app/core/config.py +24 -0
- package/templates/stacks/python-fastapi/app/core/security.py +34 -0
- package/templates/stacks/python-fastapi/app/db/__init__.py +0 -0
- package/templates/stacks/python-fastapi/app/db/session.py +22 -0
- package/templates/stacks/python-fastapi/app/main.py.j2 +36 -0
- package/templates/stacks/python-fastapi/app/models/__init__.py +3 -0
- package/templates/stacks/python-fastapi/app/models/user.py +30 -0
- package/templates/stacks/python-fastapi/app/repositories/__init__.py +0 -0
- package/templates/stacks/python-fastapi/app/repositories/user_repository.py +34 -0
- package/templates/stacks/python-fastapi/app/routers/__init__.py +0 -0
- package/templates/stacks/python-fastapi/app/routers/auth.py +37 -0
- package/templates/stacks/python-fastapi/app/routers/users.py +46 -0
- package/templates/stacks/python-fastapi/app/schemas/__init__.py +0 -0
- package/templates/stacks/python-fastapi/app/schemas/user.py +56 -0
- package/templates/stacks/python-fastapi/app/services/__init__.py +0 -0
- package/templates/stacks/python-fastapi/app/services/auth_service.py +22 -0
- package/templates/stacks/python-fastapi/app/services/user_service.py +31 -0
- package/templates/stacks/python-fastapi/gitignore +12 -0
- package/templates/stacks/python-fastapi/llms.txt.j2 +53 -0
- package/templates/stacks/python-fastapi/openapi.json.j2 +43 -0
- package/templates/stacks/python-fastapi/pyproject.toml.j2 +45 -0
- package/templates/stacks/python-fastapi/tests/__init__.py +0 -0
- package/templates/stacks/python-fastapi/tests/test_auth.py +22 -0
- package/templates/stacks/ruby-rails-8/.dare/skills.yml +50 -0
- package/templates/stacks/ruby-rails-8/.env.example +20 -0
- package/templates/stacks/ruby-rails-8/.github/workflows/dare-ci.yml +112 -0
- package/templates/stacks/ruby-rails-8/Gemfile.erb +61 -0
- package/templates/stacks/ruby-rails-8/app/channels/application_cable/channel.rb +11 -0
- package/templates/stacks/ruby-rails-8/app/channels/application_cable/connection.rb +34 -0
- package/templates/stacks/ruby-rails-8/app/channels/dare_updates_channel.rb +18 -0
- package/templates/stacks/ruby-rails-8/app/channels/user_updates_channel.rb +23 -0
- package/templates/stacks/ruby-rails-8/app/controllers/application_controller.rb +44 -0
- package/templates/stacks/ruby-rails-8/app/controllers/concerns/problem_details.rb +93 -0
- package/templates/stacks/ruby-rails-8/app/handlers/summarize_handler.rb +33 -0
- package/templates/stacks/ruby-rails-8/app/handlers/users_handler.rb +68 -0
- package/templates/stacks/ruby-rails-8/app/llm/cache/llm_cache.rb +44 -0
- package/templates/stacks/ruby-rails-8/app/llm/prompts/prompt_loader.rb +54 -0
- package/templates/stacks/ruby-rails-8/app/llm/prompts/summarize_v1.jinja2 +12 -0
- package/templates/stacks/ruby-rails-8/app/llm/providers/dummy_provider.rb +35 -0
- package/templates/stacks/ruby-rails-8/app/llm/providers/llm_provider.rb +67 -0
- package/templates/stacks/ruby-rails-8/app/llm/providers/openai_provider.rb +62 -0
- package/templates/stacks/ruby-rails-8/app/llm/rate_limit/token_bucket.rb +82 -0
- package/templates/stacks/ruby-rails-8/app/llm/validators/summarize_output_schema.json +21 -0
- package/templates/stacks/ruby-rails-8/app/llm/validators/validator.rb +52 -0
- package/templates/stacks/ruby-rails-8/app/models/user.rb +36 -0
- package/templates/stacks/ruby-rails-8/app/presenters/user_presenter.rb +48 -0
- package/templates/stacks/ruby-rails-8/app/repositories/document_repository.rb +57 -0
- package/templates/stacks/ruby-rails-8/app/repositories/user_repository.rb +73 -0
- package/templates/stacks/ruby-rails-8/app/services/create_user_service.rb +67 -0
- package/templates/stacks/ruby-rails-8/app/services/realtime_service.rb +53 -0
- package/templates/stacks/ruby-rails-8/app/services/summarize_document_service.rb +57 -0
- package/templates/stacks/ruby-rails-8/config/dare.yml +42 -0
- package/templates/stacks/ruby-rails-8/config/initializers/dare.rb +31 -0
- package/templates/stacks/ruby-rails-8/config/initializers/rack_attack.rb +64 -0
- package/templates/stacks/ruby-rails-8/config/initializers/rswag_api.rb +12 -0
- package/templates/stacks/ruby-rails-8/lib/tasks/dare.rake +159 -0
- package/templates/stacks/ruby-rails-8/llms.txt.erb +69 -0
- package/templates/stacks/ruby-rails-8/spec/api/summarize_spec.rb +56 -0
- package/templates/stacks/ruby-rails-8/spec/api/users_spec.rb +72 -0
- package/templates/stacks/ruby-rails-8/spec/channels/dare_updates_channel_spec.rb +61 -0
- package/templates/stacks/ruby-rails-8/spec/channels/user_updates_channel_spec.rb +56 -0
- package/templates/stacks/ruby-rails-8/spec/factories/users.rb +27 -0
- package/templates/stacks/ruby-rails-8/spec/handlers/users_handler_spec.rb +88 -0
- package/templates/stacks/ruby-rails-8/spec/rails_helper.rb +31 -0
- package/templates/stacks/ruby-rails-8/spec/services/create_user_service_spec.rb +88 -0
- package/templates/stacks/ruby-rails-8/spec/services/summarize_document_service_spec.rb +142 -0
- package/templates/stacks/ruby-rails-8/spec/swagger_helper.rb +73 -0
- package/templates/stacks/rust-axum/.dare/skills.yml +11 -0
- package/templates/stacks/rust-axum/.env.example +26 -0
- package/templates/stacks/rust-axum/.github/workflows/dare-ci.yml +40 -0
- package/templates/stacks/rust-axum/Cargo.toml.tera +53 -0
- package/templates/stacks/rust-axum/README.md.tera +37 -0
- package/templates/stacks/rust-axum/gitignore +5 -0
- package/templates/stacks/rust-axum/llms.txt.tera +54 -0
- package/templates/stacks/rust-axum/migrations/0001_create_users.sql +13 -0
- package/templates/stacks/rust-axum/openapi.json.tera +46 -0
- package/templates/stacks/rust-axum/src/config.rs +45 -0
- package/templates/stacks/rust-axum/src/errors.rs +48 -0
- package/templates/stacks/rust-axum/src/handlers/auth.rs +48 -0
- package/templates/stacks/rust-axum/src/handlers/mod.rs +3 -0
- package/templates/stacks/rust-axum/src/handlers/users.rs +81 -0
- package/templates/stacks/rust-axum/src/handlers/ws.rs +24 -0
- package/templates/stacks/rust-axum/src/lib.rs +19 -0
- package/templates/stacks/rust-axum/src/llm/mod.rs +1 -0
- package/templates/stacks/rust-axum/src/llm/provider.rs +48 -0
- package/templates/stacks/rust-axum/src/main.rs.tera +64 -0
- package/templates/stacks/rust-axum/src/middleware/auth.rs +20 -0
- package/templates/stacks/rust-axum/src/middleware/mod.rs +2 -0
- package/templates/stacks/rust-axum/src/middleware/rate_limit.rs +27 -0
- package/templates/stacks/rust-axum/src/models/mod.rs +1 -0
- package/templates/stacks/rust-axum/src/models/user.rs +13 -0
- package/templates/stacks/rust-axum/src/repositories/mod.rs +1 -0
- package/templates/stacks/rust-axum/src/repositories/user_repository.rs +62 -0
- package/templates/stacks/rust-axum/src/services/auth_service.rs +50 -0
- package/templates/stacks/rust-axum/src/services/mod.rs +2 -0
- package/templates/stacks/rust-axum/src/services/user_service.rs +53 -0
- package/templates/stacks/rust-axum/tests/integration_test.rs.tera +13 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — ApplicationController
|
|
4
|
+
# - CSRF protection active (RS-01)
|
|
5
|
+
# - RFC 7807 Problem Details included (D-006)
|
|
6
|
+
# - Thin controller: HTTP concerns only, delegates to Services
|
|
7
|
+
|
|
8
|
+
class ApplicationController < ActionController::API
|
|
9
|
+
include ProblemDetails
|
|
10
|
+
|
|
11
|
+
# Enforce JSON responses for all API endpoints
|
|
12
|
+
before_action :set_default_response_format
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def set_default_response_format
|
|
17
|
+
request.format = :json
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Override in specific controllers if authentication is needed
|
|
21
|
+
def current_user
|
|
22
|
+
@current_user ||= authenticate_user_from_token
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def authenticate_user_from_token
|
|
26
|
+
token = request.headers["Authorization"]&.sub(/\ABearer /, "")
|
|
27
|
+
return nil unless token
|
|
28
|
+
|
|
29
|
+
# TODO: implement your token strategy here (JWT, session, etc.)
|
|
30
|
+
# User.find_by_token(token)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def require_authentication!
|
|
35
|
+
return if current_user
|
|
36
|
+
|
|
37
|
+
render_problem(
|
|
38
|
+
status: :unauthorized,
|
|
39
|
+
title: "Unauthorized",
|
|
40
|
+
detail: "You must be authenticated to access this resource.",
|
|
41
|
+
type: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — RFC 7807 Problem Details concern
|
|
4
|
+
# Decision: D-006 — all HTTP errors use this format
|
|
5
|
+
#
|
|
6
|
+
# Usage: include ProblemDetails in ApplicationController
|
|
7
|
+
#
|
|
8
|
+
# Format:
|
|
9
|
+
# {
|
|
10
|
+
# "type": "https://example.com/problems/not-found",
|
|
11
|
+
# "title": "Resource Not Found",
|
|
12
|
+
# "status": 404,
|
|
13
|
+
# "detail": "User with id=42 was not found.",
|
|
14
|
+
# "instance": "/api/users/42"
|
|
15
|
+
# }
|
|
16
|
+
|
|
17
|
+
module ProblemDetails
|
|
18
|
+
extend ActiveSupport::Concern
|
|
19
|
+
|
|
20
|
+
included do
|
|
21
|
+
rescue_from StandardError, with: :render_internal_error
|
|
22
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
|
23
|
+
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity
|
|
24
|
+
rescue_from ActionController::ParameterMissing, with: :render_bad_request
|
|
25
|
+
rescue_from ArgumentError, with: :render_bad_request
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ── Public helpers ──────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
# Render a Problem Details response manually.
|
|
31
|
+
#
|
|
32
|
+
# render_problem(status: :not_found, title: "User Not Found", detail: "...")
|
|
33
|
+
def render_problem(status:, title:, detail: nil, type: nil, **extra)
|
|
34
|
+
http_status = Rack::Utils.status_code(status)
|
|
35
|
+
|
|
36
|
+
body = {
|
|
37
|
+
type: type || default_type_uri(http_status),
|
|
38
|
+
title: title,
|
|
39
|
+
status: http_status,
|
|
40
|
+
detail: detail,
|
|
41
|
+
instance: request.path,
|
|
42
|
+
}.merge(extra).compact
|
|
43
|
+
|
|
44
|
+
render json: body,
|
|
45
|
+
status: http_status,
|
|
46
|
+
content_type: "application/problem+json"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def render_not_found(exception)
|
|
52
|
+
render_problem(
|
|
53
|
+
status: :not_found,
|
|
54
|
+
title: "Not Found",
|
|
55
|
+
detail: exception.message.presence || "The requested resource does not exist."
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_unprocessable_entity(exception)
|
|
60
|
+
errors = exception.record&.errors&.full_messages || [exception.message]
|
|
61
|
+
render_problem(
|
|
62
|
+
status: :unprocessable_entity,
|
|
63
|
+
title: "Validation Failed",
|
|
64
|
+
detail: "One or more fields failed validation.",
|
|
65
|
+
errors: errors
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_bad_request(exception)
|
|
70
|
+
render_problem(
|
|
71
|
+
status: :bad_request,
|
|
72
|
+
title: "Bad Request",
|
|
73
|
+
detail: exception.message
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_internal_error(exception)
|
|
78
|
+
# Never leak internals in production
|
|
79
|
+
detail = Rails.env.production? ? "An unexpected error occurred." : exception.message
|
|
80
|
+
|
|
81
|
+
Rails.logger.error "[ProblemDetails] #{exception.class}: #{exception.message}\n#{exception.backtrace&.first(10)&.join("\n")}"
|
|
82
|
+
|
|
83
|
+
render_problem(
|
|
84
|
+
status: :internal_server_error,
|
|
85
|
+
title: "Internal Server Error",
|
|
86
|
+
detail: detail
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def default_type_uri(status_code)
|
|
91
|
+
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/#{status_code}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — SummarizeHandler (thin controller, delegates to service)
|
|
4
|
+
# Convention (ADR-01): HTTP concerns only — params, response codes, delegation
|
|
5
|
+
# No business logic here.
|
|
6
|
+
#
|
|
7
|
+
# POST /api/v1/documents/:id/summarize
|
|
8
|
+
# RFC 7807 errors via ProblemDetails concern (D-006)
|
|
9
|
+
|
|
10
|
+
class SummarizeHandler < ApplicationController
|
|
11
|
+
include ProblemDetails
|
|
12
|
+
|
|
13
|
+
before_action :authenticate_user! # assume Devise or similar
|
|
14
|
+
|
|
15
|
+
# POST /api/v1/documents/:id/summarize
|
|
16
|
+
def create
|
|
17
|
+
result = Services::SummarizeDocumentService.new(
|
|
18
|
+
document_repository: Repositories::DocumentRepository.new,
|
|
19
|
+
llm_provider: LLM::Providers::LLMProvider.instance,
|
|
20
|
+
event_publisher: RealtimeService.instance
|
|
21
|
+
).execute(
|
|
22
|
+
document_id: params[:id],
|
|
23
|
+
user_id: current_user.id
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
render json: { summary: result.summary, document_id: result.document_id }, status: :ok
|
|
27
|
+
|
|
28
|
+
rescue Services::SummarizeDocumentService::DocumentNotFoundError => e
|
|
29
|
+
render_problem(status: 404, title: "Not Found", detail: e.message)
|
|
30
|
+
rescue Services::SummarizeDocumentService::SummarizationError => e
|
|
31
|
+
render_problem(status: 422, title: "Summarization Failed", detail: e.message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — UsersHandler (thin controller)
|
|
4
|
+
# Convention (ADR-01): HTTP concerns only — params, response codes, delegation
|
|
5
|
+
# No business logic, no repository calls, no queries here.
|
|
6
|
+
|
|
7
|
+
class UsersHandler < ApplicationController
|
|
8
|
+
before_action :require_authentication!, only: %i[update destroy]
|
|
9
|
+
|
|
10
|
+
# GET /api/users
|
|
11
|
+
def index
|
|
12
|
+
repo = Repositories::UserRepository.new
|
|
13
|
+
users = repo.all_active
|
|
14
|
+
|
|
15
|
+
render json: UserPresenter.collection(users), status: :ok
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# GET /api/users/:id
|
|
19
|
+
def show
|
|
20
|
+
repo = Repositories::UserRepository.new
|
|
21
|
+
user = repo.find!(params[:id])
|
|
22
|
+
|
|
23
|
+
render json: UserPresenter.new(user).as_json, status: :ok
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# POST /api/users
|
|
27
|
+
def create
|
|
28
|
+
input = params.require(:user).permit(:email, :name, :password)
|
|
29
|
+
|
|
30
|
+
user = Services::CreateUserService.new(
|
|
31
|
+
user_repository: Repositories::UserRepository.new,
|
|
32
|
+
event_publisher: RealtimeService.instance
|
|
33
|
+
).execute(
|
|
34
|
+
email: input[:email],
|
|
35
|
+
name: input[:name],
|
|
36
|
+
password: input[:password]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
render json: UserPresenter.new(user).as_json, status: :created
|
|
40
|
+
|
|
41
|
+
rescue Services::CreateUserService::EmailTakenError => e
|
|
42
|
+
render_problem(
|
|
43
|
+
status: :conflict,
|
|
44
|
+
title: "Email Already Taken",
|
|
45
|
+
detail: e.message
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# PATCH /api/users/:id
|
|
50
|
+
def update
|
|
51
|
+
repo = Repositories::UserRepository.new
|
|
52
|
+
user = repo.find!(params[:id])
|
|
53
|
+
input = params.require(:user).permit(:name)
|
|
54
|
+
|
|
55
|
+
repo.update!(user, input.to_h)
|
|
56
|
+
|
|
57
|
+
render json: UserPresenter.new(user).as_json, status: :ok
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# DELETE /api/users/:id
|
|
61
|
+
def destroy
|
|
62
|
+
repo = Repositories::UserRepository.new
|
|
63
|
+
user = repo.find!(params[:id])
|
|
64
|
+
repo.destroy!(user)
|
|
65
|
+
|
|
66
|
+
head :no_content
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — LLM Cache (Redis wrapper)
|
|
4
|
+
# Caches LLM responses to reduce API costs and latency
|
|
5
|
+
# Uses SHA256 of (model + prompt) as cache key
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# result = LLM::Cache::LlmCache.instance.fetch(model: "gpt-4o", prompt: prompt) do
|
|
9
|
+
# LLMProvider.instance.complete(model: "gpt-4o", prompt: prompt)
|
|
10
|
+
# end
|
|
11
|
+
|
|
12
|
+
module LLM
|
|
13
|
+
module Cache
|
|
14
|
+
class LlmCache
|
|
15
|
+
include Singleton
|
|
16
|
+
|
|
17
|
+
DEFAULT_TTL = 24.hours
|
|
18
|
+
|
|
19
|
+
def fetch(model:, prompt:, ttl: DEFAULT_TTL, &block)
|
|
20
|
+
key = cache_key(model, prompt)
|
|
21
|
+
|
|
22
|
+
Rails.cache.fetch(key, expires_in: ttl, &block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def invalidate(model:, prompt:)
|
|
26
|
+
key = cache_key(model, prompt)
|
|
27
|
+
Rails.cache.delete(key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear_all!
|
|
31
|
+
# WARNING: clears entire Rails cache — use only in test/dev
|
|
32
|
+
raise "clear_all! not allowed in production!" if Rails.env.production?
|
|
33
|
+
Rails.cache.clear
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def cache_key(model, prompt)
|
|
39
|
+
hash = Digest::SHA256.hexdigest("#{model}:#{prompt}")
|
|
40
|
+
"llm_cache:#{hash}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — PromptLoader
|
|
4
|
+
# Loads .txt or .jinja2 prompt templates from app/llm/prompts/
|
|
5
|
+
# Performs simple variable interpolation
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# prompt = LLM::Prompts::PromptLoader.load("summarize", "v1", text: "Long document text...")
|
|
9
|
+
# response = LLMProvider.instance.complete(model: "gpt-4o", prompt: prompt)
|
|
10
|
+
|
|
11
|
+
module LLM
|
|
12
|
+
module Prompts
|
|
13
|
+
class PromptLoader
|
|
14
|
+
PROMPTS_DIR = Rails.root.join("app", "llm", "prompts")
|
|
15
|
+
|
|
16
|
+
# Load a prompt template and interpolate variables.
|
|
17
|
+
#
|
|
18
|
+
# @param name [String] template name (e.g. "summarize")
|
|
19
|
+
# @param version [String] version suffix (e.g. "v1")
|
|
20
|
+
# @param vars [Hash] variables to interpolate into the template
|
|
21
|
+
# @return [String] rendered prompt
|
|
22
|
+
def self.load(name, version = "v1", **vars)
|
|
23
|
+
template = find_template!(name, version)
|
|
24
|
+
interpolate(template, vars)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private_class_method def self.find_template!(name, version)
|
|
28
|
+
# Search order: .jinja2 first, then .txt, then .erb
|
|
29
|
+
candidates = [
|
|
30
|
+
PROMPTS_DIR.join("#{name}_#{version}.jinja2"),
|
|
31
|
+
PROMPTS_DIR.join("#{name}_#{version}.txt"),
|
|
32
|
+
PROMPTS_DIR.join("#{name}.jinja2"),
|
|
33
|
+
PROMPTS_DIR.join("#{name}.txt"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
path = candidates.find(&:exist?)
|
|
37
|
+
raise ArgumentError, "Prompt template not found: #{name}/#{version}. Searched:\n#{candidates.map(&:to_s).join("\n")}" unless path
|
|
38
|
+
|
|
39
|
+
path.read
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Simple variable interpolation: {{ variable_name }} syntax (Jinja2-compatible)
|
|
43
|
+
# Also supports <%= variable_name %> (ERB) and {{variable_name}} without spaces
|
|
44
|
+
private_class_method def self.interpolate(template, vars)
|
|
45
|
+
result = template.dup
|
|
46
|
+
vars.each do |key, value|
|
|
47
|
+
result.gsub!(/\{\{\s*#{Regexp.escape(key.to_s)}\s*\}\}/, value.to_s)
|
|
48
|
+
result.gsub!(/<%=\s*#{Regexp.escape(key.to_s)}\s*%>/, value.to_s)
|
|
49
|
+
end
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
You are a professional document summarizer. Summarize the following text concisely.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- Be factual and objective
|
|
5
|
+
- Keep the summary under 150 words
|
|
6
|
+
- Preserve the main ideas and key facts
|
|
7
|
+
- Use plain language
|
|
8
|
+
|
|
9
|
+
Text to summarize:
|
|
10
|
+
{{ text }}
|
|
11
|
+
|
|
12
|
+
Summary:
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — DummyProvider for testing and development
|
|
4
|
+
# Returns deterministic canned responses — no network calls
|
|
5
|
+
|
|
6
|
+
module LLM
|
|
7
|
+
module Providers
|
|
8
|
+
class DummyProvider < LLMProvider
|
|
9
|
+
DEFAULT_RESPONSE = "This is a dummy LLM response. Configure a real provider in config/dare.yml."
|
|
10
|
+
|
|
11
|
+
def initialize(responses: {})
|
|
12
|
+
# Allow injecting specific responses by prompt keyword for tests:
|
|
13
|
+
# DummyProvider.new(responses: { "summarize" => "Summary here" })
|
|
14
|
+
@responses = responses
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def complete(model:, prompt:, max_tokens: 1024, temperature: 0.7, **_opts)
|
|
18
|
+
matched_key = @responses.keys.find { |k| prompt.to_s.downcase.include?(k.to_s.downcase) }
|
|
19
|
+
@responses.fetch(matched_key, DEFAULT_RESPONSE)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def chat(model:, messages:, max_tokens: 1024, temperature: 0.7, **_opts)
|
|
23
|
+
last_content = messages.last&.dig(:content) || messages.last&.dig("content") || ""
|
|
24
|
+
complete(model: model, prompt: last_content, max_tokens: max_tokens)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stream(model:, prompt:, max_tokens: 1024, &block)
|
|
28
|
+
response = complete(model: model, prompt: prompt, max_tokens: max_tokens)
|
|
29
|
+
response.chars.each_slice(10).map(&:join).each do |chunk|
|
|
30
|
+
block.call(chunk)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — LLMProvider interface
|
|
4
|
+
# Convention (ADR-04): all LLM calls go through this interface
|
|
5
|
+
# Implementations: OpenaiProvider, DummyProvider, LocalLlamaProvider
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# LLMProvider.instance.complete(model: "gpt-4o", prompt: "Summarize...", max_tokens: 150)
|
|
9
|
+
|
|
10
|
+
module LLM
|
|
11
|
+
module Providers
|
|
12
|
+
class LLMProvider
|
|
13
|
+
include Singleton
|
|
14
|
+
|
|
15
|
+
# ── Factory: configure at boot from dare.yml ─────────────────────────
|
|
16
|
+
|
|
17
|
+
# Override the singleton instance (useful in tests to inject DummyProvider)
|
|
18
|
+
def self.configure(provider_instance)
|
|
19
|
+
@configured_instance = provider_instance
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.instance
|
|
23
|
+
@configured_instance ||= build_from_config
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.build_from_config
|
|
27
|
+
provider_name = Rails.configuration.dare.llm_provider
|
|
28
|
+
|
|
29
|
+
case provider_name
|
|
30
|
+
when "openai"
|
|
31
|
+
LLM::Providers::OpenaiProvider.new
|
|
32
|
+
when "dummy"
|
|
33
|
+
LLM::Providers::DummyProvider.new
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError, "Unknown LLM provider: #{provider_name}. Valid: openai, dummy"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ── Interface — subclasses must implement ─────────────────────────────
|
|
40
|
+
|
|
41
|
+
# Complete a prompt and return the response text.
|
|
42
|
+
#
|
|
43
|
+
# @param model [String] e.g. "gpt-4o"
|
|
44
|
+
# @param prompt [String] the full prompt text
|
|
45
|
+
# @param max_tokens [Integer] maximum tokens in response
|
|
46
|
+
# @param temperature [Float] 0.0 to 1.0
|
|
47
|
+
# @return [String] the completion text
|
|
48
|
+
def complete(model:, prompt:, max_tokens: 1024, temperature: 0.7, **_opts)
|
|
49
|
+
raise NotImplementedError, "#{self.class} must implement #complete"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Chat-style completion with messages array
|
|
53
|
+
#
|
|
54
|
+
# @param model [String]
|
|
55
|
+
# @param messages [Array<Hash>] [{role: "user", content: "..."}]
|
|
56
|
+
# @return [String] assistant message content
|
|
57
|
+
def chat(model:, messages:, max_tokens: 1024, temperature: 0.7, **_opts)
|
|
58
|
+
raise NotImplementedError, "#{self.class} must implement #chat"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Stream a completion — yields chunks as they arrive.
|
|
62
|
+
def stream(model:, prompt:, max_tokens: 1024, &block)
|
|
63
|
+
raise NotImplementedError, "#{self.class} must implement #stream"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — OpenAI LLM provider
|
|
4
|
+
# Uses Faraday for HTTP — no heavy SDK dependency
|
|
5
|
+
|
|
6
|
+
module LLM
|
|
7
|
+
module Providers
|
|
8
|
+
class OpenaiProvider < LLMProvider
|
|
9
|
+
BASE_URL = "https://api.openai.com/v1"
|
|
10
|
+
|
|
11
|
+
def initialize(api_key: nil)
|
|
12
|
+
@api_key = api_key || Rails.configuration.dare.llm_api_key
|
|
13
|
+
raise ArgumentError, "OpenAI API key not configured. Set OPENAI_API_KEY." if @api_key.blank?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def complete(model:, prompt:, max_tokens: 1024, temperature: 0.7, **_opts)
|
|
17
|
+
messages = [{ role: "user", content: prompt }]
|
|
18
|
+
chat(model: model, messages: messages, max_tokens: max_tokens, temperature: temperature)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def chat(model:, messages:, max_tokens: 1024, temperature: 0.7, **_opts)
|
|
22
|
+
response = connection.post("/v1/chat/completions") do |req|
|
|
23
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
24
|
+
req.headers["Content-Type"] = "application/json"
|
|
25
|
+
req.body = {
|
|
26
|
+
model: model,
|
|
27
|
+
messages: messages,
|
|
28
|
+
max_tokens: max_tokens,
|
|
29
|
+
temperature: temperature
|
|
30
|
+
}.to_json
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
handle_response(response)
|
|
34
|
+
.dig("choices", 0, "message", "content")
|
|
35
|
+
.to_s
|
|
36
|
+
.strip
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stream(model:, prompt:, max_tokens: 1024, &block)
|
|
40
|
+
# Streaming via SSE — basic implementation
|
|
41
|
+
# For production use consider openai-ruby gem with native streaming
|
|
42
|
+
raise NotImplementedError, "Streaming not yet implemented in OpenaiProvider. Use chat() instead."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def connection
|
|
48
|
+
@connection ||= Faraday.new(url: BASE_URL) do |f|
|
|
49
|
+
f.request :retry, max: 2, interval: 0.5, retry_statuses: [429, 503]
|
|
50
|
+
f.response :raise_error
|
|
51
|
+
f.adapter Faraday.default_adapter
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def handle_response(response)
|
|
56
|
+
JSON.parse(response.body)
|
|
57
|
+
rescue JSON::ParserError => e
|
|
58
|
+
raise "OpenAI returned non-JSON response: #{response.body.truncate(200)}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — Token Bucket rate limiter for LLM API calls
|
|
4
|
+
# Prevents exceeding OpenAI/provider rate limits (tokens per minute)
|
|
5
|
+
# Uses Redis for distributed state across multiple app instances
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bucket = LLM::RateLimit::TokenBucket.new(user_id: current_user.id)
|
|
9
|
+
# unless bucket.consume!(tokens: estimated_tokens)
|
|
10
|
+
# raise RateLimitExceeded, "LLM token limit reached. Retry after #{bucket.retry_after}s"
|
|
11
|
+
# end
|
|
12
|
+
|
|
13
|
+
module LLM
|
|
14
|
+
module RateLimit
|
|
15
|
+
class TokenBucket
|
|
16
|
+
class RateLimitExceeded < StandardError
|
|
17
|
+
attr_reader :retry_after
|
|
18
|
+
|
|
19
|
+
def initialize(retry_after: 60)
|
|
20
|
+
@retry_after = retry_after
|
|
21
|
+
super("LLM token rate limit exceeded. Retry after #{retry_after} seconds.")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Default: 100K tokens per minute per user (well below OpenAI limits)
|
|
26
|
+
DEFAULT_CAPACITY = 100_000
|
|
27
|
+
DEFAULT_REFILL_RPM = 100_000 # tokens refilled per minute
|
|
28
|
+
WINDOW_SECONDS = 60
|
|
29
|
+
|
|
30
|
+
def initialize(user_id:, capacity: DEFAULT_CAPACITY)
|
|
31
|
+
@user_id = user_id
|
|
32
|
+
@capacity = capacity
|
|
33
|
+
@redis = Redis.new(url: Rails.configuration.dare.redis_url)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Attempt to consume tokens. Returns true if allowed, false if rate-limited.
|
|
37
|
+
def consume!(tokens: 1)
|
|
38
|
+
key = redis_key
|
|
39
|
+
now = Time.now.to_f
|
|
40
|
+
window_start = now - WINDOW_SECONDS
|
|
41
|
+
|
|
42
|
+
# Redis pipeline: count tokens used in current window + add new entry
|
|
43
|
+
used = @redis.multi do |pipeline|
|
|
44
|
+
pipeline.zremrangebyscore(key, "-inf", window_start)
|
|
45
|
+
pipeline.zadd(key, now, "#{now}:#{tokens}")
|
|
46
|
+
pipeline.zrange(key, 0, -1)
|
|
47
|
+
pipeline.expire(key, WINDOW_SECONDS * 2)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Tally tokens from sorted set entries
|
|
51
|
+
entries = used[2] || []
|
|
52
|
+
total_used = entries.sum do |entry|
|
|
53
|
+
entry.split(":").last.to_i
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if total_used > @capacity
|
|
57
|
+
# Roll back the zadd we just did
|
|
58
|
+
@redis.zrem(key, "#{now}:#{tokens}")
|
|
59
|
+
return false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def retry_after
|
|
66
|
+
# Time until oldest entry expires
|
|
67
|
+
oldest = @redis.zrange(redis_key, 0, 0, with_scores: true).first
|
|
68
|
+
return 0 unless oldest
|
|
69
|
+
|
|
70
|
+
oldest_time = oldest[1]
|
|
71
|
+
remaining = (oldest_time + WINDOW_SECONDS) - Time.now.to_f
|
|
72
|
+
[remaining.ceil, 0].max
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def redis_key
|
|
78
|
+
"llm_token_bucket:#{@user_id}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "SummarizeOutput",
|
|
4
|
+
"description": "Schema for LLM summarize operation output",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["summary"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"summary": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"minLength": 10,
|
|
11
|
+
"maxLength": 2000,
|
|
12
|
+
"description": "The summarized text"
|
|
13
|
+
},
|
|
14
|
+
"word_count": {
|
|
15
|
+
"type": "integer",
|
|
16
|
+
"minimum": 1,
|
|
17
|
+
"description": "Optional word count of the summary"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"additionalProperties": false
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DARE v3.0 — LLM Output Validator
|
|
4
|
+
# Validates LLM responses against JSON Schema definitions
|
|
5
|
+
# Schemas live in app/llm/validators/*.json
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# LLM::Validators::Validator.validate!("summarize_output", response_hash)
|
|
9
|
+
|
|
10
|
+
module LLM
|
|
11
|
+
module Validators
|
|
12
|
+
class Validator
|
|
13
|
+
SCHEMAS_DIR = Rails.root.join("app", "llm", "validators")
|
|
14
|
+
|
|
15
|
+
class ValidationError < StandardError
|
|
16
|
+
attr_reader :errors
|
|
17
|
+
|
|
18
|
+
def initialize(schema_name, errors)
|
|
19
|
+
@errors = errors
|
|
20
|
+
super("LLM output failed validation for '#{schema_name}': #{errors.map { |e| e[:error] }.join(", ")}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Validate a hash against a named JSON Schema.
|
|
25
|
+
# Raises ValidationError if invalid.
|
|
26
|
+
def self.validate!(schema_name, data)
|
|
27
|
+
schema = load_schema!(schema_name)
|
|
28
|
+
validator = JSONSchemer.schema(schema)
|
|
29
|
+
errors = validator.validate(data).to_a
|
|
30
|
+
|
|
31
|
+
raise ValidationError.new(schema_name, errors) if errors.any?
|
|
32
|
+
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns true/false without raising
|
|
37
|
+
def self.valid?(schema_name, data)
|
|
38
|
+
validate!(schema_name, data)
|
|
39
|
+
true
|
|
40
|
+
rescue ValidationError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method def self.load_schema!(name)
|
|
45
|
+
path = SCHEMAS_DIR.join("#{name}.json")
|
|
46
|
+
raise ArgumentError, "Schema not found: #{name}.json in #{SCHEMAS_DIR}" unless path.exist?
|
|
47
|
+
|
|
48
|
+
JSON.parse(path.read)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|