@ripla/godd-mcp 1.0.1 → 1.0.2-canary.2

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.
Files changed (46) hide show
  1. package/README.md +2 -2
  2. package/dist/godd.cjs +116 -111
  3. package/dist/godd.js +114 -17
  4. package/dist/index.js +1 -1
  5. package/notes-api/app/config.py +24 -3
  6. package/notes-api/app/database.py +10 -4
  7. package/notes-api/app/routers/comments.py +1 -0
  8. package/notes-api/app/routers/files.py +0 -1
  9. package/notes-api/app/routers/settings.py +4 -2
  10. package/notes-api/app/routers/tree.py +10 -2
  11. package/notes-api/app/services/github_issues.py +1 -1
  12. package/notes-api/tests/test_config_ssl.py +28 -0
  13. package/notes-api/tests/test_issues.py +15 -4
  14. package/notes-api/tests/test_pr.py +0 -1
  15. package/notes-api/tests/test_pr_chain.py +2 -2
  16. package/notes-api/tests/test_tree.py +15 -3
  17. package/notes-app/src/components/IssueList.tsx +36 -16
  18. package/notes-app/vitest.config.ts +1 -0
  19. package/package.json +1 -1
  20. package/templates/agents/architect.hbs +4 -4
  21. package/templates/agents/auto-documenter.hbs +4 -4
  22. package/templates/agents/backend-developer.hbs +3 -3
  23. package/templates/agents/code-reviewer.hbs +2 -2
  24. package/templates/agents/database-developer.hbs +4 -4
  25. package/templates/agents/debug-specialist.hbs +2 -2
  26. package/templates/agents/environment-setup.hbs +1 -1
  27. package/templates/agents/frontend-developer.hbs +5 -5
  28. package/templates/agents/integration-qa.hbs +1 -1
  29. package/templates/agents/pr-writer.hbs +1 -1
  30. package/templates/agents/quality-lead.hbs +3 -3
  31. package/templates/agents/requirements-analyst.hbs +2 -2
  32. package/templates/agents/security-analyst.hbs +1 -1
  33. package/templates/agents/ssot-updater.hbs +3 -3
  34. package/templates/agents/technical-writer.hbs +3 -3
  35. package/templates/agents/unit-test-engineer.hbs +1 -1
  36. package/templates/prompts/agent-dev-workflow.hbs +1 -1
  37. package/templates/prompts/docs-init.hbs +41 -59
  38. package/templates/prompts/docs-update.hbs +10 -10
  39. package/templates/prompts/notes-deploy.hbs +2 -1
  40. package/templates/prompts/pr-create.hbs +2 -2
  41. package/templates/prompts/push-execute.hbs +2 -2
  42. package/templates/prompts/push.hbs +1 -1
  43. package/templates/prompts/requirements-to-business-flow.hbs +1 -1
  44. package/templates/prompts/specification-to-tickets.hbs +1 -1
  45. package/templates/prompts/submit.hbs +2 -2
  46. package/templates/prompts/sync.hbs +1 -1
package/dist/godd.js CHANGED
@@ -32431,7 +32431,7 @@ var init_spec_to_tickets = __esm({
32431
32431
  init_zod();
32432
32432
  init_base();
32433
32433
  specToTicketsToolName = "godd_spec_to_tickets";
32434
- specToTicketsToolDescription = "\u4ED5\u69D8\u2192\u30C1\u30B1\u30C3\u30C8\u5909\u63DB\u3002Spec\uFF08docs/009_spec/\uFF09\u304B\u3089\u5B9F\u88C5\u53EF\u80FD\u306A\u30BF\u30B9\u30AF\u30EA\u30B9\u30C8\u3092\u751F\u6210\u3057\u3001\u5F71\u97FF\u30EC\u30A4\u30E4\u30FC\u30FB\u30C6\u30B9\u30C8\u89B3\u70B9\u30FB\u30EA\u30B9\u30AF\u3092\u4ED8\u4E0E\u3057\u307E\u3059\u3002";
32434
+ specToTicketsToolDescription = "\u4ED5\u69D8\u2192\u30C1\u30B1\u30C3\u30C8\u5909\u63DB\u3002Spec\uFF08docs/003_requirements/spec/\uFF09\u304B\u3089\u5B9F\u88C5\u53EF\u80FD\u306A\u30BF\u30B9\u30AF\u30EA\u30B9\u30C8\u3092\u751F\u6210\u3057\u3001\u5F71\u97FF\u30EC\u30A4\u30E4\u30FC\u30FB\u30C6\u30B9\u30C8\u89B3\u70B9\u30FB\u30EA\u30B9\u30AF\u3092\u4ED8\u4E0E\u3057\u307E\u3059\u3002";
32435
32435
  specToTicketsToolInputSchema = external_exports.object({
32436
32436
  specification: external_exports.string().max(2e5).optional().describe("\u5BFE\u8C61Spec\uFF08API/UI/DB/Feature/Usecase/Error Codes\uFF09"),
32437
32437
  project_root: external_exports.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30EB\u30FC\u30C8\u30D1\u30B9\uFF08\u81EA\u52D5\u691C\u51FA\u7528\uFF09")
@@ -33896,7 +33896,10 @@ var init_install2 = __esm({
33896
33896
  var notes_infra_exports = {};
33897
33897
  __export(notes_infra_exports, {
33898
33898
  PROVIDER_LABELS: () => PROVIDER_LABELS,
33899
- runNotesInfra: () => runNotesInfra
33899
+ findNotesSourceDirs: () => findNotesSourceDirs,
33900
+ runNotesInfra: () => runNotesInfra,
33901
+ scaffoldNotesApplicationSources: () => scaffoldNotesApplicationSources,
33902
+ shouldExcludeNotesSourceEntry: () => shouldExcludeNotesSourceEntry
33900
33903
  });
33901
33904
  import * as readline3 from "node:readline";
33902
33905
  import {
@@ -33905,7 +33908,9 @@ import {
33905
33908
  writeFileSync as writeFileSync4,
33906
33909
  readFileSync as readFileSync10,
33907
33910
  readdirSync as readdirSync3,
33908
- chmodSync as chmodSync2
33911
+ chmodSync as chmodSync2,
33912
+ copyFileSync as copyFileSync2,
33913
+ statSync as statSync2
33909
33914
  } from "node:fs";
33910
33915
  import { join as join9, dirname as dirname3, resolve as resolve3 } from "node:path";
33911
33916
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -33937,6 +33942,78 @@ function findTemplatesDir(provider) {
33937
33942
  }
33938
33943
  return null;
33939
33944
  }
33945
+ function shouldExcludeNotesSourceEntry(entry, extraExclude) {
33946
+ if (entry === ".env") return true;
33947
+ if (entry.startsWith(".env.") && entry !== ".env.example") return true;
33948
+ if (entry.endsWith(".pyc") || entry.endsWith(".tsbuildinfo")) return true;
33949
+ return COMMON_NOTES_SOURCE_EXCLUDE.has(entry) || extraExclude.has(entry);
33950
+ }
33951
+ function hasNotesSourceMarkers(apiDir, appDir) {
33952
+ return existsSync10(join9(apiDir, "app", "main.py")) && existsSync10(join9(appDir, "package.json")) && existsSync10(join9(appDir, "src"));
33953
+ }
33954
+ function findNotesSourceDirs() {
33955
+ const pkgRoot = getPackageRoot();
33956
+ const candidates = [
33957
+ resolve3(pkgRoot, ".."),
33958
+ pkgRoot,
33959
+ dirname3(process.execPath),
33960
+ process.cwd()
33961
+ ];
33962
+ for (const root of candidates) {
33963
+ const apiDir = join9(root, "notes-api");
33964
+ const appDir = join9(root, "notes-app");
33965
+ if (existsSync10(apiDir) && existsSync10(appDir) && hasNotesSourceMarkers(apiDir, appDir)) {
33966
+ return { apiDir, appDir };
33967
+ }
33968
+ }
33969
+ return null;
33970
+ }
33971
+ function copyDirRecursive(src, dest, exclude) {
33972
+ mkdirSync4(dest, { recursive: true });
33973
+ for (const entry of readdirSync3(src)) {
33974
+ if (shouldExcludeNotesSourceEntry(entry, exclude)) continue;
33975
+ const srcPath = join9(src, entry);
33976
+ const destPath = join9(dest, entry);
33977
+ if (statSync2(srcPath).isDirectory()) {
33978
+ copyDirRecursive(srcPath, destPath, exclude);
33979
+ } else {
33980
+ copyFileSync2(srcPath, destPath);
33981
+ }
33982
+ }
33983
+ }
33984
+ function scaffoldNotesApplicationSources(outputDir) {
33985
+ const notesDirs = findNotesSourceDirs();
33986
+ if (!notesDirs) {
33987
+ console.warn(
33988
+ "\n \u26A0 notes-api / notes-app \u306E\u540C\u68B1\u30BD\u30FC\u30B9\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081\u3001\u30A2\u30D7\u30EA scaffold \u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F\u3002"
33989
+ );
33990
+ return [];
33991
+ }
33992
+ const scaffolded = [];
33993
+ const targets = [
33994
+ {
33995
+ label: "notes-api",
33996
+ src: notesDirs.apiDir,
33997
+ dest: join9(outputDir, "notes-api"),
33998
+ exclude: NOTES_API_SOURCE_EXCLUDE
33999
+ },
34000
+ {
34001
+ label: "notes-app",
34002
+ src: notesDirs.appDir,
34003
+ dest: join9(outputDir, "notes-app"),
34004
+ exclude: NOTES_APP_SOURCE_EXCLUDE
34005
+ }
34006
+ ];
34007
+ console.log(`
34008
+ [\u30A2\u30D7\u30EA] Notes \u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u30BD\u30FC\u30B9\u3092\u540C\u671F: ${outputDir}`);
34009
+ for (const target of targets) {
34010
+ const action = existsSync10(target.dest) ? "\u66F4\u65B0" : "\u914D\u7F6E";
34011
+ copyDirRecursive(target.src, target.dest, target.exclude);
34012
+ scaffolded.push(target.label);
34013
+ console.log(` \u2713 ${target.label}/ \u3092${action}`);
34014
+ }
34015
+ return scaffolded;
34016
+ }
33940
34017
  function loadProjectConfig(dir) {
33941
34018
  const configPath = join9(dir, CONFIG_FILE_NAME);
33942
34019
  if (!existsSync10(configPath)) return null;
@@ -34169,7 +34246,7 @@ async function runInfraWizard(rl2) {
34169
34246
  console.log(`
34170
34247
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
34171
34248
  \u2551 ripla Notes \u30A4\u30F3\u30D5\u30E9\u69CB\u7BC9\u30A6\u30A3\u30B6\u30FC\u30C9 \u2551
34172
- \u2551 Terraform + GitHub Actions \u81EA\u52D5\u751F\u6210 \u2551
34249
+ \u2551 \u30A2\u30D7\u30EA + Terraform + GitHub Actions \u81EA\u52D5\u751F\u6210 \u2551
34173
34250
  \u2551 \u2551
34174
34251
  \u2551 \u5BFE\u5FDC\u30D7\u30ED\u30D0\u30A4\u30C0: \u2551
34175
34252
  \u2551 AWS / GCP / Azure / Vercel\xD7Railway \u2551
@@ -34268,6 +34345,7 @@ function generateFiles(config2) {
34268
34345
  } else {
34269
34346
  generateGenericFiles(terraformDir, ghaDir, config2, context);
34270
34347
  }
34348
+ scaffoldNotesApplicationSources(config2.outputDir);
34271
34349
  saveProjectConfig(config2);
34272
34350
  console.log(
34273
34351
  `
@@ -34446,6 +34524,9 @@ function printNextSteps(config2) {
34446
34524
  \u2551 - admin-credentials \u2551
34447
34525
  \u2551 - github-token (\u4EFB\u610F) \u2551
34448
34526
  \u2551 \u2551
34527
+ \u2551 5. \u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u3092\u30C7\u30D7\u30ED\u30A4 \u2551
34528
+ \u2551 godd notes deploy -y \u2551
34529
+ \u2551 \u2551
34449
34530
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
34450
34531
  `);
34451
34532
  break;
@@ -34526,7 +34607,7 @@ function printNextSteps(config2) {
34526
34607
  async function runNotesInfra(args) {
34527
34608
  if (args.includes("--help") || args.includes("-h")) {
34528
34609
  console.log(`
34529
- GoDD Notes Infra \u2014 \u30DE\u30EB\u30C1\u30AF\u30E9\u30A6\u30C9 \u30A4\u30F3\u30D5\u30E9\u81EA\u52D5\u751F\u6210
34610
+ GoDD Notes Infra \u2014 \u30A2\u30D7\u30EA + \u30DE\u30EB\u30C1\u30AF\u30E9\u30A6\u30C9 \u30A4\u30F3\u30D5\u30E9\u81EA\u52D5\u751F\u6210
34530
34611
 
34531
34612
  Usage: godd notes infra [options]
34532
34613
 
@@ -34560,7 +34641,7 @@ Options:
34560
34641
  rl2.close();
34561
34642
  }
34562
34643
  }
34563
- var import_handlebars2, PROVIDER_LABELS, CONFIG_FILE_NAME;
34644
+ var import_handlebars2, PROVIDER_LABELS, COMMON_NOTES_SOURCE_EXCLUDE, NOTES_APP_SOURCE_EXCLUDE, NOTES_API_SOURCE_EXCLUDE, CONFIG_FILE_NAME;
34564
34645
  var init_notes_infra = __esm({
34565
34646
  "src/cli/notes-infra.ts"() {
34566
34647
  "use strict";
@@ -34572,6 +34653,22 @@ var init_notes_infra = __esm({
34572
34653
  azure: "Azure (Container Apps + Blob Storage + PostgreSQL)",
34573
34654
  "vercel-railway": "Vercel \xD7 Railway (PaaS)"
34574
34655
  };
34656
+ COMMON_NOTES_SOURCE_EXCLUDE = /* @__PURE__ */ new Set([
34657
+ ".git",
34658
+ "coverage",
34659
+ "dist",
34660
+ "node_modules"
34661
+ ]);
34662
+ NOTES_APP_SOURCE_EXCLUDE = /* @__PURE__ */ new Set([
34663
+ "playwright-report",
34664
+ "test-results"
34665
+ ]);
34666
+ NOTES_API_SOURCE_EXCLUDE = /* @__PURE__ */ new Set([
34667
+ ".pytest_cache",
34668
+ ".ruff_cache",
34669
+ ".venv",
34670
+ "__pycache__"
34671
+ ]);
34575
34672
  CONFIG_FILE_NAME = ".godd-notes-infra.json";
34576
34673
  }
34577
34674
  });
@@ -34597,9 +34694,9 @@ import {
34597
34694
  mkdirSync as mkdirSync5,
34598
34695
  writeFileSync as writeFileSync5,
34599
34696
  readFileSync as readFileSync11,
34600
- copyFileSync as copyFileSync2,
34697
+ copyFileSync as copyFileSync3,
34601
34698
  readdirSync as readdirSync4,
34602
- statSync as statSync2
34699
+ statSync as statSync3
34603
34700
  } from "node:fs";
34604
34701
  import { join as join10, dirname as dirname4, resolve as resolve4 } from "node:path";
34605
34702
  import * as childProcess from "node:child_process";
@@ -34646,17 +34743,17 @@ function findNotesDir() {
34646
34743
  }
34647
34744
  return null;
34648
34745
  }
34649
- function copyDirRecursive(src, dest) {
34746
+ function copyDirRecursive2(src, dest) {
34650
34747
  mkdirSync5(dest, { recursive: true });
34651
34748
  for (const entry of readdirSync4(src)) {
34652
34749
  if (COPY_EXCLUDE.has(entry)) continue;
34653
34750
  if (entry.endsWith(".pyc") || entry.endsWith(".tsbuildinfo")) continue;
34654
34751
  const srcPath = join10(src, entry);
34655
34752
  const destPath = join10(dest, entry);
34656
- if (statSync2(srcPath).isDirectory()) {
34657
- copyDirRecursive(srcPath, destPath);
34753
+ if (statSync3(srcPath).isDirectory()) {
34754
+ copyDirRecursive2(srcPath, destPath);
34658
34755
  } else {
34659
- copyFileSync2(srcPath, destPath);
34756
+ copyFileSync3(srcPath, destPath);
34660
34757
  }
34661
34758
  }
34662
34759
  }
@@ -34961,9 +35058,9 @@ async function deploy(config2) {
34961
35058
  [1/5] \u30C7\u30D7\u30ED\u30A4\u5148\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u6E96\u5099: ${config2.deployDir}`);
34962
35059
  mkdirSync5(config2.deployDir, { recursive: true });
34963
35060
  console.log("[2/5] notes-api \u3092\u30B3\u30D4\u30FC...");
34964
- copyDirRecursive(apiDir, join10(config2.deployDir, "notes-api"));
35061
+ copyDirRecursive2(apiDir, join10(config2.deployDir, "notes-api"));
34965
35062
  console.log("[3/5] notes-app \u3092\u30B3\u30D4\u30FC...");
34966
- copyDirRecursive(appDir, join10(config2.deployDir, "notes-app"));
35063
+ copyDirRecursive2(appDir, join10(config2.deployDir, "notes-app"));
34967
35064
  console.log("[4/5] docker-compose.yml \u3068 .env \u3092\u751F\u6210...");
34968
35065
  const composeContent = readFileSync11(composeTemplate, "utf-8");
34969
35066
  writeFileSync5(join10(config2.deployDir, "docker-compose.yml"), composeContent, "utf-8");
@@ -35195,7 +35292,7 @@ Usage: godd notes <command> [options]
35195
35292
  Commands:
35196
35293
  compose \u30ED\u30FC\u30AB\u30EB Docker Compose \u3067\u30C7\u30D7\u30ED\u30A4
35197
35294
  deploy AWS \u306B\u76F4\u63A5\u30C7\u30D7\u30ED\u30A4\uFF08ECR push \u2192 ECS \u66F4\u65B0 \u2192 S3 sync \u2192 CloudFront \u7121\u52B9\u5316\uFF09
35198
- infra \u30AF\u30E9\u30A6\u30C9\u30A4\u30F3\u30D5\u30E9\u3092 Terraform + GitHub Actions \u3067\u69CB\u7BC9
35295
+ infra Notes \u30A2\u30D7\u30EA\u96DB\u5F62\u3068\u30AF\u30E9\u30A6\u30C9\u30A4\u30F3\u30D5\u30E9\u3092 Terraform + GitHub Actions \u3067\u69CB\u7BC9
35199
35296
  status \u30C7\u30D7\u30ED\u30A4\u72B6\u614B\u30FB\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u3092\u78BA\u8A8D
35200
35297
 
35201
35298
  compose \u30AA\u30D7\u30B7\u30E7\u30F3:
@@ -35222,7 +35319,7 @@ Examples:
35222
35319
  godd notes compose --config config.json \u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u304B\u3089\u30C7\u30D7\u30ED\u30A4
35223
35320
  godd notes deploy AWS \u306B\u30C7\u30D7\u30ED\u30A4\uFF08\u5BFE\u8A71\u5F62\u5F0F\u3067\u78BA\u8A8D\uFF09
35224
35321
  godd notes deploy -y \u78BA\u8A8D\u306A\u3057\u3067 AWS \u306B\u30C7\u30D7\u30ED\u30A4
35225
- godd notes infra Terraform + CI/CD \u30D5\u30A1\u30A4\u30EB\u751F\u6210
35322
+ godd notes infra \u30A2\u30D7\u30EA\u96DB\u5F62 + Terraform + CI/CD \u30D5\u30A1\u30A4\u30EB\u751F\u6210
35226
35323
  godd notes status \u30C7\u30D7\u30ED\u30A4\u72B6\u614B\u3092\u78BA\u8A8D
35227
35324
 
35228
35325
  AI \u9023\u643A:
@@ -35459,7 +35556,7 @@ Examples:
35459
35556
  godd init \u73FE\u5728\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7
35460
35557
  godd notes compose \u30ED\u30FC\u30AB\u30EB Docker Compose \u3067\u30C7\u30D7\u30ED\u30A4
35461
35558
  godd notes deploy AWS \u306B\u30C7\u30D7\u30ED\u30A4\uFF08ECR/ECS/S3/CloudFront\uFF09
35462
- godd notes infra \u30AF\u30E9\u30A6\u30C9\u30A4\u30F3\u30D5\u30E9\u3092\u5BFE\u8A71\u5F62\u5F0F\u3067\u69CB\u7BC9 (AWS/GCP/Azure/Vercel\xD7Railway)
35559
+ godd notes infra Notes \u30A2\u30D7\u30EA\u96DB\u5F62\u3068\u30AF\u30E9\u30A6\u30C9\u30A4\u30F3\u30D5\u30E9\u3092\u5BFE\u8A71\u5F62\u5F0F\u3067\u69CB\u7BC9
35463
35560
  godd version \u30D0\u30FC\u30B8\u30E7\u30F3\u78BA\u8A8D
35464
35561
  `);
35465
35562
  }
package/dist/index.js CHANGED
@@ -197,7 +197,7 @@ ${t.report_type}`),j(e,"analyze-report",r.join(`
197
197
 
198
198
  `),t.project_root)}var kS="godd_req_to_flow",wS="\u8981\u6C42\u2192\u30D3\u30B8\u30CD\u30B9\u30D5\u30ED\u30FC\u5909\u63DB\u3002\u81EA\u7136\u8A00\u8A9E\u306E\u8981\u6C42\u304B\u3089As-Is/To-Be\u30D5\u30ED\u30FC\u3068\u30E6\u30FC\u30B9\u30B1\u30FC\u30B9\u3092\u751F\u6210\u3057\u307E\u3059\u3002",SS=m.object({requirements:m.string().max(2e5).optional().describe("\u8981\u6C42\u5185\u5BB9\uFF08\u81EA\u7136\u8A00\u8A9E\uFF09"),project_root:m.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30EB\u30FC\u30C8\u30D1\u30B9\uFF08\u81EA\u52D5\u691C\u51FA\u7528\uFF09")});function TS(e,t){return j(e,"requirements-to-business-flow",`## \u8981\u6C42
199
199
  ${t.requirements??"(Chat \u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u304B\u3089\u5224\u65AD)"}`,t.project_root)}var $S="godd_req_to_tickets",PS="\u8981\u6C42\u2192\u30C1\u30B1\u30C3\u30C8\u5206\u89E3\u3002\u8981\u6C42\u3092\u5B9F\u88C5\u30BF\u30B9\u30AF\uFF08\u30C1\u30B1\u30C3\u30C8\uFF09\u306B\u5206\u89E3\u3057\u3001\u4F9D\u5B58\u95A2\u4FC2\u3068\u691C\u8A3C\u65B9\u6CD5\u3092\u542B\u3081\u3066\u4E00\u89A7\u5316\u3057\u307E\u3059\u3002",ES=m.object({requirements:m.string().max(2e5).optional().describe("\u8981\u6C42\u5185\u5BB9\uFF08\u81EA\u7136\u8A00\u8A9E\uFF09"),project_root:m.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30EB\u30FC\u30C8\u30D1\u30B9\uFF08\u81EA\u52D5\u691C\u51FA\u7528\uFF09")});function zS(e,t){return j(e,"requirements-to-tickets",`## \u8981\u6C42
200
- ${t.requirements??"(Chat \u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u304B\u3089\u5224\u65AD)"}`,t.project_root)}var CS="godd_spec_to_tickets",IS="\u4ED5\u69D8\u2192\u30C1\u30B1\u30C3\u30C8\u5909\u63DB\u3002Spec\uFF08docs/009_spec/\uFF09\u304B\u3089\u5B9F\u88C5\u53EF\u80FD\u306A\u30BF\u30B9\u30AF\u30EA\u30B9\u30C8\u3092\u751F\u6210\u3057\u3001\u5F71\u97FF\u30EC\u30A4\u30E4\u30FC\u30FB\u30C6\u30B9\u30C8\u89B3\u70B9\u30FB\u30EA\u30B9\u30AF\u3092\u4ED8\u4E0E\u3057\u307E\u3059\u3002",RS=m.object({specification:m.string().max(2e5).optional().describe("\u5BFE\u8C61Spec\uFF08API/UI/DB/Feature/Usecase/Error Codes\uFF09"),project_root:m.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30EB\u30FC\u30C8\u30D1\u30B9\uFF08\u81EA\u52D5\u691C\u51FA\u7528\uFF09")});function AS(e,t){return j(e,"specification-to-tickets",`## \u5BFE\u8C61Spec
200
+ ${t.requirements??"(Chat \u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u304B\u3089\u5224\u65AD)"}`,t.project_root)}var CS="godd_spec_to_tickets",IS="\u4ED5\u69D8\u2192\u30C1\u30B1\u30C3\u30C8\u5909\u63DB\u3002Spec\uFF08docs/003_requirements/spec/\uFF09\u304B\u3089\u5B9F\u88C5\u53EF\u80FD\u306A\u30BF\u30B9\u30AF\u30EA\u30B9\u30C8\u3092\u751F\u6210\u3057\u3001\u5F71\u97FF\u30EC\u30A4\u30E4\u30FC\u30FB\u30C6\u30B9\u30C8\u89B3\u70B9\u30FB\u30EA\u30B9\u30AF\u3092\u4ED8\u4E0E\u3057\u307E\u3059\u3002",RS=m.object({specification:m.string().max(2e5).optional().describe("\u5BFE\u8C61Spec\uFF08API/UI/DB/Feature/Usecase/Error Codes\uFF09"),project_root:m.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30EB\u30FC\u30C8\u30D1\u30B9\uFF08\u81EA\u52D5\u691C\u51FA\u7528\uFF09")});function AS(e,t){return j(e,"specification-to-tickets",`## \u5BFE\u8C61Spec
201
201
  ${t.specification??"(Chat \u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u304B\u3089\u5224\u65AD)"}`,t.project_root)}var OS="godd_install",NS="\u30C4\u30FC\u30EB/MCP\u30B5\u30FC\u30D0\u30FC\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3002\u6307\u5B9A\u3055\u308C\u305F\u30C4\u30FC\u30EB\u306E\u74B0\u5883\u306B\u9069\u3057\u305F\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u624B\u9806\u30FB\u8A2D\u5B9A\u30FB\u758E\u901A\u78BA\u8A8D\u3092\u6848\u5185\u3057\u307E\u3059\u3002",jS=m.object({target:m.string().max(1e3).describe("\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u5BFE\u8C61\u306E\u30C4\u30FC\u30EB/MCP\u540D\uFF08\u4F8B: context7, chrome-devtools-mcp, markitdown-mcp, mcp-ocr, serena, vibe-odf-read-mcp\uFF09"),project_root:m.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30EB\u30FC\u30C8\u30D1\u30B9\uFF08\u81EA\u52D5\u691C\u51FA\u7528\uFF09")});function MS(e,t){return j(e,"install",`## \u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u5BFE\u8C61
202
202
  ${t.target}`,t.project_root)}var DS="godd_docs_init",LS="\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306B docs/ \u30D5\u30A9\u30EB\u30C0\u69CB\u9020\u3092\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u304B\u3089\u751F\u6210\u3057\u307E\u3059\u3002ripla Notes \u3067\u95B2\u89A7\u30FB\u7DE8\u96C6\u53EF\u80FD\u306A MD/CSV/drawio \u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u306E\u521D\u671F\u69CB\u9020\u3092\u4F5C\u6210\u3057\u307E\u3059\u3002",ZS=m.object({project_name:m.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D"),roles:m.array(m.string().max(1e3)).optional().describe("\u30ED\u30FC\u30EB\u540D\u4E00\u89A7\uFF08\u4F8B: \u7BA1\u7406\u8005, \u904B\u55B6\u8005, \u5229\u7528\u8005\uFF09"),screens:m.array(m.string().max(1e3)).optional().describe("\u753B\u9762ID\u4E00\u89A7\uFF08\u4F8B: SC-OP-01, SC-US-01\uFF09"),project_root:m.string().max(1e3).optional().describe("\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u30EB\u30FC\u30C8\u30D1\u30B9\uFF08\u81EA\u52D5\u691C\u51FA\u7528\uFF09")});function qS(e,t){let r=[`## \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D
203
203
  ${t.project_name??"(\u81EA\u52D5\u691C\u51FA)"}`];return t.roles?.length&&r.push(`## \u30ED\u30FC\u30EB
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import re
7
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
7
8
 
8
9
  from pydantic import AliasChoices, Field, model_validator
9
10
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -11,6 +12,24 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
11
12
  _logger = logging.getLogger(__name__)
12
13
 
13
14
 
15
+ def _remove_query_param(url: str, key: str) -> str:
16
+ parts = urlsplit(url)
17
+ query = urlencode(
18
+ [(k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True) if k != key],
19
+ doseq=True,
20
+ )
21
+ return urlunsplit((parts.scheme, parts.netloc, parts.path, query, parts.fragment))
22
+
23
+
24
+ def _set_query_param(url: str, key: str, value: str) -> str:
25
+ parts = urlsplit(url)
26
+ query_items = [
27
+ (k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True) if k != key
28
+ ]
29
+ query = urlencode([*query_items, (key, value)], doseq=True)
30
+ return urlunsplit((parts.scheme, parts.netloc, parts.path, query, parts.fragment))
31
+
32
+
14
33
  class Settings(BaseSettings):
15
34
  """ripla Notes API settings."""
16
35
 
@@ -72,7 +91,10 @@ class Settings(BaseSettings):
72
91
  def async_database_url(self) -> str:
73
92
  """Async DB URL for SQLAlchemy (asyncpg driver)."""
74
93
  if self.database_url:
75
- return re.sub(r"^postgres(ql)?://", "postgresql+asyncpg://", self.database_url)
94
+ url = re.sub(r"^postgres(ql)?://", "postgresql+asyncpg://", self.database_url)
95
+ if self.database_ssl:
96
+ return _remove_query_param(url, "sslmode")
97
+ return url
76
98
  return (
77
99
  f"postgresql+asyncpg://{self.db_user}:{self.db_password}"
78
100
  f"@{self.db_host}:{self.db_port}/{self.db_name}"
@@ -89,8 +111,7 @@ class Settings(BaseSettings):
89
111
  f"@{self.db_host}:{self.db_port}/{self.db_name}"
90
112
  )
91
113
  if self.database_ssl:
92
- sep = "&" if "?" in url else "?"
93
- url = f"{url}{sep}sslmode=require"
114
+ url = _set_query_param(url, "sslmode", "require")
94
115
  return url
95
116
 
96
117
  @property
@@ -7,10 +7,16 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn
7
7
 
8
8
  from app.config import settings
9
9
 
10
- _async_connect_args: dict[str, Any] = {}
11
- if settings.database_ssl:
12
- # asyncpg: required for many managed Postgres providers (e.g. AWS RDS with force_ssl)
13
- _async_connect_args["ssl"] = True
10
+
11
+ def _build_async_connect_args(database_ssl: bool) -> dict[str, Any]:
12
+ if not database_ssl:
13
+ return {}
14
+ # asyncpg: "require" = encrypt connection without certificate verification
15
+ # (equivalent to psycopg2's sslmode=require, works with AWS RDS / GCP Cloud SQL / Azure)
16
+ return {"ssl": "require"}
17
+
18
+
19
+ _async_connect_args = _build_async_connect_args(settings.database_ssl)
14
20
 
15
21
  engine = create_async_engine(
16
22
  settings.async_database_url,
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from datetime import UTC, datetime
6
6
  from typing import Any
7
+
7
8
  from fastapi import APIRouter, Depends, HTTPException, Query, status
8
9
  from pydantic import BaseModel, Field
9
10
  from sqlalchemy import func, select
@@ -43,7 +43,6 @@ STYLES_SUFFIX = ".styles.json"
43
43
 
44
44
  from app.path_validation import validate_docs_path # noqa: E402
45
45
 
46
-
47
46
  _validate_path = validate_docs_path
48
47
 
49
48
 
@@ -1,7 +1,8 @@
1
1
  """Settings router."""
2
2
 
3
3
  import logging
4
- from typing import Any, Callable
4
+ from collections.abc import Callable
5
+ from typing import Any
5
6
 
6
7
  from fastapi import APIRouter, Depends, HTTPException
7
8
  from fastapi.encoders import jsonable_encoder
@@ -72,9 +73,10 @@ async def update_setting(
72
73
  """Update setting (admin only). Only whitelisted keys are accepted."""
73
74
  validator = ALLOWED_SETTINGS.get(key)
74
75
  if validator is None:
76
+ allowed_keys = ", ".join(sorted(ALLOWED_SETTINGS))
75
77
  raise HTTPException(
76
78
  status_code=400,
77
- detail=f"Unknown setting key: {key}. Allowed keys: {', '.join(sorted(ALLOWED_SETTINGS))}",
79
+ detail=f"Unknown setting key: {key}. Allowed keys: {allowed_keys}",
78
80
  )
79
81
  if not validator(body.value):
80
82
  raise HTTPException(
@@ -21,7 +21,12 @@ async def get_tree(
21
21
  ):
22
22
  """Get file tree. Pass *ref* to read a specific branch. Mock mode when GITHUB_TOKEN not set."""
23
23
  if settings.is_mock_mode():
24
- return {"mode": "mock", "source": "mock", "reason": "GITHUB_TOKEN が未設定です", "tree": get_mock_tree()}
24
+ return {
25
+ "mode": "mock",
26
+ "source": "mock",
27
+ "reason": "GITHUB_TOKEN が未設定です",
28
+ "tree": get_mock_tree(),
29
+ }
25
30
 
26
31
  try:
27
32
  config = get_github_config()
@@ -37,7 +42,10 @@ async def get_tree(
37
42
  "Organization の承認が必要な場合があります。"
38
43
  )
39
44
  elif code == 404:
40
- detail = f"リポジトリまたはパスが見つかりません: {settings.github_owner}/{settings.github_repo}"
45
+ detail = (
46
+ "リポジトリまたはパスが見つかりません: "
47
+ f"{settings.github_owner}/{settings.github_repo}"
48
+ )
41
49
  else:
42
50
  detail = f"GitHub API エラー (HTTP {code})"
43
51
  raise HTTPException(
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
 
7
7
  from app.http_client import get_github_client
8
- from app.services.github import parse_link_next, resolve_token, _auth_headers
8
+ from app.services.github import _auth_headers, parse_link_next, resolve_token
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
@@ -1,6 +1,7 @@
1
1
  """DATABASE_SSL / DB_SSL settings for managed Postgres (e.g. AWS RDS)."""
2
2
 
3
3
  from app.config import Settings
4
+ from app.database import _build_async_connect_args
4
5
 
5
6
 
6
7
  def test_database_ssl_defaults_false(monkeypatch):
@@ -33,3 +34,30 @@ def test_sync_database_url_appends_sslmode_when_ssl_enabled():
33
34
  database_ssl=True,
34
35
  )
35
36
  assert "sslmode=require" in s.sync_database_url
37
+
38
+
39
+ def test_sync_database_url_overrides_existing_sslmode():
40
+ s = Settings(
41
+ database_url="postgresql://u:p@db.example.com:5432/app?sslmode=disable",
42
+ database_ssl=True,
43
+ )
44
+ assert s.sync_database_url.count("sslmode=require") == 1
45
+ assert "sslmode=disable" not in s.sync_database_url
46
+
47
+
48
+ def test_async_database_url_removes_sslmode_when_connect_args_handle_ssl():
49
+ s = Settings(
50
+ database_url="postgresql://u:p@db.example.com:5432/app?sslmode=require&application_name=notes",
51
+ database_ssl=True,
52
+ )
53
+ assert s.async_database_url == (
54
+ "postgresql+asyncpg://u:p@db.example.com:5432/app?application_name=notes"
55
+ )
56
+
57
+
58
+ def test_async_database_connect_args_require_ssl_without_cert_verification():
59
+ assert _build_async_connect_args(database_ssl=True) == {"ssl": "require"}
60
+
61
+
62
+ def test_async_database_connect_args_empty_when_ssl_disabled():
63
+ assert _build_async_connect_args(database_ssl=False) == {}
@@ -3,7 +3,6 @@
3
3
  from unittest.mock import AsyncMock, patch
4
4
 
5
5
  import httpx
6
- import pytest
7
6
  from httpx import AsyncClient
8
7
 
9
8
  from tests.conftest import auth_headers
@@ -46,7 +45,11 @@ class TestListIssues:
46
45
  ):
47
46
  mock_settings.is_mock_mode.return_value = False
48
47
  mock_config.return_value = {"token": "t", "owner": "o", "repo": "r", "branch": "main"}
49
- mock_list.side_effect = httpx.HTTPStatusError("", request=mock_resp.request, response=mock_resp)
48
+ mock_list.side_effect = httpx.HTTPStatusError(
49
+ "",
50
+ request=mock_resp.request,
51
+ response=mock_resp,
52
+ )
50
53
  resp = await client.get("/api/issues", headers=auth_headers(admin_token))
51
54
  assert resp.status_code == 401
52
55
 
@@ -81,7 +84,11 @@ class TestGetIssue:
81
84
  ):
82
85
  mock_settings.is_mock_mode.return_value = False
83
86
  mock_config.return_value = {"token": "t", "owner": "o", "repo": "r", "branch": "main"}
84
- mock_get.side_effect = httpx.HTTPStatusError("", request=mock_resp.request, response=mock_resp)
87
+ mock_get.side_effect = httpx.HTTPStatusError(
88
+ "",
89
+ request=mock_resp.request,
90
+ response=mock_resp,
91
+ )
85
92
  resp = await client.get("/api/issues/999", headers=auth_headers(admin_token))
86
93
  assert resp.status_code == 404
87
94
 
@@ -176,7 +183,11 @@ class TestUpdateIssue:
176
183
  ):
177
184
  mock_settings.is_mock_mode.return_value = False
178
185
  mock_config.return_value = {"token": "t", "owner": "o", "repo": "r", "branch": "main"}
179
- mock_update.side_effect = httpx.HTTPStatusError("", request=mock_resp.request, response=mock_resp)
186
+ mock_update.side_effect = httpx.HTTPStatusError(
187
+ "",
188
+ request=mock_resp.request,
189
+ response=mock_resp,
190
+ )
180
191
  resp = await client.patch(
181
192
  "/api/issues/999",
182
193
  json={"title": "Does not exist"},
@@ -1,6 +1,5 @@
1
1
  """Tests for PR endpoints."""
2
2
 
3
- from unittest.mock import AsyncMock, patch
4
3
 
5
4
  from httpx import AsyncClient
6
5
 
@@ -649,7 +649,7 @@ class TestRenameOnWorkingBranch:
649
649
 
650
650
  with (
651
651
  patch(
652
- "app.services.business_date.get_business_date",
652
+ "app.services.pr_chain.get_business_date",
653
653
  new_callable=AsyncMock, return_value="2026-04-17",
654
654
  ),
655
655
  patch(
@@ -714,7 +714,7 @@ class TestRenameOnWorkingBranch:
714
714
 
715
715
  with (
716
716
  patch(
717
- "app.services.business_date.get_business_date",
717
+ "app.services.pr_chain.get_business_date",
718
718
  new_callable=AsyncMock, return_value="2026-04-17",
719
719
  ),
720
720
  patch(
@@ -68,7 +68,11 @@ class TestTree:
68
68
  mock_config.return_value = {
69
69
  "token": "t", "owner": "o", "repo": "r", "branch": "main", "docs_path": "docs",
70
70
  }
71
- mock_fetch.side_effect = httpx.HTTPStatusError("", request=mock_resp.request, response=mock_resp)
71
+ mock_fetch.side_effect = httpx.HTTPStatusError(
72
+ "",
73
+ request=mock_resp.request,
74
+ response=mock_resp,
75
+ )
72
76
  resp = await client.get("/api/tree", headers=self._headers)
73
77
  assert resp.status_code == 502
74
78
 
@@ -84,7 +88,11 @@ class TestTree:
84
88
  mock_config.return_value = {
85
89
  "token": "t", "owner": "o", "repo": "r", "branch": "main", "docs_path": "docs",
86
90
  }
87
- mock_fetch.side_effect = httpx.HTTPStatusError("", request=mock_resp.request, response=mock_resp)
91
+ mock_fetch.side_effect = httpx.HTTPStatusError(
92
+ "",
93
+ request=mock_resp.request,
94
+ response=mock_resp,
95
+ )
88
96
  resp = await client.get("/api/tree", headers=self._headers)
89
97
  assert resp.status_code == 502
90
98
  assert "権限" in resp.json()["detail"]
@@ -124,7 +132,11 @@ class TestTree:
124
132
  mock_config.return_value = {
125
133
  "token": "t", "owner": "o", "repo": "r", "branch": "main", "docs_path": "docs",
126
134
  }
127
- mock_fetch.side_effect = httpx.HTTPStatusError("", request=mock_resp.request, response=mock_resp)
135
+ mock_fetch.side_effect = httpx.HTTPStatusError(
136
+ "",
137
+ request=mock_resp.request,
138
+ response=mock_resp,
139
+ )
128
140
  resp = await client.get("/api/tree", headers=self._headers)
129
141
  assert resp.status_code == 502
130
142
  assert "見つかりません" in resp.json()["detail"]