Skip to content

fix(gates): gate-57 1→0 (retire groupfolder storage, closes #87), gate-16 84→0, CVE-2026-65954 - #100

Merged
rubenvdlinde merged 6 commits into
developmentfrom
fix/launchpad-gates-round-4
Aug 11, 2026
Merged

fix(gates): gate-57 1→0 (retire groupfolder storage, closes #87), gate-16 84→0, CVE-2026-65954#100
rubenvdlinde merged 6 commits into
developmentfrom
fix/launchpad-gates-round-4

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Takes launchpad's Hydra Gates job from three red gates to one. gate-19 is untouched and is the only remaining failure.

Starting point, measured not assumed

From the failing development → beta run 31516037194 at e24334b6 (job log pulled via gh api …/jobs/93866053044/logs, 101,235 bytes), reproduced locally byte-for-byte on a detached worktree with a freshly cloned canonical ConductionNL/.github@57bcb2b and HYDRA_GATE_BASE_REF=origin/beta:

[hydra-gates] COVERAGE: 59 of 64 declared gates reported a result
              (5 not applicable; 59 of 59 applicable gates ran).
[gate-16] spec-coverage:              FAIL — 84
[gate-19] e2e-coverage:               FAIL — 77
[gate-57] orphaned-write-capability:  FAIL — 1

Hydra Gates is the only red job — every other job is green and Quality Report is the pure aggregator.

⚠️ gate-19 at CI scope is 77, not the 128 quoted in the round-3 handover. Both are correct measurements of different things: 128 is unscoped whole-tree, 77 is the development → beta diff scope CI actually gates on. Do not read the two as a regression or a win.

What this PR does

gate before after
gate-16 spec-coverage 84 0
gate-57 orphaned-write-capability 1 0
gate-19 e2e-coverage 77 77 (untouched)

1. CVE-2026-65954 (phpcsstandards/phpcsutils 1.2.2 → 1.2.3)

Lock-only. composer audit --locked exit 1 → exit 0. Exercised by running phpcs against the new library on PHP 8.4 (0 ERRORS AND 468 WARNINGS IN 211 FILES, exit 0), not merely locked. The Security (composer) job passed at 17:25Z only because it ran before the advisory landed; the next run would have gone red with no diff to explain it.

2. gate-57 → 0 — retire the inert groupfolder-storage-backend capability (closes #87)

The finding was DashboardService::writeDashboardContent, and the real scope was the whole capability. Task 5 of the original change — the wiring — was ticked in tasks.md but never performed. Every absence claim below was taken with a positive control (the same search resolves callers for getUserDashboards and ->findByUuid):

symbol callers
writeDashboardContent / readDashboardContent / deleteDashboardContent 0 each
DashboardContentStorageFactory::getStorage() 0 (only the three above)
DashboardApiController::storageUnavailableResponse() 0
DashboardMapper::findAll() 1 — the migration command, retired here

Version002009Date20260811000000 drops the content and locale columns. That loses no data: the only writer was reachable solely through the orphaned facade methods, so the columns were NULL on every row of every install. The step is hasColumn-guarded and idempotent. Version002001Date20260603000000 and DashboardTableBuilder::addContentStorageColumns() are deliberately kept — a shipped migration is part of the version ledger.

Deliberately NOT removed, tracked as Task 8 on #87: launchpad.content_storage, AdminSettingKey::CONTENT_STORAGE, SetupWizardService::{get,set}ContentStorage/hasGroupfolderApp, AdminController::setWizardStorage and setup-wizard step 2. Those belong to the setup-wizard capability, which writes the setting; this one was the reader. The setting was already inert, so removing the reader does not make it worse, and retiring the wizard step edits a different spec and deserves its own review.

The spec is marked withdrawn rather than deleted. That also retires its 16 @e2e exclude markers, whose reasons cited DbContentStorageTest.php — a file this PR deletes, which would otherwise have left sixteen exemptions resting on a class that no longer exists.

3. gate-16 → 0 — annotate the 84 changed frontend methods

All 84 were in src/; zero were PHP. That the PHP side is genuinely clean rather than unscanned was established with a positive control (273 lib/**/*.php files in the same diff; WeatherService.php carries 2 tags, HealthPingController.php 3).

Closed with 84 real @spec anchors and exactly 2 reason-bearing exclusions, both verified against the source:

  • ClockWidget.vue::pointOnCircle — pure polar-to-cartesian trigonometry; body reads neither this.now nor this.content. Verified its only two callers are tickCoords/handCoords, both carrying REQ-CLOCK-003.
  • useTileClickTracking.js::__resetTileClickTrackingForTest — test-only harness hook; verified its only importer in the repo is useTileClickTracking.spec.js.

Both reasons name a concrete artifact rather than a state of the world. This diff is comment-only and provably so: 0 added and 0 removed lines fall outside a docblock, so no runtime behaviour can change.

Verification

Every number below was produced by the canonical checker on this branch.

gate-16   84 -> 0     negative control (committed, per the gate's own
                      changed-method-from-history semantics):
                      0 -> 1 naming ClockWidget.vue::tickCoords -> 0
gate-57    1 -> 0     planted true positive: a tracked post* write method with
                      no caller. 0 -> 1 naming exactly the plant -> 0.
                      post* deliberately: WRITE_VERB_PREFIXES excludes delete*
gate-19   77 -> 77    unchanged, as intended
PHPUnit   1570 tests, 4032 assertions, 0 failures, 3 pre-existing skips
phpcs     0 errors, 468 warnings, 211 files — identical to the pre-change baseline
anchors   check_spec_anchors.py: 0 dangling across lib/ AND src/

No gate was weakened: no baseline, no allowlist widening, no @e2e exclude added, no skipped or deleted test other than the five PHPUnit classes covering deleted production code.

⚠️ A near-miss worth recording: deleting the longest @param type from the DashboardService constructor docblock broke PEAR's param-alignment sniff across all 22 remaining params — 44 phpcs errors into a file that had zero. Caught only by re-measuring against the pre-change baseline. It was fixed with the repo's own phpcbf, not by adjusting the standard.

Not addressed here

…5954

phpcsstandards/phpcsutils < 1.2.3 carries CVE-2026-65954 (arbitrary code
execution, GHSA-r6hr-vr92-vv28, affected >=1.0.0-alpha1,<1.2.3). The advisory
was published today, so composer audit turns red on a lock file that has not
changed. The Security (composer) job in run 31516037194 passed at 17:25Z only
because it ran before the advisory landed.

Negative control before the bump, on this tree:

  composer audit --locked
  -> Found 1 security vulnerability advisory affecting 1 package
     phpcsstandards/phpcsutils / CVE-2026-65954 / exit 1

After 'composer update phpcsstandards/phpcsutils --no-install --no-scripts'
(1.2.2 => 1.2.3, a lock-only change, 0 installs 0 removals):

  composer audit --locked
  -> No security vulnerability advisories found / exit 0

The bump is exercised rather than merely locked: phpcs runs green against the
new library on PHP 8.4 --
  0 ERRORS AND 468 WARNINGS IN 211 FILES, exit 0
so no sniff regressed on the upgrade.
…oses #87, gate-57 1 -> 0)

gate-57 (orphaned-write-capability) reported exactly one finding on this repo:

  lib/Service/DashboardService.php:3189 method=writeDashboardContent
  rule=orphaned-write-capability class=DashboardService

It is a true positive and the real scope is larger than one method. Task 5 of
the original change -- the wiring that would have routed dashboard
get/create/update/delete through the storage factory -- was ticked in tasks.md
but never performed. Everything around it landed, which is why the capability
looked complete from every angle except the one that mattered.

Measured on this tree, every absence claim taken with a positive control (the
same search resolves callers for getUserDashboards and ->findByUuid):

  DashboardService::writeDashboardContent()          0 callers
  DashboardService::readDashboardContent()           0 callers
  DashboardService::deleteDashboardContent()         0 callers
  DashboardContentStorageFactory::getStorage()       0 (only the three above)
  DashboardApiController::storageUnavailableResponse() 0 callers
  DashboardMapper::findAll()                         1 -- the migration command

So nothing ever populated oc_launchpad_dashboards.content, Dashboard::
jsonSerialize() emitted a content key that was always null, launchpad.
content_storage was a no-op setting, and launchpad:storage:migrate-to-groupfolder
migrated nothing.

Retired rather than completed, per Ruben's decision on #87: finishing it needs
an ADR-level ruling on whether the content blob or the WidgetPlacement rows are
the source of truth for a dashboard's layout. The spec asserts the blob, the
code has used the rows since long before this capability existed, and wiring
the blob in as a second source of truth invites divergence. Nobody uses it, so
retiring costs nothing and removes that risk permanently.

Removed: lib/Service/DashboardContentStorage/ (interface, both backends, three
exceptions), DashboardContentStorageFactory, the three DashboardService facade
methods and the factory constructor parameter, storageUnavailableResponse(),
DashboardMapper::findAll(), both CLI commands and their <command> registrations,
the $content/$locale properties with decodeContent() and their jsonSerialize()
keys, and the five PHPUnit classes covering the removed tree.

Version002009Date20260811000000 drops the content and locale columns. That loses
no data: the only writer, DbContentStorage::write(), was reachable only through
the factory, which was reachable only through the three orphaned facade methods,
so the columns were NULL on every row of every install. The step is idempotent
(hasColumn-guarded). Version002001Date20260603000000 and
DashboardTableBuilder::addContentStorageColumns() are deliberately KEPT -- a
shipped migration is part of the version ledger and deleting it would
desynchronise instances that have already run it.

Deliberately NOT removed, and tracked as Task 8 on #87: launchpad.content_storage,
AdminSettingKey::CONTENT_STORAGE, SetupWizardService::{get,set}ContentStorage /
hasGroupfolderApp, AdminController::setWizardStorage and setup-wizard step 2.
Those belong to the setup-wizard capability, which WRITES the setting; this
capability was the READER. The setting was already inert before this change, so
removing the reader does not make it worse, and retiring the wizard step edits a
different spec and deserves its own review.

The spec is marked withdrawn rather than deleted, which also retires its 16
@e2e exclude markers -- their stated reasons cited DbContentStorageTest.php, a
file this change deletes, so they would otherwise have become sixteen
exemptions resting on a class that no longer exists.

Verification, all on this tree:

  gate-57 over lib/Service/*.php    1 -> 0
  planted true positive (postGate57Probe, a tracked write method with no
    caller)                          0 -> 1, naming exactly the plant
  plant removed                      1 -> 0
  PHPUnit                            1570 tests, 4032 assertions, 0 failures,
                                     3 pre-existing skips
  phpcs                              0 errors, 468 warnings, 211 files --
                                     identical to the pre-change baseline
  check_spec_anchors.py over lib/    0 dangling anchors
  gate-19                            unchanged at 77 (base origin/beta)

NOTE for anyone reproducing gate-57: its checker takes a FILE LIST, not a
directory. `check_orphaned_write_capability.py .` prints nothing and exits 0
whatever the tree contains, which reads exactly like a clean repo.
Every other active change dir under openspec/changes/ carries one
(schema/created/status); the retirement change was missing it.
…(84 -> 0)

gate-16 (spec-coverage) reported 84 changed methods without an @SPEC tag
against base origin/beta. All 84 were in src/; zero were PHP -- the 273
lib/**/*.php files in the same diff are fully annotated already, established
with a positive control (WeatherService.php carries 2 tags, HealthPingController
3) so that "all in one layer" is a measurement rather than a failed lookup.

Closed with 84 real @SPEC anchors and exactly 2 reason-bearing exclusions.

The anchors point at the specs the methods actually implement -- principally
clock-weather-widgets, iframe-embed-widget, live-data-tile-widget,
service-health-ping and tile-quick-search. Every one resolves: the canonical
check_spec_anchors.py reports 0 dangling anchors across all of src/. That
matters because a dangling anchor fails SILENTLY in this gate family, so an
unresolvable tag would have made the gate green while asserting nothing.

The two exclusions, both verified against the source rather than argued from
the scenario text:

  ClockWidget.vue::pointOnCircle -- pure polar-to-cartesian trigonometry. The
  body reads neither this.now nor this.content, so it encodes no clock,
  timezone, format or accessibility behaviour. Verified its only two callers
  are tickCoords and handCoords, and both carry the REQ-CLOCK-003 tag.

  useTileClickTracking.js::__resetTileClickTrackingForTest -- test-only harness
  hook. Verified by grep that its only importer anywhere in the repo is
  src/composables/__tests__/useTileClickTracking.spec.js; it implements no
  product behaviour, only clearing cachedEnabled / inflightConfigPromise so
  each test starts from a cold cache.

Both reasons name a concrete artifact (a caller, an importing test file) rather
than a state of the world, which is the shape that survives re-checking.

This diff is comment-only and provably so: 0 added and 0 removed lines fall
outside a docblock. No runtime behaviour can change.

Verification:

  gate-16 (base origin/beta)   84 -> 0
  check_spec_anchors.py        0 dangling anchors across src/
  tools/check-spec-annotations.php  unchanged at 12 pre-existing PHP findings
                               (TileClickMapper, WidgetPlacement, TileClick,
                               ImportLaunchpadRegister, Version002007) -- none
                               introduced here, none in a file this PR touches
  phpcs                        0 errors, 468 warnings, 211 files (baseline)

No gate was weakened: no baseline, no allowlist widening, no skipped test.
…-003

Landed a moment after the main annotation commit. gate-16 was already 0
without it, so this adds no gate movement -- but the method builds the
accessible label that spells out the temperature unit, which is exactly what
REQ-WEATHER-003 ("Locale-aware units and language, WCAG AA") governs, so the
tag belongs there.

Verified: check_spec_anchors.py reports 0 dangling anchors for the file, and
REQ-WEATHER-003 exists at openspec/specs/clock-weather-widgets/spec.md:119.
The note claimed a 'REQ-GRID-010 "Placement key regeneration"' requirement.
REQ-GRID-010 is titled **Grid Styling**; the stale scenario merely sits inside
it, at spec.md:405. Reworded to say exactly that, to name the line, and to
record that getPlacementKey() -- the method the scenario requires -- no longer
exists.

A wrong diagnosis written into a code comment is worse than no comment: the
next reader inherits it as established fact and nobody re-measures it. Filed
the substance as #101 rather than leaving it only in a docblock.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ 237fb1e

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
composer ✅ 102/102
npm ✅ 548/548
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-11 21:06 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ 46fad76

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
composer ✅ 102/102
npm ✅ 548/548
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-11 21:24 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 19e9b49 into development Aug 11, 2026
27 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/launchpad-gates-round-4 branch August 11, 2026 21:29
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.

1 participant