Live means live. White-label advertising management for WordPress.
Advertisers build campaigns. Staff review them. Approval publishes. Native fill
is the only publisher: there is no AdSanity, no downstream ads CPT, and no
theme dependency. Display name and logo are settings; the code prefix is
aggr_ forever.
| Plugin | Aggressive Ads 1.5.0 |
| Slug / text domain | aggressive-ads |
| Namespace | Aggressive\Ads |
| REST | aggr/v1 |
| Default UI name | Advertising (Brand settings may replace it) |
| Author | The Aggressive, LLC |
| Repository | TheAggressive/Aggressive-Ads |
Advertiser portal at /advertiser/ — its own document, not a page with a
block. Sign-in, signup, password setup and recovery are portal-owned;
wp-login.php is untouched and is how staff reach wp-admin. Advertisers never
enter wp-admin (Admin_Guard). Screens: dashboard, campaigns, five-step wizard,
organization, account, help.
Staff admin under one Advertising menu: Review, Organizations, Placements,
Packages, Settings. Distinct capabilities; aggr_access_staff is derived, never
granted on a role. Placements is the slot catalogue (common IAB sizes or
custom W×H). Packages are staff-written; campaigns keep snapshots.
Native delivery is always on, not a Modules checkbox. Editors place
aggr/placement (or aggr_placement( 'header-728x90' ) / the shortcode).
Cached HTML is a reserved slot. After paint: GET /aggr/v1/fill/{slot},
POST /aggr/v1/i beacon, first-party click hop /ads/c/{token}. Impressions
and clicks follow the filled token.
The decision engine picks what fills a slot. An ordered pipeline of pure
stages — eligibility, exact schedule and dayparts, targeting, frequency, pacing,
priority, weighted selection — and every candidate leaves every stage with a
reason, so "why is this ad here?" has an answer. POST /aggr/v1/decisions
decides a whole page at once with competitive separation.
GET /aggr/v1/placements/{id}/decision returns the decision and its full trace
to staff only, replayable from a supplied clock and seed.
Variants. A placement may carry up to ten creatives, and an advertiser sets each one's share of the rotation, its own dates inside the campaign's, and whether it is paused — from the campaign's creative step, without staff. The weighting has always been real; what was missing until recently was any way to reach it outside the REST route.
Reporting (when the Reporting module is on) reads aggr_rollups: dashboard
tiles, a daily impressions chart, campaign list/detail columns, REST
impressions / clicks / ctr. House and other orgs are excluded in SQL.
Spend stays absent until billing has a source.
White-label is Brand settings: product name, logo, tagline, contrast-gated
--aggr-* tokens. A host theme may override tokens; the portal is fully
functional without that.
Built: campaign domain (eleven statuses, 22 legal edges), org-scoped ownership, private creative storage, portal and staff UI, notifications, campaign clock (approved → scheduled → live → complete), assignment-based native fill, the decision engine with targeting, frequency, pacing and priority stages plus staff-only traces, the request-to-conversion measurement model, CSV reporting export, i18n tooling and four locale catalogs, site-scoped tenancy, GitHub updater with SHA-256 verification, Playwright + axe.
Not built, even where docs/ describes the design: spend/billing. If a doc
describes something you cannot find, it has not been built.
| PHP | 8.4+ |
| WordPress | 6.7+ (tests run 7.1) |
| Node | 24.x |
| pnpm | 11.x (packageManager is 11.1.2) |
No runtime Composer packages. vendor/ is dev tooling and never ships. The
production autoloader is inc/class-autoloader.php. See
build-and-release.md.
composer install # PHPCS, PHPStan, PHPUnit — vendor/ never ships
pnpm install # frontend tools + staged-file Git hooks
pnpm build # src/ → dist/
pnpm dev:seed # an advertiser, an org, and five campaigns
pnpm qa:fast # Docker-free code quality and unit gate
pnpm qa:local # qa:fast + the WordPress suites + Studio browser workflows
pnpm qa # exact containerized CI rehearsal
pnpm qa:fresh # recreate the environment, then run the same rehearsalqa:local discovers the Studio site whose
wp-content/plugins/aggressive-ads resolves to this checkout, starts it, and
runs Playwright against the URL Studio reports for it — never one assembled
from a port, which would get the scheme wrong for a site with HTTPS enabled. Set
AGGR_STUDIO_PATH=/path/to/site when two sites serve the checkout and the runner
asks you to choose, and AGGR_STUDIO_URL when Studio reports no address or you
need to override the one it gives.
That site has to opt in before anything runs, because the suite resets the
admin and advertiser passwords to match its fixtures and seeds fixture
campaigns — and nothing puts either back:
touch /path/to/studio/site/.aggr-e2e-site # or: AGGR_STUDIO_E2E_ALLOW=1Theme, permalink structure and the mail-capture mu-plugin are captured up front and restored on the way out, whether Playwright passes or fails.
home and siteurl are the exception: they are set from Studio and left that
way. Studio assigns the port and can reassign it, so a URL captured before a run
can be stale by the next one — restoring it would put the site back to an
address nothing serves. Studio is the source of truth for where a Studio site
lives; AGGR_STUDIO_URL overrides it.
Docker Compose remains the reproducible CI environment. It mounts this
checkout into the pinned Docker Official WordPress image and starts a
disposable MySQL database; local Studio uses SQLite, so MySQL integration,
multisite, combined coverage, release-artifact, and forward-PHP checks remain
authoritative in GitHub CI and available locally through pnpm qa.
pnpm lint:php # PHPCS — WordPress + VIP-Go + PHPCompatibility
pnpm analyse:php # PHPStan level 8, no baseline
pnpm test:php:unit # no WordPress, no database
pnpm test:php:integration # integration / security / rest / upgrade
pnpm test:php:multisite # colliding-id tenancy
pnpm lint:js # ESLint on src/
pnpm typecheck # tsc --noEmit
pnpm lint:css # Stylelint on every authored CSS file under src/
pnpm test:js # Jest on Interactivity helpers
pnpm lint:files # file length, boundaries, permission callbacks, client contract
pnpm ci:coverage # combined unit + integration PCOV coverage
pnpm test:e2e:browsers # install Chromium and WebKit
pnpm test:e2e:install # the same, plus system libraries (needs sudo; what CI runs)
pnpm test:e2e # Playwright + axe (after pnpm build and env:start)
pnpm test:php:native # the WP suites natively; no Docker, no sudo
pnpm db:local # start|stop|status|destroy the local test MySQL
pnpm test:e2e:studio # Playwright + axe against the current Studio site
pnpm qa:fast # Docker-free pre-push code and unit checks
pnpm qa:local # qa:fast + native WP suites + Studio browser workflows
pnpm qa # every CI lane, serially; requires a clean worktree
pnpm qa:fresh # clean database/container rehearsal
pnpm env:stop # remove the disposable containers and databaseEach ci:* script maps 1:1 onto a GitHub Actions job. The local rehearsal
derives its commands from that workflow so the two cannot drift.
pnpm install enables the repository's Git hooks. Pre-commit formats and
re-stages only selected files, commit-msg enforces Conventional Commits, and
pre-push runs pnpm qa:fast, which needs no Docker at all — ShellCheck comes
from a checksum-pinned binary in .cache/ci/, the same way the i18n lane gets
WP-CLI. Run pnpm qa:local when a change touches a browser workflow. The full pnpm qa rehearsal installs Playwright's
browsers but not their system libraries, so it never asks for a password; CI
installs those with --with-deps on a bare runner. See
build-and-release.md for the CI graph, release
artifact chain, and branch-protection contract.
aggressive-ads.php header, constants, floor guard, hand-off
└ inc/class-autoloader.php Aggressive\Ads\X\Y_Z → inc/X/class-y-z.php
└ inc/class-plugin.php boot + ordered init_services()
└ inc/class-service-registrar.php factories — instantiates nothing
Three boundaries fail the build when crossed, all enforced by
bin/ci/check-repository-boundary.sh:
inc/Repository/is the only place data access appears. NoWP_Query,get_posts(),get_post_meta(),$wpdbanywhere else ininc/.- AdSanity identifiers appear nowhere in
inc/ortemplates/. Native fill is the only publisher, and the guard refuses a bareadsanity_get_ads()as readily as a class name. inc/Domain/calls no WordPress function. Campaign rules stay unit-testable without a bootstrap.
Start with docs/README.md. Short version:
| Doc | Covers |
|---|---|
| architecture.md | Layers, native fill, Placements, no downstream ads CPT |
| domain-model.md | Five entities, storage, invariants |
| campaign-workflow.md | The state machine every status write goes through |
| roles-and-capabilities.md | Who may do what |
| threat-model.md | Attack surfaces and the test proving each |
| platform-p3-decision-engine.md | The serving pipeline, its stages and trace contract |
| roadmap.md | What shipped vs remaining product |
| suite-roadmap.md | Durable suite/product direction |
| work-tracking.md | Issue hierarchy, dependencies and execution order |
GPL-2.0-or-later.