Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7ab6289
Add website-spec audit engine: source contract, PHP checks, AI bridge
ilicfilip May 29, 2026
8d6dd1c
Wire spec audit into suggested tasks with per-window throttle
ilicfilip May 29, 2026
c07307c
Add tests for the spec audit engine, throttle, and C/B contract
ilicfilip May 29, 2026
ba74aa5
Fix Spec_Mcp_Client against real WP 7.0 AI client API
ilicfilip May 29, 2026
9caef26
Keep audit's outbound HTTP off the admin_init / FPM path
ilicfilip May 30, 2026
03b286e
Replace robots.txt check with meta-description
ilicfilip May 30, 2026
bbfaf44
Self-heal stale tasks when a PHP-check rule is retired
ilicfilip May 30, 2026
c45f7fd
Fix the AI audit: schema, checklist source, and silent errors
ilicfilip May 30, 2026
deb03b1
Add handoff doc for the spec-audit feature
ilicfilip May 30, 2026
6b977d0
Use spec.website canonical slugs as rule_ids
ilicfilip May 30, 2026
cbaae1c
Don't auto-complete LLM audit tasks when the model omits a rule
ilicfilip Jun 1, 2026
902d36c
Report accurate injected count from run_audit_now; fix handoff cleanup
ilicfilip Jun 1, 2026
f08065b
Merge branch 'develop' into filip/spec-audit
ilicfilip Aug 14, 2026
0408230
Fix Plugin Check CI: WP-CLI nightly for token, bump action to v1.1.9
ilicfilip Aug 14, 2026
c96d1ca
Fix Plugin Check CI: bump plugin-check-action to v1.1.9, drop nightly…
ilicfilip Aug 14, 2026
df71e84
Bump Tested up to: 7.1
ilicfilip Aug 14, 2026
2efc8f2
Bump php_codesniffer to 3.13.6 (CVE-2026-67434)
ilicfilip Aug 14, 2026
3f52e7a
Fix intermittent Plugin Check token failure: unset GITHUB_TOKEN + COM…
ilicfilip Aug 14, 2026
f0ed110
Strip stored Composer github-oauth credential, not just env vars
ilicfilip Aug 14, 2026
3997df7
Tidy plugin-check workflow; drop shipped handoff doc
ilicfilip Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions .github/workflows/plugin-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,42 @@ jobs:
coverage: none
tools: wp-cli

# setup-php stores the ephemeral Actions token in ~/.composer/auth.json
# as a github-oauth credential. Composer rejects that token whenever it
# happens to contain a character its validator dislikes ("Your github
# oauth token for github.com contains invalid characters",
# composer#12076), so this step failed on roughly half of all runs
# depending on the token minted for that run.
#
# dist-archive-command is a PUBLIC package and needs no auth, so the
# fix is to delete the stored credential before installing. Clearing
# the env vars alone does NOT work — they are already empty here, and
# the credential lives on disk (verified in run 31784727273).
#
# Do NOT switch to the WP-CLI nightly to dodge this: it reports as
# wp-cli 3.0.0-alpha, and every released dist-archive-command requires
# wp-cli ^2 / ^2.13, so the install cannot resolve against it.
#
# Pinned to v3.1.0 deliberately: newest release accepting wp-cli ^2.
# v3.2.x requires ^2.13, which has no stable release (latest is 2.12.0).
- name: Install latest version of dist-archive-command
run: wp package install wp-cli/dist-archive-command:v3.1.0
run: |
rm -f "$(composer config --global home 2>/dev/null)/auth.json"
wp package install wp-cli/dist-archive-command:v3.1.0

- name: Build plugin
run: |
wp dist-archive . ./${{ github.event.repository.name }}.zip
mkdir build
unzip ${{ github.event.repository.name }}.zip -d build

# v1.1.7 fixed the wp-env silent-startup failure ("Environment not
# initialized. Run `wp-env start` first.") that broke this job on newer
# runner images; v1.1.9 fixes a bundle regression in v1.1.8. Because
# that fix landed upstream, @wordpress/env needs no version pin here.
# https://github.com/WordPress/plugin-check-action/pull/590
- name: Run plugin check
uses: wordpress/plugin-check-action@v1.1.5
uses: wordpress/plugin-check-action@v1.1.9
with:
build-dir: './build/${{ github.event.repository.name }}'
exclude-checks: |
Expand Down
1 change: 1 addition & 0 deletions classes/class-base.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ public function init() {
if ( \defined( 'WP_CLI' ) && \WP_CLI ) {
$this->get_wp_cli__get_stats_command();
$this->get_wp_cli__task_command();
$this->get_wp_cli__audit_command();
}

// Init the enqueue class.
Expand Down
56 changes: 56 additions & 0 deletions classes/suggested-tasks/audit/checks/class-charset-check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* Check that the homepage declares a UTF-8 charset.
*
* @package Progress_Planner
*/

namespace Progress_Planner\Suggested_Tasks\Audit\Checks;

/**
* Charset check.
*/
class Charset_Check implements Check {

/**
* {@inheritDoc}
*
* @return string
*/
public function get_rule_id(): string {
return 'meta-charset';
}

/**
* {@inheritDoc}
*
* @param string $url The audited URL.
* @param string $html The fetched homepage HTML.
* @param array $context Shared fetch context.
*
* @return array<string, mixed>
*/
public function run( string $url, string $html, array $context ): array {
if ( '' === \trim( $html ) ) {
return [];
}

// Pass if declared either via <meta charset> or a Content-Type response header.
$pass = (bool) \preg_match( '/<meta\b[^>]*charset\s*=\s*["\']?\s*utf-?8/i', $html );

if ( ! $pass && isset( $context['headers']['content-type'] ) ) {
$pass = false !== \stripos( (string) $context['headers']['content-type'], 'utf-8' );
}

return [
'rule_id' => $this->get_rule_id(),
'category' => 'foundations',
'title' => \__( 'Declare a UTF-8 charset', 'progress-planner' ),
'description' => \__( 'Add <meta charset="utf-8"> near the top of your <head>. Without an explicit charset, special characters and emoji can render as garbled text.', 'progress-planner' ),
'severity' => 'medium',
'status' => $pass ? 'pass' : 'fail',
'doc_url' => 'https://specification.website/spec/foundations/meta-charset/',
'source' => 'php-check',
];
}
}
41 changes: 41 additions & 0 deletions classes/suggested-tasks/audit/checks/class-check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Interface for a deterministic (non-AI) spec check.
*
* @package Progress_Planner
*/

namespace Progress_Planner\Suggested_Tasks\Audit\Checks;

/**
* A deterministic check verifies a single specification rule in PHP.
*
* Checks are mechanical — no LLM — so they are fast, free, and unit-testable.
* The registry fetches the homepage HTML once and hands it to every check.
*/
interface Check {

/**
* The stable rule ID this check reports on (e.g. 'html-lang-attribute').
*
* @return string
*/
public function get_rule_id(): string;

/**
* Run the check.
*
* @param string $url The audited URL.
* @param string $html The fetched homepage HTML (may be empty if the fetch failed).
* @param array $context {
* Shared context, so checks don't each make their own request.
*
* @type int $response_code The HTTP status code of the homepage fetch.
* @type string[] $headers Lower-cased response headers from the homepage fetch.
* }
*
* @return array<string, mixed> A single finding (see Audit_Runner schema), with at least
* 'rule_id' and 'status'.
*/
public function run( string $url, string $html, array $context ): array;
}
103 changes: 103 additions & 0 deletions classes/suggested-tasks/audit/checks/class-checks-registry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
/**
* Registry of deterministic spec checks.
*
* @package Progress_Planner
*/

namespace Progress_Planner\Suggested_Tasks\Audit\Checks;

/**
* Holds the deterministic checks and runs them against a single homepage fetch.
*/
class Checks_Registry {

/**
* Get the registered checks.
*
* @return Check[]
*/
public function get_checks(): array {
$checks = [
new Doctype_Check(),
new Lang_Attribute_Check(),
new Charset_Check(),
new Meta_Description_Check(),
new Sitemap_Check(),
];

/**
* Filter the deterministic spec checks.
*
* @param array $checks The registered checks.
*/
$checks = (array) \apply_filters( 'progress_planner_audit_checks', $checks );

return \array_values(
\array_filter( $checks, static fn( $check ) => $check instanceof Check )
);
}

/**
* Run all checks against a URL and return their findings.
*
* The URL is fetched once; the HTML and response metadata are shared with
* every check.
*
* @param string $url The URL to audit.
*
* @return array<int, array<string, mixed>> Findings (one per check that returns one).
*/
public function run( string $url ): array {
$context = $this->fetch( $url );

$findings = [];
foreach ( $this->get_checks() as $check ) {
$finding = $check->run( $url, $context['html'], $context );
if ( ! empty( $finding ) && ! empty( $finding['rule_id'] ) ) {
$findings[] = $finding;
}
}

return $findings;
}

/**
* Fetch the URL once and return shared context.
*
* @param string $url The URL to fetch.
*
* @return array{html: string, response_code: int, headers: array<string, string>}
*/
protected function fetch( string $url ): array {
$response = \wp_remote_get(
$url,
[
'timeout' => 10,
'user-agent' => 'Progress Planner Spec Audit',
]
);

if ( \is_wp_error( $response ) ) {
return [
'html' => '',
'response_code' => 0,
'headers' => [],
];
}

$raw_headers = \wp_remote_retrieve_headers( $response );
$headers = [];
// wp_remote_retrieve_headers() returns a CaseInsensitiveDictionary; getAll() yields a plain array.
$header_array = \is_object( $raw_headers ) ? $raw_headers->getAll() : (array) $raw_headers;
foreach ( $header_array as $name => $value ) {
$headers[ \strtolower( (string) $name ) ] = \is_array( $value ) ? \implode( ', ', $value ) : (string) $value;
}

return [
'html' => (string) \wp_remote_retrieve_body( $response ),
'response_code' => (int) \wp_remote_retrieve_response_code( $response ),
'headers' => $headers,
];
}
}
53 changes: 53 additions & 0 deletions classes/suggested-tasks/audit/checks/class-doctype-check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
/**
* Check that the homepage declares an HTML5 doctype.
*
* @package Progress_Planner
*/

namespace Progress_Planner\Suggested_Tasks\Audit\Checks;

/**
* Doctype check.
*/
class Doctype_Check implements Check {

/**
* {@inheritDoc}
*
* @return string
*/
public function get_rule_id(): string {
return 'doctype';
}

/**
* {@inheritDoc}
*
* @param string $url The audited URL.
* @param string $html The fetched homepage HTML.
* @param array $context Shared fetch context.
*
* @return array<string, mixed>
*/
public function run( string $url, string $html, array $context ): array {
// Can't determine from an empty body — don't emit a false failure.
if ( '' === \trim( $html ) ) {
return [];
}

// Allow an optional UTF-8 BOM and leading whitespace before the doctype.
$pass = (bool) \preg_match( '/^(\xEF\xBB\xBF)?\s*<!doctype\s+html/i', \ltrim( $html ) );

return [
'rule_id' => $this->get_rule_id(),
'category' => 'foundations',
'title' => \__( 'Add an HTML5 doctype to your homepage', 'progress-planner' ),
'description' => \__( 'Every page should start with <!doctype html> so browsers render it in standards mode. Without it, browsers fall back to quirks mode, which can break your layout.', 'progress-planner' ),
'severity' => 'high',
'status' => $pass ? 'pass' : 'fail',
'doc_url' => 'https://specification.website/spec/foundations/doctype/',
'source' => 'php-check',
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
/**
* Check that the <html> tag declares a lang attribute.
*
* @package Progress_Planner
*/

namespace Progress_Planner\Suggested_Tasks\Audit\Checks;

/**
* Lang attribute check.
*/
class Lang_Attribute_Check implements Check {

/**
* {@inheritDoc}
*
* @return string
*/
public function get_rule_id(): string {
return 'html-lang';
}

/**
* {@inheritDoc}
*
* @param string $url The audited URL.
* @param string $html The fetched homepage HTML.
* @param array $context Shared fetch context.
*
* @return array<string, mixed>
*/
public function run( string $url, string $html, array $context ): array {
if ( '' === \trim( $html ) ) {
return [];
}

$pass = (bool) \preg_match( '/<html\b[^>]*\blang\s*=\s*["\']?\s*[a-z]{2,3}\b/i', $html );

return [
'rule_id' => $this->get_rule_id(),
'category' => 'foundations',
'title' => \__( "Declare your site's language", 'progress-planner' ),
'description' => \__( 'Add a lang attribute to the <html> tag (for example lang="en"). It helps screen readers pronounce content correctly and search engines serve the right language.', 'progress-planner' ),
'severity' => 'medium',
'status' => $pass ? 'pass' : 'fail',
'doc_url' => 'https://specification.website/spec/foundations/html-lang/',
'source' => 'php-check',
];
}
}
Loading
Loading