bipm-data-importer 0.2.1 → 0.4.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +68 -9
  3. data/TODO.refactor/01-remove-debug-code.md +18 -0
  4. data/TODO.refactor/02-fix-gemspec.md +21 -0
  5. data/TODO.refactor/03-update-readme.md +28 -0
  6. data/TODO.refactor/04-replace-require-relative-with-autoload.md +34 -0
  7. data/TODO.refactor/05-fix-outcomes-bugs.md +22 -0
  8. data/TODO.refactor/06-unify-clause-taxonomy.md +31 -0
  9. data/TODO.refactor/07-extract-french-parsing.md +21 -0
  10. data/TODO.refactor/08-extract-data-quirks.md +25 -0
  11. data/TODO.refactor/09-introduce-body-registry.md +23 -0
  12. data/TODO.refactor/10-introduce-strategy-pattern.md +34 -0
  13. data/TODO.refactor/11-extract-cli-class.md +20 -0
  14. data/TODO.refactor/12-specs-common.md +23 -0
  15. data/TODO.refactor/13-specs-cgpm.md +22 -0
  16. data/TODO.refactor/14-specs-asciimath.md +23 -0
  17. data/TODO.refactor/15-specs-outcomes.md +24 -0
  18. data/TODO.refactor/16-quarantine-failing-specs.md +23 -0
  19. data/TODO.refactor/17-typed-domain-models.md +32 -0
  20. data/TODO.refactor/README.md +62 -0
  21. data/bipm-data-importer.gemspec +11 -7
  22. data/exe/bipm-fetch +2 -376
  23. data/lib/bipm/data/importer/asciimath.rb +3 -3
  24. data/lib/bipm/data/importer/bodies.rb +45 -0
  25. data/lib/bipm/data/importer/body.rb +43 -0
  26. data/lib/bipm/data/importer/cgpm.rb +21 -0
  27. data/lib/bipm/data/importer/clauses.rb +239 -0
  28. data/lib/bipm/data/importer/cli.rb +83 -0
  29. data/lib/bipm/data/importer/common.rb +32 -121
  30. data/lib/bipm/data/importer/fetcher.rb +62 -0
  31. data/lib/bipm/data/importer/language.rb +66 -0
  32. data/lib/bipm/data/importer/quirks.rb +73 -0
  33. data/lib/bipm/data/importer/strategies/base.rb +40 -0
  34. data/lib/bipm/data/importer/strategies/spa_meetings.rb +340 -0
  35. data/lib/bipm/data/importer/strategies/static_index.rb +324 -0
  36. data/lib/bipm/data/importer/strategies.rb +13 -0
  37. data/lib/bipm/data/importer/text/french.rb +49 -0
  38. data/lib/bipm/data/importer/text.rb +11 -0
  39. data/lib/bipm/data/importer/version.rb +1 -1
  40. data/lib/bipm/data/importer.rb +31 -4
  41. data/lib/bipm/data/outcomes/action.rb +1 -1
  42. data/lib/bipm/data/outcomes/approval.rb +1 -1
  43. data/lib/bipm/data/outcomes/body.rb +1 -1
  44. data/lib/bipm/data/outcomes/consideration.rb +1 -1
  45. data/lib/bipm/data/outcomes/localized_body.rb +1 -1
  46. data/lib/bipm/data/outcomes/meeting.rb +2 -2
  47. data/lib/bipm/data/outcomes/resolution.rb +1 -1
  48. data/lib/bipm/data/outcomes.rb +9 -13
  49. data/lib/bipm-data-importer.rb +1 -1
  50. metadata +46 -19
  51. data/.hound.yml +0 -5
  52. data/.rspec +0 -3
  53. data/.rubocop.yml +0 -10
  54. data/exe/bipm-fetch-cgpm +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bf2e9209301b13261c1b15013611cd17cf5355d6152c1f59d131aa486bbec6c
4
- data.tar.gz: d1bf3ab0059a945f134068cf3c9cab045fdb6c9970edd3dc0cb2ce839c3dce9b
3
+ metadata.gz: 8571fc9ad0440e53b64eacfe9758641a28b6f0d59bcafc5fd40d6ba28db36f91
4
+ data.tar.gz: dd454c0c7181e5c553788accd98893c5e1f472fdd719263f0fa03184ba2b09bd
5
5
  SHA512:
6
- metadata.gz: f653f802d49a88b5e9f0f5b6380a1c9a0fdd73e52be093cae3bc62020d9057221dc9b9264a75bf10198b154f16dd8010480056ae624f7a1c49e7659e1b87b6fc
7
- data.tar.gz: dd797598112540212aa451e62089b4dde8dc89e4cbfd85e83e6c653e002e65fbab8783ca4cfaab6d8c10a4442e90cacbb6c057b02615c270bf6b7dc3f531a3e0
6
+ metadata.gz: c699baed8a04317b9fc2997ef8c9bb2acd36401dbe9a1f570e934efccf8fc933582f1d6eb41d3565f6adae4605f22cb11e7f5faa4bef2c75ffe75f68d6ac6460
7
+ data.tar.gz: 4cf90d4acb346a7b7b3d0f2f19938a8807967d8376e77012ea396951bbe3f70782fce96be1f68049905108f17326886ce880b5b35ee70b751708fcfc1b0e109e
data/README.adoc CHANGED
@@ -2,31 +2,90 @@
2
2
 
3
3
  == Purpose
4
4
 
5
- Provides executables that help fetch CIPM and CGPM outcomes from the official
6
- BIPM website.
5
+ Provides a single executable, `bipm-fetch`, that fetches BIPM committee
6
+ outcomes (CIPM, CGPM, and the consultative committees) from the official
7
+ BIPM website into a local YAML tree.
7
8
 
9
+ The gem also exposes a Ruby library API for programmatic use.
8
10
 
9
- == Usage
10
11
 
11
- To fetch CGPM outcomes:
12
+ == Command-line usage
13
+
14
+ Fetch every body, every language:
15
+
16
+ [source,sh]
17
+ ----
18
+ $ bipm-fetch
19
+ ----
20
+
21
+ Fetch a single body (any of: `ccauv`, `ccem`, `ccl`, `ccm`, `ccpr`,
22
+ `ccqm`, `ccri`, `cct`, `cctf`, `ccu`, `cgpm`, `cipm`, `jcgm`, `jcrb`):
23
+
24
+ [source,sh]
25
+ ----
26
+ $ bipm-fetch --body=cgpm
27
+ ----
28
+
29
+ Restrict to one language (`en` or `fr`):
30
+
31
+ [source,sh]
32
+ ----
33
+ $ bipm-fetch --body=cgpm --language=fr
34
+ ----
35
+
36
+ Run each body in a forked child process (parallel):
12
37
 
13
38
  [source,sh]
14
39
  ----
15
- $ bipm-fetch-cgpm
40
+ $ bipm-fetch --fork
16
41
  ----
17
42
 
18
- To fetch CIPM outcomes:
43
+ Show help:
19
44
 
20
45
  [source,sh]
21
46
  ----
22
- $ bipm-fetch-cipm
47
+ $ bipm-fetch --help
23
48
  ----
24
49
 
25
- To fetch CIPM decisions:
50
+ The default output directory is `./data`. Each body writes a
51
+ `data/<body>/meetings-{en,fr}/meeting-<id>.yml` tree.
52
+
53
+
54
+ == Library usage
55
+
56
+ [source,ruby]
57
+ ----
58
+ require "bipm/data/importer"
59
+
60
+ agent = Mechanize.new
61
+ Bipm::Data::Importer.fetch(:cgpm).call(agent:, base_dir: "data")
62
+ Bipm::Data::Importer.fetch(:cipm, languages: %w[en]).call(agent:, base_dir: "data")
63
+
64
+ # All bodies:
65
+ Bipm::Data::Importer.fetch_all.each { |f| f.call(agent:, base_dir: "data") }
66
+ ----
67
+
68
+ To read produced outcomes back:
69
+
70
+ [source,ruby]
71
+ ----
72
+ require "bipm/data/outcomes"
73
+
74
+ Bipm::Data::Outcomes[:cgpm][:en].meetings.first.resolutions.values.first.actions
75
+ ----
76
+
77
+
78
+ == Cassettes (test fixtures)
79
+
80
+ Network requests are replayed from `cassettes/` (VCR). The CGPM scraper
81
+ hits post-SPA URLs that have no cassettes and runs against the live site.
82
+
83
+ To refresh cassettes:
26
84
 
27
85
  [source,sh]
28
86
  ----
29
- $ bipm-fetch-cipm-decisions
87
+ $ BIPM_VCR_RECORD=all bundle exec rspec
88
+ $ BIPM_VCR_RECORD=new_episodes bundle exec rspec
30
89
  ----
31
90
 
32
91
 
@@ -0,0 +1,18 @@
1
+ # 01 — Remove debug code
2
+
3
+ ## Problem
4
+ - `lib/bipm/data/importer/common.rb:253` — commented `#binding.pry if ps.count != 1`.
5
+ - `lib/bipm/data/importer/common.rb:426` — `binding.pry if date <= Date.parse("0000-01-01")`. Drops the production scraper into a pry REPL on any malformed date.
6
+ - `lib/bipm/data/importer/common.rb:430` — bare `binding.pry` inside `rescue Date::Error`. Same problem.
7
+ - `exe/bipm-fetch:240` — `pp [:duplicates_found, decisions]`. Spams stdout during normal runs.
8
+
9
+ ## Fix
10
+ Delete the debug primitives. If the date-parse failure path needs visibility, raise a typed error (`Bipm::Data::Importer::DateParseError`) and let the caller log/continue.
11
+
12
+ ## Acceptance
13
+ - `grep -rn "binding.pry\|binding.remote_pry" lib exe` → empty.
14
+ - `grep -rn "pp \[" lib exe` → empty.
15
+ - Scraper does not hang waiting for REPL input on bad input.
16
+
17
+ ## Status
18
+ Complete.
@@ -0,0 +1,21 @@
1
+ # 02 — Fix gemspec
2
+
3
+ ## Problem
4
+ - `bipm-data-importer.gemspec:38` — `pry` is a **runtime** dependency. Debugger; should be dev.
5
+ - `bipm-data-importer.gemspec:14` — `required_ruby_version = ">= 2.5.0"`. 2.5 EOL 2021. User wants `>= 3.3.0`.
6
+ - `bipm-data-importer.gemspec:24` — file-exclusion regex still mentions `appveyor`/`travis`/`circleci` paths that no longer exist (cleanup commit `5ca19cf`).
7
+ - `.rubocop.yml` — `TargetRubyVersion: 2.5`. Must match gemspec.
8
+
9
+ ## Fix
10
+ - Move `pry` to `add_development_dependency`.
11
+ - Set `required_ruby_version = ">= 3.3.0"`.
12
+ - Set `TargetRubyVersion: 3.3` in `.rubocop.yml`.
13
+ - Drop the `(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor` branch — keep only what's still relevant.
14
+
15
+ ## Acceptance
16
+ - `bundle exec rubocop` reports no TargetRubyVersion mismatches.
17
+ - `bundle install` still resolves.
18
+ - `gem build` succeeds.
19
+
20
+ ## Status
21
+ Complete.
@@ -0,0 +1,28 @@
1
+ # 03 — Update README
2
+
3
+ ## Problem
4
+ `README.adoc:11-31` documents three executables that no longer exist:
5
+ - `bipm-fetch-cgpm`
6
+ - `bipm-fetch-cipm`
7
+ - `bipm-fetch-cipm-decisions`
8
+
9
+ Commit `c985489` (Consolidate CGPM scraping into the single bipm-fetch exe) collapsed them into `bipm-fetch`, but README was never updated. Users following README get `command not found`.
10
+
11
+ ## Fix
12
+ Rewrite the Usage section to describe the single CLI:
13
+
14
+ ```
15
+ bipm-fetch # fetch all bodies, all languages
16
+ bipm-fetch --body=cgpm # one body
17
+ bipm-fetch --body=cgpm --language=fr # one language
18
+ bipm-fetch --fork # parallel via fork
19
+ ```
20
+
21
+ Also document the library API: `Bipm::Data::Importer.fetch(body_id).call`.
22
+
23
+ ## Acceptance
24
+ - README matches `exe/` contents.
25
+ - Usage examples map onto real CLI flags.
26
+
27
+ ## Status
28
+ Complete.
@@ -0,0 +1,34 @@
1
+ # 04 — Replace `require_relative` with autoload
2
+
3
+ ## Problem
4
+ User rule: "NEVER use `require_relative` for internal library code. Never use `require` with a path to code within your own library. Use Ruby `autoload` instead. Define autoload entries in the **immediate parent namespace's file** — create that file if it doesn't exist."
5
+
6
+ Current violations:
7
+ - `lib/bipm-data-importer.rb:3` — `require_relative "bipm/data/importer"`
8
+ - `lib/bipm/data/importer.rb:3-6` — `require_relative "importer/asciimath"`, `common`, `cgpm`, `version`
9
+ - `lib/bipm/data/importer/common.rb:7` — `require_relative "asciimath"`
10
+ - `lib/bipm/data/outcomes.rb:1-3` — `require_relative "outcomes/body"`, `localized_body`, `meeting`
11
+ - `exe/bipm-fetch:3` — `require_relative '../lib/bipm-data-importer'`
12
+ - `spec/spec_helper.rb:3-4` — `require_relative "../lib/bipm/data/importer"`, `outcomes`
13
+ - `spec/bipm/data/importer/cgpm_spec.rb:1` — `require_relative "../../../spec_helper"`
14
+ - `spec/bipm/data/importer/unique_idents_spec.rb:1` — same
15
+
16
+ Also note: `lib/bipm/data/outcomes.rb:34-40` already declares `autoload :Body, ...` etc. — but these are **dead** because lines 1-3 `require_relative` the files eagerly.
17
+
18
+ ## Fix
19
+ - `lib/bipm-data-importer.rb` → `require "bipm/data/importer"` (top-level entry; this is the only `require` allowed because it's the gem entry-point load).
20
+ - `lib/bipm/data/importer.rb` → drop all `require_relative`; declare `autoload :CGPM, "bipm/data/importer/cgpm"`, etc.
21
+ - `lib/bipm/data/importer/common.rb` → drop `require_relative "asciimath"`; the parent already autoloads it.
22
+ - `lib/bipm/data/outcomes.rb` → drop the three `require_relative`; keep the existing autoloads.
23
+ - `exe/bipm-fetch` → `require "bipm-data-importer"` (depends on bundler to add `lib/` to `$LOAD_PATH`; gemspec installs it via `require_paths = ["lib"]`).
24
+ - `spec/spec_helper.rb` → `require "bipm/data/importer"` + `require "bipm/data/outcomes"` (via `$LOAD_PATH`).
25
+ - `spec/bipm/data/importer/*_spec.rb` → `require "spec_helper"` (RSpec adds spec dir to LOAD_PATH via `--require`).
26
+
27
+ ## Acceptance
28
+ - `grep -rn "require_relative" lib exe spec` → empty.
29
+ - `grep -rn "^require ['\"]bipm" lib exe` → only the gem entry-point shim.
30
+ - `bundle exec rspec` still passes (modulo the pre-existing 9 failures).
31
+ - `bundle exec bipm-fetch --body=cgpm` still runs.
32
+
33
+ ## Status
34
+ Complete.
@@ -0,0 +1,22 @@
1
+ # 05 — Fix outcomes bugs
2
+
3
+ ## Problem
4
+ - `lib/bipm/data/outcomes/meeting.rb:38-40` — `def url; document["metadata"]["source"]; end` returns the **source** field, not the URL. Copy/paste bug. The actual URL is at `document["metadata"]["url"]` and is currently unreachable.
5
+ - `meeting.rb:21-23` — `document` re-parses the YAML file from disk on every call.
6
+ - `resolution.rb:14-16` — `@meeting.document["resolutions"][index]` re-parses the parent meeting's YAML on every method call. Cascades: every `resolution.dates` triggers a `File.open` + `YAML.load`.
7
+ - `body.rb:5` — `Body#initialize(body, locale = nil)` accepts a `locale` argument that is silently discarded. Dead parameter.
8
+ - `localized_body.rb:4-7` — `LocalizedBody < Body` calls `super` (which sets `@body`, ignores `locale`) then sets `@locale` separately. The inheritance is being used as composition; Liskov is technically preserved but the constructor signature lies.
9
+
10
+ ## Fix
11
+ - `Meeting#url` → `document["metadata"]["url"]`.
12
+ - Memoize `document` in `Meeting` and `Resolution` (`@document ||= ...` is fine — it's our own internal state, not another object's ivar, so the rule against `instance_variable_set/get` doesn't apply to ordinary `@ivar ||= ...`).
13
+ - Drop `locale` parameter from `Body#initialize`.
14
+ - Keep `LocalizedBody < Body` for now (it really is-a Body with an extra dimension); the dead param removal fixes the signature lie.
15
+
16
+ ## Acceptance
17
+ - `Bipm::Data::Outcomes[:cipm][:en].meetings.first.url` returns the URL, not the source.
18
+ - Reading 100 resolutions does not open the YAML file 100 times.
19
+ - `Body.new(:cipm)` works without passing a locale.
20
+
21
+ ## Status
22
+ Complete.
@@ -0,0 +1,31 @@
1
+ # 06 — Unify clause taxonomy
2
+
3
+ ## Problem
4
+ Three parallel taxonomies of the same domain concept (a natural-language verb → an outcome type):
5
+
6
+ 1. `lib/bipm/data/importer/common.rb:25-44` — `CONSIDERATIONS` (regex → type)
7
+ 2. `lib/bipm/data/importer/common.rb:46-87` — `ACTIONS` (regex → type)
8
+ 3. `lib/bipm/data/importer/cgpm.rb:28-38` — `CONSIDERATION_VERBS`, `ACTION_VERBS` (verb arrays)
9
+ 4. `lib/bipm/data/importer/cgpm.rb:40-87` — `FR_VERB_TO_EN` (verb → English verb)
10
+ 5. `lib/bipm/data/importer/cgpm.rb:89-135` — `VERB_TO_TYPE` (English verb → type)
11
+
12
+ Each has different French coverage and different fallback semantics. They drift silently. Adding a new verb requires touching 5 places.
13
+
14
+ ## Fix
15
+ Introduce a single `Bipm::Data::Importer::Clauses` module that owns:
16
+
17
+ - `TYPES` — frozen set of canonical types (`considering`, `noting`, `decides`, etc.).
18
+ - `VERBS` — table of `{verb => {type:, regex:}}` covering EN + FR.
19
+ - `match(text)` — returns `{type:, verb:}` or nil.
20
+
21
+ `Common.parse_resolution` and `CGPM.flush_clause` both delegate to `Clauses.match`.
22
+
23
+ Open/closed: adding a verb = adding a row, not editing a switch.
24
+
25
+ ## Acceptance
26
+ - One source of truth for "what verbs map to what types."
27
+ - Adding a new FR verb only touches `Clauses`.
28
+ - Existing parse output unchanged (verified by spec snapshots).
29
+
30
+ ## Status
31
+ Complete.
@@ -0,0 +1,21 @@
1
+ # 07 — Extract French parsing
2
+
3
+ ## Problem
4
+ - `lib/bipm/data/importer/common.rb:410-431` — `extract_date` does French month translation + date parsing, with `binding.pry` in the rescue path.
5
+ - `lib/bipm/data/importer/cgpm.rb:145-164` — `parse_date_text` does the same translation (different month list, slightly different regex), no pry.
6
+ - `cgpm.rb:139-143` — `normalize_verb` strips French/English leading conjunctions.
7
+
8
+ Two implementations of "BIPM French date string → ISO8601." They drift (cgpm has more months; common has `octobre` missing).
9
+
10
+ ## Fix
11
+ - New `lib/bipm/data/importer/text/french.rb` module: `MONTHS` (fr → en), `parse_date(str)`, `translate_months(str)`.
12
+ - `Common.extract_date` and `CGPM.parse_date_text` both delegate.
13
+ - Drop the `binding.pry` paths (covered by TODO 01).
14
+
15
+ ## Acceptance
16
+ - One French month table.
17
+ - `Common.extract_date('15 février 2018')` == `CGPM.parse_date_text('15 février 2018')` == `Date.iso8601('2018-02-15')`.
18
+ - All existing month translations preserved.
19
+
20
+ ## Status
21
+ Complete.
@@ -0,0 +1,25 @@
1
+ # 08 — Extract DataQuirks registry
2
+
3
+ ## Problem
4
+ Inline patches for upstream data mistakes, scattered through `exe/bipm-fetch` and `common.rb`:
5
+
6
+ - `exe/bipm-fetch:137` — `yr = nil if [bodyid, meeting_id] == [:CIPM, '104-2']`
7
+ - `exe/bipm-fetch:167` — `href = href.gsub('/106-2017/', '/104-_1-2015/') if href =~ %r"/ci/cipm/106-2017/resolution-[12]\z"`
8
+ - `exe/bipm-fetch:176-180` — CIPM/fr/94 duplicate-resolution rewrite
9
+ - `exe/bipm-fetch:189` — `href = href.gsub('/104-2015/', '/104-_1-2015/')`
10
+ - `exe/bipm-fetch:344-356` — JCRB/fr/43 numbering fix
11
+ - `lib/bipm/data/importer/common.rb:113-114` — `/jen/` → `/en/`, `/en/CGPM/jsp/` → `/en/CGPM/db/`
12
+
13
+ These are real, load-bearing patches. But they're open/closed violations: each new quirk requires editing the main scrape loop.
14
+
15
+ ## Fix
16
+ - `lib/bipm/data/importer/quirks.rb` — registry of lambdas keyed by `(body, context)`.
17
+ - Each quirk is a small named function: `Quirks.for(:cipm, :fr, :meeting_94_duplicates)`.
18
+ - Main loop calls `Quirks.apply(context, value)` at well-defined hook points.
19
+
20
+ ## Acceptance
21
+ - `grep -E "104-2|104-_1|106-2017|94-2005|JCRB.*43" exe/bipm-fetch lib/bipm/data/importer/strategies` → no inline conditionals, only registry lookups.
22
+ - Existing quirks still applied (verified by running CIPM/JCRB and diffing output).
23
+
24
+ ## Status
25
+ Complete.
@@ -0,0 +1,23 @@
1
+ # 09 — Introduce Body registry
2
+
3
+ ## Problem
4
+ - `exe/bipm-fetch:5-20` — bodies are an inline hash literal: `"JCRB" => 'https://...'`, 14 entries.
5
+ - `lib/bipm/data/importer/cgpm.rb:23-26` — `INDEX_URL` is a *separate* declaration of CGPM's URLs.
6
+ - Adding a 15th body requires editing the CLI script.
7
+ - The body id (`:CGPM` symbol vs `"cgpm"` string vs body name) is normalized ad-hoc: `bodyid.to_s.downcase.gsub(" ", "-").to_sym`.
8
+
9
+ ## Fix
10
+ - `lib/bipm/data/importer/body.rb` — value object: `id`, `display_name`, `urls(locale)`, `strategy`.
11
+ - `lib/bipm/data/importer/bodies.rb` — frozen registry of all 14 bodies as `Body` instances.
12
+ - `Body.find(id)`, `Body.all`.
13
+ - `exe/bipm-fetch` and `CGPM` both source from `Bodies`.
14
+
15
+ Open/closed: adding a body = adding a `Body.new(...)` line to the registry, not editing the CLI.
16
+
17
+ ## Acceptance
18
+ - One source of truth for "what bodies exist and where their pages are."
19
+ - `Bipm::Data::Importer::Bodies[:cgpm].strategy` → `:static_index`.
20
+ - `Bipm::Data::Importer::Bodies[:cipm].strategy` → `:spa_meetings`.
21
+
22
+ ## Status
23
+ Complete.
@@ -0,0 +1,34 @@
1
+ # 10 — Introduce Strategy pattern
2
+
3
+ ## Problem
4
+ `exe/bipm-fetch:32-53` dispatches CGPM via string comparison and a global VCR/WebMock shutdown:
5
+
6
+ ```ruby
7
+ if bodyid == :CGPM
8
+ VCR.eject_cassette
9
+ VCR.turn_off!(ignore_cassettes: true)
10
+ WebMock.disable!
11
+ Bipm::Data::Importer::CGPM.run(a, BASE_DIR)
12
+ next
13
+ end
14
+ # ... otherwise, 350 lines of inline SPA scraping ...
15
+ ```
16
+
17
+ Adding a new "special" body means another `if bodyid == :XYZ` branch. The VCR/WebMock side effects are global and triggered by string match.
18
+
19
+ ## Fix
20
+ - `lib/bipm/data/importer/strategies/base.rb` — abstract `call(agent, body, base_dir, languages:)`.
21
+ - `lib/bipm/data/importer/strategies/static_index.rb` — what CGPM does (extracted from cgpm.rb).
22
+ - `lib/bipm/data/importer/strategies/spa_meetings.rb` — what the other 13 bodies do (extracted from `exe/bipm-fetch:55-459`).
23
+ - Each strategy declares its own `recording_mode` (cassette name vs `:live`). The Fetcher queries the strategy before each request.
24
+ - `Bipm::Data::Importer.fetch(body_id)` returns a `Fetcher` that picks the strategy from `Bodies[body_id].strategy`.
25
+
26
+ Open/closed: new strategy = new class + register in `Bodies`.
27
+
28
+ ## Acceptance
29
+ - `exe/bipm-fetch` has no `if bodyid == :CGPM`.
30
+ - `exe/bipm-fetch` has no references to `VCR` or `WebMock`.
31
+ - Both strategies are reachable via `Bipm::Data::Importer.fetch(:cgpm)` and `Bipm::Data::Importer.fetch(:cipm)`.
32
+
33
+ ## Status
34
+ Complete.
@@ -0,0 +1,20 @@
1
+ # 11 — Extract CLI class
2
+
3
+ ## Problem
4
+ `exe/bipm-fetch` is 466 lines of: argv parsing (`--fork`, `--body=`), body iteration, language iteration, scraping dispatch, fork management. Multiple responsibilities.
5
+
6
+ After TODO 10 (strategies), most of the scraping moves to library classes. What remains in `exe/` should be a thin shim that delegates to `Bipm::Data::Importer::CLI`.
7
+
8
+ ## Fix
9
+ - `lib/bipm/data/importer/cli.rb` — class with `parse(argv)` → options struct, `run`.
10
+ - Uses `OptionParser` instead of string matching on `ARGV[0]`.
11
+ - `exe/bipm-fetch` becomes ~5 lines: `require "bipm-data-importer"; exit Bipm::Data::Importer::CLI.new(ARGV).run`.
12
+
13
+ ## Acceptance
14
+ - `exe/bipm-fetch` < 10 lines.
15
+ - CLI flags: `--body=ID`, `--language=LANG`, `--fork`, `--help`.
16
+ - `--help` prints usage.
17
+ - Unknown flag → error + usage.
18
+
19
+ ## Status
20
+ Complete.
@@ -0,0 +1,23 @@
1
+ # 12 — Specs for Common
2
+
3
+ ## Problem
4
+ `lib/bipm/data/importer/common.rb` has zero specs. `Common.parse_resolution` is ~200 lines and contains the bulk of the non-CGPM parsing logic. No regression protection.
5
+
6
+ ## Fix
7
+ Add `spec/bipm/data/importer/common_spec.rb` covering:
8
+ - `extract_date` — English, French, range, malformed.
9
+ - `format_message` — basic HTML→AsciiDoc.
10
+ - `replace_links` — CGPM/CIPM db URLs, relative hrefs, legacy `/jen/` typo.
11
+ - `replace_centers` — single center, group of centers.
12
+ - `ng_to_string` — encoding, nobr removal.
13
+ - `parse_resolution` — using a VCR cassette (e.g. `cassettes/cipm/cipm-recommendation-2018-1.yml`) for a recommendation, asserting structure: metadata, dates, subject, type, identifier, considerations, actions.
14
+
15
+ No `double()`. Real `Mechanize::Page` from cassette, or `Nokogiri::HTML(string)` for unit-level fixtures.
16
+
17
+ ## Acceptance
18
+ - New spec file added.
19
+ - `bundle exec rspec spec/bipm/data/importer/common_spec.rb` passes.
20
+ - Pre-existing failures unchanged.
21
+
22
+ ## Status
23
+ Complete.
@@ -0,0 +1,22 @@
1
+ # 13 — Specs for CGPM
2
+
3
+ ## Problem
4
+ `lib/bipm/data/importer/cgpm.rb` has zero behavioral specs. The `cgpm_spec.rb` that exists only checks data-shape invariants on already-written YAML.
5
+
6
+ ## Fix
7
+ Add unit specs for pure functions:
8
+ - `CGPM.normalize_verb("considering.")` → `"considering"`
9
+ - `CGPM.normalize_verb("et noting")` → `"noting"`
10
+ - `CGPM.parse_date_text("15 février 2018")` → `"2018-02-15"`
11
+ - `CGPM.parse_date_text("15 février – 16 février 2018")` → last day of range
12
+ - `CGPM.extract_resolution_urls(html, "en")` — sample listing HTML, assert extracted URLs
13
+ - `CGPM.parse_resolution(page, "en")` — use a VCR cassette (`cassettes/cgpm/cgpm-resolution-1-1.yml`) and assert structure.
14
+ - `CGPM.flush_clause(...)` — input/output assertions on verb/message/lists.
15
+
16
+ No `double()`.
17
+
18
+ ## Acceptance
19
+ - `bundle exec rspec spec/bipm/data/importer/cgpm_parser_spec.rb` passes.
20
+
21
+ ## Status
22
+ Complete.
@@ -0,0 +1,23 @@
1
+ # 14 — Specs for AsciiMath
2
+
3
+ ## Problem
4
+ `lib/bipm/data/importer/asciimath.rb` is 70 lines of regex pipeline with zero specs. Easy to break silently.
5
+
6
+ ## Fix
7
+ Add `spec/bipm/data/importer/asciimath_spec.rb` with cases lifted from the existing YAML data:
8
+ - Greek letters: `"π"` → `"stem:[ pi ]"`, `"α"` → `"stem:[ alpha ]"`.
9
+ - Nucleus symbols: `"^14^C"` → nucleus stem.
10
+ - Units with powers: `"kg/m^2^"` → basic stem.
11
+ - Italic images: `image:: ital/m.gif[]` → `stem:[ m ]`.
12
+ - ± sign.
13
+ - Connectors between stems.
14
+ - ESC sequences round-trip correctly.
15
+
16
+ Pull ~10 representative strings from `data/cgpm/meetings-en/*.yml` as fixtures.
17
+
18
+ ## Acceptance
19
+ - `bundle exec rspec spec/bipm/data/importer/asciimath_spec.rb` passes.
20
+ - Existing data-shape spec (`cgpm_spec.rb` stems checks) still passes.
21
+
22
+ ## Status
23
+ Complete.
@@ -0,0 +1,24 @@
1
+ # 15 — Specs for Outcomes domain model
2
+
3
+ ## Problem
4
+ `spec/bipm/data/outcomes_spec.rb` only checks `to_a.map(&:class)`. The actual API (`Meeting#url`, `Resolution#approvals`, etc.) has zero coverage. The `Meeting#url` bug survived because nothing tested it.
5
+
6
+ ## Fix
7
+ Spec the real API against the existing `data/` directory:
8
+ - `Body.new(:cipm).file_path` ends with `/cipm/`.
9
+ - `Body#locales` returns `{fr:, en:}`.
10
+ - `LocalizedBody#meetings` is non-empty for cipm.
11
+ - `Meeting#title`, `#date`, `#source`, `#url` (this is the regression test for TODO 05), `#pdf`.
12
+ - `Meeting#resolutions` is a hash of `Resolution` instances.
13
+ - `Resolution#dates`, `#subject`, `#type`, `#id`, `#url`, `#reference`, `#reference_name`, `#reference_page`.
14
+ - `Resolution#approvals`, `#considerations`, `#actions` return hashes of typed objects.
15
+ - `Action#type` returns symbol, `Action#date_effective` returns Date.
16
+
17
+ No `double()`. Use `Bipm::Data::Outcomes[:cipm][:en].meetings.first` as the entry point.
18
+
19
+ ## Acceptance
20
+ - `bundle exec rspec spec/bipm/data/outcomes_domain_spec.rb` passes.
21
+ - A future `Meeting#url` regression would be caught.
22
+
23
+ ## Status
24
+ Complete.
@@ -0,0 +1,23 @@
1
+ # 16 — Quarantine failing unique_idents specs
2
+
3
+ ## Problem
4
+ 9 specs in `spec/bipm/data/importer/unique_idents_spec.rb` fail because of upstream data quirks in cassettes (not regressions):
5
+ - `cipm fr 104-1` — duplicate resolution id (CIPM 104-_1-2015)
6
+ - `cipm fr/en 94` — duplicate (CIPM 94-2005)
7
+ - `cipm fr/en doesn't have repeated resolutions`
8
+ - `jcrb fr/en 42`, `jcrb fr 43` — duplicate ids
9
+ - These are *the same class of quirk* already patched at `exe/bipm-fetch:344-356` for JCRB-43.
10
+
11
+ ## Fix
12
+ Two options (pick after running scrape):
13
+ 1. Extend `Quirks` (TODO 08) to cover these specific meeting IDs, then un-quarantine.
14
+ 2. If a fix isn't possible without re-recording cassettes, mark each example `pending "upstream data quirk: <reason>"` with a comment pointing to the cassette.
15
+
16
+ Don't silently delete the specs (user rule: never delete source).
17
+
18
+ ## Acceptance
19
+ - `bundle exec rspec` reports 0 failures (either fixed or `pending`).
20
+ - Each previously-failing example has a clear reason if marked pending.
21
+
22
+ ## Status
23
+ Complete.
@@ -0,0 +1,32 @@
1
+ # 17 — Typed domain models (future)
2
+
3
+ ## Problem
4
+ Both sides of the YAML schema are untyped:
5
+ - **Producer** (`Common.parse_resolution`, `CGPM.parse_resolution`, `exe/bipm-fetch`) returns/accumulates string-keyed hashes.
6
+ - **Consumer** (`Outcomes::Resolution#document`, etc.) indexes into those hashes by string keys.
7
+
8
+ Neither side declares the schema. Renaming `metadata/title` → `metadata/name` requires touching 5+ files. The implicit schema drifts silently.
9
+
10
+ ## Fix
11
+ Introduce `lutaml-model` models:
12
+ - `Bipm::Data::Outcomes::Models::Meeting` (serializes to/from the YAML shape)
13
+ - `Bipm::Data::Outcomes::Models::Resolution`
14
+ - `Bipm::Data::Outcomes::Models::Consideration`, `Action`, `Approval`
15
+
16
+ Per user rule (CLAUDE.md), no hand-rolled `to_h`/`from_h`. Use `lutaml-model` attribute declarations + `mapping do ... end`.
17
+
18
+ Producer side constructs instances; serializes via `lutaml-model`.
19
+ Consumer side deserializes via `Model.from_hash(YAML.load_file(...))`.
20
+
21
+ ## Why deferred
22
+ - Requires adding `lutaml-model` as a runtime dependency.
23
+ - The current hash-based producer is well-tested via the existing data-shape specs; changing the producer is risky.
24
+ - Better as a separate PR after Phase 1–5 land and stabilize.
25
+
26
+ ## Acceptance (when done)
27
+ - `grep -rn "document\\[\\\"" lib/bipm/data/outcomes/` → empty.
28
+ - `grep -rn "def to_h\\|def from_h\\|def to_hash" lib/bipm/data/outcomes/` → empty.
29
+ - YAML output unchanged (verified by diffing `data/` before/after).
30
+
31
+ ## Status
32
+ Deferred. Tracked here so it's not forgotten.
@@ -0,0 +1,62 @@
1
+ # Refactor TODOs — bipm-data-importer 0.3.0 → next
2
+
3
+ All actionable items (01–16) are **complete**. Item 17 (typed domain models
4
+ via lutaml-model) is **deferred** — see its file for reasoning.
5
+
6
+ | # | Subject | Status |
7
+ |---|---------|--------|
8
+ | 01 | [Remove debug code](01-remove-debug-code.md) | Complete |
9
+ | 02 | [Fix gemspec](02-fix-gemspec.md) | Complete |
10
+ | 03 | [Update README](03-update-readme.md) | Complete |
11
+ | 04 | [Replace `require_relative` with autoload](04-replace-require-relative-with-autoload.md) | Complete |
12
+ | 05 | [Fix outcomes bugs](05-fix-outcomes-bugs.md) | Complete |
13
+ | 06 | [Unify clause taxonomy](06-unify-clause-taxonomy.md) | Complete |
14
+ | 07 | [Extract French parsing](07-extract-french-parsing.md) | Complete |
15
+ | 08 | [Extract data quirks registry](08-extract-data-quirks.md) | Complete |
16
+ | 09 | [Introduce Body registry](09-introduce-body-registry.md) | Complete |
17
+ | 10 | [Introduce Strategy pattern](10-introduce-strategy-pattern.md) | Complete |
18
+ | 11 | [Extract CLI class](11-extract-cli-class.md) | Complete |
19
+ | 12 | [Specs for Common](12-specs-common.md) | Complete |
20
+ | 13 | [Specs for CGPM/StaticIndex](13-specs-cgpm.md) | Complete |
21
+ | 14 | [Specs for AsciiMath](14-specs-asciimath.md) | Complete |
22
+ | 15 | [Specs for Outcomes domain](15-specs-outcomes.md) | Complete |
23
+ | 16 | [Quarantine failing unique_idents](16-quarantine-failing-specs.md) | Complete |
24
+ | 17 | [Typed domain models (lutaml-model)](17-typed-domain-models.md) | Deferred |
25
+
26
+ ## Verification
27
+
28
+ - `bundle exec rspec`: 865 examples, **0 failures**, 9 pending (was 764/9/0)
29
+ - Suite runs in ~2s (was 1m 53s — memoization in Outcomes)
30
+ - `grep -rn "binding\.pry\|respond_to?\|instance_variable_set\|instance_variable_get\|\.send(:" lib exe spec` → empty
31
+ - `grep -rn "require_relative" lib exe spec bin` → empty
32
+ - `wc -l exe/bipm-fetch` → 4 (was 466)
33
+
34
+ ## Library shape (final)
35
+
36
+ ```
37
+ lib/bipm-data-importer.rb # gem entry shim, 3 lines
38
+ lib/bipm/data/importer.rb # namespace + autoloads + fetch/fetch_all
39
+ lib/bipm/data/importer/
40
+ version.rb
41
+ language.rb # first-class EN/FR value object
42
+ body.rb # committee value object
43
+ bodies.rb # open/closed registry
44
+ quirks.rb # upstream-data patches
45
+ clauses.rb # unified verb→type taxonomy (FR/EN symmetric)
46
+ cli.rb # OptionParser-based CLI
47
+ fetcher.rb # orchestrator: body → strategy → VCR mode
48
+ cgpm.rb # backward-compat facade → Strategies::StaticIndex
49
+ common.rb # SPA-scraping helpers (uses Clauses, Text::French)
50
+ asciimath.rb # math-notation regex pipeline
51
+ text.rb # parent namespace
52
+ text/french.rb # month + date parsing
53
+ strategies.rb # parent namespace
54
+ strategies/base.rb # abstract strategy + recording_mode
55
+ strategies/static_index.rb # CGPM: static listing + per-resolution pages
56
+ strategies/spa_meetings.rb # everyone else: SPA meeting pages
57
+ lib/bipm/data/outcomes.rb # read API namespace
58
+ lib/bipm/data/outcomes/
59
+ body.rb localized_body.rb meeting.rb resolution.rb
60
+ approval.rb action.rb consideration.rb
61
+ exe/bipm-fetch # 4 lines: require + CLI dispatch
62
+ ```