Problem/Motivation

ai_disclosure.grade_lookup was merged into ai_disclosure 1.0.x on 9 September 2026 but was in no release. This module therefore declared drupal/ai_disclosure: ^1.0@alpha, which also resolves to 1.0.0-alpha1, where the service does not exist — so a shipped stand-in lookup is what such a site actually runs, and the test asserting that the stand-in and the adapter answer the same (ShippedGradeLookupTest::testAc6TheStandInAndTheAdapterAgree) could only skip there.

ai_disclosure 1.0.0-alpha2 was released on 12 September 2026 and carries the service with both of its published methods, gradesForIptcUri() and getCacheTags().

Proposed resolution

Raise the requirement to ^1.0.0-alpha2, so every resolvable install reads grades through the real service rather than the stand-in.

alpha2 also widened DisclosureRecorderInterface with attestHumanReview(). An alpha may. The consequence here is narrow: nothing in src/ implements that interface, but a hand-written test double in the kernel suite does, and it stops satisfying the interface the moment the dependency moves. PHPStan reports it as a non-ignorable method.abstract error. Doubles built with createMock() absorb such an addition silently; a hand-written one does not.

What was measured, rather than inferred

  • composer update resolves to 1.0.0-alpha2 with no minimum-stability complaint. Naming the pre-release inside the constraint carries its own stability flag, so no @alpha suffix is needed.
  • alpha1 is genuinely excluded, not merely expected to be: composer update drupal/ai_disclosure --with=drupal/ai_disclosure:1.0.0-alpha1 is rejected with "must be a subset of the constraint in your composer.json (^1.0.0-alpha2)".
  • The equivalence test no longer skips: 96 tests, 211 assertions, 0 skipped, with phpcs and phpstan clean.

What this does not change

The stand-in lookup and the service provider's check that the service answers to gradesForIptcUri() both stay. What they guard is no longer "a site on an older release" — under this constraint no such site can resolve — but a fork or a later rename, where swapping in an adapter that then throws would poison a queue item on every cron run. A skip of the equivalence test now means that case and nothing else.

Remaining tasks

None.

API changes

None in this module's own API. The declared floor of its drupal/ai_disclosure requirement moves from ^1.0@alpha to ^1.0.0-alpha2; a site pinned to alpha1 will not receive the update until it allows alpha2.

Data model changes

None.

Comments

maurice1969 created an issue. See original summary.

  • maurice1969 committed 195e3903 on main
    Issue #3622690 by maurice1969: require ai_disclosure 1.0.0-alpha2...

  • maurice1969 committed 195e3903 on 1.0.x
    Issue #3622690 by maurice1969: require ai_disclosure 1.0.0-alpha2...
maurice1969’s picture

Status: Active » Fixed

Committed as 195e390, pushed to both main and 1.0.x.

What landed:

  • composer.json — drupal/ai_disclosure from ^1.0@alpha to ^1.0.0-alpha2.
  • README.md — the install instructions name the new floor, and say why it is alpha2 rather than any alpha. The measured minimum-stability error message quoted there still names alpha1, with a note that the version in that message is whichever alpha is current; it was not silently edited to say alpha2, because that output has not been re-measured.
  • The recorder spy in the kernel suite implements attestHumanReview(), which alpha2 added to DisclosureRecorderInterface.
  • The equivalence test's skip message no longer claims the constraint resolves to alpha1, since it cannot any more.

Verified before the push: phpcs clean, phpstan clean, 96 tests and 211 assertions with none skipped, and the traceability check reporting nothing unresolved across seven specs. CI pipelines 958543 (main) and 958544 (1.0.x) were still running when this was written; job statuses rather than the pipeline status are the thing to read there, since two jobs in the shared template are allowed to fail and leave a pipeline green.

Marking fixed.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

  • maurice1969 committed cddd954d on main
    Issue #3622690 by maurice1969: let two phpstan ignores go unmatched
    
    The...

  • maurice1969 committed cddd954d on 1.0.x
    Issue #3622690 by maurice1969: let two phpstan ignores go unmatched
    
    The...
maurice1969’s picture

Correction to the comment above: marking this fixed while the pipelines were still running was premature, and the phpstan job went red on job 12154818. It is green now, in cddd954, and the cause is worth recording because it is a consequence of the alpha2 bump rather than a separate problem.

Nothing in the code was wrong. phpstan.neon.dist carried two ignoreErrors entries for ContentCredentialsServiceProvider — booleanNot.alwaysTrue and deadCode.unreachable — and they failed by being unmatched, not by matching something new.

They exist because phpstan-drupal builds its service map from the services.yml files it can see. Analysed standalone, with no Drupal root, that is this module's own file alone, so asking the container whether it has ai_disclosure.grade_lookup folds to a constant false, the negation reads as always true, and the swap below it reads as dead code. On drupal.org's CI the module sits inside a site, every installed module's services.yml is read — and alpha2 is the first release whose file declares that service. So the two errors stopped being reported there, reportUnmatchedIgnoredErrors did what it is for, and the job failed.

The entries were written to be self-expiring: the comment beside them said that the day the analysis could see the service, the run would fail until they were deleted. That day arrived and deleting them turned out to be the wrong answer, because the two environments now disagree about the same code — deleted ignores are red standalone, strict ignores are red on CI. Both now carry reportUnmatched: false, which the loadIncludes.moduleNotFound entry below them already had for the same underlying reason: the error is an artefact of analysing without a Drupal root.

The cost is written into the file rather than glossed over — those two ignores no longer expire on their own, so if that swap ever does become genuinely dead code, the configuration will not say so.

Pipelines 958568 (main) and 958569 (1.0.x) are green on every job, checked per job rather than by pipeline status, including the two the shared template allows to fail. Leaving this fixed.

One thing a local run could not have caught: phpstan was clean here both before and after the fix. The disagreement only exists where there is a Drupal root.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

sjpagan’s picture

@maurice1969 I went through your fix in #3622690, including the stand-in lookup and the gradesForIptcUri() check you kept to guard against a fork or a rename.

It makes sense given that GradeLookup is currently a final class with no interface, unlike the resolver and the recorder.
If you're on board, I can support that on this side: add a GradeLookupInterface with both current methods and unchanged signatures, alias it in services.yml for autowiring, and state in the developer docs that the service ID and these signatures won't change within 1.x.

The change is fully backwards compatible, and it turns what your guard currently assumes into an explicit contract.
You could then type-hint the interface, or have your stand-in implement it, instead of checking for the method. If there's anything else you need from the lookup, let me know now, before it's frozen.

maurice1969’s picture

@sjpagan Thanks for looking at this so carefully, and for offering. Yes, I'd really like that. An interface with a 1.x promise is a lot nicer to build on than my method_exists() check.

One small thing before you freeze it: could the interface docblock also spell out the behaviour, not just the signatures? What my adapter actually relies on is in your current docblock. Results are keyed by grade ID in both formats, the URI match is exact, an empty URI just returns [], and the order is for stability rather than a ranking. If those stay part of the promise, that's everything I need. I don't use getCacheTags() and I don't touch GradeLookupFormat, so I have no wishes there.

Nothing needs to change on my side until it's in a release, since the current check keeps working as long as the method is there. Once it ships, I'll bump the requirement, type-hint the interface and swap the check for an instanceof test.

I'll probably keep the stand-in on my own small interface rather than have it implement yours. It's only there for the case where someone swaps in their own version of the service, and implementing the full interface there wouldn't add much.

Would you mind opening an issue for it in the ai_disclosure queue? I'll follow along there. Thanks again!