Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
468ca7e
chore: create the spec repo with the standards and templates
MobileMage Oct 1, 2026
4306d18
feat: add the postcode grammar and data files
MobileMage Oct 1, 2026
c5ee5f8
fix: apply Task 2 review findings
MobileMage Oct 1, 2026
24b25dc
fix: restore the encoding detector for the licence check
MobileMage Oct 1, 2026
6a97d50
test: name the data folder and the threshold precisions exactly
MobileMage Oct 1, 2026
66d8806
feat: add check-tells for agent tells that linters miss
MobileMage Oct 1, 2026
5f93379
fix: skip doctest examples in the TELL-13 check
MobileMage Oct 1, 2026
4452d5c
fix: close gaps in the check-tells rules
MobileMage Oct 1, 2026
776f093
docs: define message catalogues for check-tells
MobileMage Oct 1, 2026
b73198b
feat: add the shared vectors and their builder
MobileMage Oct 1, 2026
ae50806
docs: say that vector ids hold within one spec version
MobileMage Oct 1, 2026
bf4eba3
fix: close gaps in the shared vectors
MobileMage Oct 1, 2026
13f96dc
test: tie the separators and GPS limits to the vectors
MobileMage Oct 2, 2026
2948a22
ci: run make check and a secret scan
MobileMage Oct 2, 2026
aa8ca57
ci: pin the gitleaks image and test the newest Python
MobileMage Oct 2, 2026
fc7f808
docs: name the CI triggers in each language standard
MobileMage Oct 2, 2026
eaa3760
feat: limit the input of parse to 64 code points
MobileMage Oct 2, 2026
3966ed6
test: pin how parse counts its input
MobileMage Oct 2, 2026
8c3f436
fix: use NIPOST's codes for five states
MobileMage Oct 2, 2026
548d783
test: add vectors for partial codes, state names and counting
MobileMage Oct 2, 2026
80172d2
docs: add glossary terms and settle TypeScript rules
MobileMage Oct 2, 2026
7e7756f
test: reject the ISO codes that NIPOST does not use
MobileMage Oct 2, 2026
cd98823
docs: point the templates to NIPOST's website
MobileMage Oct 2, 2026
2d67dbc
fix: derive the ISO cases from the data and drop a guess
MobileMage Oct 2, 2026
0e60119
docs: say how SDKs treat broken text and NFKC tables
MobileMage Oct 2, 2026
aa1b4c2
docs: put the broken-text rule after the parse checks
MobileMage Oct 2, 2026
a66d39d
docs: name the Unicode baseline and count broken text exactly
MobileMage Oct 2, 2026
c8b85f8
docs: drop a NIPOST claim with no source and fix the Spec entry
MobileMage Oct 2, 2026
a3925ed
test: run every vector through a plain reading of the grammar
MobileMage Oct 2, 2026
3c0b677
test: add vectors for table gaps, an area digit and negative zero
MobileMage Oct 2, 2026
0119d83
docs: define the public interface and correct the grammar text
MobileMage Oct 2, 2026
0612882
fix: make check-tells check what the standards say
MobileMage Oct 2, 2026
206ce99
ci: check the message that reaches main and pin the tools
MobileMage Oct 2, 2026
9a02477
docs: state each rule in the standards and fix the templates
MobileMage Oct 2, 2026
8ed85fa
docs: name the interface and the grammar reading in the README
MobileMage Oct 2, 2026
6ca79a0
refactor: split check-tells into modules
MobileMage Oct 2, 2026
4b3c8a7
fix: check HTML comments in the squash message
MobileMage Oct 2, 2026
66b9c5f
test: add vectors for a letter after a digit in the LGA and the unit
MobileMage Oct 2, 2026
55cc84c
ci: end the Renovate pull request body with the sign-off
MobileMage Oct 2, 2026
94ded1b
docs: add require to the banned globals of the TypeScript rules
MobileMage Oct 2, 2026
dc5a657
fix: match only ASCII white space after the Renovate comment
MobileMage Oct 2, 2026
ac80e34
docs: make the text agree with the tools and the vectors
MobileMage Oct 2, 2026
bbc0eed
test: add vectors for long input to normalize and for contains
MobileMage Oct 2, 2026
195999a
test: add contains vectors for another LGA and another area
MobileMage Oct 2, 2026
d042905
test: add a normalize vector with each lower-case ASCII letter
MobileMage Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# SPDX-FileCopyrightText: 2026 The Gatepost authors
# SPDX-License-Identifier: Apache-2.0
name: ci

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
# A failure on one Python version must not hide the result on the other.
fail-fast: false
matrix:
# The oldest Python that the scripts support, and the newest release.
python-version: ["3.11", "3.x"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
# The repo has no lock file to key a cache, and the tools are small.
enable-cache: false
- run: make check

secrets:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# gitleaks must read the whole history.
fetch-depth: 0
persist-credentials: false
- run: |
image=ghcr.io/gitleaks/gitleaks # v8.30.1
digest=sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f
docker run --rm -v "$PWD:/repo" "$image@$digest" git /repo
52 changes: 52 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: 2026 The Gatepost authors
# SPDX-License-Identifier: Apache-2.0
name: pull request

# A squash merge puts the title and the body of the pull request on main as the commit message.
# An edit of the title or the body does not push a commit, so it must start these jobs again.
on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
contents: read

concurrency:
group: pull-request-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
sign-off:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The check reads each commit from the base to the head.
fetch-depth: 0
persist-credentials: false
# Only the message that reaches main gets the other message rules, so a commit needs a
# sign-off and nothing more.
- name: Check that each commit has a sign-off
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: python3 scripts/check-tells --signed-off "$BASE..$HEAD"

squash-message:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The title and the body come through env, so that no text of the pull request ends up
# inside the script.
- name: Check the message that the squash merge puts on main
env:
TITLE: ${{ github.event.pull_request.title }}
NUMBER: ${{ github.event.pull_request.number }}
BODY: ${{ github.event.pull_request.body }}
run: |
printf '%s (#%s)\n\n%s\n' "$TITLE" "$NUMBER" "$BODY" > "$RUNNER_TEMP/message"
python3 scripts/check-tells --squash-msg "$RUNNER_TEMP/message" --no-scope
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2026 The Gatepost authors
# SPDX-License-Identifier: Apache-2.0
observed-raw/
__pycache__/
.mypy_cache/
.ruff_cache/
162 changes: 162 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Gatepost

Unofficial, open-source developer tools for Nigeria's National Digital Postcode. This glossary fixes one name for each concept, in every language and every repo.

## Language

### The postcode

**Postcode**:
The 11-character code that NIPOST gives to one addressable building or location.
_Avoid_: digital address, address code, zip code, postal code

**Legacy postcode**:
An old 6-digit NIPOST postcode. It names an area, not a building.
_Avoid_: old code, zip

**Segment**:
One of the five parts of a postcode: state, LGA, district, area and unit.
_Avoid_: part, field, component, chunk

**State**:
The first segment. Two letters that name one of the 36 states or the Federal Capital Territory.
_Avoid_: region, province

**LGA**:
The second segment. Two digits that name a local government area within a state.
_Avoid_: council, local government, municipality

**District**:
The third segment. Three characters that name a district within an LGA.
_Avoid_: sorting code, sector

**Area**:
The fourth segment. Two letters that name the smallest polygon in the system. An area holds at most 99 units.
_Avoid_: zone, block, neighbourhood

**Unit**:
The fifth segment. Two digits that name one building or delivery point within an area.
_Avoid_: building number, house number, delivery unit

**Partial postcode**:
A postcode that stops after the state, LGA, district or area segment.
_Avoid_: prefix, incomplete code
Exception: `prefix` is the name of the first parameter of `contains`.

**Precision**:
The most specific segment that a postcode or a result contains.
_Avoid_: level, granularity, resolution

### Forms of a postcode

**Compact form**:
The postcode with no separators, for example `EK01A03FK01`.
_Avoid_: raw, plain

**Canonical form**:
The postcode with hyphens between segments, for example `EK-01-A03-FK-01`.
_Avoid_: formatted, standard form

**Display form**:
The postcode with spaces between segments, for example `EK 01 A03 FK 01`.
_Avoid_: pretty, human form

**Normalise**:
To change input in three steps, in this order: apply NFKC, remove the separators, and change the ASCII letters to upper case. `grammar.md` defines the steps.
_Avoid_: sanitise, canonicalise

**Code point**:
One Unicode code point. It is not a UTF-16 unit, a byte or a grapheme cluster. For example, U+1D404 is one code point and two UTF-16 units.
_Avoid_: character (when you count input), char

**Input limit**:
The most code points that `parse` and `isLegacy` read. It is `maxInputCodePoints` in `data/format.json`. Over the limit, `parse` gives `bad_length` and `isLegacy` gives false. Both functions apply the limit before they normalise.
_Avoid_: max length, length limit, size limit

**Suggestion**:
The canonical form that `parse` offers with `unknown_state` or `bad_segment`, when a fix of look-alike characters makes the code parse. It is a hint, never a success. The word means this hint from `parse`. It does not mean the suggestions that NIPOST's autocomplete API returns.
_Avoid_: correction, autocorrect

**Parse result**:
What `parse` returns: a postcode, or a parse error. The Interface section of `grammar.md` defines its fields.
_Avoid_: outcome, response

**Parse error**:
The reason that `parse` rejected its input. It has a code, the failing segment and a suggestion. It is a value that `parse` returns, not an exception.
_Avoid_: validation error, exception

### NIPOST's API

**Gateway**:
NIPOST's public API at `api.postcode.gov.ng`.
_Avoid_: server, backend, endpoint (for the whole API)

**Lookup**:
A request to the gateway for the facts about one postcode.
_Avoid_: query, fetch, resolve

**Lookup level**:
How much data a lookup returns, from 1 for validity only to 5 for point geometry.
_Avoid_: tier, access level, depth

**Reverse geocode**:
A request for the nearest unit to a coordinate.
_Avoid_: reverse lookup, locate

**Accuracy**:
The radius in metres within which a GPS fix is likely correct.
_Avoid_: precision, error margin

**Secret key**:
An API key for servers. It starts with `nipost_test_` or `nipost_live_`.
_Avoid_: private key, API secret

**Publishable key**:
An API key that can ship inside an app or a web page. It starts with `nipost_pk_`.
_Avoid_: public key, client key

**Widget**:
NIPOST's own postcode picker. Gatepost does not build widgets.
_Avoid_: using this word for a Gatepost component

### Gatepost

**Spec**:
The `spec` repo. It holds the grammar, the data, the vectors and the standards. The API fixtures and the completed OpenAPI file come later.
_Avoid_: schema, contract (for the repo)

**Vector**:
One shared test case in `spec/vectors/` that every SDK must pass.
_Avoid_: fixture, example, golden file

**Fixture**:
Synthetic data that the mock server returns.
_Avoid_: vector, stub, sample

**Mock server**:
The local server that imitates the gateway with fixtures.
_Avoid_: fake API, stub server, sandbox

**SDK**:
The core and the client for one language.
_Avoid_: library (for the pair), wrapper, binding

**Core**:
The part of an SDK that parses and formats postcodes without network access.
_Avoid_: utils, base, common

**Client**:
The part of an SDK that calls the gateway.
_Avoid_: API wrapper, service, fetcher

**Field**:
A Gatepost UI component in which a user enters or picks a postcode.
_Avoid_: input, widget, picker

**Confirm**:
To show the user what a postcode points to before the app stores it.
_Avoid_: verify, validate (for this step)

**Tell**:
A habit that makes code look machine-written, such as very long lines.
_Avoid_: smell (smells are design problems), anti-pattern
Loading