v0.11.0
Added
-
S10: what the run could not check.
okmeant two things — every check ran and found nothing,
or a check never ran — and the difference was invisible on the finding. Anonymously the activity
round asks only about packages already stale on release age, so a repository archived a week
after its last release cannot be seen, and the only trace was a note counting how many packages
were skipped without naming one. A package whose newest releases are dated by a commit their tags
share has the same shape: S2 has nothing to measure, and the finding read asok. S10 carries
the missing check on the finding, with the reason and the signals it blocked (repository_activity
→ S3, S4;release_dates→ S2, S8). It is informational, like S7 and S9: it never decides a
verdict, and it is raised only where the missing check could have changed one — never on a package
the repository already marks abandoned, nor on an allowlisted one. Credentials for every host take
away therepository_activityreasons and only those —release_datesasks no forge — and a
branch snapshot, which is on no release branch, is told about S2 alone.
--fail-on=uncheckedfails a run that could not check everything,
which is how a pipeline catches the workflow that never passedGITHUB_TOKENthrough. See
What was not checked. -
One number for how far behind the lock is: libyears. For each package, the years between
the release installed and the package's newest stable release, summed over the analysed lock —
171.3 on the wallabag fixture. Every finding carries itslibyearsin--format=json(null
when not measured: a branch snapshot, no dated stable release, not from a Composer repository,
metadata unavailable), and the document alibyearsblock that is the arithmetic over them —
total,direct_requirements(the same sum over the direct requirements, the nearest number
to php-libyear's, which readscomposer.json),measured,unmeasuredby reason and
furthest_behind.totalanddirect_requirementsare null where nothing could be measured, so
that a reader adding the field up over several projects never counts an unmeasurable lock as a
lock with nothing to fix; a measured lock with nothing behind reports0. The table and markdown footers print one line (libyears: 171.3 behind across 195 of 200 packages · 111.7 from direct requirements · furthest behind smalot/pdfparser v1.1.0 at 4.7); the HTML page shows the total in its ledger, a sortable column, and the counts
by reason on the Run tab;--explainprints the package's own value under its verdict, or the
reason it was not measured. It is laid over the verdicts, not one of them: it counts every drift,
healthy patches included, and enters no priority,--fail-onor baseline. The schemas gain the
fields under the same number, optional so that older documents still validate. No new request
is made: both dates were already in the data. A split package's installed version is dated by
its monorepo parent's tag of the same version, not by the lock: the lock copies the date
Packagist gave the split's tag, the commit its tags share, and illuminate/contracts v8.83.27
would read as 4.65 libyears behind for 3.75.--explainprints that date asinstalled release
and names the monorepo it came from. That holds for an installed tag on a shared commit
under a branch whose newest tag has a commit of its own, as after a change to the split's
directory: the parent is asked for the installed version, not only for the branch.--explain
says whose date it is under
installed_releaseandinstalled_release_dated_by, and so does the package card. Where no
parent dates it, such a tag is not measured at all rather than measured from the commit's date:
the installed tag's own entry in the repository decides that, not what dated the package's
newest release. -
left-behindsuggests a branch the project can actually move to. S8 named the newest
releasing branch and wrote the constraint that follows it, whatever PHP that branch requires:
Matomo supportsphp >=7.2.5, locks monolog 1.27.1, and was toldrequire ^3.12— monolog
3.x needs PHP 8.1. On the weekly watch, 99 of the 138 constraints suggested to projects that
declare arequire.phpcontradicted it. Every release branch now carries the php requirement of
the release it names (read from the same repository data, no new request), and S8 holds the
higher branches to two floors — the project's ownrequire.phpand the target PHP, the version
Composer resolves against. The newest branch still proves the upstream moved on; the branch the
evidence tells the project to follow is the newest releasing one within both floors, and when
that is not the newest the line says what holds the newest back:3.x released 3.12.0 (2026-09-09), needs php >=8.1 above the project's php >=7.2.5; 2.x released 2.11.1 (2026-09-02); require ^2.11 to follow. With no releasing branch within reach it says so and suggests nothing:
the way forward is a PHP upgrade, not acomposer.jsonline.--format=jsongainsnewest_php,
newest_within_reach,floor_php,floor_sourceandreachable_branch/_version/_release
on the signal;--explainshows each branch's php requirement in the branch table and in the
JSONbranchesrows. See Within reach. -
abandonedsays whether there is somewhere to go. Packagist's marker comes with a free-text
replacement, and on the weekly watch 19 of 72 abandoned packages carried one — 17 naming a
package, two namingSymfonyand a sentence. The verdict stays one (it is what--fail-on, the
baseline, the SARIF rule and every count key on); what changes is the reading. Each finding
carriesreplacementin--format=json— the named package when it is a Composer package name,
null otherwise, the free text staying in the evidence as text — the document carries
"abandoned": {"total", "with_replacement"}next tocounts, the summary line reads
abandoned 7 (6 with a replacement)where that is not zero, and the HTML page tags the row with
the replacement and links it on the card. Both fields are optional in the schemas. See
Abandoned, and where to.
Changed
-
One reading of the installed version's date. The lock's
timeis a release date only
sometimes — a branch snapshot carries its commit's, a subtree split's tag the date of a commit
its tags share, a monorepo parent can date the version instead — and each surface re-derived
that for itself, which is how one explanation came to call a date a release four lines above
saying it was not one.InstalledReleaseanswers it once. No output changes. -
old-promisereads the date against the target's major, not its minor. S5 held a release
against the GA of the target PHP minor: anything older than 2024-11-21 with a>=7.xconstraint
was an old promise about PHP 8.4. That caught the wrong thing. A>=7.2cut in 2022 was written
with PHP 8.1 on every CI matrix and differs from a^7.2 || ^8.0of the same day in spelling
alone, yet only the first was flagged — and the evidence blamed the style (has no upper bound),
which is Symfony's own convention. The line is now the GA of the target's major (8.0,
2020-11-26, for any 8.x target): the release predates the major it admits, and its constraint was
written for an older one. On the weekly watch that is 22 of the 178old-promiseverdicts the
minor line produced; the other 156 were releases of the PHP 8 era. The evidence readsreleased 2020-01-11 for PHP 5 (php ">=5.3.2"), before PHP 8 existed (8.0 GA 2020-11-26); admits 8.4 untested. In--format=jsonthe signal'sga_dateis now the major's, next to a new
target_major, andwritten_for_phpnames the constraint's lower major. The verdict, its
priority and--fail-onare unchanged; a project's count ofold-promisewill drop.
Fixed
--explainsays what the lock's date is a date of. Thecomposer.lockblock called that
date a release whenever the entry carried one, so a branch snapshot readreleased 2026-09-21 · branch snapshotand a subtree split readreleased 2023-06-05four lines above the block saying
that same date is a commit its tags share, not a release. It now readsdated … by its commit
for a snapshot anddated … by a commit its tags sharefor a split, andreleased …only where
the repository dated the version by a release. The JSON keeps itslock.releasedfield, whose
schema description now says which of the three it is carrying.- The report schema accepts the empty chain a run can really produce.
chainwas declared with
minItems: 1, and two ordinary runs break that: acomposer.lockwith nocomposer.jsonbeside
it has no direct requirements at all, so nothing reaches any package, and a lock can hold a
package only the skippedrequire-devasks for. Every finding of such a run was valid JSON that
its own published schema rejected. Documents themselves are unchanged. migrate tonames a package, or says nothing. The clause read the abandoned marker as the
repository wrote it, so free text became an instruction —no fix expected; migrate to Symfony,
migrate to EnglishInflector from the String component— and a repository that names the package
itself sent the reader back to what they were leaving, while the new
abandoned.with_replacementcounted it. The clause, the JSONreplacementand that count now
all read the same validated successor: a Composer package name that is not this package's own.
The repository's text is still shown in the evidence, as all free text is.