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.
- checksums.yaml +4 -4
- data/.simplecov +13 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +31 -26
- data/README.md +180 -25
- data/Rakefile +7 -2
- data/checksums/gov_codes-0.1.1.gem.sha512 +1 -0
- data/lib/gov_codes/afsc/enlisted.rb +111 -80
- data/lib/gov_codes/afsc/officer.rb +121 -65
- data/lib/gov_codes/afsc/releases/dafecd/2025-10-31/enlisted.yml +2726 -0
- data/lib/gov_codes/afsc/releases/dafecd/2025-10-31/ri.yml +369 -0
- data/lib/gov_codes/afsc/releases/dafocd/2025-10-31/officer.yml +2393 -0
- data/lib/gov_codes/afsc/releases/dafocd/2025-10-31/ri.yml +193 -0
- data/lib/gov_codes/afsc/releases.rb +219 -0
- data/lib/gov_codes/afsc/releases.yml +11 -0
- data/lib/gov_codes/afsc/ri.rb +161 -126
- data/lib/gov_codes/afsc.rb +29 -8
- data/lib/gov_codes/dafecd/index_builder.rb +416 -0
- data/lib/gov_codes/dafecd/patterns.rb +50 -0
- data/lib/gov_codes/dafecd/publication.rb +252 -0
- data/lib/gov_codes/dafecd/record_splitter.rb +77 -0
- data/lib/gov_codes/dafecd/ri_sdi/change_date.rb +63 -0
- data/lib/gov_codes/dafecd/ri_sdi/config.rb +137 -0
- data/lib/gov_codes/dafecd/ri_sdi/index_builder.rb +265 -0
- data/lib/gov_codes/dafecd/ri_sdi/ri_list_parser.rb +125 -0
- data/lib/gov_codes/dafecd/ri_sdi/sdi_card_parser.rb +151 -0
- data/lib/gov_codes/dafecd/ri_sdi/sdi_section_splitter.rb +77 -0
- data/lib/gov_codes/dafecd/ri_sdi/section_slicer.rb +45 -0
- data/lib/gov_codes/dafecd/ri_sdi/title.rb +76 -0
- data/lib/gov_codes/dafecd/ri_sdi/title_overrides/dafecd.yml +124 -0
- data/lib/gov_codes/dafecd/ri_sdi/title_overrides/dafocd.yml +78 -0
- data/lib/gov_codes/dafecd/shredout_acronyms.rb +47 -0
- data/lib/gov_codes/dafecd/shredout_degluer.rb +73 -0
- data/lib/gov_codes/dafecd/shredout_overrides/dafecd.yml +27 -0
- data/lib/gov_codes/dafecd/shredout_overrides/dafocd.yml +27 -0
- data/lib/gov_codes/dafecd/shredout_parser.rb +75 -0
- data/lib/gov_codes/dafecd/specialty_parser.rb +200 -0
- data/lib/gov_codes/dafecd/text.rb +48 -0
- data/lib/gov_codes/dafecd/title_degluer.rb +67 -0
- data/lib/gov_codes/dafecd/title_overrides/dafocd.yml +141 -0
- data/lib/gov_codes/dafecd/title_overrides.yml +142 -0
- data/lib/gov_codes/data_loader.rb +19 -0
- data/lib/gov_codes/version.rb +1 -1
- metadata +34 -6
- data/lib/gov_codes/afsc/enlisted.yml +0 -532
- data/lib/gov_codes/afsc/officer.yml +0 -1072
- data/lib/gov_codes/afsc/ri.yml +0 -237
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# DAFOCD Reporting Identifiers (RI) + Special Duty Identifiers (SDI)
|
|
2
|
+
# Source: DAFOCD 31 Oct 25 v3.pdf
|
|
3
|
+
# Directory: Department of the Air Force Officer Classification Directory
|
|
4
|
+
# Effective date: 2025-10-31 (v3)
|
|
5
|
+
# Generated deterministically by bin/extract_afsc_from_pdf.rb.
|
|
6
|
+
# Do not edit by hand; re-run the extractor against the source PDF.
|
|
7
|
+
# Titles are de-glued via human-verified overrides (spacing/case only, gated
|
|
8
|
+
# against the verbatim source); a title without an override is kept verbatim.
|
|
9
|
+
---
|
|
10
|
+
:80C0:
|
|
11
|
+
:name: Commander, Cadet Squadron, USAF Academy
|
|
12
|
+
:changed_date: '2022-10-31'
|
|
13
|
+
:81C0:
|
|
14
|
+
:name: Instructor, Officer Training School
|
|
15
|
+
:changed_date: '2019-04-30'
|
|
16
|
+
:81D0:
|
|
17
|
+
:name: Air Force Reserve Officer Training Corps Detachment Commander and Professor
|
|
18
|
+
of Aerospace Studies
|
|
19
|
+
:changed_date: '2020-04-30'
|
|
20
|
+
:81L0:
|
|
21
|
+
:name: Education and Training Leader
|
|
22
|
+
:changed_date: '2020-04-30'
|
|
23
|
+
:81T0:
|
|
24
|
+
:name: Instructor
|
|
25
|
+
:82A0:
|
|
26
|
+
:name: Academic Program Manager
|
|
27
|
+
:83R0:
|
|
28
|
+
:name: Recruiting Service
|
|
29
|
+
:84H0:
|
|
30
|
+
:name: Historian
|
|
31
|
+
:changed_date: '2024-10-31'
|
|
32
|
+
:85G0:
|
|
33
|
+
:name: Honor Guard
|
|
34
|
+
:changed_date: '2025-10-31'
|
|
35
|
+
:86M0:
|
|
36
|
+
:name: Operations Management
|
|
37
|
+
:86P0:
|
|
38
|
+
:name: Command and Control
|
|
39
|
+
:87G0:
|
|
40
|
+
:name: Wing Inspector General
|
|
41
|
+
:changed_date: '2018-04-30'
|
|
42
|
+
:87I0:
|
|
43
|
+
:name: Director, Wing Inspections
|
|
44
|
+
:changed_date: '2012-10-31'
|
|
45
|
+
:87Q0:
|
|
46
|
+
:name: Director, Complaints Resolution
|
|
47
|
+
:changed_date: '2012-10-31'
|
|
48
|
+
:88A0:
|
|
49
|
+
:name: Aide-de-camp
|
|
50
|
+
:88B0:
|
|
51
|
+
:name: Protocol Officer
|
|
52
|
+
:changed_date: '2019-10-31'
|
|
53
|
+
:88C0:
|
|
54
|
+
:name: Sexual Assault Response Coordinator (SARC)
|
|
55
|
+
:acronym: SARC
|
|
56
|
+
:changed_date: '2020-04-30'
|
|
57
|
+
:88I0:
|
|
58
|
+
:name: Innovation Officer
|
|
59
|
+
:changed_date: '2022-10-31'
|
|
60
|
+
:88M0:
|
|
61
|
+
:name: United States Air Force Medical Pinnacle Position
|
|
62
|
+
:changed_date: '2024-04-30'
|
|
63
|
+
:88X0:
|
|
64
|
+
:name: Operational STEM Officer
|
|
65
|
+
:changed_date: '2024-10-31'
|
|
66
|
+
:88Y0:
|
|
67
|
+
:name: Pathfinder Officer
|
|
68
|
+
:changed_date: '2023-10-31'
|
|
69
|
+
:89A0:
|
|
70
|
+
:name: Air Advisor (Basic)
|
|
71
|
+
:changed_date: '2022-10-31'
|
|
72
|
+
:89B0:
|
|
73
|
+
:name: Air Advisor (Basic) Team Leader
|
|
74
|
+
:changed_date: '2022-10-31'
|
|
75
|
+
:89C0:
|
|
76
|
+
:name: Air Advisor (Basic) Mission Commander
|
|
77
|
+
:changed_date: '2022-10-31'
|
|
78
|
+
:89D0:
|
|
79
|
+
:name: Air Advisor (Advanced)
|
|
80
|
+
:changed_date: '2022-10-31'
|
|
81
|
+
:89E0:
|
|
82
|
+
:name: Air Advisor (Advanced) Team Leader
|
|
83
|
+
:changed_date: '2022-10-31'
|
|
84
|
+
:89F0:
|
|
85
|
+
:name: Air Advisor (Advanced) Mission Commander
|
|
86
|
+
:changed_date: '2022-10-31'
|
|
87
|
+
:89G0:
|
|
88
|
+
:name: Combat Aviation Advisor
|
|
89
|
+
:changed_date: '2025-04-30'
|
|
90
|
+
:89H0:
|
|
91
|
+
:name: Combat Aviation Advisor Team Leader
|
|
92
|
+
:changed_date: '2025-04-30'
|
|
93
|
+
:89I0:
|
|
94
|
+
:name: Combat Aviation Advisor Mission Commander
|
|
95
|
+
:changed_date: '2025-04-30'
|
|
96
|
+
:89W0:
|
|
97
|
+
:name: Weapons of Mass Destruction Civil Support Team (WMD-CST)
|
|
98
|
+
:changed_date: '2020-10-31'
|
|
99
|
+
:90G0:
|
|
100
|
+
:name: General Officer
|
|
101
|
+
:91C0:
|
|
102
|
+
:name: Commander
|
|
103
|
+
:91E0:
|
|
104
|
+
:name: Wing Commander Equivalent
|
|
105
|
+
:91W0:
|
|
106
|
+
:name: Wing Commander
|
|
107
|
+
:92F0:
|
|
108
|
+
:name: Foreign Area Officer (FAO) Trainee
|
|
109
|
+
:92J0:
|
|
110
|
+
:name: Non-designated Lawyer
|
|
111
|
+
:92J1:
|
|
112
|
+
:name: AFROTC Educational Delay Law Student
|
|
113
|
+
:92J2:
|
|
114
|
+
:name: Funded Legal Education Program Law Student
|
|
115
|
+
:92J3:
|
|
116
|
+
:name: Excess Leave Law Student
|
|
117
|
+
:92M0:
|
|
118
|
+
:name: Health Professions Scholarship Program (HPSP) Medical Student
|
|
119
|
+
:92M1:
|
|
120
|
+
:name: Uniformed Services University of Health Sciences (USUHS) Student
|
|
121
|
+
:92M2:
|
|
122
|
+
:name: HPSP Biomedical Science Student
|
|
123
|
+
:92P0:
|
|
124
|
+
:name: Physician Assistant Student
|
|
125
|
+
:changed_date: '2017-10-31'
|
|
126
|
+
:92R0:
|
|
127
|
+
:name: Chaplain Candidate
|
|
128
|
+
:92S0:
|
|
129
|
+
:name: Student Officer Authorization
|
|
130
|
+
:92T0:
|
|
131
|
+
:name: Pilot Trainee
|
|
132
|
+
:92T1:
|
|
133
|
+
:name: Combat Systems Officer Trainee
|
|
134
|
+
:92T2:
|
|
135
|
+
:name: Air Battle Manager Trainee
|
|
136
|
+
:92T3:
|
|
137
|
+
:name: Remotely Piloted Aircraft Pilot Trainee
|
|
138
|
+
:92W0:
|
|
139
|
+
:name: Combat Wounded Warrior
|
|
140
|
+
:changed_date: '2015-05-11'
|
|
141
|
+
:92W1:
|
|
142
|
+
:name: Reserved for Future Use
|
|
143
|
+
:changed_date: '2015-05-11'
|
|
144
|
+
:92W2:
|
|
145
|
+
:name: Combat Wounded Warrior with Exemptions
|
|
146
|
+
:changed_date: '2015-05-11'
|
|
147
|
+
:92W3:
|
|
148
|
+
:name: Non-Combat Wounded Warrior
|
|
149
|
+
:changed_date: '2015-05-11'
|
|
150
|
+
:92W4:
|
|
151
|
+
:name: Wounded Warrior - Limited Assignment Status (LAS)
|
|
152
|
+
:acronym: LAS
|
|
153
|
+
:changed_date: '2015-05-11'
|
|
154
|
+
:92W5:
|
|
155
|
+
:name: Wounded Warrior - Retired/Discharged
|
|
156
|
+
:changed_date: '2015-05-11'
|
|
157
|
+
:92W6:
|
|
158
|
+
:name: Reserved for Future Use
|
|
159
|
+
:changed_date: '2015-05-11'
|
|
160
|
+
:92W7:
|
|
161
|
+
:name: Reserved for Future Use
|
|
162
|
+
:changed_date: '2015-05-11'
|
|
163
|
+
:92W8:
|
|
164
|
+
:name: Wounded Warrior - Ambassador
|
|
165
|
+
:changed_date: '2019-10-31'
|
|
166
|
+
:92W9:
|
|
167
|
+
:name: Wounded Warrior - Project Planner/Officer
|
|
168
|
+
:changed_date: '2019-10-31'
|
|
169
|
+
:93P0:
|
|
170
|
+
:name: Patient
|
|
171
|
+
:94N0:
|
|
172
|
+
:name: Nuclear Weapons Custodian
|
|
173
|
+
:95A0:
|
|
174
|
+
:name: Non-EAD USAFR Academy Liaison Officer (ALO) or Civil Air Patrol Reserve Assistance
|
|
175
|
+
Program (CAPRAP) Officer
|
|
176
|
+
:96A0:
|
|
177
|
+
:name: Disqualified Officer, Reasons Beyond Their Control
|
|
178
|
+
:96B0:
|
|
179
|
+
:name: Disqualified Officer, Reasons Within Their Control
|
|
180
|
+
:96D0:
|
|
181
|
+
:name: Officer Not Available for Use in Awarded AFSC for Cause
|
|
182
|
+
:96U0:
|
|
183
|
+
:name: Unclassified Officer
|
|
184
|
+
:96V0:
|
|
185
|
+
:name: Un-allotted
|
|
186
|
+
:97E0:
|
|
187
|
+
:name: Executive Officer
|
|
188
|
+
:99A0:
|
|
189
|
+
:name: Unspecified AFSC
|
|
190
|
+
:changed_date: '2015-04-20'
|
|
191
|
+
:99G0:
|
|
192
|
+
:name: Gold Bar Recruiter
|
|
193
|
+
:changed_date: '2013-06-05'
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
require "yaml"
|
|
3
|
+
|
|
4
|
+
module GovCodes
|
|
5
|
+
module AFSC
|
|
6
|
+
# Loads the versioned release manifest and per-release specialty index for a
|
|
7
|
+
# publication (DAFECD enlisted, DAFOCD officer), and resolves the release in
|
|
8
|
+
# effect on a given date. Both publications share this resolve/merge/cache
|
|
9
|
+
# machinery and are resolved independently (their release dates may diverge).
|
|
10
|
+
#
|
|
11
|
+
# See docs/plans/2026-07-08-afsc-pdf-representation-design.md
|
|
12
|
+
# "Versioning by document release".
|
|
13
|
+
#
|
|
14
|
+
# Storage layout (per release):
|
|
15
|
+
# lib/gov_codes/afsc/releases.yml # manifest
|
|
16
|
+
# lib/gov_codes/afsc/releases/dafecd/<date>/enlisted.yml # enlisted index
|
|
17
|
+
# lib/gov_codes/afsc/releases/dafocd/<date>/officer.yml # officer index
|
|
18
|
+
#
|
|
19
|
+
# Resolution: releases are sorted ascending by effective_date. A given
|
|
20
|
+
# `as_of` resolves to the release with the greatest effective_date <= as_of;
|
|
21
|
+
# `as_of: nil` resolves it as of today. A release only takes effect on its
|
|
22
|
+
# own effective_date -- a release dated in the future (e.g. one a consumer
|
|
23
|
+
# pre-loads ahead of its official date, see DEC-004 below) is not resolved
|
|
24
|
+
# until that date arrives, even though it is the most recently added one. A
|
|
25
|
+
# date before the earliest shipped release resolves to no release (an empty
|
|
26
|
+
# index / nil date).
|
|
27
|
+
#
|
|
28
|
+
# Extensibility (DEC-004): both tiers merge across the load path (gem lib dir
|
|
29
|
+
# first, then the load path). The manifest's per-publication release list is
|
|
30
|
+
# unioned by effective_date -- a consumer releases.yml ADDS releases without
|
|
31
|
+
# hiding the shipped ones, and a same-date entry from the load path wins. For
|
|
32
|
+
# the resolved release, the release index found at each lookup path is merged,
|
|
33
|
+
# so a consumer may drop gov_codes/afsc/releases/dafecd/<date>/enlisted.yml to
|
|
34
|
+
# extend or override the shipped index.
|
|
35
|
+
module Releases
|
|
36
|
+
# Publication key for the enlisted directory (DAFECD).
|
|
37
|
+
ENLISTED_PUBLICATION = "dafecd"
|
|
38
|
+
# Publication key for the officer directory (DAFOCD).
|
|
39
|
+
OFFICER_PUBLICATION = "dafocd"
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def manifest(lookup: $LOAD_PATH)
|
|
43
|
+
manifest_cache[cache_key(lookup)] ||= load_manifest(lookup)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enlisted_index(as_of: nil, lookup: $LOAD_PATH)
|
|
47
|
+
release_index(ENLISTED_PUBLICATION, "enlisted.yml", as_of: as_of, lookup: lookup)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def officer_index(as_of: nil, lookup: $LOAD_PATH)
|
|
51
|
+
release_index(OFFICER_PUBLICATION, "officer.yml", as_of: as_of, lookup: lookup)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Resolve the RI/SDI index for +publication+ (enlisted DAFECD or officer
|
|
55
|
+
# DAFOCD). RI/SDI codes live in both directories with different code
|
|
56
|
+
# grammars (5-char enlisted, 4-char officer), so this wrapper is
|
|
57
|
+
# parameterized by publication; it otherwise reuses the same resolve/
|
|
58
|
+
# merge/cache and acronym-overlay machinery as enlisted_index/
|
|
59
|
+
# officer_index.
|
|
60
|
+
def ri_index(as_of: nil, lookup: $LOAD_PATH, publication: ENLISTED_PUBLICATION)
|
|
61
|
+
release_index(publication, "ri.yml", as_of: as_of, lookup: lookup)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Resolve the effective date for +publication+ (defaults to enlisted).
|
|
65
|
+
# Publications are resolved independently: an officer release date never
|
|
66
|
+
# moves the enlisted latest date and vice versa.
|
|
67
|
+
def effective_date_for(as_of: nil, lookup: $LOAD_PATH, publication: ENLISTED_PUBLICATION)
|
|
68
|
+
release = resolve_release(publication, as_of: as_of, lookup: lookup)
|
|
69
|
+
release && release[:date]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def reset!
|
|
73
|
+
@manifest_cache = {}
|
|
74
|
+
@index_cache = {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def manifest_cache
|
|
80
|
+
@manifest_cache ||= {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def index_cache
|
|
84
|
+
@index_cache ||= {}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def cache_key(lookup)
|
|
88
|
+
Array(lookup).dup
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_date(as_of)
|
|
92
|
+
return nil if as_of.nil?
|
|
93
|
+
return as_of if as_of.is_a?(Date)
|
|
94
|
+
Date.parse(as_of.to_s)
|
|
95
|
+
rescue Date::Error
|
|
96
|
+
raise ArgumentError,
|
|
97
|
+
"invalid as_of #{as_of.inspect}: expected a Date or a parseable date string (e.g. \"2025-10-31\")"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Ordered list of directories to search: the gem lib dir first, then the
|
|
101
|
+
# provided lookup path. Deduplicated to avoid loading the same file twice.
|
|
102
|
+
def lookup_paths(lookup)
|
|
103
|
+
gem_lib_dir = File.expand_path("../..", __dir__)
|
|
104
|
+
([gem_lib_dir] + Array(lookup)).uniq
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Resolve, merge, and cache the release index for +publication+. Always
|
|
108
|
+
# resolves the release first (cheap: the manifest itself is cached) and
|
|
109
|
+
# keys the expensive part -- loading and merging the index file -- on
|
|
110
|
+
# the RESOLVED release's date, not the raw +as_of+. With `as_of: nil`
|
|
111
|
+
# meaning "today," keying on the raw input would freeze the cache to
|
|
112
|
+
# whichever release happened to be current at the first call and never
|
|
113
|
+
# notice a later release's effective_date arrive.
|
|
114
|
+
def release_index(publication, index_file, as_of:, lookup:)
|
|
115
|
+
release = resolve_release(publication, as_of: as_of, lookup: lookup)
|
|
116
|
+
return {} unless release
|
|
117
|
+
|
|
118
|
+
key = [publication, index_file, release[:date], cache_key(lookup)]
|
|
119
|
+
index_cache[key] ||= load_release_index(release, publication, index_file, lookup)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resolve_release(publication, as_of:, lookup:)
|
|
123
|
+
releases = release_list(lookup, publication)
|
|
124
|
+
return nil if releases.empty?
|
|
125
|
+
|
|
126
|
+
target = to_date(as_of) || Date.today
|
|
127
|
+
releases.reverse.find { |r| r[:date] <= target }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Releases for +publication+ from the manifest, each annotated with a
|
|
131
|
+
# parsed :date and the :dir name used to locate its index, sorted
|
|
132
|
+
# ascending by date.
|
|
133
|
+
def release_list(lookup, publication)
|
|
134
|
+
entries = manifest(lookup: lookup)[publication.to_sym] || []
|
|
135
|
+
entries.filter_map do |entry|
|
|
136
|
+
raw = entry[:effective_date]
|
|
137
|
+
next unless raw
|
|
138
|
+
|
|
139
|
+
date = raw.is_a?(Date) ? raw : Date.parse(raw.to_s)
|
|
140
|
+
dir = raw.is_a?(Date) ? raw.strftime("%Y-%m-%d") : raw.to_s
|
|
141
|
+
entry.merge(date: date, dir: dir)
|
|
142
|
+
end.sort_by { |r| r[:date] }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def load_manifest(lookup)
|
|
146
|
+
merged = {}
|
|
147
|
+
each_yaml(lookup, ["gov_codes", "afsc", "releases.yml"]) do |data|
|
|
148
|
+
merge_manifest!(merged, data)
|
|
149
|
+
end
|
|
150
|
+
merged
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def load_release_index(release, publication, index_file, lookup)
|
|
154
|
+
merged = {}
|
|
155
|
+
parts = ["gov_codes", "afsc", "releases", publication, release[:dir], index_file]
|
|
156
|
+
each_yaml(lookup, parts) { |data| merged.merge!(data) }
|
|
157
|
+
apply_acronym_overlay(merged, publication, release, lookup)
|
|
158
|
+
merged
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Apply the consumer acronym overlay (Task 3, "dedicated tier"): a flat
|
|
162
|
+
# SPECIALTY => ACRONYM map at releases/<pub>/<dir>/acronyms.yml, resolved
|
|
163
|
+
# for the SAME release as the index and merged across the load path
|
|
164
|
+
# (consumer-loaded-last wins). Applied ONLY to specialties already in the
|
|
165
|
+
# index, so it sets an entry's :acronym without ever replacing the entry
|
|
166
|
+
# (the index merge above is shallow — overlaying whole entries here would
|
|
167
|
+
# drop name/skill_levels/shredouts). The consumer overlay wins over the
|
|
168
|
+
# source-extracted acronym shipped in the index. The gem ships no
|
|
169
|
+
# acronyms.yml; an absent overlay leaves the index unchanged. Resolved per
|
|
170
|
+
# release, so overlays never leak across document dates or publications.
|
|
171
|
+
def apply_acronym_overlay(merged, publication, release, lookup)
|
|
172
|
+
overlay = {}
|
|
173
|
+
parts = ["gov_codes", "afsc", "releases", publication, release[:dir], "acronyms.yml"]
|
|
174
|
+
each_yaml(lookup, parts) { |data| overlay.merge!(data) }
|
|
175
|
+
overlay.each do |specialty, acronym|
|
|
176
|
+
entry = merged[specialty]
|
|
177
|
+
entry[:acronym] = acronym if entry
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Merge a manifest hash into +acc+. Per-publication release lists are
|
|
182
|
+
# unioned by effective_date (a same-date entry from a later-loaded file
|
|
183
|
+
# wins); non-list values fall back to a plain overwrite.
|
|
184
|
+
def merge_manifest!(acc, incoming)
|
|
185
|
+
incoming.each do |publication, releases|
|
|
186
|
+
acc[publication] = if acc[publication].is_a?(Array) && releases.is_a?(Array)
|
|
187
|
+
union_releases(acc[publication], releases)
|
|
188
|
+
else
|
|
189
|
+
releases
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
acc
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def union_releases(base, overrides)
|
|
196
|
+
by_date = {}
|
|
197
|
+
(base + overrides).each do |entry|
|
|
198
|
+
by_date[entry[:effective_date]] = entry if entry.is_a?(Hash)
|
|
199
|
+
end
|
|
200
|
+
by_date.values
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Yield each parsed YAML hash found at +parts+ across the lookup paths,
|
|
204
|
+
# skipping missing files and malformed YAML (graceful degradation).
|
|
205
|
+
def each_yaml(lookup, parts)
|
|
206
|
+
lookup_paths(lookup).each do |dir|
|
|
207
|
+
path = File.join(dir, *parts)
|
|
208
|
+
next unless File.exist?(path)
|
|
209
|
+
|
|
210
|
+
data = YAML.load_file(path, symbolize_names: true)
|
|
211
|
+
yield data if data.is_a?(Hash)
|
|
212
|
+
rescue Psych::SyntaxError, TypeError
|
|
213
|
+
next
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
:dafecd:
|
|
3
|
+
- :effective_date: '2025-10-31'
|
|
4
|
+
:version_label: v3.5
|
|
5
|
+
:source: DAFECD -31 October 25 v3.5 FINAL.pdf
|
|
6
|
+
:name: Department of the Air Force Enlisted Classification Directory
|
|
7
|
+
:dafocd:
|
|
8
|
+
- :effective_date: '2025-10-31'
|
|
9
|
+
:version_label: v3
|
|
10
|
+
:source: DAFOCD 31 Oct 25 v3.pdf
|
|
11
|
+
:name: Department of the Air Force Officer Classification Directory
|