Skip to content

feat(core): add the gatepost/postcode core with its checks and CI - #1

Merged
MobileMage merged 32 commits into
mainfrom
feat/php-core
Oct 2, 2026
Merged

MobileMage merged 32 commits into
mainfrom
feat/php-core

Conversation

@MobileMage

Copy link
Copy Markdown
Contributor

gatepost/postcode parses, checks and formats Nigeria's digital
postcodes in PHP, with no network access. It implements spec 0.1.0:
normalize, parse with typo suggestions, isLegacy, truncate, parent,
contains, redact, stateName and precisionForAccuracy. It runs on PHP
8.1 and later, without the intl and mbstring extensions. A generated
table holds the NFKC forms that a postcode can use, from the Unicode
Character Database 17.0.0, and a generator writes the spec data into
the package.

Every shared vector runs in the tests, with seeded property tests and
a weekly mutation run. CI runs composer check on PHP 8.4, and the tests
on PHP 8.1 and 8.5 with ext-intl off. It also checks the squash
message, the sign-off of each commit and the workflow pins.

This change is larger than the 400 lines that GIT-5 suggests, because
it starts the package. Most of it is tests, generated data and tool
configuration. src/Postcode.php passes the 300 lines that CS-3
suggests: about 140 of its 320 lines are the doc comments and examples
that DOC-1 asks for, and a split would add a layer that only forwards
calls (API-12).

Signed-off-by: MobileMage aidelojejoshua@gmail.com

composer check runs every tool in Part A of php.md, so a contributor and CI run the same command. The core is empty until the next tasks add it, test first.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The ban lists missed the clock classes, require_once and most of intl,
the coverage floor ignored classes that no test loads, and nothing
checked the PSR-4 layout. The hook now also needs a sign-off and rejects
an AI co-author line. The repo standard lists the newer PHP features
that only the PHP 8.1 CI job can catch.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The spec now names Unicode 17.0 as its NFKC baseline and says what
parse, isLegacy and normalize do with text that is not valid UTF-8,
which the PHP package needs. Its vectors also use NIPOST's codes for
five states.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A checked generator writes the spec values into SpecData, so no value is copied by hand (CS-4) and a Packagist install, which has no submodule, still holds them.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
composer check ran only the passing path of the generator, so a check
that always passed would go unseen. New tests run the generator and the
loader on bad data. The readers now check the shapes that their comment
promises, missing files give one clear message, and a term that could
never change the result of precisionForAccuracy is gone.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
PHP has NFKC only in ext-intl, which many hosts lack (PHP-9). The table holds every character whose NFKC form can be part of a postcode, so parse gives the same result as with a full NFKC. ADR 0002 records the trade-off.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The NFKC table comes from the Unicode Character Database, whose licence
asks that its notice stay with copies of the data. The generator now
writes the Unicode copyright and licence into the table's header, and
NOTICE names it. The generator also checks a download before it caches
it, so a broken download no longer blocks later runs. New tests pin
astral characters in normalize and each half of the fix-table checks.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The input limit costs one strlen call for long text and one short PCRE call otherwise, so a large form field cannot stall a server. Text that is not valid UTF-8 gives an error code and no exception, as Proposal P1 in the plan describes.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The spec now has a plain reading of its grammar that checks every
vector, and new vectors for astral characters, for NFKC forms that hold
a dash, for a digit at the start of the area and for an accuracy of -0.
The glossary drops a claim about NIPOST that no public source supports.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Text that is not valid UTF-8 gives bad_character up to 4 bytes for
each code point of the input limit, and bad_length above it. No test
held text between 65 and 256 bytes, so a check that counted bytes
against the limit itself passed every test. Two rows now pin table
entries whose NFKC form is a dash.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A user who types O for 0 in an LGA gets one hint that parses, and never a silent change. The district stays as typed, because NIPOST has not confirmed its characters.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
check-tells now reads more kinds of file, such as phpunit.xml.dist,
phpstan.dist.neon and infection.json5, and it can check the message
that a squash merge puts on main. The spec also adds two vectors for a
letter after a digit in the LGA and in the unit.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The glossary avoids the word part for a segment, so the code now says
segment. A comment also says why the suggestion parse stops after one
nested call: a fix table never writes a character that it changes.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Apps cut a code to the precision that a GPS fix supports, and logs
need the redacted form (ERR-3). Truncate and parent read the segment
ends from the spec data, so no precision order is copied by hand.

Two unit tests cover what the vectors miss. One checks the compact
form and the precision of a truncated code. The other checks that
contains does not find a state in a later segment.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
contains() and parent() rest on facts that their code does not show:
each precision has a fixed length, and the spec data lists the
segments from the least precise. Two comments now say so. A truncate
vector that expects an error now fails with the right message when no
error comes.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
PHP has no standard property-testing tool, so a seeded generator checks
the rules that must hold for every input, such as a parse that never
throws.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The spec adds vectors that pin each segment of a contains prefix, long
input to normalize, and each lower-case ASCII letter, and its text now
lists the rules that each SDK tests itself.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A review planted mistakes that every property passed: a case
table without one letter, a canonical form with one hyphen, and a
parse that throws on random text. Each property now checks the
result in full, the typo list comes from the spec data, and a failed
run shows its input.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
DOC-2 and DOC-3 ask for a README that follows the template, with
examples that CI runs. ReadmeTest runs each PHP example and compares its
output. It also checks that the input limit in the text equals the limit
in the spec data, so a stale example or number fails the build. ADR 0001
records why the package keeps PHP 8.1 as its floor after the end of life
of that release, with the WordPress.org usage numbers of 2 Oct 2026, as
VER-1 asks.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
DOC-5 asks for a changelog that change files generate. Changie wrote
this entry from one change file for the offline core of spec 0.1.0. The
release tag comes after the branch merges, so this commit adds no tag.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
CI runs composer check on PHP 8.4. It runs the tests on PHP 8.1 and on
the newest PHP release with ext-intl off, because the standards ask for
both versions and PHP-9 forbids a need for that extension. A pull
request workflow checks the sign-off of each commit and the message that
the squash merge writes. Scheduled workflows run the mutation tests,
CodeQL and Scorecard. SEC-6 asks for pinned actions, so composer check
now runs zizmor, and the licences check pins its encoding detector as
well. The contributor notes now say how to add a missing sign-off,
because the sign-off job fails on it.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Renovate opens one pull request each week for the Composer packages, the
digests of the GitHub Actions and the versions of the tools that
composer.json and the workflows pin. Its commits and the end of its pull
request bodies carry the sign-off that the pull request workflow needs,
and its titles use the chore(deps) form. The file stays out of the
Packagist archive. Renovate runs only after the owner installs its app.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Composer installs only stable releases by default, so the old install
line failed while the only release is an alpha. The README now gives
a constraint that allows it, and it drops a claim about commit bodies
that no check enforces. CI also runs the quickstart without the dev
packages, whose polyfills could hide an intl call, and the API check
no longer compares a release with an older alpha.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
T-2 and PAR-1 ask that every vector file runs, and nothing checked it:
a file added to spec/vectors passed CI with no runner. VectorFile now
keeps one list of the file names, and a test compares the list with the
folder and with the runners.

DEP-4 asks for a permissive licence in each dependency, and no tool read
the licences. scripts/check-licences.php reads composer.lock and the
lock file of each project in tools, applies the licence list of the js
repo, and fails on a lock that is missing, broken or empty.

NPG-5 asks for an API dump before the first release.
scripts/dump-api.php lists the public symbols of src by reflection into
docs/api-dump.md, and composer check fails when that file is out of
date, which also enforces GIT-7. AGENTS.md names the file.

The tests of these scripts share one helper for temporary folders.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The generator now stops on a state code, a segment name or a separator
that appears twice, because the generated array would keep only the last
one and hide the mistake. A syntax error in a data file names the file,
and a file that no user can read gives the clear message in place of a
PHP warning.

PortabilityTest bans more names for file access, the environment, the
network, the clock and randomness, and it reads a string such as
'Normalizer::normalize' as a callable. It skips a method or a property
that only shares its name with a function. ComposerRequireChecker does
not replace the lists. It flags only the symbols of extensions that
composer.json does not require, and it reads no string, so it misses
fgets, unlink, mkdir, getenv and each string callable.

GIT-3 now refuses a co-author line by the address of the AI tool, so a
person named Claude can co-author a commit. Two more coverage fixtures
pin the boundary of the complexity check in check-coverage.php: a class
of constants must pass, and a class with one path must fail.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
DOC-3 asks that CI runs each code example. DocExamplesTest runs the php
examples in the doc comments of src, which the README names as the API
reference until the docs site is live, and it compares each result that
an example shows. ReadmeTest now checks that the install constraint
allows the newest changelog version, with composer/semver, because the
stability flag alone passed a constraint that Composer would not match.

The multiline test joins the input limit tests, and the limit of PHPMD
on public methods splits those tests by concept into InputLimitTest and
InvalidUtf8Test. One TemporaryFolder helper replaces two ways to make
and remove a temporary folder.

The words follow the glossary: normalize says Normalises, the property
tests say legacy postcode, and BadLength says what parse does with the
length. AGENTS.md repeats the line of the spec template about the spec
folder, the changelog entry names the nine functions of the first
release, and the comment in PropertiesTest gives its reason without a
measured time.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The new matcher of PortabilityTest ran one pass over src for each list
of names. Under Xdebug path coverage, that added about 35 seconds to
the tests of composer check. The test now builds one lookup from all
the lists, reads each file once, and checks a token against the lookup
only when it can hold a name. The planted code tests give the same
results as before.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
PHP 8.5 reports the class name for a type written self, and PHP 8.1 to
8.4 report self, so the tests job failed on 8.5. The dump now writes
self for the declaring class and parent for its parent class, so the
file does not depend on the version. A fixture method that names its
own class reproduces the 8.5 spelling on any version. A test checks the
committed dump, so the tests job covers it on each PHP version.

The script was 487 lines. The functions that write a symbol are now in
scripts/dump-api-symbols.php, which the entry point requires, as
generate-data.php does with its helpers.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
GIT-3 read only exact addresses, so Claude <claude@anthropic.com>, the
GitHub address of Claude Code, copilot@github.com and Gemini Code
Assist passed, and the old word list had refused the first three. The
check now refuses each address at anthropic.com and openai.com, and
adds the three addresses that other tools write. A person named Claude
at another domain still passes. The row for a person at an AI company
is gone, because the rule now refuses that address.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The string check counted a bare word such as copy, system or rand as a
banned name, so a message in src with such a word would fail the test.
A bare string now counts only when its word is not on a short list of
common words. A string that names a method of a banned class, such as
'Locale::getDefault', and a string with an uncommon function name, such
as 'strtoupper', still count. The test cannot tell where a callable goes
without tracking calls, so the common words in a callable string are not
caught.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A file that exists but that no user can read passed is_file(), so
file_get_contents() printed a PHP warning before the clear message, or
no clear message at all. The vector loader, the generator, the NFKC
table loader, the API dump check and the coverage check now test
is_readable() first. A test gives the vector loader a file with no read
access, and the others use the same one-line test.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
@MobileMage
MobileMage marked this pull request as ready for review October 2, 2026 21:59
The spec branch is merged. Its squash commit on main, tagged v0.1.0,
holds the same files as the commit that the submodule named before.

Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
@MobileMage
MobileMage merged commit 9b085b8 into main Oct 2, 2026
7 checks passed
@MobileMage
MobileMage deleted the feat/php-core branch October 2, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants