Update the architecture diagram and env var table to reflect the
ContributionStore tier and incremental fetch, document the
graph:contributions:refetch command, and note composer phpstan next to
the existing test commands.
README had drifted from the codebase: wrong test path, a stale /health
sample response, a docker-compose volume mount pointing at a path the
image never writes to (with no SQLite data volume at all), a missing
CONTRIBUTIONS_RETENTION_DAYS example, and an architecture diagram
predating the SQLite store/refetch command. Also fixes
docker-compose.prod.yml, which was missing the data volume and
CONTRIBUTIONS_RETENTION_DAYS env var present in docker-compose.yml —
without it, a prod deployment on this file loses contribution history
on every container recreate.
Add --memory-limit=512M to the phpstan composer script — the default
128M crashed the level-8 run under parallel workers. Update
PHP-Stan-Errors.md to note all 22 catalogued findings are now resolved.
Add professional PHPDoc (summary + @param/@return) to the remaining
public methods that had none or only partial coverage: GraphController's
actions, ContributionStore's accessors, SvgRenderer's render() and
private helpers (with concrete array-shape annotations), ProbeTrait,
ProviderHealthChecker, ProviderStatus, and the Contribution entity.
Also adds the matching @return shapes on SvgRendererTest's data
providers.
array_map() over the variadic constructor can produce non-sequential or
string keys, which doesn't match the array<int, Contribution> property
type; re-index with array_values(). Also documents the iterator/count
accessors.
date() expects int|null, but strtotime() returns int|false; cast to make
the type explicit for PHPStan level 8 (the date strings are always
well-formed here, so strtotime() never actually returns false).
Bound each provider's startFetch()/resolveFetch() to an optional
since/until window, wire a SQLite-backed ContributionStore into
ContributionAggregator (fetch only the trailing window past the last
stored date, merge into the store, prune by CONTRIBUTIONS_RETENTION_DAYS),
and add a graph:contributions:refetch command to force a full or ranged
re-fetch in <=365-day chunks. This is the root fix for the full
365-day-refetch timeout that used to hit on every cache miss.
docker-compose.override.yml only overrode the dockerfile, so it kept
inheriting target: final from docker-compose.yml — but Dockerfile.dev
is single-stage and has no final stage, so the dev build failed. Name
the stage in Dockerfile.dev and target it explicitly from the
override.
- Fix add()'s upsert SQL (was invalid SQL from typos) and use it for
the new merge() as well.
- Fix remove()'s missing FROM keyword and drop the unindexed LIKE scan
in favor of an exact match.
- Add latestDate(), all() with optional sinceDays filtering, and
prune() with a retention-day cutoff.
- Add Contribution (immutable value object) and ContributionCollection
(IteratorAggregate + Countable) so all() returns something typed
instead of a raw date => count array.
- Cover all of the above with unit tests against an in-memory SQLite
database.
The eightpoints/guzzle-bundle and idci/graphql-client-bundle only
existed to build one near-static query string, and the bundle's own
HTTP transport was already bypassed in favor of Symfony HttpClient.
Build the query with sprintf/json_encode instead, drop both bundles
from config/bundles.php, and update the test double accordingly.
Promoted readonly properties can't be reassigned in the constructor
body, so GitLabProvider and GiteaProvider threw on every instantiation.
Normalize the plain parameter first, then assign to a separately
declared readonly property.
Providers fetch in parallel now (cost ~= max, not sum), so the old
90s stopgap for the 3-sequential-providers-plus-pagination worst case
can come back down toward the 30s default, with headroom for
pagination and per-request HTTP timeouts.
Mocks startFetch()/resolveFetch() instead of fetch(), and splits the
old single failure test into separate start-throws and
resolve-throws cases since those are now distinct call sites.
aggregate() now fires every configured provider's startFetch() in one
pass before resolving any of them, so wall-clock cost is roughly
max(providers) instead of sum(providers). Partial-failure isolation
and per-date summation behavior are unchanged.
startFetch() resolves the user id and fires the first events page,
returning a handle without reading it; resolveFetch() continues the
existing do/while pagination starting from that first response.
Pagination itself stays sequential within this provider since each
page depends on the previous one - parallelism here only spans across
providers, not within GitLab's own pages.
startFetch() fires the GraphQL POST and returns the response without
reading it; resolveFetch() does the .toArray()/parse/log work that
used to happen inline right after the request.