Skip to content

Platform foundation: converge governance, strategy and release engineering, then close P0-4 and P0-10 - #7

Closed
Shubochandrosarker wants to merge 50 commits into
mainfrom
claude/p0-4-plugin-check-errors
Closed

Platform foundation: converge governance, strategy and release engineering, then close P0-4 and P0-10#7
Shubochandrosarker wants to merge 50 commits into
mainfrom
claude/p0-4-plugin-check-errors

Conversation

@Shubochandrosarker

Copy link
Copy Markdown
Contributor

What this changes

Wordpressistic/memberistic has been at the 2.0.0 baseline since release, while the engineering work — governance, strategy, release automation, the WordPress integration harness, and the security suites — accumulated in the working mirror. This is the Task 1 convergence: it brings the official repository up to that line and includes the two P0 backlog items closed on top of it. 50 commits, 73 files, +8,035/−61.

Read this before reviewing: this is deliberately not a one-outcome PR, and it is large. It is a repository-convergence PR, which docs/strategy/00-master-plan.md §3 treats as its own reviewed change. Every commit in it has already been reviewed and merged in the mirror; nothing here is unreviewed work. If you would rather take it in stages, say so and I will split it — the natural seams are governance/strategy docs, then CI and the test harness, then the two P0 fixes.

Closes: backlog P0-4 (errors half) and P0-10. Advances P0-0, P0-1, P0-3, P0-5, P0-6.

Type

  • Bug fix
  • Feature
  • Refactor (no behaviour change)
  • Documentation
  • Security fix
  • Release/CI plumbing

What is in it

Governance and strategy (19 files). docs/strategy/ 00–10 and sources.md, docs/governance/ (branching, release process, versioning, ADRs), issue and PR templates, CODEOWNERS. Planning material — none of it ships; .distignore excludes both trees.

Release and CI plumbing. release.yml (tag-triggered, verifies the version across its five homes, builds from .distignore, asserts no dev files leaked, SHA-256, drafts the Release), integration.yml (WP 6.8 / 6.9 / 7.0.3 × PHP 8.2 / 8.3 / 8.4 plus a non-blocking trunk canary), and the blocking plugin-check job.

Test harness (12 files). bin/install-wp-tests.sh, tests/integration/ with activation, fresh-install, REST authorization, REST ownership, route-inventory and webhook-security suites, the record factory, and the HTTP interception layer. The integration suite is pinned to PHPUnit 9.6 — the WordPress core test library still calls PHPUnit\Util\Test::parseTestMethodAnnotations(), which PHPUnit 10 removed — while the unit suite stays on 10.5. That is why there are two phpunit*.xml files.

Production code (28 files, +2,179/−48) — overwhelmingly the P0-4 fixes:

  • P0-4 — Plugin Check went from 70 errors to 0, with warnings unchanged at 266 across the two runs. That pairing is the evidence the errors were fixed rather than reclassified: nothing was added to the action's ignore list and no severity was downgraded. 44 i18n, 15 escaping, 10 filesystem, 1 heredoc.
  • P0-10 — invariant I5 is now enforced statically as well as at runtime, via an allow-list of the files permitted to make an outbound request, each mapped to the integration that gates it.
  • A Windows-only path bug in DependencyManifestTest that made the require-list guard unusable outside Linux CI.

Rules touched

  • PMPro runtime independence
  • No seeded/priced plans on fresh install
  • Entitlements fail closed
  • A lapsed licence never breaks member access
  • No outbound HTTP on fresh activation
  • Every REST route has a real permission_callback
  • None of the above

Both are strengthened, not relaxed. I5 previously held by design; FreshInstallTest now proves activation and init reach the network zero times, and OutboundHttpAllowlistTest fails any new call site regardless of whether it went through the registry. I6 gains RestRouteInventoryTest, which enumerates every route and asserts none uses __return_true, plus negative authorization tests for anonymous and subscriber callers across every parameterless GET route and a positive administrator control, so a route that rejected everybody could not pass as secure.

Two findings worth carrying forward

  • A stale phpcs:ignore fails silently. Two readfile() calls carried suppressions naming file_system_read_readfile; WPCS renamed the sniff to file_system_operations_readfile, so they matched nothing and the calls were reported anyway. The sibling in class-documents.php uses the current code and was never reported — that mismatch is what exposed it. Nothing warns you when a suppression stops matching.
  • Bulk comment insertion can produce rendered text. Adding translators: comments by line number put five of them into inline HTML in the corporate module, where a bare /* … */ line is text, not a comment, and would have rendered on the group admin screens. Caught by tokenising each file and asserting every translators: line is a real T_COMMENT — the diff looks correct either way.

Multi-edit checklist

  • N/A — no new production classes, no schema change, no new shortcode, no new capability. MEMBERISTIC_DB_VERSION is untouched at 1.11.0, and no version bump is included: this is not a release PR.

Documentation

  • No docs needed for the code changes, because no hook, integration, entitlement, shortcode or REST route changed behaviour. CHANGELOG.md and readme.txt are deliberately untouched — nothing user-visible changed. The backlog entries for P0-4 and P0-10 are updated in this PR, which is where those promises are tracked.

Follow-up not included: four translatable strings changed msgid (placeholder reordering). Only languages/memberistic.pot ships — no compiled .po/.mo — so no translation is lost, but the .pot is stale for those four entries and lacks the new translator comments. Regenerating needs WP-CLI (wp i18n make-pot).

What I ran

Locally:

$ find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l
(no output — 96 files clean)

$ find assets -name '*.js' -print0 | xargs -0 -n1 node --check
(no output — 11 files clean)

$ phpunit -c phpunit.xml
OK (56 tests, 848 assertions)

In CI on the mirror, on this exact tree — 29/29 checks green, including the full WP 6.8 / 6.9 / 7.0.3 × PHP 8.2 / 8.3 / 8.4 integration matrix, the trunk canary, and Plugin Check:

Plugin Check, run 31282568986 : TOTAL ERRORS: 0   (was 70)
warnings                      : 266  ->  266      (unchanged — nothing silenced)

The two new guard tests were mutation-tested in both directions rather than trusted because they were green: adding a wp_remote_get() to an unlisted file fails with the file and line, and flipping coreSTORE to 'default' => 'yes' fails the gating assertion.

What I did NOT test

  • Plugin Check locally — it needs a real WordPress and Composer. CI is the verification, and it is green on this tree.
  • The corporate admin screens visually. Five comments moved inside <?php ?> in inline HTML and ?> swallows the following newline, so the emitted markup shifts by a newline in five places. Inert in HTML, but not eyeballed.
  • The CSV importers end to end — only comments and annotations changed in them, but no import was run.
  • P0-2 entirely — no tag, no release artifact, no zip smoke-install, no upgrade-with-real-data test. Unchanged by this PR and still open.
  • Multisite — still open under P0-1.
  • This PR's own CI on Wordpressistic. The green run above is from the mirror. If the workflows behave differently here, that will show on this PR.

Risk and rollback

  • Blast radius if this is wrong: the docs, CI and test files carry no runtime risk — none of them ship. The production surface is the P0-4 diff: three wp_die() argument escapes and two integer casts, a CSS literal delimiter change, and comment/annotation additions. The realistic failure mode is a display regression on the corporate group screens, not data loss.
  • How to roll it back: revert the merge commit. No data migration, no coordination.
  • Does it need a data migration to reverse? no

One behaviour delta, verified rather than claimed identical

admin_css() returned a 79-line CSS heredoc, which Plugin Check disallows. The body contains no $, backslash or single quote, so the delimiter swap needs no escaping — but it is not byte-identical: a quoted string includes the newlines adjacent to its quotes where a heredoc excludes the one before its terminator.

heredoc version : 29635a0e862d33386b50561995322803  8858 bytes
string  version : ea26e9cfe34e6d85836c52bce9bc3549  8860 bytes
relationship    : new === "\n" . old . "\n"

It is enqueued as an inline stylesheet, where leading and trailing whitespace is inert.

🤖 Generated with Claude Code

Shubochandrosarker and others added 30 commits August 8, 2026 18:22
Brings the full plugin source and its commit history into this personal
repository. The placeholder README this repo was created with is folded in
via an unrelated-histories merge so `main` and this branch share an
ancestor; the upstream README wins the conflict because it is the real
product README.

`release/2.0.0` and the `public_release_v_2.0.0` tag are mirrored alongside
this branch so the published 2.0.0 artifact stays reachable here.

No plugin code is modified by this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
Organises the personal repository for long-term, multi-participant work.
Everything here is new; no plugin source is touched.

- CLAUDE.md: the agent orientation guide, with a section pointing at the
  strategy and governance material this repo now carries.
- CONTRIBUTING.md: the seven non-negotiable rules (each tied to the guard
  test that enforces it), the multi-edit traps (no autoloader, the
  three-edit database rule, shortcode registration), and WPCS style.
- SECURITY.md: private reporting, scope, and the deliberate behaviours that
  look like findings but are not — secrets-in-constants, signature-first
  webhook handling, path-validated template overrides.
- SUPPORT.md, CODE_OF_CONDUCT.md, .editorconfig.
- .github: CODEOWNERS annotated by blast radius, a PR template that asks for
  real command output rather than ticks, and structured issue forms.
- .github/workflows/release.yml: tag-triggered only, so it can never turn an
  ordinary PR red. Verifies the version string across its five homes against
  the tag, builds the zip from .distignore, asserts no dev files leaked in,
  publishes a SHA-256, and drafts — never publishes — the release.
- docs/governance: branching, versioning, the release gate, and ADRs 0001
  (why decisions get recorded) and 0002 (this migration, including the tag
  push that the proxy blocked).
- docs/README.md: one documentation index and repository map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
Turns the 2026-08-08 master audit from a report into a program of work.

docs/strategy/00-master-plan.md is the spine and carries what the audit did
not: eight invariants each tied to the guard test that enforces it (and an
explicit note where nothing enforces it yet — I4, I5 and I6), six workstreams
with a dependency graph, milestones M0-M5 with entry and exit criteria, and a
ten-entry risk register with triggers and mitigations.

The rest is detail hanging off it: audit findings with a severity index that
the backlog references by ID, the god-class refactor targets and the WPCS
migration, the required test pyramid and compatibility matrix, the
Free/Pro/Lifetime/Cloud model with the $1M arithmetic labelled as arithmetic,
the competitor cross-match, the staged Cloudflare design (shared Worker in V1,
Workers for Platforms only when a paying customer needs custom code), the
website and distribution plan, metrics with the decision each one informs, a
backlog of 27 items with acceptance criteria, the standing agent brief, and a
sources file with a re-check rule.

Every external fact is dated 2026-08-08 and flagged for re-verification before
publication. Nothing here describes shipped functionality.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
Verifying the release packaging locally showed the newly added files were
shipping to end users: CLAUDE.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md,
SUPPORT.md, .editorconfig, and — worse — docs/strategy, which contains
pricing models, revenue targets and competitor analysis. None of that
belongs in a customer's wp-content/plugins directory.

Adds them to .distignore. This is the one existing file the migration
modifies; it is packaging configuration, not plugin code, and the change is
a direct consequence of the files added alongside it. The product
documentation under docs/ (HOOKS, INSTALL, INTEGRATIONS, UPGRADE-2.0,
entitlements, guest-pass-audit) still ships, as it always has.

Also replaces rsync in the release workflow with tar plus an explicit
.distignore sweep, so the packaging step depends only on coreutils and logs
what it excluded.

The unit suite was run during this verification and reproduces the audit's
previously unverified claim exactly — 47 tests, 831 assertions, on PHP
8.4.19 with PHPUnit 10.5.64. Three strategy documents that described that
figure as an unreproduced claim are corrected, with the caveat that 746 of
those assertions come from the two source-scanning guard tests and so are
not a measure of behavioural coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
The designed web summary is a view of 00-master-plan.md, not a second
source of truth — noted so it gets regenerated rather than edited apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
…ation-profile-xxcxft

Mirror Memberistic 2.0.0 and establish the master plan + repository organisation
P0-1 asks whether Memberistic works on current WordPress. That question
cannot be answered by the unit suite, which runs with WordPress stubbed and
so cannot see a schema failure, a capability that did not land, or an API
that WordPress deprecated. This adds the harness that can.

- bin/install-wp-tests.sh installs WordPress core and the PHPUnit test
  library for any published version. It fetches the test library from the
  wordpress-develop GitHub tarballs rather than Subversion, because the
  Actions Ubuntu images no longer ship svn and a missing binary is a
  confusing way to fail a compatibility matrix. Asking for a version that
  does not exist fails with the list of versions that do.

- tests/integration/ runs against a real WordPress, a real database and a
  real activation. Any _deprecated_function/_deprecated_argument/
  _deprecated_hook/_doing_it_wrong notice fails the suite, including ones
  raised during plugin load. That is deliberate: maintaining a list of what
  each WordPress release deprecated would be stale the day it was written
  and impossible to keep correct across three release lines. Letting
  WordPress report them finds the real set on every version in the matrix.

- First tests cover the P0-1 installation flow (schema, DB version,
  capabilities, roles, the deliberately narrower PII capability, cron), and
  give invariants I2, I5 and I6 their first behavioural coverage: fresh
  activation issues zero outbound HTTP requests, no plans are seeded,
  third-party integrations default off, uninstall retains data, and every
  REST route declares a permission callback that is not __return_true.

- .github/workflows/integration.yml runs WP 6.8/6.9/7.0.2 x PHP 8.2/8.3/8.4,
  preceded by a resolve job that reports which requested versions actually
  exist, plus a non-blocking trunk canary. A green canary is not support.

Static pre-check found no PHP 8.4 problems: no implicitly nullable
parameters, no E_STRICT, no removed functions. HPOS and cart/checkout blocks
compatibility is already declared in the bootstrap.

`Tested up to` is deliberately unchanged. It moves when the matrix passes,
not when the harness that will run it is written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
The harness's first CI run failed all nine matrix jobs before running a
single test. The failure was in my installer, not in the plugin.

wordpress-develop does not tag every release the way wordpress.org
publishes it — `refs/tags/6.8` is a 404 — and my fallback derived the
branch name with `${ref%.*}`, which turns "6.8" into "6" rather than "6.8".
Both fetches 404'd and the unguarded second one tripped `set -e`.

Replaces the single guess plus one fallback with an ordered candidate list
(exact tag, minor-line branch, minor tag, trunk), tried in turn and
reported. The minor-line branch is the reliable entry: it always exists and
always carries a test library compatible with that line. Failing every
candidate now prints what was tried instead of a bare curl exit code.

Also makes the resolve job write to stdout as well as the step summary. The
summary is only readable in the web UI, and that job exists precisely so
"does WordPress 7.0.2 exist" is answerable from the logs.

WordPress core downloaded fine on every job, and the resolver correctly
reported `Requested WordPress '6.8' → installing '6.8'`, so version
resolution and core installation are already proven to work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
Second harness failure, and the last blocker before tests actually run.
The WordPress test suite hard-requires Yoast's PHPUnit Polyfills and
refuses to bootstrap without them:

  Error: The PHPUnit Polyfills library is a requirement for running the
  WP test suite.

Adds the dependency and defines WP_TESTS_PHPUNIT_POLYFILLS_PATH in the
integration bootstrap before anything from the test library loads, plus the
Composer autoloader for the case where the path resolves but the classes
are not registered. This also brings composer.lock back in step with
composer.json, which CI had been warning about.

What the run proved, and it is the answer P0-1 needed: WordPress 6.8, 6.9
and 7.0.2 all exist and install. Every "Install WordPress <version> and the
test library" step reported success across all nine jobs, so the requested
compatibility matrix is valid and needs no correction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
…ation-profile-xxcxft

P0-1: WordPress integration test harness and compatibility matrix
Third harness failure. With the polyfills in place the suite got as far as
WordPress's own bootstrap and fatalled there, traced back to my activation
hook.

Activator::activate() ends in flush_rewrite_rules(), and WordPress does not
instantiate $wp_rewrite until after `plugins_loaded` has fired — so hooking
activation there fatals before a single test runs. Moving the call to after
the WP bootstrap fixes the ordering and, as a bonus, puts the plugin's
tables outside the per-test transaction so they survive the rollback
WP_UnitTestCase performs between tests.

This is a harness bug, not a plugin bug: a real activation runs on an admin
request, long after $wp_rewrite exists. Nothing in includes/ is implicated.

The deprecation check now runs after activation as well as after plugin
load, so a deprecated call made during activation also fails the suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
…ation-profile-xxcxft

Fix integration bootstrap: activate after the WordPress bootstrap
F-03, F-04, F-05 and F-07 are each written as a missing test suite, and
none of them can be started. tests/bootstrap.php does not load WordPress:
it stubs the WP functions the two behavioural tests need and shadows the
Database\* repositories with static fixtures, so there is no request, no
user, no capability map and no real $wpdb. Nothing to authorise against,
upload to, or post a webhook at.

03-quality-and-release.md already calls a real WordPress test environment
"the missing piece that unlocks most of the remaining coverage", and
00-master-plan.md makes it the M1 entry condition — but the backlog is
where work is picked from, and it had no item. The dependency would have
surfaced on the first day someone opened P0-5.

Adds P0-0 with acceptance criteria, marks P0-5/6/7/8/10 and P1-9 blocked
by it, and records F-15. F-15 is appended rather than inserted in
severity order because renumbering would break every cross-reference.

No scope is added: those six items always needed a harness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The strategy docs targeted 7.0.2 (2026-07-17) throughout. That was already
superseded when they were written: 7.0.3 shipped 2026-08-06, two days
before the audit date. 7.1 ships 2026-08-19, so this number has a short
shelf life — sources.md now carries the 7.1 schedule alongside it, and
the audit section says to re-check wordpress.org rather than trust the
figure written down.

Also records the static deprecation audit against the 7.0 field guide:
no block APIs, no Interactivity API, no add_theme_support, no PHP 8.4
implicit nullables, no deprecated functions, textdomain correctly on
init. No source changes are needed for 7.0.3.

That result matters for sequencing: it narrows P0-1 to running the
matrix, so P0-0 rather than any code fix is what stands between here and
closing it. The section says plainly that a static audit is not a passing
matrix.

`Tested up to` is untouched — VERSIONING.md governs when it moves, and
P0-1 acceptance already requires the matrix first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…em-and-wp-version

Docs/backlog harness item and wp version
Three changes, all prompted by the run on main failing all nine jobs in
about a second with no readable output.

1. The bootstrap no longer exits on a captured deprecation. That was wrong
   twice over: it cannot tell a notice the plugin caused from one WordPress
   core raised during its own bootstrap, so a core-internal notice would
   block an otherwise-fine version; and a bootstrap that exits produces no
   PHPUnit output at all, which is exactly the failure mode that has cost
   several rounds to diagnose. The notices are now printed and asserted by
   LoadDeprecationsTest, so the same condition arrives as a named failing
   test with the offending notices in its message.

2. The workflow tees the suite output and repeats it in an `if: failure()`
   step. The output otherwise sits mid-log above forty lines of MySQL
   container chatter, where it is awkward to reach through the API.

3. Matrix corrected 7.0.2 → 7.0.3, following the docs correction in #4.
   7.0.2 was superseded on 2026-08-06, before the audit was written.

LoadDeprecationsTest also echoes the WordPress, PHP, plugin and schema
versions it ran against, so a green run states what it was green on instead
of leaving that to be inferred from the job name.

Still no plugin source change, and `Tested up to` is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
P0-4 asks for Plugin Check running on every PR, blocking, and P0-0 asks for
the unit suite's coverage source to describe the plugin rather than the one
file it was scoped to. Both land here.

Plugin Check runs against the distributable tree, not the repository. The
tree is rebuilt from .distignore using the same copy-then-remove construction
release.yml already uses, so the check sees exactly what the release job zips
and what a wordpress.org reviewer would open. Restating the exclusions as
action inputs would have created a second list to keep in step with the
first. The slug is left as the real install directory rather than overridden
to match the shorter "memberistic" text domain: if those two disagree, that
is a finding for P0-4 to resolve, not one to hide from the check that exists
to find it.

No categories filter and no pinned wp-version, so the job runs the full
default check set the way the .org review environment does.

The unit suite now runs on PHP 8.2, 8.3 and 8.4 instead of 8.3 alone. It is
the only suite with no database behind it, so it is the cheapest place to
catch a change that parses everywhere but only misbehaves on 8.4. The
Composer cache key gains the PHP version, because one shared key would let
whichever job finished first decide what the other two actually tested.

Also corrects the integration matrix from WP 7.0.2 to 7.0.3, which is the
target the strategy docs were moved to in d0b719a and which integration.yml
was left behind on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
The integration harness found a real bug on its first successful run, and
it is not a compatibility problem — it reproduces identically on WordPress
6.8, 6.9 and 7.0.3 across PHP 8.2, 8.3 and 8.4:

  Error: Class "WordPressistic\Memberistic\Integrations\Booking_Adapter"
  not found
  #0 includes/class-plugin.php(145):
     WordPressistic\Memberistic\Waivers\Waiver_Booking_Bridge::register()

includes/integrations/class-booking-adapter.php ships in the plugin but was
never listed in Plugin::load_dependencies(). With no autoloader, the class
therefore does not exist at runtime. `php -l` passes, the unit suite passes,
and the file looks present in the tree — which is exactly the trap CLAUDE.md
warns about.

The consequence is not theoretical. Waiver_Booking_Bridge::register() runs
on `init` priority 4 whenever the Waiver Manager integration is enabled —
one of only two integrations that default to *on* — and its first statement
is Booking_Adapter::hook(), with no class_exists guard. Staff_Dashboard and
POS_Bridge reach the same class outside the booking toggle. A fresh install
fatals on init.

Fixes it the way the codebase expects: one entry in the require list, next
to its siblings, with a comment on why it cannot be gated behind the booking
toggle.

Adds tests/unit/DependencyManifestTest.php, which compares every PHP file
shipped under includes/ against the require list in both directions and
allow-lists class-plugin.php with its reason. It fails without this fix and
passes with it, runs in milliseconds, needs no WordPress, and would have
caught this before CI ever ran. A scan of the whole tree found exactly one
such omission — this one.

  vendor/bin/phpunit -c phpunit.xml
  OK (51 tests, 839 assertions)   # was 47/831

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
With the Booking_Adapter fatal fixed, the plugin boots and 21 tests actually
ran — and all 21 errored identically:

  Error: Call to undefined method
  PHPUnit\Util\Test::parseTestMethodAnnotations()
  /tmp/wordpress-tests-lib/includes/abstract-testcase.php:568

PHPUnit 10 removed that method; the WordPress core test library still calls
it. This is not a matter of picking a newer WordPress branch — it reproduces
on 6.8, 6.9, 7.0.3 and trunk alike.

So the integration suite runs on PHPUnit 9.6 while the unit suite keeps the
10.5 that composer.lock pins. The integration jobs install 9.6 over the top
after `composer install`, leaving composer.json, composer.lock and the unit
suite untouched. phpunit-integration.xml moves to the 9.6 schema, dropping
the four attributes that are 10-only.

This is a constraint WordPress imposes, not a preference, and it has a
consequence worth recording: the two suites cannot share one phpunit.xml,
because they cannot share a PHPUnit major. P0-0's acceptance criterion asking
for exactly that is amended in the backlog with the reason and a note to
merge them if WordPress ever adopts PHPUnit 10+.

PHP-level deprecations are deliberately not converted to exceptions under
9.6: WordPress core raises its own on newer PHP, and failing on those would
report a Memberistic incompatibility that is not one. The signal that matters
— deprecations WordPress reports about the plugin — stays with
LoadDeprecationsTest, which can name what triggered it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
The two CI gates added in the previous commit both went red on their first
run, on the same pre-existing bug, on every WordPress and PHP combination:

  Fatal error: Class "WordPressistic\Memberistic\Integrations\Booking_Adapter"
  not found in includes/waivers/class-waiver-booking-bridge.php:29

includes/integrations/class-booking-adapter.php has been present since the
2.0.0 baseline and was never added to Plugin::load_dependencies(). Nothing
autoloads, so the class simply did not exist at runtime.
Waiver_Booking_Bridge::register() calls Booking_Adapter::hook() with no
class_exists guard, on init at priority 4, whenever the Waiver Manager
integration is enabled — and that one defaults to on. So this fatalled on
load for every install, not only for anyone who had mapped a booking engine.
Booking_Engine, POS_Bridge and Staff_Dashboard consult the same adapter.

php -l could never have caught it: every file parses perfectly on its own.
The unit suite could not either, since it loads exactly two production files.
It took booting a real WordPress, which is precisely what the new gates do.

The adapter is now required ahead of its consumers, and a guard test asserts
the two lists agree in both directions — every file under includes/ appears in
the require list, and every required path exists. Verified it fails when the
fix is reverted, rather than passing vacuously. It scans source rather than
booting anything, matching FreshInstallDefaultsTest and PmproRemovalTest, so
it stays in the fast unit suite where the whole point is catching the omission
before a real WordPress ever sees it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
The suite ran properly for the first time: 21 tests, 132 assertions, 2
failures, 1 risky — on WordPress 7.0.3 / PHP 8.3. Both failures were mine,
not the plugin's.

1. RestRouteInventoryTest treated `/memberistic/v1` — the namespace index —
   as a plugin route. WordPress core registers it for every namespace
   (`/wp/v2` behaves identically); it is the API discovery document, public
   by design, and it exposes route schemas rather than member data. Holding
   the plugin's permission-callback rule against a core-owned route asserts
   the wrong thing. Excluded, with the reasoning recorded.

2. LoadDeprecationsTest echoed the environment, which makes a test risky
   under beStrictAboutOutputDuringTests. Replaced with something that earns
   its place: an assertion that the running WordPress matches the
   WP_RESOLVED_VERSION the installer reported. A job that silently tested a
   different version than its name claims is exactly the failure mode
   `Tested up to` exists to prevent.

Everything else passed, which is the first real evidence about the plugin
rather than about the harness — including invariants I2 and I5 now proven
behaviourally, and no deprecation reported on load or activation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Raja36yUJCZzJ2ZapzKQzV
…baseline

Two blockers, both surfaced by the gates added earlier in this branch.

The integration suite could never have run. The WordPress test library calls
PHPUnit\Util\Test::parseTestMethodAnnotations() from WP_UnitTestCase::set_up(),
to read @expectedDeprecated docblocks, and PHPUnit 10 removed that method.
composer.json pins ^10.5, so all 19 tests errored before their first assertion
on every WordPress and PHP combination. The trunk canary failed identically,
which rules out this being something a newer WordPress fixes. CI now installs
the 9.6 stack for this suite only; the unit suite keeps 10.5 because it never
loads WordPress. phpunit-integration.xml moves to the 9.x schema, trading
failOnDeprecation/failOnNotice for convertDeprecationsToExceptions and
convertNoticesToExceptions. The suite's real job is unaffected either way:
WordPress-level deprecations are caught by WP_UnitTestCase and the load-time
hooks in the bootstrap, neither of which depends on the PHPUnit version.

Plugin Check stays blocking, with a documented ignore list rather than a
loosened one. The ignored codes are the five DB sniffs this plugin answers
deliberately — it stores data in its own tables and queries them with raw $wpdb
and prepare(), and the interpolated values are table names from a repository's
table() method, never input. All five are already suppressed inline case by
case, with reasons, in fifty places. Nothing sets ignore-warnings or
ignore-errors, so any other finding still fails the job.

The unprefixed globals it flagged in uninstall.php were a real finding, not an
architectural one, so they are fixed rather than ignored: that file runs at
file scope, which makes $site_ids and $site_id globals in WordPress's namespace.

The report is now printed into the job log as well as uploaded, so the finding
codes are greppable without downloading an artifact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…uite-oj82x9

Add Plugin Check to CI and widen the unit suite's PHP matrix
…P0-6)

Written against the REST surface as it actually is, which differs from the
audit's description in three ways that matter.

The IDOR shape does not apply to most of this surface. Every
/memberships/{id}/* route is gated on a staff capability, never on ownership
of {id}: a member never reaches the ownership question because they never
clear the capability gate, and a user who does clear it is staff, who are
meant to read every member. "Member A cannot read member B's payments" passes
today for the wrong reason, and would keep passing if ownership scoping were
removed entirely. It is recorded here as capability enforcement rather than as
an isolation guarantee the code does not make. The one genuinely
ownership-scoped member-facing route is /profile/image, and that is tested in
both directions — a subscriber without a membership is refused, a member with
one is admitted.

There are no /documents or /waivers sub-routes. Tests against them would have
404'd, and a 404 satisfies "expected a rejection", so they would have been
permanent false passes. Every route these suites name is asserted to exist
first, and the fixtures assert their own inserts succeeded — the schema needs
membership_uuid and billing_cycle, and a silently failed insert would leave
every assertion pointing at id 0.

memberistic_staff holds view_memberistic_pii and memberistic_cashier holds
view_memberistic_dashboard, so the PII boundary runs between manager/staff and
cashier/instructor/POS staff. It is asserted in both directions, alongside an
administrator positive control, because a suite of negative-only assertions
cannot tell a working gate from an endpoint that rejects everyone.

The webhook suite constructs valid signatures as well as bad ones for the same
reason: malformed JSON only reaches the parser behind a valid signature, which
is what proves verification happens before parsing. The WooCommerce HMAC path
cannot run while WooCommerce is absent from the matrix — the handler returns
503 first — so rather than leave three permanently-skipped tests reading like
coverage, that gap is asserted narrowly and recorded in the backlog.

Also stops RestRouteInventoryTest asserting against /memberistic/v1, the
namespace index WordPress core registers for every namespace. It has no
permission callback and is public on every WordPress site for every plugin;
the two failures it caused were core's behaviour, not the plugin's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…heck baseline

The new suites ran for the first time: 43 tests, 385 assertions, one error —
and the error was a real bug in production code, not in the tests.

WP_REST_Request::get_header() returns NULL for a header that was not sent, and
the '' === $header guard in stripe_webhook() is strict, so an unsigned request
slipped past it and reached explode() inside verify_webhook_signature() as
NULL. The request was still refused — the parse simply found no t/v1 pair — so
this was never an authentication bypass. But it wrote a PHP deprecation on
every unsigned POST to a public, unauthenticated endpoint, which is a log
flooding primitive available to anyone. Both layers now normalise to a string
before the strict comparison: the handler because that is where the NULL
enters, and Stripe_Service::verify_webhook_signature() because it is a public
static entry point that should not trust its caller.

The Plugin Check ignore list guessed in the previous PR matched nothing. Plugin
Check ships its own PluginCheck.* sniffs rather than reusing the WPCS names the
inline phpcs:ignore annotations use, so the real code is
PluginCheck.Security.DirectDB.UnescapedDBParameter. Corrected from the report,
which the printed-report step added in #6 made readable.

Two of its findings were not architectural and are fixed rather than ignored:
readme.txt declared Tested up to 6.8 while the integration matrix exercises
7.0.3, and the short description and 2.0.0 upgrade notice both exceeded the
lengths wordpress.org supports.

One more was checked by hand and is a false positive: the
ReplacementsWrongNumber hit in memberistic_user_has_membership() builds its
IN() placeholder list dynamically and passes the values as one array, which the
sniff cannot count statically. Four placeholders, four values.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
Two corrections to the previous commit, both read out of the printed report
rather than guessed again.

Plugin Check runs two sniff families over the same code and both fire on the
repository pattern: its own PluginCheck.* sniffs and the WPCS WordPress.DB.*
ones. The first attempt named only the WPCS family and matched nothing; the
second replaced them with only the PluginCheck family, which cleared those
errors and left the WordPress.DB.* warnings behind. The list is now the union,
which is what the report actually contains.

readme.txt asked for a major.minor value: "Tested up to: 7.0.3 The version
number should only include major versions 7.0." Corrected to 7.0. The
integration matrix still pins 7.0.3, since that resolves to a real release to
install; only the readme header is coarser.

Also merges the two "### Added" blocks that accumulated under Unreleased into
one, per Keep a Changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…uite-oj82x9

Add REST authorization, ownership and webhook security suites (P0-5, P0-6)
Shubochandrosarker and others added 20 commits August 9, 2026 00:27
…ation-profile-xxcxft

Make integration failures diagnosable; correct matrix to 7.0.3
wordpress.org requires a plugin's slug — its directory name — to equal its Text
Domain, and Plugin Check reported ~20 WordPress.WP.I18n.TextDomainMismatch
errors because ours did not: the folder was memberistic-membership-solutions
while the domain has always been memberistic. This was the last blocking
finding on the Plugin Check job, and the one predicted when that job was first
added with the slug deliberately left unaligned rather than overridden to hide
the mismatch.

The folder moves to the domain rather than the domain to the folder. The domain
appears at roughly 2,000 call sites — 1,508 in PHP across 50 files, 487 in JS —
plus languages/memberistic.pot and any translation derived from it. The folder
name appears in two workflow variables. Moving the domain would also contradict
the documented text domain and make every __() call longer for no user-visible
gain.

The main PHP file keeps its longer historical name. WordPress derives the slug
from the directory, not the filename, so renaming it as well would churn
plugin_basename() a second time for nothing. release.yml previously used one
variable for both the folder and the main file, so that is now split into
PLUGIN_SLUG and PLUGIN_MAIN_FILE.

Licensing::build_info() no longer hard-codes the slug. It derives it from
MEMBERISTIC_BASENAME, because a literal there goes stale the moment the folder
changes — precisely the "update client that silently never offers an update"
failure its own docblock warns about.

The rename is not free for existing installs: WordPress treats a renamed
directory as a different plugin, so upgrading leaves two copies listed. Nothing
breaks, because the bootstrap's duplicate-copy guard makes the second inert,
but the old entry has to be deleted by hand. That is documented in INSTALL.md
and the changelog rather than left to be discovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
CI reports, on PHP 8.2, 8.3 and 8.4 alike:

  syntax error, unexpected token "*" in ./tests/unit/DependencyManifestTest.php
  on line 90

Line 90 is `continue;`. The blob at the failing SHA is byte-identical to the
working copy, and that same blob parses cleanly here on 8.4 — verified by
linting `git show <sha>:<path>` directly, not just the checkout. The file has
not been modified since it was added, and it passed lint in every earlier run.

I do not have a diagnosis. This commit removes the two variables I can control
without changing behaviour: the only two non-ASCII characters in the file (em
dashes in a docblock and an assertion message, now `--`), and a double-quoted
regex containing backslash sequences, now single-quoted so the string parser
has nothing to interpret. Both are cosmetic; the three tests and their
assertions are untouched and still pass.

If CI still fails at the same line after this, the file is not the cause and
the checkout or runner is, which is worth knowing on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…eable

main is currently red, and PR #8 inherited it: every lint and unit job fails
with

  syntax error, unexpected token "*" in tests/unit/DependencyManifestTest.php
  on line 90

The file on main is a mis-resolved merge. PR #5 wrote its own
DependencyManifestTest independently of the one added in PR #6, and
46329ad ("Merge branch 'main' into claude/memberistic-migration-profile-xxcxft")
spliced the two together textually without conflict markers. The result is 276
lines containing two `class DependencyManifestTest` declarations — one plain,
one final — and a cut straight through the middle of a method body:

  private function shipped_php_files(): array {
      $root  = $this->plugin_root();
   * The paths listed in Plugin::load_dependencies().

That docblock continuation sitting in statement position is the unexpected "*".
It merged to main as d41997d, after PR #7, so main has been unparseable since.

This restores PR #5's version as it stood before that merge — the more recent
of the two intents, and the richer one: it carries reasons alongside each
allowed absence, checks the require list is non-empty, and asserts every
required path exists. It subsumes what PR #6's version tested, so nothing is
lost by dropping the duplicate rather than merging them again.

Verified rather than assumed: it lints, the suite is green at 51 tests, and
re-removing the Booking_Adapter require line makes it fail with two named
assertions instead of passing quietly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…uite-oj82x9

Ship as slug "memberistic" so the directory matches the text domain
main is unparseable and has been through more than one merge. Two sessions
edited the same files in parallel and git merged them textually, producing code
neither author wrote and no conflict marker to flag it.

tests/unit/DependencyManifestTest.php was fatal: 285 lines, two
`class DependencyManifestTest` declarations, and nearly every method present
twice, with the splice cutting through a method body --

    private function shipped_php_files(): array {
        $root  = $this->plugin_root();
     * The paths listed in Plugin::load_dependencies().

That docblock line in statement position is the "unexpected *" that failed
every lint and unit job. Restored to the coherent pre-merge version, which is
the richer of the two: it attaches a reason to each allowed absence, checks the
require list is non-empty, and asserts every required path exists. Verified it
is not a vacuous pass -- removing the Booking_Adapter require line again makes
it fail with two named assertions.

phpunit-integration.xml had two headers describing the same PHPUnit 9 pin and a
second set of attributes stranded after the element had already closed. Kept
the incumbent version, including its better judgement call: PHP-level
deprecations are deliberately NOT converted to exceptions, because WordPress
core raises its own on newer PHP and failing on those would report a
Memberistic incompatibility that is not one. LoadDeprecationsTest asserts the
signal that actually matters and can name what triggered it.

integration.yml had both sessions' PHPUnit-9 steps in each job, installing the
same thing twice. Kept the incumbent step and folded in the one thing the
removed step contributed: an explicit polyfills constraint. composer.json
allows ^1.1 || ^2.0, and resolution without a pin lands on 2.x, but the
combination the matrix was actually verified green on is 9.6 with 1.x. 2.x
declares PHPUnit 9 support and would very likely work -- "very likely" is not
what a compatibility matrix is for.

RestRouteInventoryTest.php had the same namespace-index explanation twice, in
two authors' words. Kept one.

Verified: every tracked PHP file parses, both phpunit configs are valid XML,
all three workflows are valid YAML, assets lint, and the unit suite is green at
51 tests / 839 assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…ices-on-main

Repair four files left spliced by concurrent merges on main
…tegration suite

The two genuinely missing pieces of P0-0. The harness itself, the HTTP
interception layer and the CI matrix already exist and are green; what was
missing was a shared way to create fixture rows, and any documentation of how
to run the suite at all.

Memberistic_Record_Factory covers plans, memberships, people, payments,
check-ins, notes, waiver signatures and documents, plus a member() convenience
that returns user + plan + membership together. Every column default is taken
from Schema::create_tables() rather than guessed.

It exists mainly to make one failure mode impossible. $wpdb->insert() returns
false on a column mismatch instead of raising, so an unchecked fixture insert
leaves insert_id at 0; the test then addresses /memberships/0/..., gets a 404,
and a "member A cannot read member B's data" assertion passes for entirely the
wrong reason. The factory throws at the point of insert, naming the table, the
DB error and the columns it was given. It also carries the NOT NULL knowledge
that is easy to omit: membership_uuid and billing_cycle on memberships,
person_id and checked_in_at on check-ins, note and created_by on notes.

There is no teardown, deliberately. WP_UnitTestCase wraps each test in a
transaction and rolls it back, and fixture rows are written inside it. The
plugin's tables survive because they are created before the suite starts,
outside any transaction.

RestOwnershipTest's four hand-rolled inserts now delegate to it, which both
removes the duplication and exercises the factory — an unused factory would be
unproven code.

tests/README.md gains an integration section: the PHPUnit 9.6 constraint and
why it is forced rather than chosen, how to run the suite locally including how
to get back to the PHPUnit 10 stack afterwards, how to add a fixture, and how
to assert no outbound HTTP.

Not verified locally: this container has no MySQL, so the integration suite
cannot run here. The unit suite is unchanged at 51 tests / 839 assertions and
every file lints. CI is the first execution of the factory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…tory

Add a fixture factory for the Memberistic tables, and document the integration suite
P0-4 asks two questions repeatedly — what is still failing, and is any of it an
ERROR rather than a WARNING — and the current output answers neither without
reading a thousand lines. Every attempt so far has been sampling the tail and
inferring, which is how the ignore list ended up naming the wrong sniff family
twice.

The full report still prints. A per-code histogram and ERROR/WARNING totals now
print after it, so they land at the end of the log where tail reaches them.

Diagnostic only. No behaviour change to what Plugin Check runs or accepts, and
the job still fails on anything not explicitly ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
Improve Plugin Check report readability in CI logs
The per-code histogram answers "what kind of thing is failing"; fixing any of
it needs "and where". The flat grep the histogram uses discards the FILE:
headers, so this adds a second block that walks the report properly and prints
one line per ERROR as path:line code.

Errors only. All 70 of them have to be fixed individually; the 266 warnings are
triaged as classes and do not need per-location output.

Diagnostic only, same as the histogram — nothing about what Plugin Check runs
or accepts changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RKmgnySPtyzaiquDn8DM6
…tions

List Plugin Check errors by file and line
The plugin-check job has been red since it was introduced. It is blocking by
design — the point was to see the real number, not to hide it — but the number
never came down. This clears every error it reports. No warnings are silenced
and no check is downgraded.

i18n (44). Forty-one __()/esc_html__()/_n() calls with placeholders had no
translators comment, and three strings used bare %s/%d twice, which leaves a
translator unable to reorder them for languages that need different word order.
The comments say what each placeholder actually is, because "%s: string" would
satisfy the sniff and help nobody.

Five of the corporate comments had to be wrapped in <?php ?>. They sit in inline
HTML, where a bare /* ... */ line is not a comment at all — it is text, and it
would have rendered on the group admin screens. Caught by tokenising the file
and asserting every translators comment is a real T_COMMENT rather than by
reading the diff.

Escaping (15). The ten in account.php are the CSS prefix, already reduced to
[a-z0-9_-] fifteen lines above the output; esc_attr() is a no-op on that value
and is added so the escaping is visible where the output happens. The other five
are real: two unescaped counts in a corporate notice, and three unescaped
wp_die() arguments in the Stripe service, including a link_url.

Filesystem (10). Two of these were already decided and merely mis-suppressed:
the readfile() calls carried phpcs:ignore comments naming
file_system_read_readfile, but WPCS renamed the sniff to
file_system_operations_readfile, so the suppressions matched nothing. The
sibling call in class-documents.php uses the current code and was never
reported, which is what gave the game away. The rest are php://output export
streams — the HTTP response body, not a file — and two streaming CSV readers.
WP_Filesystem has no streaming API and no CSV parser, and get_contents_array()
splits on newlines, which corrupts any quoted field containing one; member notes
and waiver text both do. Suppressed per line with the reason, the same way the
~50 UnescapedDBParameter cases already are.

Heredoc (1). admin_css() returned a 79-line CSS heredoc. The body contains no
$, no backslash and no single quote, so swapping the delimiters for a
single-quoted string needs no escaping and loses no interpolation. The result
is "\n" . old . "\n" — a quoted string includes the newlines adjacent to its
quotes where a heredoc excludes the one before its terminator — verified by
hashing both. It is enqueued as an inline stylesheet, where leading and
trailing whitespace is inert.

Verification is CI: Plugin Check needs a real WordPress and cannot run in this
environment. Lint is clean on all 96 files. The unit suite is 51/839 with the
one pre-existing Windows-only DependencyManifestTest path failure, which is
fixed separately in #13 and passes on Linux.

Backlog stays unticked until the job is actually green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plugin Check reports 0 errors on run 31282568986, down from 70. Warnings are
unchanged at 266 across the two runs, which is the evidence that the errors
were fixed rather than reclassified or ignored.

The warnings acceptance item stays open, with the outstanding count and the two
dominant codes named so the next person knows what they are picking up.

Also records the two findings worth remembering: a phpcs:ignore naming a
renamed sniff fails silently, and inserting comments by line number can drop
them into inline HTML where they render as text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Shubochandrosarker

Copy link
Copy Markdown
Contributor Author

Closing this — I opened it on a stale view of main.

When I fetched at the start of the session main was at the 2.0.0 baseline, so I built this as the Task 1 convergence. It is now at 2.0.1 and already contains that convergence via #3, the Booking_Adapter fix via #4, and the integration harness via #5. This PR therefore duplicates work that is already here through a divergent history, which is why it conflicts on .distignore, ci.yml, CHANGELOG.md, class-plugin.php and readme.txt.

It also cuts against this repository's own porting policy. docs/strategy/09-execution-backlog.md states that ported acceptance criteria were deliberately reset to unchecked, that a box is ticked only when the evidence is in this repository and its CI is green, and that each portable artifact needs its own pull request and its own verification — porting is not merging. A 50-commit bulk merge that re-ticks boxes is what that policy exists to prevent.

Replacing it with a focused PR off current main covering only what is genuinely missing here: the Plugin Check error fixes (P0-4) and the outbound-HTTP guard (P0-10).

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.

2 participants