Gov Codes
Understand and process information used by the US government.
Installation
Add this line to your application's Gemfile:
gem 'gov_codes'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install gov_codes
Usage
Basic Usage
require 'gov_codes/afsc'
# Find an enlisted AFSC code
code = GovCodes::AFSC.find("1A1X2")
puts code.name # => "Mobility Force Aviator"
puts code.career_field # => "1A"
puts code.career_field_subdivision # => "1A1"
puts code.skill_level # => "X"
puts code.specific_afsc # => "1A1X2"
puts code.shredout # => nil
puts code.effective_date # => #<Date: 2025-10-31>
# Look up the concrete code an HR system actually stores
# (the "7" is the airman's skill level: Craftsman)
code = GovCodes::AFSC.find("1A172Y")
code.specialty_name # => "Mobility Force Aviator"
code.skill_level_number # => 7
code.skill_level_name # => "Craftsman" (title comes from the directory)
code.specialty # => :"1A1X2"
# shredout_name resolves the shredout's meaning when that shredout
# exists in the data (nil otherwise)
code = GovCodes::AFSC.find("1A172A")
code.shredout_name # => "C-5 Flight Engineer"
# Enlisted lookups are versioned by DAFECD release (published each 30 Apr and
# 31 Oct). `find`/`search` default to today's date; pass `as_of:` (a Date or
# "YYYY-MM-DD" string) to resolve the release in effect on a different date. A
# release only takes effect on its own effective_date, even if it's the most
# recently added one (e.g. one you pre-load ahead of its official date).
code = GovCodes::AFSC.find("1A172Y", as_of: "2025-11-01")
code.effective_date # => #<Date: 2025-10-31>
# A date before the earliest shipped release has no data and returns nil.
GovCodes::AFSC.find("1A172Y", as_of: "2000-01-01") # => nil
# Find an officer AFSC code. Officer lookups are versioned by DAFOCD release
# (like enlisted); `find`/`search` default to today's date and accept the
# same `as_of:`.
code = GovCodes::AFSC.find("11MX")
puts code.name # => "Mobility Pilot"
puts code.career_group # => "11"
puts code.functional_area # => "M"
puts code.qualification_level # => "X"
puts code.shredout # => nil
puts code.effective_date # => #<Date: 2025-10-31>
# A concrete qualification level derives the X-form specialty and its title
code = GovCodes::AFSC.find("11B3")
code.specialty # => :"11BX"
code.specialty_name # => "Bomber Pilot"
code.qualification_level_number # => 3
code.qualification_level_name # => "Aircraft Commander" (title from the directory)
# Some officer specialties are keyed by a literal bare code (a single command/
# materiel-leader position, not a qualification ladder): 10C0, 62S0, 63G0, 63S0.
# These resolve ONLY via their literal code -- there is no X-form for them, so
# find("10CX") / find("62SX") return nil by design.
GovCodes::AFSC.find("10C0").name # => "Operations Commander"
GovCodes::AFSC.find("10CX") # => nil (bare-code specialty; no X-form)
# An officer code carries its acronym; a shredout acronym wins for a shredded code
GovCodes::AFSC.find("16F3").acronym # => "FAO"
GovCodes::AFSC.find("19ZXB").acronym # => "TACPO"
# Find a Reporting Identifier (RI) or Special Duty Identifier (SDI)
code = GovCodes::AFSC.find("8A400")
puts code.name # => "Talent Management Consultant"
puts code.career_field # => "8A"
puts code.identifier # => "400"
puts code.suffix # => nil
# An RI/SDI code carries its source-verified acronym, and reverse lookup is
# case-insensitive and as_of-aware, just like enlisted/officer AFSCs:
GovCodes::AFSC.find("9M200").acronym # => "IHS"
GovCodes::AFSC.find_by_acronym("IHS", as_of: "2025-11-01").name
# => "International Health Specialists (IHS)"
# Find a code with a shredout/suffix
code = GovCodes::AFSC.find("11BXA")
puts code.name # => "B-1"
puts code.specific_afsc # => "11BX"
puts code.shredout # => "A"
Searching for Codes
You can search for all codes matching a prefix:
# Search for all Special Warfare codes
results = GovCodes::AFSC.search("1Z")
results.each do |code|
puts "#{code.specific_afsc}: #{code.name}"
end
# Output:
# 1Z1X1: Pararescue
# 1Z2X1: Combat Control
# 1Z3X1: Tactical Air Control Party (TACP)
# 1Z4X1: Special Reconnaissance
# Search for Bomber Pilot shredouts
results = GovCodes::AFSC.search("11BX")
results.each do |code|
shredout = code.shredout ? code.shredout.to_s : ""
puts "#{code.specific_afsc}#{shredout}: #{code.name}"
end
# Output:
# 11BX: Bomber pilot
# 11BXA: B-1
# 11BXB: B-2
# 11BXC: B-52
# ...
# Search is case-insensitive
GovCodes::AFSC.search("1z1") # Same as search("1Z1")
Extending with Custom AFSC Codes
Enlisted codes are stored as a specialty-keyed index per DAFECD release. You can extend or override a release by dropping an index file for that release's effective date onto your application's load path:
# In your application's lib/gov_codes/afsc/releases/dafecd/2025-10-31/enlisted.yml
:"9Z9X9":
:name: Custom Specialty
:career_field: :"9Z"
:skill_levels:
7:
:code: 9Z979
:title: Craftsman
:shredouts:
:A: Custom Shredout
The gem merges your index over the shipped index for the matching release, adding new specialties and overriding existing ones.
You can also add a whole new release (e.g. a newer directory the gem has not
shipped yet) by listing it in a releases.yml on your load path. Release lists
are unioned by effective date, so adding a release never hides the shipped ones;
a same-date entry from your file overrides the shipped manifest entry:
# In your application's lib/gov_codes/afsc/releases.yml
:dafecd:
- :effective_date: '2026-04-30'
:version_label: v3.6
:source: DAFECD-30-April-26.pdf
:name: Department of the Air Force Enlisted Classification Directory
Pair it with a matching releases/dafecd/2026-04-30/enlisted.yml index, and
find(code, as_of: "2026-04-30") will resolve against it.
Data source & provenance
AFSC data is extracted from the official Department of the Air Force classification directories — the DAFECD (enlisted) and DAFOCD (officer) — not third-party sources. Extraction is deterministic, and every code is verified to appear verbatim in the source directory: no predicted or hallucinated codes.
The data is versioned by each directory's effective date (the directories are republished roughly semi-annually, on 30 April and 31 October). Look a code up as it stands today, or as it stood for a given release:
GovCodes::AFSC.find("1A172Y") # the release in effect today
GovCodes::AFSC.find("1A172Y", as_of: "2025-11-01") # the release in effect on that date
GovCodes::AFSC.find("1A172Y").effective_date # => the release the result came from
as_of: nil (the default) is today's date, not "whatever's newest" — a release only takes effect on its own effective_date. This only differs from "the latest shipped release" if a release dated in the future has been added to the load path (see "Extending with Custom AFSC Codes" above); until that date arrives, lookups keep resolving against the current release.
Currently shipped: enlisted AFSCs from the DAFECD and officer AFSCs from the DAFOCD, plus the reporting/special-duty identifiers (RI/SDI) from both directories (enlisted 5-char from the DAFECD, officer 4-char from the DAFOCD) — all effective 31 October 2025. SEIs, prefixes, and the full Space Force Specialty Codes (SFSC) are planned.
Specialty acronyms
A resolved code carries an acronym field:
GovCodes::AFSC.find("1C8X3").acronym # => "RAWS"
GovCodes::AFSC.find("1Z3X1").acronym # => "TACP"
GovCodes::AFSC.find("1A1X2").acronym # => nil (no acronym)
# Reverse lookup by acronym (case-insensitive, and `as_of`-aware — it searches
# the acronyms of the release in effect on that date, so overlays never leak
# across document dates):
GovCodes::AFSC.find_by_acronym("RAWS") # => the 1C8X3 Code
GovCodes::AFSC.find_by_acronym("raws") # => same (case-insensitive)
GovCodes::AFSC.find_by_acronym("PJ", as_of: "2025-11-01") # => resolves that release's overlay
GovCodes::AFSC.find_by_acronym("nope") # => nil
# Officer acronyms resolve too — including shredout acronyms, which return the
# concrete shredded code:
GovCodes::AFSC.find_by_acronym("TACPO") # => the 19ZXB Code (Special Warfare, TACP Officer)
GovCodes::AFSC.find_by_acronym("FAO") # => the 16FX Code (Foreign Area Officer)
Shipped acronyms are source-verified: they are captured only from a trailing parenthetical in the directory title (e.g. Radar, Airfield & Weather Systems (RAWS)) and the build gate rejects any acronym that does not appear verbatim in the source title. Five enlisted specialties ship an acronym this way (1A8X1, 1C8X3, 1N1X1, 1Z3X1, 4B0X1); nothing else is invented.
Everything else — colloquial acronyms not printed in the directory (e.g. PJ for Pararescue) — is consumer-curated via an optional overlay you drop on your load path. Overlays are flat SPECIALTY => ACRONYM (or CODE => ACRONYM) maps:
# Enlisted, per DAFECD release date:
# lib/gov_codes/afsc/releases/dafecd/2025-10-31/acronyms.yml
:"1Z1X1": PJ # augments the Pararescue entry; name/skill_levels/shredouts untouched
:"1Z3X1": JTAC # overrides the shipped TACP acronym
# Officer, per DAFOCD release date:
# lib/gov_codes/afsc/releases/dafocd/2025-10-31/acronyms.yml
:"11MX": MOBPLT # augments the Mobility Pilot entry (ships no acronym)
:"16FX": FAREA # overrides the shipped FAO acronym
# RI/SDI acronyms live in the SAME per-release acronyms.yml, keyed by RI code:
# 5-char enlisted RI in the DAFECD file, 4-char officer RI in the DAFOCD file.
:"8A400": TMC # enlisted RI/SDI -> dafecd/<date>/acronyms.yml
:"90G0": GENOFF # officer RI/SDI -> dafocd/<date>/acronyms.yml
Key points:
- Enlisted, officer, and RI/SDI overlays are all per-release-date, scoped by the directory's effective date, so
find(code, as_of: "2025-11-01")applies that release's overlay — and each publication resolves independently, so an officer overlay never affects an enlisted lookup or vice versa. RI/SDI reuses the same per-release, per-publicationacronyms.ymltier (enlisted RI from the DAFECD file, officer RI from the DAFOCD file), so an RI overlay never crosses into an enlisted/officer lookup either. - The overlay is a separate tier — it only sets
:acronymon entries that already exist and never replaces an entry, soname,qual_levels/skill_levels, andshredoutsstay intact. - Precedence: the consumer overlay wins over the shipped, source-verified acronym. For officer codes a documented shredout acronym still wins for a shredded code. The gem ships no overlay file, so absent one the shipped index is used as-is.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
Regenerating the data
Enlisted (DAFECD) and officer (DAFOCD) indexes are regenerated deterministically
from the official classification-directory PDFs by bin/extract_afsc_from_pdf.rb
(offline dev tooling; never loaded by the gem runtime). It writes the versioned
release artifact under lib/gov_codes/afsc/releases/<publication>/<date>/, updates
the manifest, and fails loudly before writing if any code, title, acronym, or
shredout override is not grounded in the source:
mise exec -- ruby bin/extract_afsc_from_pdf.rb "DAFECD -31 October 25 v3.5 FINAL.pdf"
mise exec -- ruby bin/extract_afsc_from_pdf.rb "DAFOCD 31 Oct 25 v3.pdf"
The retired bin/extract_afsc_from_wikipedia.rb (which generated the old flat
enlisted/officer YAML) has been removed in favor of the PDF extractor. RI/SDI
data is now sourced from the same DAFECD (enlisted, 5-char) and DAFOCD (officer,
4-char) directories, under the same anti-hallucination gate — every RI code,
title, and acronym must appear verbatim in the source — and ships as versioned
artifacts (releases/<publication>/<date>/ri.yml) alongside the enlisted and
officer indexes.
To install this gem onto your local machine, run bundle exec rake install.
This project is managed with Reissue.
Releases are automated via the shared release workflow. Trigger a release by running the "Release gem to RubyGems.org" workflow from the Actions tab.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/SOFware/gov_codes.
License
The gem is available as open source under the terms of the MIT License.