Repository navigation
Fleet suppression & baseline audit: the fleet reports 185 PHPMD violations; the true figure is 8766 #155
Description
Activity
- added a commit that references this issue
on Aug 5, 2026 Burn-down execution — measured update, and three corrections to this audit
Execution pass on this audit. Everything below was re-measured from clean exports of each
repo'sdevelopmentHEAD, PHPMD 2.15.0 / PHP 8.4.22 innextcloud:latest, exit code read
directly on every run and every zero positive-controlled.The rig reproduced this audit: 11 of 12 repos exact
Before trusting any new number I re-derived the audit's
truecolumn independently.
11 of 12 match to the unit — decidesk 21, docudesk 24, doriath 207, hermiq 289, hrmq 389,
larpingapp 23, launchpad 694, nextcloud-app-template 4, nldesign 40, opencatalogi 318,
openconnector 1144. The twelfth, openbuild, reads 211 against the audit's 218 — code moved
since. The audit's figures stand.New column: what deleting the baseline ACTUALLY costs
The audit reported baseline entry counts. Those are not finding counts — baselines drift
stale as code moves underneath them. Measured, baseline file deleted,@SuppressWarningsleft
intact (this is the number that decides whether a repo can go baseline-free):repo entries in baseline findings the deletion exposes true (also suppression-stripped) openconnector 306 567 1144 launchpad 235 513 694 hrmq 209 360 389 openbuild 174 195 211 opencatalogi 56 99 318 nldesign 9 5 40 Every one of them hides more than its entry count, except nldesign which hides fewer.
Reading the entry count as the workload would have mis-sized five of six repos.nldesign is 5 findings away from a deleted baseline — the fleet's one clean full win.
🔴 Correction 1 — the fleet's one "working" exclude-pattern is swallowing real findings
This audit named openregister as the only repo with a correct top-level
<exclude-pattern>*/Migration/*</exclude-pattern>. It is correct in the sense that it fires —
and that is the problem. A direct child of<ruleset>is applied by PDepend at file-collection
time, so it drops the file from every rule, not just the one it was written for.Measured with the pattern removed, suppressions left intact: 11 genuine, unsuppressed findings
inlib/Migrationthat CI cannot currently see.Version1Date20260521120000.php:153 ElseExpression Version1Date20260521120000.php:207 ElseExpression Version1Date20260524100000.php:74 ExcessiveMethodLength Version1Date20260524100000.php:74 NPathComplexity Version1Date20260524130000.php:61 ExcessiveMethodLength Version1Date20260525240000.php:145 NPathComplexity Version1Date20260614100000.php:75 CyclomaticComplexity Version1Date20260614100000.php:75 NPathComplexity Version1Date20260706100000.php:202 StaticAccess Version1Date20260706100000.php:203 StaticAccess Version1Date20260726000000.php:104 ElseExpressionSpot-checked line 153 in the file: a real
else, no suppression on it.So the recommendation "fix the nested
<exclude-pattern>" needs a caveat: promoting it to
top-level is not the fix — it trades an inert pattern for an over-broad one. The same trap is
loaded in four more repos, where a naive promotion would silently swallow 48 further findings:
launchpad 34, openconnector 10, doriath 3, opencatalogi 1.The shape that actually works — measured, not proposed
UnusedFormalParametermoves into its own ruleset carrying the top-level exclude, invoked as
a second leg; both legs always run and the worst exit code wins. The exclusion then binds to
that one rule and every other rule still analyseslib/Migration.Proven end-to-end on doriath: 207 → 140, 65
UnusedFormalParameterretired,
zero newly-appearing findings, and all 3 non-UFP Migration findings still reported.Fleet reach of this one change, measured per repo:
repo UFP retired non-UFP in Migration preserved now-redundant @SuppressWarningsdeletableopenregister 376 11 236 openconnector 152 10 18 launchpad 80 34 8 doriath 64 3 32 opencatalogi 32 1 4 larpingapp 6 0 2 planix 3 0 1 total 713 59 301 713 of the 8581 delta is interface-mandated
IMigrationStepnoise that this retires
legitimately, and it lets 301 existing suppressions be deleted rather than added to.The remaining 12 PHP repos have no
lib/Migrationdirectory at all, so the fix is a genuine
no-op there. Their inert nested pattern is harmless today and becomes a trap the moment they
grow migrations — worth a follow-up, not churn now.🔴 Correction 2 — hrmq's CI is wired, and running
The audit says "wire up CI for hrmq — until then its baselines suppress findings for nobody."
hrmq/.github/workflows/code-quality.yml:21calls
ConductionNL/.github/.github/workflows/quality.yml@main;enable-phpmddefaults totrue
and hrmq does not override it.gh run listshows Code Quality executing today. Its
209-entry baseline is hiding findings from a gate that genuinely runs.Checked fleet-wide off the back of that: phpmd runs in CI in all 21 PHP repos. The single
enable-phpmd: falsein the fleet is in opencatalogi'sdocs-capture.yml, a docs-only workflow
that disables every check; opencatalogi's real gate leaves the default. I nearly filed that as a
disabled analyser leg — the flag looked like a smoking gun, the file it sat in decided what it
meant.🔴 Correction 3 — the "not installed, skipping" swallow is already gone
Four repos appear to carry
phpmd ... || echo 'PHPMD not installed, skipping...'. That is true
of stale local checkouts only. Ondevelopment, all 21 composerphpmdscripts invoke the
tool unconditionally. Nothing to remove.Suppression census, re-counted on current
development5471
@SuppressWarnings— 2672 with a written reason, 2799 bare. Biggest bare populations:
openregister 1287, openconnector 390, softwarecatalog 289, shillinq 218, opencatalogi 194,
procest 153. (Drift from the audit's 5100/2690/2410 is real movement in the repos.)What is NOT done
- No psalm or phpstan re-measurement — unchanged from this audit, still baseline entry counts only.
- No PHPCS reported-vs-true; shillinq (749) and launchpad (181) inline
phpcs:ignoreremain unquantified. - The 2799 bare suppressions are counted, not adjudicated.
- openregister's true count was not re-derived by me — the audit's 3266 is carried forward.
- added 7 commits that reference this issue
on Aug 5, 2026 ⚠️ Fleet-wide CI outage since 13:29Z — the shared quality workflow startup-fails with ZERO jobsSurfaced while running the small-baseline slice of this audit. Not caused by any burn-down PR — flagging here because it is blocking every repo's quality gate right now.
Every
code-qualityrun created after 2026-08-05T13:29:41Z fails at startup with 0 jobs. The shared reusable workflowConductionNL/.github/.github/workflows/quality.yml@mainis unparseable to GitHub as of commitd68fb72("fix(quality): scope axe to the app's own DOM…", #161, 13:29:41Z), which added theaxe-include-selector/axe-exclude-selectorinputs.Evidence — openregister, same workflow, callers untouched:
run created run namejobs 13:22:46Z Code Quality28 13:33:34Z .github/workflows/code-quality.yml0 Reproduced on opencatalogi at 13:36:40Z, and again at 13:39:41Z after a close/reopen. The affected branch has zero changes under
.github/.Two discriminators worth keeping
- The run's
nameis the RAW PATH, not the workflow'sname:value. GitHub never got far enough to resolvename:. A run listed as.github/workflows/code-quality.ymlinstead ofCode Qualityis a startup failure, every time. - jobs = 0. This is the count the JOBS, not the conclusions shape. It reports
conclusion: failure, so a rollup-only reader sees an ordinary red and starts debugging their own diff — but no gate ran at all.gh run rerunrefuses it with "its workflow file may be broken", andcreated_at == updated_atto the second.
The failure mode is loud-but-misattributed rather than silently green, so nothing is passing that shouldn't. But no quality gate in any repo has produced a verdict since 13:29Z, and anyone reading a red PR check right now will blame their own change.
quality.ymlparses fine as plain YAML (18 jobs, 45workflow_callinputs), so whatever GitHub rejects sits above YAML syntax — an Actions-level limit or expression validation, most likely in the two new inputs.cc the author of #161 — suggest reverting
@mainand re-landing.- The run's
- added 7 commits that reference this issue
on Aug 5, 2026
Fleet suppression & baseline audit — reported vs true, across 22 repos
Measurement pass only. Nothing was removed or changed in any repo. Every number below comes from neutralising suppressions in a throwaway copy and re-running the tool in a container (PHP 8.4 / PHPMD 2.15.0), checking the exit code on every run.
Related: hydra#473 (
composer check:strictgreen-but-dead), #146 (a disabled analyser leg publishes a green check named after the analyser).The fleet table
reported= whatcomposer phpmdprints today.true= baseline neutralised and@SuppressWarningsstripped.@SuppressWarnings(bare)eslint-disableThe fleet reports 185 PHPMD violations. The true figure is 8766. 17 of 21 PHP repos report a green zero over a non-zero true count.
Four systemic mechanisms
1. PHPMD auto-discovers
phpmd.baseline.xml— launchpadlaunchpad's composer script isvendor/bin/phpmd lib text phpmd.xml— no--baseline-file. The 235-entry baseline applies anyway, because PHPMD auto-discovers that filename in the working directory.Positive control, same ruleset and command: 4 violations / exit 2 without the file, 0 / exit 0 with it. The arms genuinely differ.
Consequence: deleting the flag does not disable a baseline — only deleting the file does. Any repo that "removed the baseline" by dropping the flag still has it fully active.
2. A nested
<exclude-pattern>is INERT — 19 of 21 reposPHPMD honours
<exclude-pattern>only as a direct child of<ruleset>. 19 repos nest it inside a<rule>element, where it is silently ignored:openregisteris the only repo with a working direct child (*/Migration/*) — and correspondingly the only one showing 0 Migration hits. Everywhere else the intended exemption never applied: doriath 64, opencatalogi 32, openconnector 153, larpingapp 6, planix 3 UnusedFormalParameter findings sit inMigration/.launchpadhas no exclude-pattern at all and carries 80.This cuts both ways: a meaningful slice of the +8581 is interface-mandated Migration noise that a one-line template fix legitimately retires. Fix the template first, then re-measure, then burn down.
3. Orphan baselines — files that protect nothing
psalm-baseline.xmlwith 6 entries, butpsalm.xmldeclares noerrorBaseline. Suppresses nothing; reads like protection..github/workflows, no.forgejo/workflows. Carries phpmd/psalm/phpstan baselines, phpcs/phpmd/psalm/phpunit/playwright/eslint/stylelint configs, and nothing executes any of it. Baselines captured to green a gate that does not run.4. Exit-swallowing — mostly a non-issue, and the live CI is clean
I checked
composer.jsonscripts, all.github/workflowsand all.forgejo/workflows. No live GitHub workflow swallows a gate's exit code, and there is nocontinue-on-erroron any gate.The
E=0; … || E=1; … exit $Eidiom in every repo'scheck:strictis correct and is not reported. The 10|| truehits in live workflows are all justified and commented (user seeding, log capture on failure, release-diagnosticls). The one genuine soft-fail —doriath/.forgejo/workflows/pre-merge-check-strict.yaml, whose header says psalm/phpstan/phpmd/test:all "are wrapped in|| echo skippingso they never" fail — is Codeberg residue on a retired forge and does not run.🔴 One outright bug, not debt
openregister — the PDF→ODT anonymisation fallback ("Path B") is unreachable in production. Full write-up: ConductionNL/openregister#2339.
PdfTextReplacer::validateOutput()acceptsbool $strictand never reads it (body lines 321–455). Hidden byphpmd.baseline.xml:251.REASON_VALIDATION_FAILEDis never constructed anywhere inlib/.PdfOdtFallbackOrchestrator::attempt()rethrows unless$cause->getReason() === REASON_VALIDATION_FAILED— so the whole fallback subsystem below that guard can never execute.Named pattern: the test supplies the precondition that production can no longer produce
PdfOdtFallbackOrchestratorTesthand-constructs the trigger it should be provoking:…at eight separate call sites. Every test passes, over a subsystem production cannot reach. The suite is evidence about its own constructor call.
This is the fourth instance of this shape this session:
method_exists()-guarded restore loop that can never be true for NC Entity magic setters; every test stubbed the collaborator, so the dead branch was never exercised.PublicationRepository:258,OpenCatalogiPublisher:125,MeetingFolderListener:91), samemethod_exists/__callcause.'seed-missing'.A test that constructs its own trigger is the same failure as a stub looser than production. Both make a whole class of assertion unwritable while reading as coverage. The discriminator is one question asked of the fixture, not the assertion: what in production constructs this input?
Worth a gate: flag a test that constructs a domain exception/sentinel which no
lib/code path ever constructs.What the numbers do NOT mean
Rule names are not defect signals. Measured, not assumed:
UndefinedVariableis overwhelmingly false positives. All 17 in doriath are by-ref named arguments (openssl_*(output: $var)); fleet-wide the pattern ispreg_match($re, $s, $matches). PHPMD does not track by-ref out-params. I checked these before reporting and they are not defects.UnusedFormalParameteris mostly interface-mandated — 75 of doriath's 88; 153 of openconnector's 200 are Migration signatures.catchis usually not missing auth. openregister'sDead catch – NotAuthorizedExceptionin Merge/Transition/Aggregation controllers looked like the orphan-auth shape. I read the code:MergeServiceenforces RBAC via_rbac: trueon everyObjectServicecall. The catch is dead because the service declares only@throws RuntimeException— so unauthorized attempts surface as 404 rather than 403. Misleading error semantics, not an IDOR.The productive discriminator is justification quality, not rule type: does a written reason exist, and does it still hold? Fleet split: 5100
@SuppressWarnings, 2690 with a reason, 2410 bare. The bare ones are the burn-down target. The heuristic that actually found the openregister bug was narrower — a parameter whose name promises behaviour but is never read ($strict,$fallbackCn) — which surfaced exactly 3 candidates fleet-wide, of which 1 was a real defect and 2 were documented as intentional.Prioritised fleet burn-down
<exclude-pattern>in the shared template (19 repos, one line) — then re-measure. Retires interface-mandated noise legitimately and shrinks the real target.@SuppressWarnings, biggest first: openregister 1305, openconnector 390, softwarecatalog 289, shillinq 218, opencatalogi 194.phpmd.baseline.xml/psalm-baseline.xmlexists without a matching declaration.Per-repo issues
What I did NOT do
vendor/autoloader and stubs. Their baseline entry counts are reported instead (that is what the file hides by construction), but I have no measured true count for psalm/phpstan. The +8581 delta is PHPMD only.phpcs.xmlconfig is uniform fleet-wide (5 exclude-patterns, 16 excluded sniffs, noseverity="0"), so there is no per-repo drift to chase, but the inlinephpcs:ignorecount is not quantified in violations — shillinq (749) and launchpad (181) are the outliers and remain unmeasured.@SuppressWarningsindividually. The reason/bare split is mechanical (does text follow the closing paren); I did not verify that each written reason is still true.phpmd.baseline.xml"). docudesk has nophpmd.baseline.xmlondevelopmenttoday — it was deleted per its own openspec archive — so that number is not reproducible against current HEAD. My measurement is 0 → 24.valtimo,openklant,opentalk,sapp,n8n-nextcloud,hermiq-exec), which the org enumeration surfaced but which are outside the briefed scope.mydashskipped (archived).