gov_codes 0.1.1 → 0.1.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +13 -1
  3. data/.tool-versions +1 -1
  4. data/CHANGELOG.md +31 -26
  5. data/README.md +180 -25
  6. data/Rakefile +7 -2
  7. data/checksums/gov_codes-0.1.1.gem.sha512 +1 -0
  8. data/lib/gov_codes/afsc/enlisted.rb +111 -80
  9. data/lib/gov_codes/afsc/officer.rb +121 -65
  10. data/lib/gov_codes/afsc/releases/dafecd/2025-10-31/enlisted.yml +2726 -0
  11. data/lib/gov_codes/afsc/releases/dafecd/2025-10-31/ri.yml +369 -0
  12. data/lib/gov_codes/afsc/releases/dafocd/2025-10-31/officer.yml +2393 -0
  13. data/lib/gov_codes/afsc/releases/dafocd/2025-10-31/ri.yml +193 -0
  14. data/lib/gov_codes/afsc/releases.rb +219 -0
  15. data/lib/gov_codes/afsc/releases.yml +11 -0
  16. data/lib/gov_codes/afsc/ri.rb +161 -126
  17. data/lib/gov_codes/afsc.rb +29 -8
  18. data/lib/gov_codes/dafecd/index_builder.rb +416 -0
  19. data/lib/gov_codes/dafecd/patterns.rb +50 -0
  20. data/lib/gov_codes/dafecd/publication.rb +252 -0
  21. data/lib/gov_codes/dafecd/record_splitter.rb +77 -0
  22. data/lib/gov_codes/dafecd/ri_sdi/change_date.rb +63 -0
  23. data/lib/gov_codes/dafecd/ri_sdi/config.rb +137 -0
  24. data/lib/gov_codes/dafecd/ri_sdi/index_builder.rb +265 -0
  25. data/lib/gov_codes/dafecd/ri_sdi/ri_list_parser.rb +125 -0
  26. data/lib/gov_codes/dafecd/ri_sdi/sdi_card_parser.rb +151 -0
  27. data/lib/gov_codes/dafecd/ri_sdi/sdi_section_splitter.rb +77 -0
  28. data/lib/gov_codes/dafecd/ri_sdi/section_slicer.rb +45 -0
  29. data/lib/gov_codes/dafecd/ri_sdi/title.rb +76 -0
  30. data/lib/gov_codes/dafecd/ri_sdi/title_overrides/dafecd.yml +124 -0
  31. data/lib/gov_codes/dafecd/ri_sdi/title_overrides/dafocd.yml +78 -0
  32. data/lib/gov_codes/dafecd/shredout_acronyms.rb +47 -0
  33. data/lib/gov_codes/dafecd/shredout_degluer.rb +73 -0
  34. data/lib/gov_codes/dafecd/shredout_overrides/dafecd.yml +27 -0
  35. data/lib/gov_codes/dafecd/shredout_overrides/dafocd.yml +27 -0
  36. data/lib/gov_codes/dafecd/shredout_parser.rb +75 -0
  37. data/lib/gov_codes/dafecd/specialty_parser.rb +200 -0
  38. data/lib/gov_codes/dafecd/text.rb +48 -0
  39. data/lib/gov_codes/dafecd/title_degluer.rb +67 -0
  40. data/lib/gov_codes/dafecd/title_overrides/dafocd.yml +141 -0
  41. data/lib/gov_codes/dafecd/title_overrides.yml +142 -0
  42. data/lib/gov_codes/data_loader.rb +19 -0
  43. data/lib/gov_codes/version.rb +1 -1
  44. metadata +34 -6
  45. data/lib/gov_codes/afsc/enlisted.yml +0 -532
  46. data/lib/gov_codes/afsc/officer.yml +0 -1072
  47. data/lib/gov_codes/afsc/ri.yml +0 -237
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "patterns"
4
+ require_relative "publication"
5
+ require_relative "text"
6
+
7
+ module GovCodes
8
+ module Dafecd
9
+ # Parses a single specialty record (as produced by RecordSplitter) into a
10
+ # structured header: X-form specialty, career field, CEM code (enlisted),
11
+ # per-specialty change date, skill/qualification ladder, and the
12
+ # (conservatively normalized) specialty title.
13
+ #
14
+ # Behavior is publication-specific (injected Publication); the default is the
15
+ # enlisted directory.
16
+ #
17
+ # Deliberately does NOT attempt to de-glue titles that pdf-reader ran
18
+ # together (e.g. "FORCEAVIATOR" / "SPECIALWARFARE"). Such titles are
19
+ # title-cased verbatim and flagged via :glued_title so the deferred LLM
20
+ # despacer can address them.
21
+ class SpecialtyParser
22
+ DECORATIVE = Patterns::DECORATIVE
23
+ UNICODE_DASHES = Patterns::UNICODE_DASHES
24
+
25
+ def initialize(record, publication: Publication.dafecd)
26
+ @publication = publication
27
+ @record = Text.split_glued_afsc(record, publication.glued_afsc)
28
+ @lines = @record.lines
29
+ end
30
+
31
+ def parse
32
+ codes = ladder_codes
33
+ {
34
+ specialty: specialty(codes),
35
+ career_field: career_field(codes),
36
+ cem_code: cem_code,
37
+ bare_code: bare_code,
38
+ changed_date: changed_date,
39
+ name: name,
40
+ raw_title: raw_title,
41
+ glued_title: glued_title?
42
+ }.merge(@publication.levels_key => levels)
43
+ end
44
+
45
+ private
46
+
47
+ def ladder
48
+ @publication.ladder
49
+ end
50
+
51
+ # @return [Array<String>] concrete ladder AFSCs in document order
52
+ def ladder_codes
53
+ @lines.filter_map { |line| line[ladder, 1] }
54
+ end
55
+
56
+ # The specialty key: the ladder-family X-form, or (for a bare single-code
57
+ # record with no ladder) the literal code.
58
+ def specialty(codes)
59
+ return @publication.specialty_key(codes) if codes.any?
60
+ bare = bare_code
61
+ bare&.to_sym
62
+ end
63
+
64
+ def career_field(codes)
65
+ return @publication.career_field(codes) if codes.any?
66
+ bare = bare_code
67
+ bare && :"#{bare[0, 2]}"
68
+ end
69
+
70
+ # The bare standalone code of a single-code record (officer), or nil.
71
+ def bare_code
72
+ return @bare_code if defined?(@bare_code)
73
+ pattern = @publication.bare_code
74
+ @bare_code = pattern && @record[pattern, 1]
75
+ end
76
+
77
+ def cem_code
78
+ pattern = @publication.cem
79
+ pattern && @record[pattern, 1]
80
+ end
81
+
82
+ def changed_date
83
+ raw = @record[@publication.change_date, 1]
84
+ return nil unless raw
85
+ Text.parse_date(raw)
86
+ end
87
+
88
+ # @return [Hash{Integer=>Hash}] level digit => {code:, title:}
89
+ def levels
90
+ result = {}
91
+ @lines.each do |line|
92
+ next unless line =~ ladder
93
+ code = $1
94
+ title = normalize_level($2)
95
+ digit = code[3].to_i
96
+ result[digit] = {code: code, title: title}
97
+ end
98
+ result
99
+ end
100
+
101
+ def normalize_level(word)
102
+ collapsed = word.gsub(/\s+/, " ").strip
103
+ (collapsed == "Senior Enlisted") ? "Senior Enlisted Leader" : collapsed
104
+ end
105
+
106
+ def name
107
+ raw = raw_title
108
+ return nil if raw.nil? || raw.empty?
109
+ titleize(raw)
110
+ end
111
+
112
+ def glued_title?
113
+ title = name
114
+ return false if title.nil?
115
+ # Heuristic proxy for pdf-reader glue: an unusually long single token
116
+ # (excluding preserved acronyms). Reported, not relied upon, for the
117
+ # deferred despacer. False positives are possible for genuinely long
118
+ # words; the coverage report lists them for human review.
119
+ title.split(/\s+/).any? do |token|
120
+ bare = token.delete("()/-")
121
+ bare.length >= 12 && bare.match?(/\A[A-Za-z]+\z/)
122
+ end
123
+ end
124
+
125
+ # The title line(s) between the last ladder/bare-code line and the change
126
+ # date / first numbered section, joined and sanitized (pre-titlecase).
127
+ # Exposed verbatim so the de-gluer can diff against the source.
128
+ def raw_title
129
+ return @raw_title if defined?(@raw_title)
130
+ @raw_title = compute_raw_title
131
+ end
132
+
133
+ def compute_raw_title
134
+ last_anchor = last_anchor_index
135
+ return nil if last_anchor.nil?
136
+
137
+ collected = []
138
+ @lines[(last_anchor + 1)..].each do |line|
139
+ stripped = sanitize(line)
140
+ next if stripped.empty?
141
+ next if stripped.match?(/\A\d+\z/) # bare page number
142
+ next if stripped.match?(/\A[A-Z][a-z]+\z/) # wrapped ladder word (e.g. "Leader")
143
+ break if stripped.match?(/\A\(?(?:Changed|Established|Effective)\b/) # change date
144
+ break if stripped.match?(/\A\d+\./) # numbered section (1. / 1.Specialty / 3.4.1)
145
+ break if stripped.match?(/\b(?:Specialty Summary|Special Duty Summary|Duties and Responsibilities)\b/)
146
+
147
+ if title_line?(stripped)
148
+ collected << stripped
149
+ else
150
+ break
151
+ end
152
+ end
153
+
154
+ return nil if collected.empty?
155
+ collected.join(" ")
156
+ end
157
+
158
+ # The last line that anchors a record: a ladder line, or (for a bare
159
+ # single-code officer record) the standalone code line.
160
+ def last_anchor_index
161
+ @lines.each_index.reverse_each.find do |i|
162
+ @lines[i] =~ ladder || (@publication.bare_code && @lines[i] =~ @publication.bare_code)
163
+ end
164
+ end
165
+
166
+ # Remove decorative symbol glyphs, normalize unicode dashes, and collapse
167
+ # runs of whitespace (pdf-reader pads some titles with long space runs) so
168
+ # a title line reduces to its plain-text content.
169
+ def sanitize(line)
170
+ line.gsub(DECORATIVE, "").gsub(UNICODE_DASHES, "-").gsub(/\s+/, " ").strip
171
+ end
172
+
173
+ # A title line is short, starts with a capital / paren / digit, and is not
174
+ # a prose sentence. Accepts ALL-CAPS and Title-Case forms; the boundary
175
+ # breaks in compute_raw_title stop collection before the summary prose.
176
+ def title_line?(stripped)
177
+ return false if stripped.length > 70
178
+ return false unless stripped.match?(/\A[A-Z0-9(]/)
179
+ return false if stripped.split(/\s+/).size > 10
180
+ return false if stripped.match?(/[a-z]\.\s+[A-Z]/) # mid-line sentence break = prose
181
+ stripped.match?(/[A-Za-z]/)
182
+ end
183
+
184
+ def titleize(raw)
185
+ raw.split(/\s+/).map { |word| titleize_word(word) }.join(" ")
186
+ end
187
+
188
+ def titleize_word(word)
189
+ # Preserve parenthesized acronyms, e.g. "(RPA)", "(ISR)", "(C2)".
190
+ return word if word.match?(/\A\([A-Z0-9&\/.-]{2,}\)\z/)
191
+ # Preserve all-caps tokens containing a digit, e.g. "C2", "F16".
192
+ return word if word.match?(/\A[A-Z]*\d[A-Z0-9]*\z/) && word.match?(/[A-Z0-9]/)
193
+
194
+ word.split(/([\/-])/).map { |seg|
195
+ seg.match?(/[A-Za-z]/) ? seg.capitalize : seg
196
+ }.join
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GovCodes
4
+ module Dafecd
5
+ # Shared text utilities for the DAFECD extractor: normalization of
6
+ # pdf-reader glue artifacts and date parsing. Used by RecordSplitter,
7
+ # SpecialtyParser, and the extractor CLI so the behavior stays consistent.
8
+ module Text
9
+ MONTHS = %w[jan feb mar apr may jun jul aug sep oct nov dec]
10
+ .each_with_index.to_h { |m, i| [m, i + 1] }.freeze
11
+
12
+ # Default (enlisted) glue boundary: a letter immediately followed by an
13
+ # "AFSC <5-char enlisted code>".
14
+ DEFAULT_GLUED_AFSC = /(?<=[A-Za-z])(?=AFSC \d[A-Z]\d\d\d)/
15
+
16
+ module_function
17
+
18
+ # pdf-reader frequently glues the word "AFSC" to a preceding word when a
19
+ # ladder line wraps, e.g. "LeaderAFSC 1A178*, Craftsman". Insert a newline
20
+ # before an "AFSC <code>" that is glued to a letter so the ladder line is
21
+ # detectable at line start. This is safe for prose mentions such as
22
+ # "possession ofAFSC 1A132" because ladder detection additionally requires
23
+ # a skill-level word at end of line, which prose lines do not satisfy.
24
+ #
25
+ # The glue boundary is publication-specific (the enlisted and officer AFSC
26
+ # code shapes differ); pass the publication's pattern to override the
27
+ # enlisted default.
28
+ def split_glued_afsc(text, pattern = DEFAULT_GLUED_AFSC)
29
+ text.gsub(pattern, "\n")
30
+ end
31
+
32
+ # Normalize a directory date such as "31 Oct 25", "31 Oct 2024", or the
33
+ # officer directory's glued "30Apr 25" to ISO 8601 ("2025-10-31" /
34
+ # "2024-10-31"). The day/month gap is optional (\s*) to absorb pdf-reader's
35
+ # glued day/month; the enlisted (always-spaced) dates are unaffected.
36
+ # Returns nil when unparseable.
37
+ def parse_date(raw)
38
+ return nil unless raw =~ /(\d{1,2})\s*(\w{3,9})\s+(\d{2,4})/
39
+ day = $1.to_i
40
+ month = MONTHS[$2[0, 3].downcase]
41
+ year = $3.to_i
42
+ year += 2000 if year < 100
43
+ return nil unless month
44
+ format("%04d-%02d-%02d", year, month, day)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module GovCodes
6
+ module Dafecd
7
+ # Applies verified de-glued specialty titles.
8
+ #
9
+ # pdf-reader drops spaces inside DAFECD titles ("MOBILITY FORCEAVIATOR").
10
+ # The deterministic extractor title-cases those verbatim ("Mobility
11
+ # Forceaviator") and cannot safely re-insert the missing spaces. The clean
12
+ # titles are produced out of band and shipped in title_overrides.yml.
13
+ #
14
+ # The de-gluing invariant — enforced at build time by IndexBuilder — is that
15
+ # an override may only change SPACING and CASE relative to the raw source
16
+ # title; no letter, digit, or punctuation may change. Any drift (e.g. a
17
+ # future release renamed the specialty) fails the build loudly rather than
18
+ # silently applying a stale title.
19
+ class TitleDegluer
20
+ DEFAULT_PATH = File.expand_path("title_overrides.yml", __dir__)
21
+
22
+ # @return [TitleDegluer] loaded from the shipped overrides file
23
+ def self.load(path = DEFAULT_PATH)
24
+ overrides = YAML.safe_load_file(path, permitted_classes: [Symbol]) || {}
25
+ new(overrides)
26
+ end
27
+
28
+ # @return [TitleDegluer] loaded from +publication+'s overrides file, or an
29
+ # empty de-gluer when that file does not yet exist (officer titles are
30
+ # supplied out of band in a later phase).
31
+ def self.for(publication)
32
+ path = publication.title_overrides_path
33
+ File.exist?(path) ? load(path) : empty
34
+ end
35
+
36
+ # An empty de-gluer applies no overrides (keeps the auto-titlecased names).
37
+ def self.empty
38
+ new({})
39
+ end
40
+
41
+ # Whitespace-and-case-insensitive normalization used for the invariant.
42
+ def self.norm(str)
43
+ str.to_s.gsub(/\s+/, "").downcase
44
+ end
45
+
46
+ # Does +override+ differ from +raw_title+ only in spacing/case?
47
+ def self.matches_source?(override, raw_title)
48
+ return false if raw_title.nil?
49
+ norm(override) == norm(raw_title)
50
+ end
51
+
52
+ # @param overrides [Hash{Symbol=>String}] specialty => clean title
53
+ def initialize(overrides = {})
54
+ @overrides = overrides
55
+ end
56
+
57
+ # @return [String, nil] the clean title for +specialty+, or nil
58
+ def override_for(specialty)
59
+ @overrides[specialty]
60
+ end
61
+
62
+ def any?
63
+ !@overrides.empty?
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,141 @@
1
+ # De-glued specialty titles for DAFOCD, effective 2025-10-31.
2
+ # Each value is the source PDF title with ONLY spacing/case adjusted --
3
+ # no letters, digits, or punctuation changed. Enforced at build time by
4
+ # the IndexBuilder verification gate (whitespace/case-insensitive
5
+ # equality against the raw source title); drift fails the build.
6
+ :10C0: Operations Commander
7
+ :11BX: Bomber Pilot
8
+ :11EX: Experimental Test Pilot
9
+ :11FX: Fighter Pilot
10
+ :11GX: Generalist Pilot
11
+ :11HX: Rescue Pilot
12
+ :11KX: Trainer Pilot
13
+ :11MX: Mobility Pilot
14
+ :11RX: Reconnaissance/Surveillance/Electronic Warfare Pilot
15
+ :11SX: Special Operations Pilot
16
+ :11TX: Student Pilot
17
+ :11UX: Remotely Piloted Aircraft (RPA) Pilot
18
+ :12BX: Bomber Combat Systems Officer
19
+ :12EX: Experimental Test Combat Systems Officer
20
+ :12FX: Fighter Combat Systems Officer
21
+ :12GX: Generalist Combat Systems Officer
22
+ :12HX: Rescue Combat Systems Officer
23
+ :12KX: Trainer Combat Systems Officer
24
+ :12MX: Mobility Combat Systems Officer
25
+ :12RX: Reconnaissance/Surveillance/Electronic Warfare Combat Systems Officer
26
+ :12SX: Special Operations Combat Systems Officer
27
+ :12UX: Remotely Piloted Aircraft (RPA) Combat Systems Officer
28
+ :13AX: Astronaut
29
+ :13BX: Air Battle Manager
30
+ :13HX: Aerospace Physiologist
31
+ :13MX: Airfield Operations
32
+ :13NX: Nuclear and Missile Operations
33
+ :13OX: Multi-Domain Warfare Officer
34
+ :13SX: Space Operations
35
+ :13ZX: Rated Multi-Domain Warfare Officer
36
+ :14FX: Information Operations
37
+ :14NX: Intelligence
38
+ :15AX: Operations Analysis Officer
39
+ :15WX: Weather and Environmental Sciences
40
+ :16FX: Foreign Area Officer (FAO)
41
+ :16GX: Air Force Operations Staff Officer
42
+ :16KX: Software Development Officer (SDO)
43
+ :16PX: Political-Military Affairs Strategist (PAS)
44
+ :16RX: Planning and Programming
45
+ :16ZX: Rated Foreign Area Officer (FAO)
46
+ :17DX: Warfighter Communications
47
+ :17SX: Cyberspace Effects Operations
48
+ :17WX: Warfighter Communications & IT Systems
49
+ :17YX: Cyber Effects & Warfare Operations
50
+ :18AX: Attack Remotely Piloted Aircraft Pilot
51
+ :18EX: Experimental Test Remotely Piloted Aircraft Pilot
52
+ :18GX: Generalist Remotely Piloted Aircraft Pilot
53
+ :18RX: Reconnaissance Remotely Piloted Aircraft Pilot
54
+ :18SX: Special Operations Remotely Piloted Aircraft Pilot
55
+ :19ZX: Special Warfare
56
+ :20C0: Logistics Commander
57
+ :21AX: Aircraft Maintenance
58
+ :21MX: Munitions and Missile Maintenance
59
+ :21RX: Logistics Readiness
60
+ :30C0: Support Commander
61
+ :31PX: Security Forces
62
+ :32EX: Civil Engineer
63
+ :35BX: Band
64
+ :35PX: Public Affairs
65
+ :38FX: Force Support
66
+ :40C0: Medical Commander
67
+ :41AX: Healthcare Administrator
68
+ :42BX: Physical Therapist
69
+ :42EX: Optometrist
70
+ :42FX: Podiatric Surgeon
71
+ :42GX: Physician Assistant
72
+ :42NX: Audiologist
73
+ :42PX: Clinical Psychologist
74
+ :42SX: Clinical Social Worker
75
+ :42TX: Occupational Therapist
76
+ :43BX: Biomedical Scientist
77
+ :43DX: Dietitian
78
+ :43EX: Bioenvironmental Engineer
79
+ :43HX: Public Health Officer
80
+ :43PX: Pharmacist
81
+ :43TX: Biomedical Laboratory
82
+ :44AX: Chief of Medical Staff
83
+ :44BX: Preventive Medicine Physician
84
+ :44DX: Pathologist
85
+ :44EX: Emergency Medicine Physician
86
+ :44FX: Family Physician
87
+ :44GX: General Practice Physician
88
+ :44HX: Nuclear Medicine Physician
89
+ :44JX: Medical Geneticist and Genomicist
90
+ :44KX: Pediatrician
91
+ :44MX: Internal Medicine Physician
92
+ :44NX: Neurologist
93
+ :44OX: Physician
94
+ :44PX: Psychiatrist
95
+ :44RX: Radiologist
96
+ :44SX: Dermatologist
97
+ :44TX: Radiation Oncologist
98
+ :44UX: Occupational Medicine Physician
99
+ :44YX: Critical Care Medicine Physician
100
+ :44ZX: Allergist and Immunologist
101
+ :45AX: Anesthesiologist
102
+ :45BX: Orthopaedic Surgeon
103
+ :45EX: Ophthalmologist
104
+ :45GX: Gynecologic Surgeon and Obstetrician
105
+ :45NX: Otolaryngologist - Head and Neck Surgeon
106
+ :45PX: Physical Medicine Physician
107
+ :45SX: Surgeon
108
+ :45UX: Urologist
109
+ :46AX: Nursing Administrator
110
+ :46FX: Flight Nurse
111
+ :46NX: Clinical Nurse
112
+ :46PX: Mental Health Nurse
113
+ :46SX: Operating Room Nurse
114
+ :46YX: Advanced Practice Registered Nurse (APRN)
115
+ :47BX: Orthodontist
116
+ :47DX: Oral and Maxillofacial Pathologist
117
+ :47EX: Endodontist
118
+ :47GX: Dentist
119
+ :47HX: Periodontist
120
+ :47KX: Pediatric Dentist
121
+ :47PX: Prosthodontist
122
+ :47SX: Oral and Maxillofacial Surgeon
123
+ :48AX: Aerospace Medicine Physician Specialist
124
+ :48GX: General Medical Officer (GMO), Flight Surgeon
125
+ :48OX: Aeromedical Physician
126
+ :48RX: Residency Trained Flight Surgeon
127
+ :48VX: Pilot-Physician
128
+ :51JX: Judge Advocate
129
+ :52RX: Chaplain
130
+ :60C0: Senior Materiel Leader-Upper Echelon
131
+ :61CX: Chemist/Nuclear Chemist
132
+ :61DX: Physicist/Nuclear Engineer
133
+ :62EX: Developmental Engineer
134
+ :62S0: Materiel Leader
135
+ :63AX: Acquisition Manager
136
+ :63G0: Senior Materiel Leader-Lower Echelon
137
+ :63S0: Materiel Leader
138
+ :64PX: Contracting
139
+ :65FX: Financial Management
140
+ :65WX: Cost Analysis
141
+ :71SX: Special Investigations
@@ -0,0 +1,142 @@
1
+ # De-glued specialty titles for DAFECD, effective 2025-10-31.
2
+ # Each value is the source PDF title with ONLY spacing/case adjusted --
3
+ # no letters, digits, or punctuation changed. Enforced at build time:
4
+ # the IndexBuilder verifies each override against the raw source title
5
+ # (whitespace-and-case-insensitive equality) and fails the build on drift.
6
+ # Produced as the governed de-gluing step for pdf-reader space loss.
7
+ :1A1X2: Mobility Force Aviator
8
+ :1A1X3: Special Mission Aviator
9
+ :1A1X4: Multi-Domain Operations Aviator
10
+ :1A1X8: Executive Mission Aviator
11
+ :1A8X0: SEL Airborne Intelligence, Surveillance and Reconnaissance (ISR)
12
+ :1A8X1: Airborne Cryptologic Language Analyst (ACLA)
13
+ :1A8X2: Airborne Intelligence, Surveillance, and Reconnaissance (ISR) Operator
14
+ :1B4X1: Cyber Warfare Operations
15
+ :1C0X2: Aviation Resource Management
16
+ :1C1X1: Air Traffic Control
17
+ :1C3X1: All-Domain Command and Control Operations
18
+ :1C5X1: Battle Management Operations
19
+ :1C6X1: Space Systems Operations
20
+ :1C7X1: Airfield Management
21
+ :1C8X3: Radar, Airfield & Weather Systems (RAWS)
22
+ :1D7X1: Warfighter Communications
23
+ :1D7X2: Radio Frequency Transmissions and Electromagnetic Activities (EMA)
24
+ :1D7X3: Cable and Antenna
25
+ :1D7X4: Data Engineering
26
+ :1D7X5: Cybersecurity
27
+ :1H0X1: Aerospace Physiology
28
+ :1N0X1: All Source Intelligence Analyst
29
+ :1N0X2: Intelligence Superintendent
30
+ :1N1X1: Geospatial Intelligence (GEOINT)
31
+ :1N2X1: Signals Intelligence
32
+ :1N2X2: Cryptologic Intelligence Superintendent
33
+ :1N3X1: Cryptologic Language Analyst
34
+ :1N4X1: Cyber Intelligence
35
+ :1N4X2: Cryptologic Analyst & Reporter
36
+ :1N7X1: Human Intelligence Specialist
37
+ :1N8X1: Targeting Analyst
38
+ :1P0X1: Aircrew Flight Equipment
39
+ :1S0X1: Safety
40
+ :1T0X1: Survival, Evasion, Resistance, Escape (SERE) Specialist
41
+ :1U1X1: Remotely Piloted Aircraft (RPA) Pilot
42
+ :1W0X1: Weather
43
+ :1Z1X1: Pararescue
44
+ :1Z2X1: Combat Control
45
+ :1Z3X1: Tactical Air Control Party (TACP)
46
+ :1Z4X1: Special Reconnaissance
47
+ :2A0X0: Avionics
48
+ :2A0X1: Avionics Test Station, Components, and Electronic Warfare Systems
49
+ :2A2X1: MHU-139 Electrical, Environmental and Avionics Technician
50
+ :2A3X0: Fighter Aircraft Maintenance
51
+ :2A3X3: Tactical Aircraft Maintenance
52
+ :2A3X4: Fighter Aircraft Integrated Avionics
53
+ :2A3X5: Advanced Fighter Aircraft Integrated Avionics
54
+ :2A3X7: Tactical Aircraft Maintenance (5th Generation)
55
+ :2A5X0: Airlift/Special Mission Aircraft Maintenance Superintendent
56
+ :2A5X1: Airlift/Special Mission Aircraft Maintenance
57
+ :2A5X2: Helicopter/Tiltrotor Aircraft Maintenance
58
+ :2A5X4: Refuel/Bomber Aircraft Maintenance
59
+ :2A6X0: Aircraft Accessories
60
+ :2A6X1: Aerospace Propulsion
61
+ :2A6X2: Aerospace Ground Equipment
62
+ :2A6X3: Aircrew Egress Systems
63
+ :2A6X4: Aircraft Fuel Systems
64
+ :2A6X5: Aircraft Hydraulic Systems
65
+ :2A6X6: Aircraft Electrical and Environmental Systems
66
+ :2A7X0: Aircraft Fabrication
67
+ :2A7X1: Aircraft Metals Technology
68
+ :2A7X2: Nondestructive Inspection
69
+ :2A7X3: Aircraft Structural Maintenance
70
+ :2A9X4: Heavy Aircraft Integrated Avionics
71
+ :2F0X1: Fuels
72
+ :2G0X1: Logistics Plans
73
+ :2M0X0: Missile and Space Systems Maintenance
74
+ :2M0X1: Missile and Space Systems Electronic Maintenance
75
+ :2M0X2: Missile and Space Systems Maintenance
76
+ :2M0X3: Missile and Space Facilities
77
+ :2P0X1: Precision Measurement Equipment Laboratory
78
+ :2R2X1: Maintenance Management
79
+ :2S0X1: Materiel Management
80
+ :2T0X1: Traffic Management Operations
81
+ :2T1X1: Ground Transportation
82
+ :2T2X1: Air Transportation
83
+ :2T3X0: Vehicle Management
84
+ :2T3X1: Mission Generation Vehicular Equipment Maintenance
85
+ :2T3X7: Fleet Management and Analysis
86
+ :2W0X1: Munitions Systems
87
+ :2W1X1: Aircraft Armament Systems
88
+ :2W2X1: Nuclear Weapons
89
+ :3E0X0: Facility Systems
90
+ :3E0X1: Electrical Systems
91
+ :3E0X2: Electrical Power Production
92
+ :3E1X1: Heating, Ventilation, Air Conditioning, and Refrigeration
93
+ :3E2X0: Heavy Repair
94
+ :3E2X1: Pavements and Construction Equipment
95
+ :3E3X1: Structural
96
+ :3E4X0: Infrastructure Systems
97
+ :3E4X1: Water and Fuel Systems Maintenance
98
+ :3E4X3: Pest Management
99
+ :3E5X1: Engineering
100
+ :3E6X1: Operations Management
101
+ :3E7X1: Fire Protection
102
+ :3E8X1: Explosive Ordnance Disposal
103
+ :3E9X1: Emergency Management
104
+ :3F0X1: Human Resources and Administration
105
+ :3F1X1: Services
106
+ :3F2X1: Education and Training
107
+ :3F3X1: Manpower
108
+ :3F4X1: Equal Opportunity
109
+ :3G0X1: Talent Acquisition
110
+ :3H0X1: Historian
111
+ :3N0X0: Public Affairs
112
+ :3N0X6: Public Affairs
113
+ :3N1X1: Regional Band
114
+ :3N2X1: Premier Band - The USAF Band
115
+ :3N3X1: Premier Band - The USAF Academy Band
116
+ :3P0X1: Security Forces
117
+ :4A0X1: Health Services Management
118
+ :4A1X1: Medical Materiel
119
+ :4A2X1: Biomedical Equipment
120
+ :4B0X1: Bioenvironmental Engineering (BE)
121
+ :4C0X1: Mental Health Service
122
+ :4D0X1: Diet Therapy
123
+ :4E0X1: Public Health
124
+ :4H0X1: Respiratory Care Practitioner
125
+ :4J0X2: Physical Medicine
126
+ :4N0X1: Aerospace Medical Service
127
+ :4N1X1: Surgical Technologist
128
+ :4P0X1: Pharmacy
129
+ :4R0X1: Diagnostic Imaging
130
+ :4T0X0: Medical Laboratory
131
+ :4T0X1: Medical Laboratory
132
+ :4T0X2: Histopathology
133
+ :4V0X1: Ophthalmic
134
+ :4Y0X0: Dental
135
+ :4Y0X1: Dental Assistant
136
+ :4Y0X2: Dental Laboratory
137
+ :5J0X1: Paralegal
138
+ :5R0X1: Religious Affairs
139
+ :6C0X1: Contracting
140
+ :6F0X1: Financial Management and Comptroller
141
+ :7S0X1: Special Investigations
142
+ :8G0X1: Premier Honor Guard
@@ -45,6 +45,25 @@ module GovCodes
45
45
  const_set(:CODES, {})
46
46
  end
47
47
 
48
+ # Load and merge a flat overlay YAML map (e.g. a consumer acronyms overlay)
49
+ # named +basename+ under gov_codes/afsc across the load path. Files loaded
50
+ # later win, so a consumer file merges over any shipped one. Returns {} when
51
+ # none exist; malformed files are skipped (graceful degradation).
52
+ def flat_overlay(basename, lookup: $LOAD_PATH)
53
+ merged = {}
54
+ gem_lib_dir = File.expand_path("..", __dir__)
55
+ ([gem_lib_dir] + Array(lookup)).uniq.each do |dir|
56
+ path = File.join(dir, "gov_codes", "afsc", basename)
57
+ next unless File.exist?(path)
58
+
59
+ overlay = YAML.load_file(path, symbolize_names: true)
60
+ merged.merge!(overlay) if overlay.is_a?(Hash)
61
+ rescue Psych::SyntaxError, TypeError
62
+ next
63
+ end
64
+ merged
65
+ end
66
+
48
67
  def find_name_recursive(result)
49
68
  base_code = result[:career_field].to_sym
50
69
  base_data = self::DATA[base_code]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GovCodes
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end