jp_address_complement 0.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.
Files changed (181) hide show
  1. checksums.yaml +7 -0
  2. data/.agent/rules/specify-rules.md +29 -0
  3. data/.agent/workflows/speckit.analyze.md +184 -0
  4. data/.agent/workflows/speckit.checklist.md +294 -0
  5. data/.agent/workflows/speckit.clarify.md +181 -0
  6. data/.agent/workflows/speckit.constitution.md +84 -0
  7. data/.agent/workflows/speckit.implement.md +135 -0
  8. data/.agent/workflows/speckit.plan.md +90 -0
  9. data/.agent/workflows/speckit.specify.md +258 -0
  10. data/.agent/workflows/speckit.tasks.md +137 -0
  11. data/.agent/workflows/speckit.taskstoissues.md +30 -0
  12. data/.claude/commands/speckit.analyze.md +184 -0
  13. data/.claude/commands/speckit.checklist.md +294 -0
  14. data/.claude/commands/speckit.clarify.md +181 -0
  15. data/.claude/commands/speckit.constitution.md +84 -0
  16. data/.claude/commands/speckit.implement.md +135 -0
  17. data/.claude/commands/speckit.plan.md +90 -0
  18. data/.claude/commands/speckit.specify.md +258 -0
  19. data/.claude/commands/speckit.tasks.md +137 -0
  20. data/.claude/commands/speckit.taskstoissues.md +30 -0
  21. data/.cursor/commands/speckit.analyze.md +184 -0
  22. data/.cursor/commands/speckit.checklist.md +294 -0
  23. data/.cursor/commands/speckit.clarify.md +181 -0
  24. data/.cursor/commands/speckit.constitution.md +84 -0
  25. data/.cursor/commands/speckit.implement.md +135 -0
  26. data/.cursor/commands/speckit.plan.md +90 -0
  27. data/.cursor/commands/speckit.specify.md +258 -0
  28. data/.cursor/commands/speckit.tasks.md +137 -0
  29. data/.cursor/commands/speckit.taskstoissues.md +30 -0
  30. data/.cursor/rules/specify-rules.mdc +32 -0
  31. data/.rubocop.yml +118 -0
  32. data/.specify/memory/constitution.md +130 -0
  33. data/.specify/scripts/bash/check-prerequisites.sh +166 -0
  34. data/.specify/scripts/bash/common.sh +156 -0
  35. data/.specify/scripts/bash/create-new-feature.sh +297 -0
  36. data/.specify/scripts/bash/setup-plan.sh +61 -0
  37. data/.specify/scripts/bash/update-agent-context.sh +810 -0
  38. data/.specify/templates/agent-file-template.md +28 -0
  39. data/.specify/templates/checklist-template.md +40 -0
  40. data/.specify/templates/constitution-template.md +50 -0
  41. data/.specify/templates/plan-template.md +104 -0
  42. data/.specify/templates/spec-template.md +115 -0
  43. data/.specify/templates/tasks-template.md +251 -0
  44. data/CHANGELOG.md +26 -0
  45. data/LICENSE +9 -0
  46. data/README.md +274 -0
  47. data/Rakefile +19 -0
  48. data/Steepfile +9 -0
  49. data/examples/rails/jp_address_complement_demo/.gitignore +15 -0
  50. data/examples/rails/jp_address_complement_demo/.ruby-version +1 -0
  51. data/examples/rails/jp_address_complement_demo/Gemfile +22 -0
  52. data/examples/rails/jp_address_complement_demo/Gemfile.lock +252 -0
  53. data/examples/rails/jp_address_complement_demo/README.md +57 -0
  54. data/examples/rails/jp_address_complement_demo/Rakefile +6 -0
  55. data/examples/rails/jp_address_complement_demo/app/assets/images/.keep +0 -0
  56. data/examples/rails/jp_address_complement_demo/app/assets/stylesheets/application.css +1 -0
  57. data/examples/rails/jp_address_complement_demo/app/controllers/addresses_controller.rb +59 -0
  58. data/examples/rails/jp_address_complement_demo/app/controllers/application_controller.rb +4 -0
  59. data/examples/rails/jp_address_complement_demo/app/controllers/concerns/.keep +0 -0
  60. data/examples/rails/jp_address_complement_demo/app/helpers/application_helper.rb +2 -0
  61. data/examples/rails/jp_address_complement_demo/app/models/application_record.rb +3 -0
  62. data/examples/rails/jp_address_complement_demo/app/models/concerns/.keep +0 -0
  63. data/examples/rails/jp_address_complement_demo/app/views/addresses/index.html.erb +22 -0
  64. data/examples/rails/jp_address_complement_demo/app/views/addresses/prefecture.html.erb +20 -0
  65. data/examples/rails/jp_address_complement_demo/app/views/addresses/prefix.html.erb +25 -0
  66. data/examples/rails/jp_address_complement_demo/app/views/addresses/reverse.html.erb +30 -0
  67. data/examples/rails/jp_address_complement_demo/app/views/addresses/validate.html.erb +23 -0
  68. data/examples/rails/jp_address_complement_demo/app/views/layouts/application.html.erb +40 -0
  69. data/examples/rails/jp_address_complement_demo/app/views/pwa/manifest.json.erb +22 -0
  70. data/examples/rails/jp_address_complement_demo/app/views/pwa/service-worker.js +26 -0
  71. data/examples/rails/jp_address_complement_demo/bin/ci +6 -0
  72. data/examples/rails/jp_address_complement_demo/bin/dev +2 -0
  73. data/examples/rails/jp_address_complement_demo/bin/rails +4 -0
  74. data/examples/rails/jp_address_complement_demo/bin/rake +4 -0
  75. data/examples/rails/jp_address_complement_demo/bin/setup +35 -0
  76. data/examples/rails/jp_address_complement_demo/config/application.rb +42 -0
  77. data/examples/rails/jp_address_complement_demo/config/boot.rb +3 -0
  78. data/examples/rails/jp_address_complement_demo/config/ci.rb +15 -0
  79. data/examples/rails/jp_address_complement_demo/config/credentials.yml.enc +1 -0
  80. data/examples/rails/jp_address_complement_demo/config/database.yml +31 -0
  81. data/examples/rails/jp_address_complement_demo/config/environment.rb +5 -0
  82. data/examples/rails/jp_address_complement_demo/config/environments/development.rb +54 -0
  83. data/examples/rails/jp_address_complement_demo/config/environments/production.rb +67 -0
  84. data/examples/rails/jp_address_complement_demo/config/environments/test.rb +42 -0
  85. data/examples/rails/jp_address_complement_demo/config/initializers/content_security_policy.rb +29 -0
  86. data/examples/rails/jp_address_complement_demo/config/initializers/filter_parameter_logging.rb +8 -0
  87. data/examples/rails/jp_address_complement_demo/config/initializers/inflections.rb +16 -0
  88. data/examples/rails/jp_address_complement_demo/config/locales/en.yml +31 -0
  89. data/examples/rails/jp_address_complement_demo/config/puma.rb +39 -0
  90. data/examples/rails/jp_address_complement_demo/config/routes.rb +19 -0
  91. data/examples/rails/jp_address_complement_demo/config.ru +6 -0
  92. data/examples/rails/jp_address_complement_demo/db/migrate/20260228083709_create_jp_address_complement_postal_codes.rb +44 -0
  93. data/examples/rails/jp_address_complement_demo/db/schema.rb +33 -0
  94. data/examples/rails/jp_address_complement_demo/db/seeds.rb +24 -0
  95. data/examples/rails/jp_address_complement_demo/lib/tasks/.keep +0 -0
  96. data/examples/rails/jp_address_complement_demo/log/.keep +0 -0
  97. data/examples/rails/jp_address_complement_demo/public/400.html +135 -0
  98. data/examples/rails/jp_address_complement_demo/public/404.html +135 -0
  99. data/examples/rails/jp_address_complement_demo/public/406-unsupported-browser.html +135 -0
  100. data/examples/rails/jp_address_complement_demo/public/422.html +135 -0
  101. data/examples/rails/jp_address_complement_demo/public/500.html +135 -0
  102. data/examples/rails/jp_address_complement_demo/public/icon.png +0 -0
  103. data/examples/rails/jp_address_complement_demo/public/icon.svg +3 -0
  104. data/examples/rails/jp_address_complement_demo/public/robots.txt +1 -0
  105. data/examples/rails/jp_address_complement_demo/script/.keep +0 -0
  106. data/examples/rails/jp_address_complement_demo/storage/.keep +0 -0
  107. data/examples/rails/jp_address_complement_demo/vendor/.keep +0 -0
  108. data/lib/generators/jp_address_complement/install_generator.rb +34 -0
  109. data/lib/generators/jp_address_complement/templates/create_jp_address_complement_postal_codes.rb.erb +45 -0
  110. data/lib/jp_address_complement/address_record.rb +36 -0
  111. data/lib/jp_address_complement/configuration.rb +21 -0
  112. data/lib/jp_address_complement/importers/csv_importer.rb +148 -0
  113. data/lib/jp_address_complement/ken_all_downloader.rb +122 -0
  114. data/lib/jp_address_complement/models/postal_code.rb +21 -0
  115. data/lib/jp_address_complement/normalizer.rb +77 -0
  116. data/lib/jp_address_complement/prefecture.rb +105 -0
  117. data/lib/jp_address_complement/railtie.rb +27 -0
  118. data/lib/jp_address_complement/repositories/active_record_postal_code_repository.rb +78 -0
  119. data/lib/jp_address_complement/repositories/csv_postal_code_repository.rb +200 -0
  120. data/lib/jp_address_complement/repositories/postal_code_repository.rb +36 -0
  121. data/lib/jp_address_complement/searcher.rb +85 -0
  122. data/lib/jp_address_complement/validators/address_validator.rb +41 -0
  123. data/lib/jp_address_complement/version.rb +6 -0
  124. data/lib/jp_address_complement.rb +129 -0
  125. data/lib/tasks/jp_address_complement.rake +32 -0
  126. data/rbs_collection.lock.yaml +380 -0
  127. data/rbs_collection.yaml +19 -0
  128. data/sig/generated/generators/jp_address_complement/install_generator.rbs +18 -0
  129. data/sig/generated/jp_address_complement/configuration.rbs +18 -0
  130. data/sig/generated/jp_address_complement/importers/csv_importer.rbs +78 -0
  131. data/sig/generated/jp_address_complement/ken_all_downloader.rbs +49 -0
  132. data/sig/generated/jp_address_complement/normalizer.rbs +37 -0
  133. data/sig/generated/jp_address_complement/prefecture.rbs +27 -0
  134. data/sig/generated/jp_address_complement/railtie.rbs +8 -0
  135. data/sig/generated/jp_address_complement/repositories/active_record_postal_code_repository.rbs +38 -0
  136. data/sig/generated/jp_address_complement/repositories/csv_postal_code_repository.rbs +100 -0
  137. data/sig/generated/jp_address_complement/repositories/postal_code_repository.rbs +29 -0
  138. data/sig/generated/jp_address_complement/searcher.rbs +43 -0
  139. data/sig/generated/jp_address_complement/validators/address_validator.rbs +24 -0
  140. data/sig/generated/jp_address_complement/version.rbs +5 -0
  141. data/sig/generated/jp_address_complement.rbs +84 -0
  142. data/sig/manual/address_record.rbs +40 -0
  143. data/sig/manual/gem_rubyzip.rbs +17 -0
  144. data/sig/manual/postal_code.rbs +9 -0
  145. data/sig/manual/stdlib_csv_invalid_encoding_error.rbs +5 -0
  146. data/sig/manual/stdlib_net_http.rbs +33 -0
  147. data/sig/manual/stdlib_openuri.rbs +9 -0
  148. data/sig/manual/stdlib_tmpdir.rbs +4 -0
  149. data/specs/001-jp-address-complement-gem/checklists/requirements.md +36 -0
  150. data/specs/001-jp-address-complement-gem/contracts/public-api.md +209 -0
  151. data/specs/001-jp-address-complement-gem/data-model.md +207 -0
  152. data/specs/001-jp-address-complement-gem/plan.md +124 -0
  153. data/specs/001-jp-address-complement-gem/quickstart.md +151 -0
  154. data/specs/001-jp-address-complement-gem/research.md +139 -0
  155. data/specs/001-jp-address-complement-gem/spec.md +153 -0
  156. data/specs/001-jp-address-complement-gem/tasks.md +279 -0
  157. data/specs/002-rbs-type-annotations/checklists/requirements.md +37 -0
  158. data/specs/002-rbs-type-annotations/contracts/rbs-public-api.md +116 -0
  159. data/specs/002-rbs-type-annotations/data-model.md +119 -0
  160. data/specs/002-rbs-type-annotations/plan.md +116 -0
  161. data/specs/002-rbs-type-annotations/quickstart.md +105 -0
  162. data/specs/002-rbs-type-annotations/research.md +173 -0
  163. data/specs/002-rbs-type-annotations/spec.md +125 -0
  164. data/specs/002-rbs-type-annotations/tasks.md +189 -0
  165. data/specs/003-csv-remove-obsolete/checklists/requirements.md +34 -0
  166. data/specs/003-csv-remove-obsolete/contracts/csv-import.md +41 -0
  167. data/specs/003-csv-remove-obsolete/data-model.md +47 -0
  168. data/specs/003-csv-remove-obsolete/plan.md +73 -0
  169. data/specs/003-csv-remove-obsolete/quickstart.md +40 -0
  170. data/specs/003-csv-remove-obsolete/research.md +71 -0
  171. data/specs/003-csv-remove-obsolete/spec.md +85 -0
  172. data/specs/003-csv-remove-obsolete/tasks.md +167 -0
  173. data/specs/004-prefecture-code-reverse-lookup/checklists/requirements.md +34 -0
  174. data/specs/004-prefecture-code-reverse-lookup/contracts/public-api-prefecture-and-reverse.md +122 -0
  175. data/specs/004-prefecture-code-reverse-lookup/data-model.md +81 -0
  176. data/specs/004-prefecture-code-reverse-lookup/plan.md +92 -0
  177. data/specs/004-prefecture-code-reverse-lookup/quickstart.md +91 -0
  178. data/specs/004-prefecture-code-reverse-lookup/research.md +62 -0
  179. data/specs/004-prefecture-code-reverse-lookup/spec.md +120 -0
  180. data/specs/004-prefecture-code-reverse-lookup/tasks.md +190 -0
  181. metadata +451 -0
@@ -0,0 +1,33 @@
1
+ # net/http の Steep 用スタブ(KenAllDownloader で使用)
2
+ module Net
3
+ class HTTPError < StandardError
4
+ end
5
+
6
+ class OpenTimeout < Timeout::Error
7
+ end
8
+
9
+ class ReadTimeout < Timeout::Error
10
+ end
11
+
12
+ class HTTP
13
+ def self.start: (String host, Integer port, ?use_ssl: bool, ?read_timeout: Integer, ?open_timeout: Integer) { (Net::HTTP) -> untyped } -> untyped
14
+
15
+ def request: (Net::HTTPRequest request) { (Net::HTTPResponse) -> void } -> Net::HTTPResponse
16
+ end
17
+
18
+ class HTTP::Get < Net::HTTPRequest
19
+ def initialize: (URI::Generic uri) -> void
20
+ end
21
+
22
+ class HTTPSuccess < Net::HTTPResponse
23
+ end
24
+
25
+ class HTTPResponse
26
+ attr_reader code: String
27
+ attr_reader message: String
28
+ attr_reader body_io: IO
29
+ end
30
+
31
+ class HTTPRequest
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ # open-uri が URI に追加するメソッド(Steep 用スタブ)
2
+ module URI
3
+ def self.open: (String uri, ?read_timeout: Integer, ?open_timeout: Integer) { (untyped) -> untyped } -> untyped
4
+ end
5
+
6
+ module OpenURI
7
+ class HTTPError < StandardError
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ # tmpdir が Dir に追加するメソッド(Steep 用スタブ)
2
+ class Dir
3
+ def self.mktmpdir: (?String prefix_suffix, ?String tmpdir) -> String
4
+ end
@@ -0,0 +1,36 @@
1
+ # Specification Quality Checklist: 日本住所補完 Gem
2
+
3
+ **Purpose**: Validate specification completeness and quality before proceeding to planning
4
+ **Created**: 2026-02-22
5
+ **Feature**: [spec.md](../spec.md)
6
+
7
+ ## Content Quality
8
+
9
+ - [x] No implementation details (languages, frameworks, APIs)
10
+ - [x] Focused on user value and business needs
11
+ - [x] Written for non-technical stakeholders
12
+ - [x] All mandatory sections completed
13
+
14
+ ## Requirement Completeness
15
+
16
+ - [x] No [NEEDS CLARIFICATION] markers remain
17
+ - [x] Requirements are testable and unambiguous
18
+ - [x] Success criteria are measurable
19
+ - [x] Success criteria are technology-agnostic (no implementation details)
20
+ - [x] All acceptance scenarios are defined
21
+ - [x] Edge cases are identified
22
+ - [x] Scope is clearly bounded
23
+ - [x] Dependencies and assumptions identified
24
+
25
+ ## Feature Readiness
26
+
27
+ - [x] All functional requirements have clear acceptance criteria
28
+ - [x] User scenarios cover primary flows
29
+ - [x] Feature meets measurable outcomes defined in Success Criteria
30
+ - [x] No implementation details leak into specification
31
+
32
+ ## Notes
33
+
34
+ - Constitution Compliance Criteria(CC-001〜CC-004)は、プロジェクト憲章に基づく必須成功条件として意図的に技術用語(RSpec, RuboCop)を含んでいるため、技術非依存の通常成功基準とは区別して評価している。
35
+ - 全チェック項目が1回目のバリデーションで通過した(2026-02-22 実施)。
36
+ - `/speckit.plan` または `/speckit.clarify` への移行の準備が整っている。
@@ -0,0 +1,209 @@
1
+ # Public API Contract: JpAddressComplement
2
+
3
+ **Gem**: `jp_address_complement`
4
+ **Namespace**: `JpAddressComplement`
5
+ **Contract Type**: Ruby Public API(ライブラリの公開インターフェース定義)
6
+
7
+ ---
8
+
9
+ ## モジュールメソッド(`JpAddressComplement.*`)
10
+
11
+ ### `search_by_postal_code(code) → Array<AddressRecord>`
12
+
13
+ 郵便番号(7桁)に対応する住所レコードを返す。
14
+
15
+ **引数**
16
+
17
+ | 名前 | 型 | 説明 |
18
+ |-----|----|------|
19
+ | `code` | `String` | 郵便番号。ハイフンあり(`"100-0001"`)・全角数字・`〒` 記号はすべて自動正規化する |
20
+
21
+ **戻り値**: `Array<AddressRecord>`
22
+
23
+ - 一致するレコードが存在しない場合は `[]`(空配列)
24
+ - 同一郵便番号に複数レコードが存在する場合(大口事業所等)は全件返す
25
+
26
+ **エラー**: 不正入力(`nil`・空文字・数字以外を含む文字列)に対しては例外を発生させず `[]` を返す。
27
+
28
+ ```ruby
29
+ # 成功例
30
+ JpAddressComplement.search_by_postal_code("1000001")
31
+ # => [#<data JpAddressComplement::AddressRecord
32
+ # postal_code="1000001",
33
+ # pref_code="13",
34
+ # pref="東京都",
35
+ # city="千代田区",
36
+ # town="千代田",
37
+ # kana_pref="トウキョウト",
38
+ # kana_city="チヨダク",
39
+ # kana_town="チヨダ",
40
+ # has_alias=false,
41
+ # is_partial=false,
42
+ # is_large_office=false>]
43
+
44
+ # 正規化される入力例
45
+ JpAddressComplement.search_by_postal_code("100-0001") # ハイフンあり
46
+ JpAddressComplement.search_by_postal_code("〒100-0001") # 〒付き
47
+ JpAddressComplement.search_by_postal_code("1000001") # 全角数字
48
+
49
+ # 存在しない場合
50
+ JpAddressComplement.search_by_postal_code("0000000") # => []
51
+
52
+ # 不正入力(エラーなし)
53
+ JpAddressComplement.search_by_postal_code(nil) # => []
54
+ JpAddressComplement.search_by_postal_code("") # => []
55
+ JpAddressComplement.search_by_postal_code("abc") # => []
56
+ ```
57
+
58
+ ---
59
+
60
+ ### `search_by_postal_code_prefix(prefix) → Array<AddressRecord>`
61
+
62
+ 郵便番号の先頭4桁以上に一致する住所候補一覧を返す。
63
+
64
+ **引数**
65
+
66
+ | 名前 | 型 | 説明 |
67
+ |-----|----|------|
68
+ | `prefix` | `String` | 郵便番号の先頭部分(4桁以上)。全角数字は自動正規化 |
69
+
70
+ **戻り値**: `Array<AddressRecord>`
71
+
72
+ - 先頭4桁未満の場合は `[]`(過大結果防止のガード)
73
+ - 7桁完全一致の場合は `search_by_postal_code` と同等の結果
74
+
75
+ ```ruby
76
+ # 先頭4桁
77
+ JpAddressComplement.search_by_postal_code_prefix("1000")
78
+ # => [#<data ... postal_code="1000001">, #<data ... postal_code="1000002">, ...]
79
+
80
+ # 先頭6桁
81
+ JpAddressComplement.search_by_postal_code_prefix("100000")
82
+ # => [#<data ... postal_code="1000001">]
83
+
84
+ # 先頭3桁以下 → 空返却
85
+ JpAddressComplement.search_by_postal_code_prefix("100") # => []
86
+ JpAddressComplement.search_by_postal_code_prefix("10") # => []
87
+ ```
88
+
89
+ ---
90
+
91
+ ### `valid_combination?(postal_code, address) → Boolean`
92
+
93
+ 郵便番号と住所文字列が整合しているかを検証する。
94
+
95
+ **引数**
96
+
97
+ | 名前 | 型 | 説明 |
98
+ |-----|----|------|
99
+ | `postal_code` | `String` | 郵便番号(自動正規化) |
100
+ | `address` | `String` | 住所文字列(番地・建物名が含まれていても可) |
101
+
102
+ **戻り値**: `Boolean`
103
+
104
+ - 郵便番号に対応するレコードの「都道府県名 + 市区町村名 + 町域名」が `address` に**部分一致**する場合は `true`
105
+ - 同一郵便番号に複数レコードがある場合はいずれか1件が一致すれば `true`
106
+ - 郵便番号が不正・存在しない場合は `false`
107
+
108
+ ```ruby
109
+ JpAddressComplement.valid_combination?("1000001", "東京都千代田区千代田1-1 ○○ビル") # => true
110
+ JpAddressComplement.valid_combination?("1000001", "東京都千代田区千代田") # => true
111
+ JpAddressComplement.valid_combination?("1000001", "大阪府大阪市北区梅田") # => false
112
+ JpAddressComplement.valid_combination?("0000000", "東京都千代田区千代田") # => false(存在しない)
113
+ JpAddressComplement.valid_combination?(nil, "東京都...") # => false
114
+ ```
115
+
116
+ ---
117
+
118
+ ### `configure { |config| ... }` — 設定ブロック
119
+
120
+ ```ruby
121
+ JpAddressComplement.configure do |config|
122
+ # カスタムリポジトリを注入(Rails 以外の環境向け)
123
+ config.repository = MyCustomPostalCodeRepository.new
124
+ end
125
+ ```
126
+
127
+ **設定オプション**
128
+
129
+ | オプション | 型 | デフォルト | 説明 |
130
+ |-----------|-----|-----------|------|
131
+ | `repository` | `PostalCodeRepository` 準拠オブジェクト | `ActiveRecordPostalCodeRepository.new` | データアクセス実装 |
132
+
133
+ ---
134
+
135
+ ## 値オブジェクト: `AddressRecord`
136
+
137
+ `search_by_postal_code` および `search_by_postal_code_prefix` が返す値オブジェクト。
138
+
139
+ ```ruby
140
+ record = JpAddressComplement.search_by_postal_code("1000001").first
141
+
142
+ record.postal_code # => "1000001" (String)
143
+ record.pref_code # => "13" (String)
144
+ record.pref # => "東京都" (String)
145
+ record.city # => "千代田区" (String)
146
+ record.town # => "千代田" (String or nil)
147
+ record.kana_pref # => "トウキョウト" (String or nil)
148
+ record.kana_city # => "チヨダク" (String or nil)
149
+ record.kana_town # => "チヨダ" (String or nil)
150
+ record.has_alias # => false (Boolean) 通称あり
151
+ record.is_partial # => false (Boolean) 丁目区分あり
152
+ record.is_large_office # => false (Boolean) 大口事業所専用
153
+ ```
154
+
155
+ ---
156
+
157
+ ## ActiveModel バリデーター
158
+
159
+ ```ruby
160
+ validates :postal_code_field, jp_address_complement: { address_field: :address_field_name }
161
+ ```
162
+
163
+ **オプション**
164
+
165
+ | キー | 型 | 必須 | 説明 |
166
+ |-----|----|------|------|
167
+ | `address_field` | `Symbol` | ✅ | 住所文字列が格納されているモデルの属性名 |
168
+
169
+ **バリデーションの動作**
170
+
171
+ | 条件 | 結果 |
172
+ |------|------|
173
+ | `postal_code` フィールドが空 | バリデーターをスキップ(他バリデーターに委ねる) |
174
+ | 整合している | 通過 |
175
+ | 整合しない | `errors.add(:postal_code_field, "と住所が一致しません")` |
176
+
177
+ ---
178
+
179
+ ## Rake タスク
180
+
181
+ ```bash
182
+ # インポート(Shift_JIS → UTF-8 変換を自動実行)
183
+ bundle exec rake jp_address_complement:import CSV=/path/to/KEN_ALL.CSV
184
+
185
+ # インポート進捗ログ付き
186
+ bundle exec rake jp_address_complement:import CSV=/path/to/KEN_ALL.CSV VERBOSE=true
187
+ ```
188
+
189
+ **環境変数**
190
+
191
+ | 変数 | 必須 | 説明 |
192
+ |------|------|------|
193
+ | `CSV` | ✅ | KEN_ALL.CSV へのパス |
194
+ | `VERBOSE` | ❌ | `true` で進捗ログを出力(デフォルト: `false`) |
195
+ | `BATCH_SIZE` | ❌ | バッチ件数(デフォルト: `1000`) |
196
+
197
+ ---
198
+
199
+ ## Rails Generator
200
+
201
+ ```bash
202
+ rails g jp_address_complement:install
203
+ ```
204
+
205
+ **生成ファイル**
206
+
207
+ | ファイル | 説明 |
208
+ |---------|------|
209
+ | `db/migrate/YYYYMMDDHHMMSS_create_jp_address_complement_postal_codes.rb` | 住所テーブルマイグレーション |
@@ -0,0 +1,207 @@
1
+ # Data Model: 日本住所補完 Gem
2
+
3
+ **Branch**: `001-jp-address-complement-gem`
4
+ **Date**: 2026-02-22
5
+
6
+ ---
7
+
8
+ ## エンティティ一覧
9
+
10
+ ### 1. `postal_codes` テーブル(DB永続化)
11
+
12
+ 利用アプリケーションの DB に作成される住所データテーブル。Gem からマイグレーションを提供する。
13
+
14
+ #### スキーマ定義
15
+
16
+ ```ruby
17
+ # db/migrate/YYYYMMDDHHMMSS_create_jp_address_complement_postal_codes.rb
18
+ create_table :jp_address_complement_postal_codes do |t|
19
+ t.string :postal_code, limit: 7, null: false # 郵便番号(7桁、数字のみ)
20
+ t.string :pref_code, limit: 2, null: false # 都道府県コード(JIS X 0401)
21
+ t.string :pref, limit: 10, null: false # 都道府県名(漢字)
22
+ t.string :city, limit: 50, null: false # 市区町村名(漢字)
23
+ t.string :town, limit: 100, null: true # 町域名(漢字)- nil 可(大口事業所等)
24
+ t.string :kana_pref, limit: 20, null: true # 都道府県名(カナ)
25
+ t.string :kana_city, limit: 100, null: true # 市区町村名(カナ)
26
+ t.string :kana_town, limit: 200, null: true # 町域名(カナ)
27
+ t.boolean :has_alias, default: false # 通称あり(KEN_ALL フラグ)
28
+ t.boolean :is_partial, default: false # 丁目区分あり
29
+ t.boolean :is_large_office, default: false # 大口事業所専用フラグ
30
+ t.timestamps
31
+ end
32
+
33
+ # インデックス
34
+ add_index :jp_address_complement_postal_codes, :postal_code # 完全一致
35
+ add_index :jp_address_complement_postal_codes, [:postal_code, :pref_code, :city, :town],
36
+ unique: true, name: 'idx_jp_address_complement_unique' # 冪等性保証
37
+ ```
38
+
39
+ #### フィールド定義
40
+
41
+ | カラム名 | 型 | NOT NULL | 説明 |
42
+ |---------|-----|----------|------|
43
+ | `id` | bigint | ✅ | PK(自動採番) |
44
+ | `postal_code` | varchar(7) | ✅ | ハイフンなし7桁郵便番号 |
45
+ | `pref_code` | varchar(2) | ✅ | 都道府県コード(01〜47) |
46
+ | `pref` | varchar(10) | ✅ | 都道府県名(漢字) |
47
+ | `city` | varchar(50) | ✅ | 市区町村名(漢字) |
48
+ | `town` | varchar(100) | ❌ | 町域名(漢字)、以下一律で適用 |
49
+ | `kana_pref` | varchar(20) | ❌ | 都道府県カナ |
50
+ | `kana_city` | varchar(100) | ❌ | 市区町村カナ |
51
+ | `kana_town` | varchar(200) | ❌ | 町域カナ |
52
+ | `has_alias` | boolean | ✅ | 通称フラグ |
53
+ | `is_partial` | boolean | ✅ | 丁目区分フラグ |
54
+ | `is_large_office` | boolean | ✅ | 大口事業所フラグ |
55
+ | `created_at` | datetime | ✅ | 作成日時 |
56
+ | `updated_at` | datetime | ✅ | 更新日時 |
57
+
58
+ #### インデックス戦略
59
+
60
+ | インデックス名 | カラム | 用途 |
61
+ |--------------|--------|------|
62
+ | `idx_postal_code` | `(postal_code)` | US-1: 完全一致検索 O(log n) |
63
+ | `idx_jp_address_complement_unique` | `(postal_code, pref_code, city, town)` | 冪等 upsert の一意性制約 |
64
+
65
+ **先頭4桁前方一致**: `postal_code` への B-tree インデックスは `LIKE '1000%'` の先頭一致クエリに対して有効(PostgreSQL/MySQL/SQLite 共通)。追加インデックス不要。
66
+
67
+ ---
68
+
69
+ ### 2. `AddressRecord` — Plain Ruby 値オブジェクト
70
+
71
+ DB から取得したデータを Repository が返す際の値オブジェクト。ActiveRecord インスタンスを返さないことでコアロジックの AR 依存を排除する。
72
+
73
+ ```ruby
74
+ module JpAddressComplement
75
+ # Ruby 3.2+ Data クラス(immutable struct)
76
+ AddressRecord = Data.define(
77
+ :postal_code, # String — 7桁、ハイフンなし
78
+ :pref_code, # String — 都道府県コード
79
+ :pref, # String — 都道府県名
80
+ :city, # String — 市区町村名
81
+ :town, # String? — 町域名(nilable)
82
+ :kana_pref, # String? — 都道府県カナ
83
+ :kana_city, # String? — 市区町村カナ
84
+ :kana_town, # String? — 町域カナ
85
+ :has_alias, # Boolean
86
+ :is_partial, # Boolean
87
+ :is_large_office # Boolean
88
+ )
89
+ end
90
+ ```
91
+
92
+ - **不変(Immutable)**: `Data.define` により凍結された値オブジェクト。
93
+ - **Ruby 3.0 互換**: `Data` クラスは Ruby 3.2 以降。3.0/3.1 では `Struct.new(..., keyword_init: true)` にフォールバック。
94
+
95
+ ---
96
+
97
+ ### 3. `PostalCodeRepository` — データアクセス抽象化インターフェース
98
+
99
+ ```ruby
100
+ module JpAddressComplement
101
+ module Repositories
102
+ class PostalCodeRepository
103
+ # @param code [String] 正規化済み7桁郵便番号
104
+ # @return [Array<AddressRecord>]
105
+ def find_by_code(code)
106
+ raise NotImplementedError, "#{self.class}#find_by_code is not implemented"
107
+ end
108
+
109
+ # @param prefix [String] 4桁以上の郵便番号プレフィックス
110
+ # @return [Array<AddressRecord>]
111
+ def find_by_prefix(prefix)
112
+ raise NotImplementedError, "#{self.class}#find_by_prefix is not implemented"
113
+ end
114
+ end
115
+ end
116
+ end
117
+ ```
118
+
119
+ ---
120
+
121
+ ### 4. `ActiveRecordPostalCodeRepository` — Rails 向け実装
122
+
123
+ ```ruby
124
+ module JpAddressComplement
125
+ module Repositories
126
+ class ActiveRecordPostalCodeRepository < PostalCodeRepository
127
+ MODEL_CLASS = "JpAddressComplement::PostalCode" # AR モデル(遅延定数参照)
128
+
129
+ def find_by_code(code)
130
+ model.where(postal_code: code).map { to_record(_1) }
131
+ end
132
+
133
+ def find_by_prefix(prefix)
134
+ model.where("postal_code LIKE ?", "#{prefix}%").map { to_record(_1) }
135
+ end
136
+
137
+ private
138
+
139
+ def model = Object.const_get(MODEL_CLASS)
140
+
141
+ def to_record(ar)
142
+ AddressRecord.new(
143
+ postal_code: ar.postal_code,
144
+ pref_code: ar.pref_code,
145
+ pref: ar.pref,
146
+ city: ar.city,
147
+ town: ar.town,
148
+ kana_pref: ar.kana_pref,
149
+ kana_city: ar.kana_city,
150
+ kana_town: ar.kana_town,
151
+ has_alias: ar.has_alias,
152
+ is_partial: ar.is_partial,
153
+ is_large_office: ar.is_large_office
154
+ )
155
+ end
156
+ end
157
+ end
158
+ end
159
+ ```
160
+
161
+ ---
162
+
163
+ ### 5. `PostalCode` — ActiveRecord モデル(Rails 環境のみ)
164
+
165
+ ```ruby
166
+ module JpAddressComplement
167
+ class PostalCode < ActiveRecord::Base
168
+ self.table_name = "jp_address_complement_postal_codes"
169
+
170
+ validates :postal_code, presence: true, format: { with: /\A\d{7}\z/ }
171
+ validates :pref, :city, presence: true
172
+ end
173
+ end
174
+ ```
175
+
176
+ ---
177
+
178
+ ## エンティティ関係図
179
+
180
+ ```
181
+ KEN_ALL.CSV (Shift_JIS, 外部ファイル)
182
+
183
+ │ Rake Task (Shift_JIS→UTF-8 変換 + upsert_all, 1,000件/バッチ)
184
+
185
+ jp_address_complement_postal_codes (利用アプリの DB テーブル)
186
+
187
+ │ ActiveRecordPostalCodeRepository#find_by_code / find_by_prefix
188
+
189
+ AddressRecord(Plain Ruby 値オブジェクト)
190
+
191
+ │ JpAddressComplement::Searcher (コアロジック)
192
+
193
+ 呼び出し元(Rails アプリ / Rack アプリ / RSpec テスト)
194
+ ```
195
+
196
+ ---
197
+
198
+ ## データ検証ルール
199
+
200
+ | ルール | 適用箇所 |
201
+ |--------|---------|
202
+ | 郵便番号は7桁数字のみ(正規化後) | `Normalizer` クラス、`PostalCode` モデル |
203
+ | 全角数字→半角正規化 | `Normalizer` クラス(入力受付時) |
204
+ | ハイフン除去(〒100-0001 → 1000001) | `Normalizer` クラス(入力受付時) |
205
+ | 4桁未満プレフィックスは空返却 | `Searcher#search_by_prefix` のガード節 |
206
+ | nil/空文字/数字以外はエラーなしで空返却 | `Normalizer` では nil 返却、`Searcher` でガード |
207
+ | 不正 CSV 行は警告ログ出力してスキップ | `Importer` クラス |
@@ -0,0 +1,124 @@
1
+ # Implementation Plan: 日本住所補完 Gem
2
+
3
+ **Branch**: `001-jp-address-complement-gem` | **Date**: 2026-02-22 | **Spec**: [spec.md](./spec.md)
4
+ **Input**: Feature specification from `/specs/001-jp-address-complement-gem/spec.md`
5
+
6
+ ---
7
+
8
+ ## Summary
9
+
10
+ 日本国内の住所を郵便番号から補完・検証できる Rails 7.x 以上向け Ruby gem を実装する。
11
+ **技術方針**: 住所データは利用アプリケーションの DB に永続化し(メモリロードなし)、インポートは Rake タスク経由とする。コア検索ロジックは Repository パターンで ActiveRecord 依存を排除し、Rails 以外の環境でも動作可能な設計とする。
12
+
13
+ ---
14
+
15
+ ## Technical Context
16
+
17
+ **Language/Version**: Ruby 3.0+(3.2 以上推奨)
18
+ **Primary Dependencies**: Rails 7.x 以上、RSpec、SimpleCov、RuboCop
19
+ **Storage**: 利用アプリケーションの DB(ActiveRecord 対応 DB であれば何でも可)。開発・テスト環境は SQLite。
20
+ **Testing**: RSpec(カバレッジ 90% 以上、SimpleCov 計測)
21
+ **Target Platform**: Ruby gem(Rail Engine ではなく Railtie を使用)
22
+ **Project Type**: Ruby gem(Rails 組み込み型ライブラリ)
23
+ **Performance Goals**:
24
+ - 郵便番号完全一致検索: < 10ms p99(DBインデックス使用前提)
25
+ - 先頭4桁前方一致検索: < 50ms
26
+ - Rake インポート(12万件): < 60秒
27
+ **Constraints**: Rubocop 100% PASS(disable 禁止)、TDD 必須
28
+ **Scale/Scope**: 全国郵便番号(約12万件)を管理
29
+ **Encoding**: KEN_ALL.CSV は Shift_JIS。Rake タスクが UTF-8 に自動変換。
30
+ **Batch Strategy**: `upsert_all` + 1,000件/バッチ分割。冪等性は `(postal_code, pref_code, city, town)` の一意制約で保証。
31
+
32
+ ---
33
+
34
+ ## Constitution Check
35
+
36
+ *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
37
+
38
+ - [x] **I. Gem-First**: `JpAddressComplement` 名前空間に閉じる。Railtie 経由で Rails 統合。Rails 非依存コアロジックを Repository パターンで実現。
39
+ - [x] **II. TDD**: テストを先に書く。RSpec + SimpleCov カバレッジ 90%+ の計画あり。FakeRepository でコアロジックを DB なしテスト可能。
40
+ - [x] **III. Rubocop**: `rubocop:disable` 使用禁止。`.rubocop.yml` で設定変更時は理由明記。CI に Rubocop ゲートを設定。
41
+ - [x] **IV. データ整合性**: `upsert_all`(ON CONFLICT DO UPDATE)で冪等インポート。1,000件/バッチで DB ロック最小化。Shift_JIS→UTF-8 変換を Rake タスクで自動実行。
42
+ - [x] **V. 機能要件**: 3機能(`search_by_postal_code` / `search_by_postal_code_prefix` / `valid_combination?`)のインターフェースを contracts/public-api.md で明確化。引数バリデーション・空返却挙動を定義済み。
43
+ - [x] **VI. シンプルさ**: Repository パターンの採用は FR-010/FR-015(Rails 非依存要件)で正当化済み。DI は Devise/Kaminari と同レベルのシンプルなコンストラクタ注入で実現。YAGNI 遵守(差分インポート・RBS・rom-rb は Deferred)。
44
+
45
+ **Constitution Check Result**: ✅ 全ゲート PASS
46
+
47
+ ---
48
+
49
+ ## Project Structure
50
+
51
+ ### Documentation (this feature)
52
+
53
+ ```text
54
+ specs/001-jp-address-complement-gem/
55
+ ├── plan.md ✅ このファイル
56
+ ├── spec.md ✅ 機能仕様
57
+ ├── research.md ✅ Phase 0 調査結果
58
+ ├── data-model.md ✅ データモデル設計
59
+ ├── quickstart.md ✅ 利用者向けガイド
60
+ ├── contracts/
61
+ │ └── public-api.md ✅ 公開 API コントラクト
62
+ └── tasks.md (/speckit.tasks コマンドで生成)
63
+ ```
64
+
65
+ ### Source Code (repository root)
66
+
67
+ ```text
68
+ jp_address_complement.gemspec
69
+ lib/
70
+ ├── jp_address_complement.rb # エントリポイント・モジュールメソッド定義
71
+ ├── jp_address_complement/
72
+ │ ├── version.rb # VERSION 定数
73
+ │ ├── configuration.rb # Config クラス(repository DI 設定)
74
+ │ ├── railtie.rb # Rails Railtie(Rails 統合・initializer)
75
+ │ ├── address_record.rb # AddressRecord 値オブジェクト(Data/Struct)
76
+ │ ├── normalizer.rb # 郵便番号正規化(全角→半角・ハイフン除去)
77
+ │ ├── searcher.rb # コア検索ロジック(Repository 経由)
78
+ │ ├── repositories/
79
+ │ │ ├── postal_code_repository.rb # 抽象基底クラス(インターフェース)
80
+ │ │ └── active_record_postal_code_repository.rb # ActiveRecord 実装
81
+ │ ├── models/
82
+ │ │ └── postal_code.rb # ActiveRecord モデル
83
+ │ ├── validators/
84
+ │ │ └── jp_address_complement_validator.rb # ActiveModel バリデーター
85
+ │ ├── importers/
86
+ │ │ └── csv_importer.rb # CSV インポートロジック
87
+ │ └── generators/
88
+ │ └── jp_address_complement/
89
+ │ ├── install_generator.rb # rails g jp_address_complement:install
90
+ │ └── templates/
91
+ │ └── create_postal_codes.rb.erb # マイグレーションテンプレート
92
+
93
+ lib/tasks/
94
+ └── jp_address_complement.rake # jp_address_complement:import タスク
95
+
96
+ spec/
97
+ ├── spec_helper.rb
98
+ ├── jp_address_complement_spec.rb # トップレベルモジュールメソッドの統合テスト
99
+ ├── normalizer_spec.rb
100
+ ├── searcher_spec.rb
101
+ ├── repositories/
102
+ │ └── active_record_repository_spec.rb
103
+ ├── validators/
104
+ │ └── jp_address_complement_validator_spec.rb
105
+ ├── importers/
106
+ │ └── csv_importer_spec.rb
107
+ ├── generators/
108
+ │ └── install_generator_spec.rb
109
+ └── support/
110
+ ├── fake_repository.rb # テスト用 FakeRepository
111
+ └── database_helper.rb
112
+ ```
113
+
114
+ **Structure Decision**: Ruby gem の標準構成(`lib/`, `spec/`, `lib/tasks/`)を採用。Generator テンプレートは Rails のコンベンションに従い `lib/generators/` 配下に配置。
115
+
116
+ ---
117
+
118
+ ## Complexity Tracking
119
+
120
+ | 複雑化要素 | 正当化理由 | より単純な代替案が却下される理由 |
121
+ |-----------|-----------|-------------------------------|
122
+ | Repository パターン | FR-010/FR-015: Rails 非依存コアが必須 | ActiveRecord 直接依存では Rack/Sinatra 環境での利用不可 |
123
+ | Railtie + Generator | FR-014: Rails 標準のマイグレーション管理 | 自動テーブル作成は利用者の制御を奪い、既存マイグレーション設計に干渉する |
124
+ | upsert_all バッチ分割 | SC-004: 60秒以内インポート+ Constitution IV の冪等性要件 | 逐次 `find_or_create_by` では12万件に数分かかり SC-004 を満足できない |